//! 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, output: Option, count: usize, subfolder: String, config: Option, } 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 { 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, 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 = 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 } }