Rewrite Curator in Rust with an egui GUI
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user