Refactor structure

This commit is contained in:
2025-12-14 21:34:06 +08:00
parent 952f00261b
commit 27cb9fa32f
37 changed files with 712 additions and 486 deletions

16
models/Cargo.toml Executable file
View File

@@ -0,0 +1,16 @@
[package]
name = "models"
version = "0.1.0"
edition = "2024"
[dependencies]
directories.workspace = true
color-eyre.workspace = true
serde.workspace = true
lazy_static.workspace = true
ratatui.workspace = true
serde_json.workspace = true
dashmap.workspace = true
db = { path = "../db" }
language-tags = { version = "0.3.2", features = ["serde"] }
sys-locale = "0.3.2"

72
models/src/config.rs Executable file
View File

@@ -0,0 +1,72 @@
use std::path::PathBuf;
use language_tags::LanguageTag;
use serde::{Deserialize, Serialize};
use color_eyre::Result;
use crate::{APP_CONIFG_FILE_PATH, CACHE_MAP};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ApplicationConfig {
pub basic_config: BasicConfig,
pub path_config: PathConfig,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BasicConfig {
pub locale: LanguageTag,
pub tick_rate: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PathConfig {
pub dlsite_paths: Vec<String>
}
const CONFIG_KEY: &str = "app_conf";
impl ApplicationConfig {
pub fn get_config() -> Result<Self> {
if CACHE_MAP.contains_key(CONFIG_KEY) &&
let Some(cached_config) = CACHE_MAP.get(CONFIG_KEY)
{
Ok(serde_json::from_value(cached_config.clone())?)
} else if APP_CONIFG_FILE_PATH.exists() {
ApplicationConfig::from_file(&APP_CONIFG_FILE_PATH)
} else {
ApplicationConfig::new()
}
}
fn from_file(path: &PathBuf) -> Result<Self> {
let reader = std::fs::File::open(path)?;
let result: serde_json::Value = serde_json::from_reader(reader)?;
CACHE_MAP.insert(CONFIG_KEY.to_string(), result.clone());
Ok(serde_json::from_value(result)?)
}
fn new() -> Result<Self> {
let default_locale = sys_locale::get_locale().unwrap_or(String::from("ja-JP"));
let conf = Self {
basic_config: BasicConfig {
tick_rate: 250,
locale: LanguageTag::parse(&default_locale)?,
},
path_config: PathConfig {
dlsite_paths: vec![],
},
};
conf.clone().write_to_file(APP_CONIFG_FILE_PATH.to_path_buf())?;
Ok(conf)
}
fn write_to_file(self, path: PathBuf) -> Result<()> {
let writer = std::fs::File::create(path)?;
serde_json::to_writer_pretty(writer, &self)?;
Ok(())
}
pub fn save(self) -> Result<()> {
let current_value = serde_json::to_value(&self)?;
CACHE_MAP.alter(CONFIG_KEY, |_, _| current_value);
self.write_to_file(APP_CONIFG_FILE_PATH.to_path_buf())
}
}

55
models/src/dlsite/category.rs Executable file
View File

@@ -0,0 +1,55 @@
use color_eyre::Report;
use serde::{Deserialize, Serialize};
use db::types::{RocksColumn, RocksReferences};
use crate::config::ApplicationConfig;
use crate::dlsite::genre::DLSiteGenre;
use crate::dlsite::translation::DLSiteTranslation;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DLSiteCategory {
#[serde(skip)]
pub id: String,
pub genre_ids: Vec<u16>,
pub name: DLSiteTranslation
}
impl TryFrom<super::crawler::DLSiteGenreCategory> for DLSiteCategory {
type Error = Report;
fn try_from(value: super::crawler::DLSiteGenreCategory) -> Result<Self, Self::Error> {
let category = Self {
id: format!(
"{}/{}",
value.id,
ApplicationConfig::get_config()?.basic_config.locale.primary_language()
),
genre_ids: value.values.iter()
.map(|v| v.value.parse::<u16>())
.filter_map(Result::ok)
.collect(),
name: DLSiteTranslation::try_from(value.category_name.as_str())?,
};
Ok(category)
}
}
impl RocksReferences<DLSiteGenre> for DLSiteCategory {
fn get_reference_ids(&self) -> Vec<<DLSiteGenre as RocksColumn>::Id> {
self.genre_ids.clone()
}
}
impl RocksColumn for DLSiteCategory {
type Id = String;
fn get_id(&self) -> Self::Id {
self.id.clone()
}
fn set_id(&mut self, id: Self::Id) {
self.id = id;
}
fn get_column_name() -> String {
String::from("dl_categories")
}
}

39
models/src/dlsite/crawler.rs Executable file
View File

@@ -0,0 +1,39 @@
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct DLSiteManiax {
#[serde(rename = "work_name")]
pub title: String,
#[serde(rename = "work_image")]
pub work_image_url: String,
#[serde(rename = "dl_count")]
pub sells_count: u32,
#[serde(skip)]
pub genre_ids: Vec<u16>,
#[serde(skip)]
pub rj_num: String,
#[serde(skip)]
pub folder_path: PathBuf,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct DLSiteFilter {
pub genre_all: Value
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct DLSiteGenreCategory {
pub category_name: String,
pub values: Vec<DLSiteGenre>,
#[serde(skip)]
pub id: u8
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct DLSiteGenre {
pub value: String,
pub name: String
}

26
models/src/dlsite/genre.rs Executable file
View File

@@ -0,0 +1,26 @@
use serde::{Deserialize, Serialize};
use db::types::RocksColumn;
use super::translation::DLSiteTranslation;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DLSiteGenre {
#[serde(skip)]
pub id: u16,
pub name: Vec<DLSiteTranslation>
}
impl RocksColumn for DLSiteGenre {
type Id = u16;
fn get_id(&self) -> Self::Id {
self.id.clone()
}
fn set_id(&mut self, id: Self::Id) {
self.id = id;
}
fn get_column_name() -> String {
String::from("dl_genres")
}
}

59
models/src/dlsite/maniax.rs Executable file
View File

@@ -0,0 +1,59 @@
use std::path::PathBuf;
use ratatui::text::Text;
use serde::{Deserialize, Serialize};
use db::types::{RocksColumn, RocksReferences};
use super::genre::DLSiteGenre;
use super::translation::DLSiteTranslation;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DLSiteManiax {
#[serde(skip)]
pub rj_num: String,
pub genre_ids: Vec<u16>,
pub name: Vec<DLSiteTranslation>,
pub sells_count: u32,
pub folder_path: PathBuf,
pub version: Option<String>
}
impl From<super::crawler::DLSiteManiax> for DLSiteManiax {
fn from(value: super::crawler::DLSiteManiax) -> Self {
let title = DLSiteTranslation::try_from(value.title.as_str()).unwrap();
Self {
rj_num: value.rj_num,
genre_ids: value.genre_ids,
name: vec![title],
sells_count: value.sells_count,
folder_path: value.folder_path,
version: None
}
}
}
impl RocksColumn for DLSiteManiax {
type Id = String;
fn get_id(&self) -> Self::Id {
self.rj_num.clone()
}
fn set_id(&mut self, id: Self::Id) {
self.rj_num = id;
}
fn get_column_name() -> String {
String::from("dl_games")
}
}
impl RocksReferences<DLSiteGenre> for DLSiteManiax {
fn get_reference_ids(&self) -> Vec<<DLSiteGenre as RocksColumn>::Id> {
self.genre_ids.clone()
}
}
impl Into<Text<'_>> for &DLSiteManiax {
fn into(self) -> Text<'static> {
Text::from(self.rj_num.to_string())
}
}

39
models/src/dlsite/mod.rs Executable file
View File

@@ -0,0 +1,39 @@
use color_eyre::eyre::eyre;
use color_eyre::Report;
use language_tags::LanguageTag;
mod translation;
mod category;
mod genre;
mod maniax;
pub mod crawler;
pub use translation::{EN_LOCALE, JP_LOCALE, DLSiteTranslation};
pub use category::DLSiteCategory;
pub use genre::DLSiteGenre;
pub use maniax::DLSiteManiax;
pub fn matches_primary_language(left: &LanguageTag, right: &LanguageTag) -> bool {
left.primary_language() == right.primary_language()
}
#[derive(Debug, Clone)]
pub enum PrimaryLanguage {
EN, JP
}
impl TryFrom<&LanguageTag> for PrimaryLanguage {
type Error = Report;
fn try_from(value: &LanguageTag) -> Result<Self, Self::Error> {
if matches_primary_language(&value, &EN_LOCALE) {
Ok(PrimaryLanguage::EN)
}
else if matches_primary_language(&value, &JP_LOCALE) {
Ok(PrimaryLanguage::JP)
}
else {
Err(eyre!("No matching primary language found for {}", value))
}
}
}

View File

@@ -0,0 +1,56 @@
use color_eyre::eyre::eyre;
use color_eyre::Report;
use language_tags::LanguageTag;
use serde::{Deserialize, Serialize};
use lazy_static::lazy_static;
use crate::config::ApplicationConfig;
use super::matches_primary_language;
lazy_static! {
pub static ref EN_LOCALE: LanguageTag = LanguageTag::parse("en").unwrap();
pub static ref JP_LOCALE: LanguageTag = LanguageTag::parse("ja").unwrap();
pub static ref SUPPORTED_LOCALES: [LanguageTag; 2] = [JP_LOCALE.clone(), EN_LOCALE.clone()];
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum DLSiteTranslation {
EN(String), JP(String)
}
impl TryFrom<&str> for DLSiteTranslation {
type Error = Report;
fn try_from(value: &str) -> color_eyre::Result<Self> {
Self::try_from(value.to_string())
}
}
impl TryFrom<String> for DLSiteTranslation {
type Error = Report;
fn try_from(value: String) -> color_eyre::Result<Self> {
let app_conf = ApplicationConfig::get_config()?;
let locale = app_conf.basic_config.locale;
if matches_primary_language(&locale, &EN_LOCALE) {
return Ok(DLSiteTranslation::EN(value));
}
if matches_primary_language(&locale, &JP_LOCALE) {
return Ok(DLSiteTranslation::JP(value));
}
Err(eyre!(
"Invalid Locale: {:?}; Support {:?}",
locale,
[EN_LOCALE.to_string(), JP_LOCALE.to_string()])
)
}
}
impl TryInto<String> for DLSiteTranslation {
type Error = Report;
fn try_into(self) -> Result<String, Self::Error> {
match self {
DLSiteTranslation::EN(val) => Ok(val),
DLSiteTranslation::JP(val) => Ok(val),
}
}
}

24
models/src/lib.rs Executable file
View File

@@ -0,0 +1,24 @@
use std::hash::RandomState;
use std::path::PathBuf;
use std::sync::Arc;
use dashmap::DashMap;
use directories::BaseDirs;
use lazy_static::lazy_static;
pub mod dlsite;
pub mod config;
const APP_DIR_NAME: &str = "sus_manager";
lazy_static! {
static ref BASE_DIRS: BaseDirs = BaseDirs::new().unwrap();
pub static ref APP_CONFIG_DIR: PathBuf =
BASE_DIRS.config_dir().to_path_buf().join(APP_DIR_NAME);
pub static ref APP_DATA_DIR: PathBuf = BASE_DIRS.data_dir().to_path_buf().join(APP_DIR_NAME);
pub static ref APP_CACHE_PATH: PathBuf = BASE_DIRS.cache_dir().to_path_buf().join(APP_DIR_NAME);
pub static ref APP_CONIFG_FILE_PATH: PathBuf = APP_CONFIG_DIR.clone().join("config.json");
}
lazy_static! {
pub static ref CACHE_MAP: Arc<DashMap<String, serde_json::Value>> =
Arc::new(DashMap::with_hasher(RandomState::default()));
}