Rewrite Curator in Rust with an egui GUI

This commit is contained in:
2026-10-17 11:28:17 +02:00
parent 364b515e3c
commit 65b5b74c9f
156 changed files with 24359 additions and 25795 deletions
+159
View File
@@ -0,0 +1,159 @@
//! Standalone "Tipy dne" generator — deployable next to the miniDLNA output
//! (e.g. a systemd timer). Each run reads the pool index, picks N random
//! movies, empties the tips folder and refills it with hardlinks.
//!
//! ```text
//! tipy-dne --pool /mnt/srv/Filmoteka --output /mnt/srv/Filmy --count 15
//! tipy-dne --count 15 # pool/output from the global config
//! ```
use std::path::PathBuf;
use std::process::ExitCode;
use curator::core::config::{global_config_path, load_global_config_from};
use curator::core::file::File;
use curator::core::file_manager::TIPS_SUBFOLDER;
use curator::core::hardlink_manager::HardlinkManager;
use curator::core::pool_index::INDEX_FILENAME;
const DEFAULT_COUNT: usize = 15;
struct Args {
pool: Option<PathBuf>,
output: Option<PathBuf>,
count: usize,
subfolder: String,
config: Option<PathBuf>,
}
fn usage() -> String {
format!(
"Vygeneruje složku Tipy dne.\n\n\
Použití: tipy-dne [--pool DIR] [--output DIR] [--count N] [--subfolder NAME] [--config FILE]\n\n\
--pool pool (obsahuje {INDEX_FILENAME})\n\
--output výstupní složka miniDLNA\n\
--count počet náhodných titulů (výchozí {DEFAULT_COUNT})\n\
--subfolder název složky tipů (výchozí \"{TIPS_SUBFOLDER}\")\n\
--config global config pro doplnění cest (výchozí: config Curatoru)"
)
}
fn parse_args() -> Result<Args, String> {
let mut args = Args {
pool: None,
output: None,
count: DEFAULT_COUNT,
subfolder: TIPS_SUBFOLDER.into(),
config: None,
};
let mut it = std::env::args().skip(1);
while let Some(arg) = it.next() {
let mut value = || it.next().ok_or_else(|| format!("{arg} potřebuje hodnotu"));
match arg.as_str() {
"--pool" => args.pool = Some(value()?.into()),
"--output" => args.output = Some(value()?.into()),
"--count" => {
args.count = value()?
.parse()
.map_err(|_| "--count musí být číslo".to_string())?
}
"--subfolder" => args.subfolder = value()?,
"--config" => args.config = Some(value()?.into()),
"-h" | "--help" => return Err(usage()),
other => return Err(format!("Neznámý argument: {other}\n\n{}", usage())),
}
}
Ok(args)
}
/// Pool movies listed in the index (v2: every media file of every record;
/// v1: the keys themselves are pool-relative paths). Missing files are skipped.
fn pool_movies(pool: &std::path::Path) -> Result<Vec<File>, String> {
let text = std::fs::read_to_string(pool.join(INDEX_FILENAME))
.map_err(|e| format!("Index nelze přečíst: {e}"))?;
let data: serde_json::Value =
serde_json::from_str(&text).map_err(|e| format!("Index je poškozený: {e}"))?;
let Some(movies) = data.get("movies").and_then(|m| m.as_object()) else {
return Ok(Vec::new());
};
let mut out = Vec::new();
for (key, record) in movies {
let title = record
.get("title")
.and_then(|t| t.as_str())
.filter(|t| !t.is_empty())
.map(String::from);
let files: Vec<String> = match record.get("media").and_then(|m| m.as_array()) {
Some(media) => media
.iter()
.filter_map(|m| m.get("file").and_then(|f| f.as_str()).map(String::from))
.take(1)
.collect(),
None => vec![key.clone()],
};
for rel in files {
let path = curator::core::paths::join_posix(pool, &rel);
if path.is_file() {
let mut f = File::bare(&path);
f.title = title.clone();
out.push(f);
}
}
}
Ok(out)
}
fn main() -> ExitCode {
let mut args = match parse_args() {
Ok(a) => a,
Err(msg) => {
eprintln!("{msg}");
return ExitCode::from(2);
}
};
if args.pool.is_none() || args.output.is_none() {
let cfg = load_global_config_from(&args.config.clone().unwrap_or_else(global_config_path));
args.pool = args.pool.or(cfg.pool_dir.map(PathBuf::from));
args.output = args.output.or(cfg.filmoteka_dir.map(PathBuf::from));
}
let (Some(pool), Some(output)) = (args.pool, args.output) else {
eprintln!(
"Chybí --pool a/nebo --output (a nenalezeny v global configu).\n\n{}",
usage()
);
return ExitCode::from(2);
};
if !pool.join(INDEX_FILENAME).is_file() {
eprintln!("Index nenalezen: {}", pool.join(INDEX_FILENAME).display());
return ExitCode::from(2);
}
let movies = match pool_movies(&pool) {
Ok(m) => m,
Err(e) => {
eprintln!("{e}");
return ExitCode::FAILURE;
}
};
if movies.is_empty() {
eprintln!("Index neobsahuje žádné filmy.");
return ExitCode::FAILURE;
}
let mut manager = HardlinkManager::new(&output);
let (created, failed) =
manager.generate_random_tips(&movies, args.count, &args.subfolder, false);
for link in &manager.created_links {
println!(" + {}", curator::core::paths::name(link));
}
for (src, err) in &manager.errors {
eprintln!(" ! {}: {err}", curator::core::paths::name(src));
}
println!(
"\nTipy dne: vytvořeno {created}, selhalo {failed} (z {} filmů)",
movies.len()
);
if created > 0 {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
}
}