Refactor structure
This commit is contained in:
69
Cargo.toml
69
Cargo.toml
@@ -2,6 +2,7 @@
|
|||||||
name = "sus-manager"
|
name = "sus-manager"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "manager for SuS gamers"
|
description = "manager for SuS gamers"
|
||||||
|
repository = "https://git.ouoweb.xyz/fromost/sus-manager"
|
||||||
authors = ["fromost"]
|
authors = ["fromost"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
@@ -17,60 +18,24 @@ opt-level = 0
|
|||||||
lto = "fat"
|
lto = "fat"
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
|
||||||
[dependencies]
|
[workspace]
|
||||||
color-eyre = "0.6.3"
|
resolver = "3"
|
||||||
futures = "0.3.28"
|
members = ["crawler","db","models","ui"]
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
directories = "6.0.0"
|
directories = "6.0.0"
|
||||||
|
tokio = { version = "1.48.0", features = ["full"] }
|
||||||
lazy_static = "1.5.0"
|
lazy_static = "1.5.0"
|
||||||
robotstxt = "0.3.0"
|
color-eyre = { version = "0.6.5" }
|
||||||
scraper = "0.24.0"
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
rat-cursor = "1.2.1"
|
|
||||||
serde_json = "1.0.145"
|
serde_json = "1.0.145"
|
||||||
image = "0.25.8"
|
log = "0.4.29"
|
||||||
log = "0.4.28"
|
ratatui = { version = "0.29.0", features = ["all-widgets"] }
|
||||||
num_cpus = "1.17.0"
|
|
||||||
sys-locale = "0.3.2"
|
|
||||||
jemallocator = "0.5.4"
|
|
||||||
itertools = "0.14.0"
|
|
||||||
dashmap = { version = "6.1.0", features = ["serde"] }
|
dashmap = { version = "6.1.0", features = ["serde"] }
|
||||||
|
|
||||||
[dependencies.language-tags]
|
[dependencies]
|
||||||
version = "0.3.2"
|
color-eyre = "0.6.5"
|
||||||
features = ["serde"]
|
jemallocator = "0.5.4"
|
||||||
|
tokio = { version = "1.48.0", features = ["macros"] }
|
||||||
[dependencies.indicatif]
|
clap_builder = "4.5.53"
|
||||||
version = "0.18.1"
|
ui = { path = "./ui" }
|
||||||
features = ["futures", "tokio"]
|
|
||||||
|
|
||||||
[dependencies.rocksdb]
|
|
||||||
version = "0.24.0"
|
|
||||||
features = ["multi-threaded-cf"]
|
|
||||||
|
|
||||||
[dependencies.serde]
|
|
||||||
version = "1.0.228"
|
|
||||||
features = ["derive"]
|
|
||||||
|
|
||||||
[dependencies.tui-input]
|
|
||||||
version = "0.14.0"
|
|
||||||
features = ["crossterm"]
|
|
||||||
default-features = false
|
|
||||||
|
|
||||||
[dependencies.crossterm]
|
|
||||||
version = "0.29.0"
|
|
||||||
features = ["event-stream"]
|
|
||||||
|
|
||||||
[dependencies.ratatui]
|
|
||||||
version = "0.29.0"
|
|
||||||
features = ["all-widgets"]
|
|
||||||
|
|
||||||
[dependencies.clap]
|
|
||||||
version = "4.5.48"
|
|
||||||
features = ["derive", "cargo"]
|
|
||||||
|
|
||||||
[dependencies.reqwest]
|
|
||||||
version = "0.12.23"
|
|
||||||
features = ["blocking", "json", "rustls-tls"]
|
|
||||||
|
|
||||||
[dependencies.tokio]
|
|
||||||
version = "1.47.1"
|
|
||||||
features = ["full"]
|
|
||||||
|
|||||||
23
crawler/Cargo.toml
Executable file
23
crawler/Cargo.toml
Executable file
@@ -0,0 +1,23 @@
|
|||||||
|
[package]
|
||||||
|
name = "crawler"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
image = "0.25.9"
|
||||||
|
reqwest = { version = "0.12.25", features = ["json"] }
|
||||||
|
scraper = "0.25.0"
|
||||||
|
robotstxt = "0.3.0"
|
||||||
|
|
||||||
|
models = { path = "../models" }
|
||||||
|
|
||||||
|
tokio.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
color-eyre.workspace = true
|
||||||
|
lazy_static.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
|
||||||
|
futures = "0.3.31"
|
||||||
|
itertools = "0.14.0"
|
||||||
|
language-tags = "0.3.2"
|
||||||
|
|
||||||
@@ -10,12 +10,11 @@ use itertools::Itertools;
|
|||||||
use language_tags::LanguageTag;
|
use language_tags::LanguageTag;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use scraper::{Element, Html, Selector};
|
use scraper::{Element, Html, Selector};
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use crate::constants::{APP_DATA_DIR, JP_LOCALE};
|
use models::APP_DATA_DIR;
|
||||||
use crate::crawler::Crawler;
|
use models::dlsite::{matches_primary_language, PrimaryLanguage, JP_LOCALE};
|
||||||
use crate::helpers::matches_primary_language;
|
use super::Crawler;
|
||||||
use crate::models::{PrimaryLanguage};
|
use models::dlsite::crawler::*;
|
||||||
|
|
||||||
//TODO: override locale with user one
|
//TODO: override locale with user one
|
||||||
const DLSITE_URL: &str = "https://www.dlsite.com/";
|
const DLSITE_URL: &str = "https://www.dlsite.com/";
|
||||||
@@ -31,42 +30,6 @@ pub struct DLSiteCrawler {
|
|||||||
crawler: Crawler
|
crawler: Crawler
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
|
||||||
pub(crate) struct DLSiteManiax {
|
|
||||||
#[serde(rename = "work_name")]
|
|
||||||
pub(crate) title: String,
|
|
||||||
#[serde(rename = "work_image")]
|
|
||||||
pub(crate) work_image_url: String,
|
|
||||||
#[serde(rename = "dl_count")]
|
|
||||||
pub(crate) sells_count: u32,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub(crate) genre_ids: Vec<u16>,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub(crate) rj_num: String,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub(crate) folder_path: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
|
||||||
struct DLSiteFilter {
|
|
||||||
pub(crate) genre_all: Value
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
|
||||||
pub(crate) struct DLSiteGenreCategory {
|
|
||||||
pub(crate) category_name: String,
|
|
||||||
pub(crate) values: Vec<DLSiteGenre>,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub(crate) id: u8
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
|
||||||
pub(crate) struct DLSiteGenre {
|
|
||||||
pub(crate) value: String,
|
|
||||||
pub(crate) name: String
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DLSiteCrawler {
|
impl DLSiteCrawler {
|
||||||
pub fn new() -> Result<Self> {
|
pub fn new() -> Result<Self> {
|
||||||
let url = Url::parse(DLSITE_URL)?;
|
let url = Url::parse(DLSITE_URL)?;
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
pub mod dlsite;
|
mod dlsite;
|
||||||
pub use dlsite::*;
|
|
||||||
|
|
||||||
use color_eyre::eyre::eyre;
|
use color_eyre::eyre::eyre;
|
||||||
use crate::constants::APP_CACHE_PATH;
|
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use image::DynamicImage;
|
use image::DynamicImage;
|
||||||
use reqwest::{Client, StatusCode, Url};
|
use reqwest::{Client, StatusCode, Url};
|
||||||
@@ -10,6 +8,8 @@ use robotstxt::DefaultMatcher;
|
|||||||
use scraper::Html;
|
use scraper::Html;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
|
|
||||||
|
pub use dlsite::*;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct Crawler {
|
struct Crawler {
|
||||||
id: String,
|
id: String,
|
||||||
@@ -47,7 +47,7 @@ impl Crawler {
|
|||||||
return Ok(txt.clone());
|
return Ok(txt.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
let local_robots_path = APP_CACHE_PATH.clone().join(&self.id).join("robots.txt");
|
let local_robots_path = models::APP_CACHE_PATH.clone().join(&self.id).join("robots.txt");
|
||||||
if !local_robots_path.exists() {
|
if !local_robots_path.exists() {
|
||||||
let mut robots_url = self.base_url.clone();
|
let mut robots_url = self.base_url.clone();
|
||||||
robots_url.set_path("/robots.txt");
|
robots_url.set_path("/robots.txt");
|
||||||
13
db/Cargo.toml
Executable file
13
db/Cargo.toml
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "db"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rocksdb = "0.24.0"
|
||||||
|
num_cpus = "1.17.0"
|
||||||
|
lazy_static.workspace = true
|
||||||
|
color-eyre.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
directories.workspace = true
|
||||||
@@ -1,32 +1,99 @@
|
|||||||
use crate::constants::{APP_DB_DATA_DIR, DB_CF_OPTIONS, DB_COLUMNS, DB_OPTIONS};
|
pub mod types;
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
use rocksdb::{ColumnFamilyDescriptor, IteratorMode, OptimisticTransactionDB, Options};
|
use rocksdb::{ColumnFamilyDescriptor, IteratorMode, OptimisticTransactionDB, Options};
|
||||||
use serde::{Serialize};
|
use serde::{Serialize};
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use crate::models::{RocksColumn, RocksReference, RocksReferences};
|
use crate::types::{RocksColumn, RocksReference, RocksReferences};
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
|
use directories::BaseDirs;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
|
||||||
pub struct RocksDB {
|
const APP_DIR_NAME: &str = "sus_manager";
|
||||||
db: OptimisticTransactionDB,
|
lazy_static! {
|
||||||
|
static ref BASE_DIRS: BaseDirs = BaseDirs::new().unwrap();
|
||||||
|
static ref APP_DB_DATA_DIR: PathBuf = BASE_DIRS.data_dir().to_path_buf().join(APP_DIR_NAME).join("db");
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for RocksDB {
|
fn get_db_options() -> Options {
|
||||||
|
let mut opts = Options::default();
|
||||||
|
|
||||||
|
opts.create_missing_column_families(true);
|
||||||
|
opts.create_if_missing(true);
|
||||||
|
opts.increase_parallelism(num_cpus::get() as i32);
|
||||||
|
|
||||||
|
opts
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_db_read_options() -> rocksdb::ReadOptions {
|
||||||
|
let mut opts = rocksdb::ReadOptions::default();
|
||||||
|
opts.set_async_io(true);
|
||||||
|
opts
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct RocksDBFactory {
|
||||||
|
cfs: Vec<String>,
|
||||||
|
path: PathBuf,
|
||||||
|
db_opts: Options,
|
||||||
|
cf_opts: Options,
|
||||||
|
context: Option<RocksDB>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RocksDBFactory {
|
||||||
|
pub fn new(path: PathBuf, db_opts: Options, cf_opts: Options) -> Result<Self> {
|
||||||
|
let instance = Self {
|
||||||
|
cfs: vec![],
|
||||||
|
path,
|
||||||
|
db_opts,
|
||||||
|
cf_opts,
|
||||||
|
context: None
|
||||||
|
};
|
||||||
|
if !instance.path.exists() {
|
||||||
|
std::fs::create_dir_all(instance.path.as_path())?;
|
||||||
|
}
|
||||||
|
Ok(instance)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register<T>(&mut self) where T: RocksColumn {
|
||||||
|
self.cfs.push(T::get_column_name());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_current_context(&mut self) -> Result<RocksDB> {
|
||||||
|
if let Some(context) = &self.context {
|
||||||
|
return Ok(context.clone());
|
||||||
|
}
|
||||||
|
let cfs = self.cfs
|
||||||
|
.iter()
|
||||||
|
.map(|cf| ColumnFamilyDescriptor::new(cf, self.cf_opts.clone()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let context = RocksDB::new(cfs, self.path.clone(), self.db_opts.clone())?;
|
||||||
|
self.context = Some(context.clone());
|
||||||
|
Ok(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RocksDBFactory {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
RocksDB::new(DB_OPTIONS.clone(), DB_CF_OPTIONS.clone()).unwrap()
|
Self::new(APP_DB_DATA_DIR.to_path_buf(), get_db_options(), Options::default()).unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct RocksDB {
|
||||||
|
db: Arc<OptimisticTransactionDB>,
|
||||||
|
}
|
||||||
|
|
||||||
impl RocksDB {
|
impl RocksDB {
|
||||||
pub fn new(db_opts: Options, cf_opts: Options) -> Result<Self> {
|
pub fn new(cfs: Vec<ColumnFamilyDescriptor>, path: PathBuf, db_opts: Options) -> Result<Self> {
|
||||||
let cfs = DB_COLUMNS.iter()
|
|
||||||
.map(|cf| ColumnFamilyDescriptor::new(cf.to_string(), cf_opts.clone()))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let db = OptimisticTransactionDB::open_cf_descriptors(
|
let db = OptimisticTransactionDB::open_cf_descriptors(
|
||||||
&db_opts,
|
&db_opts,
|
||||||
APP_DB_DATA_DIR.as_path(),
|
path.as_path(),
|
||||||
cfs
|
cfs
|
||||||
)?;
|
)?;
|
||||||
let rocks = Self {
|
let rocks = Self {
|
||||||
db
|
db: Arc::new(db)
|
||||||
};
|
};
|
||||||
Ok(rocks)
|
Ok(rocks)
|
||||||
}
|
}
|
||||||
@@ -91,7 +158,7 @@ impl RocksDB {
|
|||||||
where TColumn: RocksColumn + DeserializeOwned
|
where TColumn: RocksColumn + DeserializeOwned
|
||||||
{
|
{
|
||||||
let cf = self.db.cf_handle(TColumn::get_column_name().as_str()).unwrap();
|
let cf = self.db.cf_handle(TColumn::get_column_name().as_str()).unwrap();
|
||||||
let values = self.db.iterator_cf_opt(&cf, crate::constants::get_db_read_options(), IteratorMode::Start)
|
let values = self.db.iterator_cf_opt(&cf, get_db_read_options(), IteratorMode::Start)
|
||||||
.filter_map(Result::ok)
|
.filter_map(Result::ok)
|
||||||
.map(|(k, v)| {
|
.map(|(k, v)| {
|
||||||
let id = serde_json::from_slice::<TColumn::Id>(&k).unwrap();
|
let id = serde_json::from_slice::<TColumn::Id>(&k).unwrap();
|
||||||
17
db/src/types.rs
Executable file
17
db/src/types.rs
Executable file
@@ -0,0 +1,17 @@
|
|||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
pub trait RocksColumn {
|
||||||
|
type Id: Serialize + DeserializeOwned + Clone;
|
||||||
|
fn get_id(&self) -> Self::Id;
|
||||||
|
fn set_id(&mut self, id: Self::Id);
|
||||||
|
fn get_column_name() -> String;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait RocksReference<T> where T: RocksColumn {
|
||||||
|
fn get_reference_id(&self) -> T::Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait RocksReferences<T> where T: RocksColumn {
|
||||||
|
fn get_reference_ids(&self) -> Vec<T::Id>;
|
||||||
|
}
|
||||||
16
models/Cargo.toml
Executable file
16
models/Cargo.toml
Executable 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
72
models/src/config.rs
Executable 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
55
models/src/dlsite/category.rs
Executable 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
39
models/src/dlsite/crawler.rs
Executable 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
26
models/src/dlsite/genre.rs
Executable 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
59
models/src/dlsite/maniax.rs
Executable 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +1,20 @@
|
|||||||
mod game;
|
|
||||||
|
|
||||||
use color_eyre::eyre::eyre;
|
use color_eyre::eyre::eyre;
|
||||||
use color_eyre::Report;
|
use color_eyre::Report;
|
||||||
use language_tags::LanguageTag;
|
use language_tags::LanguageTag;
|
||||||
use serde::de::DeserializeOwned;
|
|
||||||
use serde::Serialize;
|
|
||||||
pub(crate) use game::*;
|
|
||||||
use crate::constants::{EN_LOCALE, JP_LOCALE};
|
|
||||||
use crate::helpers::matches_primary_language;
|
|
||||||
|
|
||||||
pub trait RocksColumn {
|
mod translation;
|
||||||
type Id: Serialize + DeserializeOwned + Clone;
|
mod category;
|
||||||
fn get_id(&self) -> Self::Id;
|
mod genre;
|
||||||
fn set_id(&mut self, id: Self::Id);
|
mod maniax;
|
||||||
fn get_column_name() -> String;
|
pub mod crawler;
|
||||||
}
|
|
||||||
|
|
||||||
pub trait RocksReference<T> where T: RocksColumn {
|
pub use translation::{EN_LOCALE, JP_LOCALE, DLSiteTranslation};
|
||||||
fn get_reference_id(&self) -> T::Id;
|
pub use category::DLSiteCategory;
|
||||||
}
|
pub use genre::DLSiteGenre;
|
||||||
|
pub use maniax::DLSiteManiax;
|
||||||
|
|
||||||
pub trait RocksReferences<T> where T: RocksColumn {
|
pub fn matches_primary_language(left: &LanguageTag, right: &LanguageTag) -> bool {
|
||||||
fn get_reference_ids(&self) -> Vec<T::Id>;
|
left.primary_language() == right.primary_language()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
56
models/src/dlsite/translation.rs
Executable file
56
models/src/dlsite/translation.rs
Executable 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
24
models/src/lib.rs
Executable 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()));
|
||||||
|
}
|
||||||
3
scripts/deps.sh
Executable file
3
scripts/deps.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
if ! (command -v rpgmaker-linux & > /dev/null 2>&1) then
|
||||||
|
wget -qO- "https://raw.githubusercontent.com/bakustarver/rpgmakermlinux-cicpoffs/main/installgithub.sh" | bash
|
||||||
|
fi
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
use std::hash::RandomState;
|
|
||||||
use directories::BaseDirs;
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use dashmap::DashMap;
|
|
||||||
use language_tags::LanguageTag;
|
|
||||||
use crate::models::{DLSiteCategory, DLSiteGenre, DLSiteManiax, RocksColumn};
|
|
||||||
|
|
||||||
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");
|
|
||||||
pub static ref APP_DB_DATA_DIR: PathBuf = APP_DATA_DIR.clone().join("db");
|
|
||||||
|
|
||||||
pub static ref DB_OPTIONS: rocksdb::Options = get_db_options();
|
|
||||||
pub static ref DB_CF_OPTIONS: rocksdb::Options = rocksdb::Options::default();
|
|
||||||
}
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
pub static ref DB_COLUMNS: [String; 3] = [
|
|
||||||
DLSiteManiax::get_column_name().to_string(),
|
|
||||||
DLSiteGenre::get_column_name().to_string(),
|
|
||||||
DLSiteCategory::get_column_name().to_string(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
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()];
|
|
||||||
}
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
pub static ref CACHE_MAP: Arc<DashMap<String, serde_json::Value>> =
|
|
||||||
Arc::new(DashMap::with_hasher(RandomState::default()));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_db_options() -> rocksdb::Options {
|
|
||||||
let mut opts = rocksdb::Options::default();
|
|
||||||
|
|
||||||
opts.create_missing_column_families(true);
|
|
||||||
opts.create_if_missing(true);
|
|
||||||
opts.increase_parallelism(num_cpus::get() as i32);
|
|
||||||
|
|
||||||
opts
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn get_db_read_options() -> rocksdb::ReadOptions {
|
|
||||||
let mut opts = rocksdb::ReadOptions::default();
|
|
||||||
opts.set_async_io(true);
|
|
||||||
opts
|
|
||||||
}
|
|
||||||
23
src/main.rs
23
src/main.rs
@@ -1,21 +1,8 @@
|
|||||||
mod app;
|
use clap_builder::Parser;
|
||||||
mod cli;
|
|
||||||
mod config;
|
|
||||||
mod constants;
|
|
||||||
mod crawler;
|
|
||||||
mod event;
|
|
||||||
mod helpers;
|
|
||||||
mod models;
|
|
||||||
mod widgets;
|
|
||||||
|
|
||||||
use crate::cli::Cli;
|
|
||||||
use clap::Parser;
|
|
||||||
use color_eyre::Result;
|
|
||||||
use tokio;
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> color_eyre::Result<()> {
|
||||||
color_eyre::install()?;
|
color_eyre::install()?;
|
||||||
let cli = Cli::parse();
|
let cli = ui::Cli::parse();
|
||||||
cli.run().await
|
cli.run().await?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1,219 +0,0 @@
|
|||||||
use color_eyre::Result;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use color_eyre::{eyre, Report};
|
|
||||||
use ratatui::prelude::Text;
|
|
||||||
use ratatui::widgets::ListState;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use serde::de::DeserializeOwned;
|
|
||||||
use crate::config::types::ApplicationConfig;
|
|
||||||
use crate::constants::{EN_LOCALE, JP_LOCALE};
|
|
||||||
use crate::crawler::DLSiteGenreCategory;
|
|
||||||
use crate::helpers::db::RocksDB;
|
|
||||||
use crate::helpers::matches_primary_language;
|
|
||||||
use crate::models::{RocksColumn, RocksReferences};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub(crate) struct GameList<T> {
|
|
||||||
pub games: Vec<T>,
|
|
||||||
pub state: ListState,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Default for GameList<T> {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
games: Vec::new(),
|
|
||||||
state: ListState::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> GameList<T>
|
|
||||||
where T: DeserializeOwned + RocksColumn
|
|
||||||
{
|
|
||||||
pub fn new() -> Result<Self> {
|
|
||||||
let db = RocksDB::default();
|
|
||||||
let mut state = ListState::default();
|
|
||||||
state.select_first();
|
|
||||||
let game_list = GameList {
|
|
||||||
games: db.get_all_values::<T>()?,
|
|
||||||
state
|
|
||||||
};
|
|
||||||
Ok(game_list)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//region Maniax
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
pub(crate) 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<crate::crawler::dlsite::DLSiteManiax> for DLSiteManiax {
|
|
||||||
fn from(value: crate::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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//endregion
|
|
||||||
|
|
||||||
//region Genre
|
|
||||||
#[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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//endregion
|
|
||||||
|
|
||||||
//region Category
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
pub(crate) struct DLSiteCategory {
|
|
||||||
#[serde(skip)]
|
|
||||||
pub id: String,
|
|
||||||
pub genre_ids: Vec<u16>,
|
|
||||||
pub name: DLSiteTranslation
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<DLSiteGenreCategory> for DLSiteCategory {
|
|
||||||
type Error = Report;
|
|
||||||
|
|
||||||
fn try_from(value: 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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//endregion
|
|
||||||
|
|
||||||
//region Translation
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
||||||
pub(crate) 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::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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//endregion
|
|
||||||
43
ui/Cargo.toml
Executable file
43
ui/Cargo.toml
Executable file
@@ -0,0 +1,43 @@
|
|||||||
|
[package]
|
||||||
|
name = "ui"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "ui"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
futures = "0.3.28"
|
||||||
|
rat-cursor = "1.2.1"
|
||||||
|
itertools = "0.14.0"
|
||||||
|
|
||||||
|
color-eyre.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
ratatui.workspace = true
|
||||||
|
|
||||||
|
models = { path = "../models" }
|
||||||
|
db = { path = "../db" }
|
||||||
|
crawler = { path = "../crawler" }
|
||||||
|
|
||||||
|
[dependencies.ratatui-image]
|
||||||
|
version = "8.0.2"
|
||||||
|
features = ["tokio", "serde"]
|
||||||
|
|
||||||
|
[dependencies.indicatif]
|
||||||
|
version = "0.18.1"
|
||||||
|
features = ["futures", "tokio"]
|
||||||
|
|
||||||
|
[dependencies.tui-input]
|
||||||
|
version = "0.14.0"
|
||||||
|
features = ["crossterm"]
|
||||||
|
default-features = false
|
||||||
|
|
||||||
|
[dependencies.crossterm]
|
||||||
|
version = "0.29.0"
|
||||||
|
features = ["event-stream"]
|
||||||
|
|
||||||
|
[dependencies.clap]
|
||||||
|
version = "4.5.48"
|
||||||
|
features = ["derive", "cargo"]
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
use crate::config::types::ApplicationConfig;
|
|
||||||
use crate::event::{AppEvent, EventHandler};
|
use crate::event::{AppEvent, EventHandler};
|
||||||
use crate::widgets::views::{AppView, MainView};
|
use crate::widgets::views::{AppView, MainView};
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
@@ -6,12 +5,17 @@ use crossterm::event::{Event};
|
|||||||
use ratatui::{DefaultTerminal, Frame};
|
use ratatui::{DefaultTerminal, Frame};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use color_eyre::eyre::eyre;
|
use color_eyre::eyre::eyre;
|
||||||
|
use db::RocksDBFactory;
|
||||||
|
use models::config::ApplicationConfig;
|
||||||
|
use models::dlsite::{DLSiteCategory, DLSiteGenre, DLSiteManiax};
|
||||||
|
|
||||||
pub(crate) struct App {
|
pub(crate) struct App {
|
||||||
events: EventHandler,
|
events: EventHandler,
|
||||||
state: AppState,
|
state: AppState,
|
||||||
|
db_factory: RocksDBFactory
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
view: Option<AppView>,
|
view: Option<AppView>,
|
||||||
}
|
}
|
||||||
@@ -19,12 +23,17 @@ pub struct AppState {
|
|||||||
impl App {
|
impl App {
|
||||||
pub async fn create() -> Result<Self> {
|
pub async fn create() -> Result<Self> {
|
||||||
let config = ApplicationConfig::get_config()?;
|
let config = ApplicationConfig::get_config()?;
|
||||||
|
let mut db_factory = RocksDBFactory::default();
|
||||||
|
db_factory.register::<DLSiteManiax>();
|
||||||
|
db_factory.register::<DLSiteGenre>();
|
||||||
|
db_factory.register::<DLSiteCategory>();
|
||||||
let state = AppState {
|
let state = AppState {
|
||||||
view: Some(AppView::Main(MainView::new()?)),
|
view: Some(AppView::Main(MainView::new(db_factory.clone())?)),
|
||||||
};
|
};
|
||||||
let app = Self {
|
let app = Self {
|
||||||
events: EventHandler::new(Duration::from_millis(config.basic_config.tick_rate)),
|
events: EventHandler::new(Duration::from_millis(config.basic_config.tick_rate)),
|
||||||
state,
|
state,
|
||||||
|
db_factory
|
||||||
};
|
};
|
||||||
Ok(app)
|
Ok(app)
|
||||||
}
|
}
|
||||||
@@ -69,7 +78,7 @@ impl App {
|
|||||||
match current_view {
|
match current_view {
|
||||||
AppView::Main(main_view) => {
|
AppView::Main(main_view) => {
|
||||||
frame.render_stateful_widget(
|
frame.render_stateful_widget(
|
||||||
MainView::new().unwrap(),
|
main_view.clone(),
|
||||||
frame.area(),
|
frame.area(),
|
||||||
&mut main_view.state,
|
&mut main_view.state,
|
||||||
);
|
);
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use clap::{Args, Command, Parser, Subcommand};
|
use clap::{Args, Command, Parser, Subcommand};
|
||||||
use color_eyre::eyre::eyre;
|
use color_eyre::eyre::eyre;
|
||||||
use crate::config::types::ApplicationConfig;
|
use models::config::ApplicationConfig;
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
pub(super) struct FolderAddCommand {
|
pub(super) struct FolderAddCommand {
|
||||||
@@ -17,7 +17,7 @@ enum CliSubCommand {
|
|||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(version, about)]
|
#[command(version, about)]
|
||||||
pub(crate) struct Cli {
|
pub struct Cli {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
subcommand: Option<CliSubCommand>,
|
subcommand: Option<CliSubCommand>,
|
||||||
}
|
}
|
||||||
@@ -36,12 +36,18 @@ impl Cli {
|
|||||||
|
|
||||||
async fn start_tui(&self) -> Result<()> {
|
async fn start_tui(&self) -> Result<()> {
|
||||||
crossterm::terminal::enable_raw_mode()?;
|
crossterm::terminal::enable_raw_mode()?;
|
||||||
|
|
||||||
let mut terminal = ratatui::init();
|
let mut terminal = ratatui::init();
|
||||||
|
struct TuiCleanup;
|
||||||
|
impl Drop for TuiCleanup {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
ratatui::restore();
|
||||||
|
let _ = crossterm::terminal::disable_raw_mode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _cleanup = TuiCleanup;
|
||||||
let app = app::App::create().await?;
|
let app = app::App::create().await?;
|
||||||
let result = app.run(&mut terminal).await;
|
let result = app.run(&mut terminal).await;
|
||||||
ratatui::restore();
|
ratatui::restore();
|
||||||
|
|
||||||
crossterm::terminal::disable_raw_mode()?;
|
crossterm::terminal::disable_raw_mode()?;
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
@@ -6,13 +6,13 @@ use crossterm::style::{style, Stylize};
|
|||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use indicatif::{ProgressBar, ProgressStyle};
|
use indicatif::{ProgressBar, ProgressStyle};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
use tokio::time::Instant;
|
use tokio::time::Instant;
|
||||||
use crate::models::{DLSiteCategory, DLSiteGenre, DLSiteManiax, DLSiteTranslation};
|
use crawler::DLSiteCrawler;
|
||||||
use crate::config::types::ApplicationConfig;
|
use db::{RocksDBFactory};
|
||||||
use crate::constants::{DB_CF_OPTIONS, DB_OPTIONS};
|
use models::config::ApplicationConfig;
|
||||||
use crate::crawler::{dlsite, DLSiteCrawler};
|
use models::dlsite::{DLSiteCategory, DLSiteGenre, DLSiteManiax, DLSiteTranslation};
|
||||||
use crate::helpers;
|
use crate::helpers;
|
||||||
use crate::helpers::db::RocksDB;
|
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
pub(super) struct DLSiteCommand {
|
pub(super) struct DLSiteCommand {
|
||||||
@@ -48,11 +48,11 @@ impl DLSiteSyncCommand {
|
|||||||
pub async fn handle(&self) -> Result<()> {
|
pub async fn handle(&self) -> Result<()> {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let app_conf = ApplicationConfig::get_config()?;
|
let app_conf = ApplicationConfig::get_config()?;
|
||||||
let mut db = RocksDB::default();
|
let db_factory = RocksDBFactory::default();
|
||||||
let crawler = DLSiteCrawler::new()?;
|
let crawler = DLSiteCrawler::new()?;
|
||||||
if self.do_sync_genre {
|
if self.do_sync_genre {
|
||||||
let genre_now = Instant::now();
|
let genre_now = Instant::now();
|
||||||
Self::sync_genres(&mut db, &app_conf, &crawler).await?;
|
Self::sync_genres(db_factory.clone(), &app_conf, &crawler).await?;
|
||||||
println!(
|
println!(
|
||||||
"{} {} Done in {:.2?}",
|
"{} {} Done in {:.2?}",
|
||||||
style("Genres").cyan(),
|
style("Genres").cyan(),
|
||||||
@@ -62,7 +62,7 @@ impl DLSiteSyncCommand {
|
|||||||
}
|
}
|
||||||
if self.do_sync_work {
|
if self.do_sync_work {
|
||||||
let work_now = Instant::now();
|
let work_now = Instant::now();
|
||||||
self.sync_works(&app_conf, &mut db, &crawler).await?;
|
self.sync_works(&app_conf, db_factory.clone(), &crawler).await?;
|
||||||
println!(
|
println!(
|
||||||
"{} {} Done in {:.2?}",
|
"{} {} Done in {:.2?}",
|
||||||
style("Works").cyan(),
|
style("Works").cyan(),
|
||||||
@@ -74,7 +74,8 @@ impl DLSiteSyncCommand {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn sync_genres(db: &mut RocksDB, app_conf: &ApplicationConfig, crawler: &DLSiteCrawler) -> Result<()> {
|
async fn sync_genres(mut db_factory: RocksDBFactory, app_conf: &ApplicationConfig, crawler: &DLSiteCrawler) -> Result<()> {
|
||||||
|
let mut db = db_factory.get_current_context()?;
|
||||||
let requested_categories = crawler.get_all_genres(&app_conf.basic_config.locale).await?;
|
let requested_categories = crawler.get_all_genres(&app_conf.basic_config.locale).await?;
|
||||||
let categories: Vec<DLSiteCategory> = requested_categories.iter()
|
let categories: Vec<DLSiteCategory> = requested_categories.iter()
|
||||||
.map(|g| g.clone().try_into())
|
.map(|g| g.clone().try_into())
|
||||||
@@ -111,7 +112,8 @@ impl DLSiteSyncCommand {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn sync_works(&self, app_conf: &ApplicationConfig, db: &mut RocksDB, crawler: &DLSiteCrawler) -> Result<()> {
|
async fn sync_works(&self, app_conf: &ApplicationConfig, mut db_factory: RocksDBFactory, crawler: &DLSiteCrawler) -> Result<()> {
|
||||||
|
let mut db = db_factory.get_current_context()?;
|
||||||
let existing_works = db.get_all_values::<DLSiteManiax>()?;
|
let existing_works = db.get_all_values::<DLSiteManiax>()?;
|
||||||
|
|
||||||
let work_list = self.get_work_list(&app_conf, &existing_works).await?;
|
let work_list = self.get_work_list(&app_conf, &existing_works).await?;
|
||||||
@@ -123,6 +125,7 @@ impl DLSiteSyncCommand {
|
|||||||
|
|
||||||
let progress = ProgressBar::new(game_infos.len() as u64)
|
let progress = ProgressBar::new(game_infos.len() as u64)
|
||||||
.with_style(ProgressStyle::default_bar());
|
.with_style(ProgressStyle::default_bar());
|
||||||
|
let shared_progress = Mutex::new(progress);
|
||||||
while let Some(info) = game_infos.next().await {
|
while let Some(info) = game_infos.next().await {
|
||||||
let maniax = info?;
|
let maniax = info?;
|
||||||
let existing_maniax = existing_game_infos.iter()
|
let existing_maniax = existing_game_infos.iter()
|
||||||
@@ -136,21 +139,22 @@ impl DLSiteSyncCommand {
|
|||||||
let mut modified_maniax = existing_maniax.clone();
|
let mut modified_maniax = existing_maniax.clone();
|
||||||
modified_maniax.name.push(name);
|
modified_maniax.name.push(name);
|
||||||
modified_maniaxes.push(modified_maniax);
|
modified_maniaxes.push(modified_maniax);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
let mut value: DLSiteManiax = maniax.into();
|
let mut value: DLSiteManiax = maniax.into();
|
||||||
let maniax_folder = work_list.get(&value.rj_num).unwrap().to_owned();
|
let maniax_folder = work_list.get(&value.rj_num).unwrap().to_owned();
|
||||||
value.folder_path = maniax_folder;
|
value.folder_path = maniax_folder;
|
||||||
modified_maniaxes.push(value);
|
modified_maniaxes.push(value);
|
||||||
}
|
}
|
||||||
|
let progress = shared_progress.lock().await;
|
||||||
progress.inc(1);
|
progress.inc(1);
|
||||||
}
|
}
|
||||||
db.set_values(&modified_maniaxes)?;
|
db.set_values(&modified_maniaxes)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_work_list(&self, app_conf: &ApplicationConfig, existing_works: &[DLSiteManiax]) -> Result<HashMap<String, PathBuf>> {
|
async fn get_work_list(&self, app_conf: &ApplicationConfig, existing_works: &[DLSiteManiax])
|
||||||
|
-> Result<HashMap<String, PathBuf>>
|
||||||
|
{
|
||||||
let existing_nums = existing_works.iter()
|
let existing_nums = existing_works.iter()
|
||||||
.map(|x| x.rj_num.clone())
|
.map(|x| x.rj_num.clone())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -176,7 +180,7 @@ impl DLSiteSyncCommand {
|
|||||||
.file_name().unwrap()
|
.file_name().unwrap()
|
||||||
.to_str().unwrap()
|
.to_str().unwrap()
|
||||||
.to_string();
|
.to_string();
|
||||||
if !dlsite::is_valid_rj_number(&dir_name) && !existing_folders.contains(&dir_path_str) {
|
if !is_valid_rj_number(&dir_name) && !existing_folders.contains(&dir_path_str) {
|
||||||
println!(
|
println!(
|
||||||
"{} {}",
|
"{} {}",
|
||||||
style(dir_path.to_str().unwrap()).blue(),
|
style(dir_path.to_str().unwrap()).blue(),
|
||||||
@@ -192,3 +196,17 @@ impl DLSiteSyncCommand {
|
|||||||
Ok(works_list)
|
Ok(works_list)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_valid_rj_number(rj_num: &str) -> bool {
|
||||||
|
let len = rj_num.len();
|
||||||
|
if len != 8 && len != 10 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if !rj_num.starts_with("RJ") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if !rj_num.chars().skip(2).all(|c| c.is_numeric()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
@@ -4,11 +4,10 @@ use futures::FutureExt;
|
|||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
|
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||||
use tokio::task::JoinHandle;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) enum AppEvent {
|
pub(crate) enum AppEvent {
|
||||||
Error,
|
Error(String),
|
||||||
Tick,
|
Tick,
|
||||||
Raw(crossterm::event::Event),
|
Raw(crossterm::event::Event),
|
||||||
}
|
}
|
||||||
@@ -16,7 +15,6 @@ pub(crate) enum AppEvent {
|
|||||||
pub(crate) struct EventHandler {
|
pub(crate) struct EventHandler {
|
||||||
_tx: UnboundedSender<AppEvent>,
|
_tx: UnboundedSender<AppEvent>,
|
||||||
rx: UnboundedReceiver<AppEvent>,
|
rx: UnboundedReceiver<AppEvent>,
|
||||||
pub task: JoinHandle<()>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventHandler {
|
impl EventHandler {
|
||||||
@@ -33,8 +31,8 @@ impl EventHandler {
|
|||||||
let crossterm_event = event_reader.next().fuse();
|
let crossterm_event = event_reader.next().fuse();
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
maybe_event = crossterm_event => {
|
maybe_event = crossterm_event => {
|
||||||
if let Some(Err(_)) = maybe_event {
|
if let Some(Err(e)) = maybe_event {
|
||||||
tx.send(AppEvent::Error).unwrap()
|
tx.send(AppEvent::Error(e.to_string())).unwrap()
|
||||||
} else if let Some(Ok(event)) = maybe_event {
|
} else if let Some(Ok(event)) = maybe_event {
|
||||||
tx.send(AppEvent::Raw(event)).unwrap();
|
tx.send(AppEvent::Raw(event)).unwrap();
|
||||||
}
|
}
|
||||||
@@ -45,7 +43,7 @@ impl EventHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Self { _tx, rx, task }
|
Self { _tx, rx }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn next(&mut self) -> Result<AppEvent> {
|
pub(crate) async fn next(&mut self) -> Result<AppEvent> {
|
||||||
@@ -1,13 +1,9 @@
|
|||||||
pub mod db;
|
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use color_eyre::eyre::eyre;
|
use color_eyre::eyre::eyre;
|
||||||
use color_eyre::owo_colors::OwoColorize;
|
use color_eyre::owo_colors::OwoColorize;
|
||||||
use language_tags::LanguageTag;
|
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
use crate::constants::{APP_CONFIG_DIR, APP_DATA_DIR, APP_DB_DATA_DIR};
|
use crawler::DLSITE_IMG_FOLDER;
|
||||||
use crate::crawler::DLSITE_IMG_FOLDER;
|
use models::{APP_CONFIG_DIR, APP_DATA_DIR};
|
||||||
|
|
||||||
|
|
||||||
pub async fn initialize_folders() -> color_eyre::Result<()> {
|
pub async fn initialize_folders() -> color_eyre::Result<()> {
|
||||||
if !APP_CONFIG_DIR.exists() {
|
if !APP_CONFIG_DIR.exists() {
|
||||||
@@ -19,9 +15,6 @@ pub async fn initialize_folders() -> color_eyre::Result<()> {
|
|||||||
if !DLSITE_IMG_FOLDER.exists() {
|
if !DLSITE_IMG_FOLDER.exists() {
|
||||||
fs::create_dir_all(DLSITE_IMG_FOLDER.as_path()).await?;
|
fs::create_dir_all(DLSITE_IMG_FOLDER.as_path()).await?;
|
||||||
}
|
}
|
||||||
if !APP_DB_DATA_DIR.exists() {
|
|
||||||
fs::create_dir_all(APP_DB_DATA_DIR.as_path()).await?;
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +33,3 @@ pub async fn get_all_folders(paths: &[&Path]) -> color_eyre::Result<Vec<PathBuf>
|
|||||||
}
|
}
|
||||||
Ok(folders)
|
Ok(folders)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn matches_primary_language(left: &LanguageTag, right: &LanguageTag) -> bool {
|
|
||||||
left.primary_language() == right.primary_language()
|
|
||||||
}
|
|
||||||
8
ui/src/lib.rs
Executable file
8
ui/src/lib.rs
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
mod app;
|
||||||
|
mod cli;
|
||||||
|
mod event;
|
||||||
|
mod helpers;
|
||||||
|
mod models;
|
||||||
|
mod widgets;
|
||||||
|
|
||||||
|
pub use cli::Cli;
|
||||||
33
ui/src/models/game_list.rs
Executable file
33
ui/src/models/game_list.rs
Executable file
@@ -0,0 +1,33 @@
|
|||||||
|
use color_eyre::Result;
|
||||||
|
use ratatui::widgets::ListState;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use db::types::RocksColumn;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub(crate) struct GameList<T> {
|
||||||
|
pub games: Vec<T>,
|
||||||
|
pub state: ListState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Default for GameList<T> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
games: Vec::new(),
|
||||||
|
state: ListState::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> GameList<T>
|
||||||
|
where T: DeserializeOwned + RocksColumn
|
||||||
|
{
|
||||||
|
pub fn new(games: Vec<T>) -> Result<Self> {
|
||||||
|
let mut state = ListState::default();
|
||||||
|
state.select_first();
|
||||||
|
let game_list = GameList {
|
||||||
|
games,
|
||||||
|
state
|
||||||
|
};
|
||||||
|
Ok(game_list)
|
||||||
|
}
|
||||||
|
}
|
||||||
2
ui/src/models/mod.rs
Executable file
2
ui/src/models/mod.rs
Executable file
@@ -0,0 +1,2 @@
|
|||||||
|
mod game_list;
|
||||||
|
pub use game_list::*;
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
use ratatui::widgets::StatefulWidget;
|
|
||||||
|
|
||||||
pub mod folder;
|
pub mod folder;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub enum AppPopup {
|
pub enum AppPopup {
|
||||||
AddFolder(folder::AddFolderPopup)
|
AddFolder(folder::AddFolderPopup)
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
use crate::config::types::ApplicationConfig;
|
|
||||||
use crate::widgets::popups::folder::AddFolderPopup;
|
use crate::widgets::popups::folder::AddFolderPopup;
|
||||||
use crossterm::event::KeyCode::Char;
|
use crossterm::event::KeyCode::Char;
|
||||||
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind};
|
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind};
|
||||||
@@ -9,14 +8,20 @@ use ratatui::prelude::{Color, Line, Span, Style, Text, Widget};
|
|||||||
use ratatui::style::Modifier;
|
use ratatui::style::Modifier;
|
||||||
use ratatui::style::palette::tailwind::SLATE;
|
use ratatui::style::palette::tailwind::SLATE;
|
||||||
use ratatui::widgets::{Block, Borders, HighlightSpacing, List, Paragraph, StatefulWidget};
|
use ratatui::widgets::{Block, Borders, HighlightSpacing, List, Paragraph, StatefulWidget};
|
||||||
use crate::models::{DLSiteManiax, GameList};
|
use db::RocksDBFactory;
|
||||||
|
use models::config::ApplicationConfig;
|
||||||
|
use models::dlsite::DLSiteManiax;
|
||||||
|
use crate::models::GameList;
|
||||||
use crate::widgets::popups::AppPopup;
|
use crate::widgets::popups::AppPopup;
|
||||||
use crate::widgets::views::View;
|
use crate::widgets::views::View;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct MainView {
|
pub struct MainView {
|
||||||
pub state: MainViewState
|
pub state: MainViewState,
|
||||||
|
db_factory: RocksDBFactory
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct MainViewState {
|
pub struct MainViewState {
|
||||||
popup: Option<AppPopup>,
|
popup: Option<AppPopup>,
|
||||||
status: Status,
|
status: Status,
|
||||||
@@ -32,15 +37,18 @@ enum Status {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MainView {
|
impl MainView {
|
||||||
pub fn new() -> color_eyre::Result<Self> {
|
pub fn new(mut db_factory: RocksDBFactory) -> color_eyre::Result<Self> {
|
||||||
let dl_game_list = GameList::new()?;
|
let db = db_factory.get_current_context()?;
|
||||||
|
let games = db.get_all_values::<DLSiteManiax>()?;
|
||||||
|
let dl_game_list = GameList::new(games)?;
|
||||||
let view = Self {
|
let view = Self {
|
||||||
state: MainViewState {
|
state: MainViewState {
|
||||||
popup: None,
|
popup: None,
|
||||||
status: Status::Running,
|
status: Status::Running,
|
||||||
list_page_size: 0,
|
list_page_size: 0,
|
||||||
dl_game_list
|
dl_game_list,
|
||||||
}
|
},
|
||||||
|
db_factory
|
||||||
};
|
};
|
||||||
Ok(view)
|
Ok(view)
|
||||||
}
|
}
|
||||||
@@ -189,10 +197,12 @@ impl MainView {
|
|||||||
let chunks = Layout::default()
|
let chunks = Layout::default()
|
||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Horizontal)
|
||||||
.constraints([
|
.constraints([
|
||||||
Constraint::Length(13),
|
Constraint::Length(14),
|
||||||
|
Constraint::Fill(0),
|
||||||
])
|
])
|
||||||
.split(area);
|
.split(area);
|
||||||
Self::render_game_list(chunks[0], buf, state);
|
Self::render_game_list(chunks[0], buf, state);
|
||||||
|
Self::render_game_box(chunks[1], buf, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_game_list(area: Rect, buf: &mut Buffer, state: &mut MainViewState) {
|
fn render_game_list(area: Rect, buf: &mut Buffer, state: &mut MainViewState) {
|
||||||
@@ -208,6 +218,14 @@ impl MainView {
|
|||||||
StatefulWidget::render(game_list, area, buf, &mut state.dl_game_list.state);
|
StatefulWidget::render(game_list, area, buf, &mut state.dl_game_list.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_game_box(area: Rect, buf: &mut Buffer, state: &mut MainViewState) {
|
||||||
|
let block = Block::new()
|
||||||
|
.title(Line::raw("Info"))
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.style(Style::default());
|
||||||
|
block.render(area, buf);
|
||||||
|
}
|
||||||
|
|
||||||
fn render_header(area: Rect, buf: &mut Buffer) {
|
fn render_header(area: Rect, buf: &mut Buffer) {
|
||||||
let title = Paragraph::new(Text::styled(
|
let title = Paragraph::new(Text::styled(
|
||||||
"SuS Manager",
|
"SuS Manager",
|
||||||
@@ -9,6 +9,7 @@ pub trait View: HasScreenCursor {
|
|||||||
fn is_running(&self) -> bool;
|
fn is_running(&self) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub enum AppView {
|
pub enum AppView {
|
||||||
Main(MainView),
|
Main(MainView),
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user