/* This file is part of jellything (https://codeberg.org/metamuffin/jellything) which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2023 metamuffin */ use anyhow::Context; use clap::{Parser, Subcommand}; use jellycommon::{config::GlobalConfig, MediaInfo, Node, NodeKind, NodePrivate, NodePublic}; use jellymatroska::read::EbmlReader; use jellyremuxer::import::import_read; use jellytools::tmdb::{tmdb_details, tmdb_image, tmdb_search}; use log::info; use std::{fs::File, io::stdin, path::PathBuf, process::exit}; #[derive(Parser)] struct Args { #[arg(short = 'N', long)] dry: bool, #[clap(subcommand)] action: Action, } #[derive(Subcommand)] enum Action { Create { path: PathBuf, title: Option, #[arg(short = 'T', long)] tmdb: Option, #[arg(short, long)] input: Option, #[arg(short, long)] series: bool, }, Remote { config: PathBuf, host: String, #[arg(short, long)] remote_id: Option, id: String, }, Set { #[arg(short = 'I', long)] item: PathBuf, #[arg(short, long)] poster: Option, #[arg(short, long)] title: Option, #[arg(short = 'D', long)] tagline: Option, #[arg(short = 'd', long)] description: Option, #[arg(short = 'c', long)] clear_inputs: bool, #[arg(short = 'i', long, num_args(0..))] input: Vec, }, } fn main() -> anyhow::Result<()> { env_logger::builder() .filter_level(log::LevelFilter::Info) .parse_env("LOG") .init(); let args = Args::parse(); match args.action { Action::Create { path, title, tmdb: id, input, series, } => { assert!(series || input.is_some(), "series or input required"); let kind = if series { "tv" } else { "movie" }; let key = std::env::var("TMDB_API_KEY").context("tmdb api key required")?; let id = if let Some(id) = id { id.parse().unwrap() } else { let title = title.as_ref().unwrap(); let results = tmdb_search(kind, title, &key)?; info!("results:"); for (i, r) in results.results.iter().enumerate() { info!( "\t[{i}] {}: {} ({})", r.id, r.title.as_ref().or(r.name.as_ref()).unwrap(), r.overview.chars().take(100).collect::() ); } let res_index = if results.results.len() > 1 { stdin() .lines() .next() .unwrap() .unwrap() .parse::() .unwrap() } else { 0 }; results.results[res_index].id }; let details = tmdb_details(kind, id, &key).context("fetching details")?; let ident = make_ident(details.title.as_ref().or(details.name.as_ref()).unwrap()); let path = path.join(&ident); std::fs::create_dir_all(&path)?; let poster = details .poster_path .map(|p| { let pu = path.join("poster.jpeg"); let mut f = File::create(&pu)?; tmdb_image(&p, &mut f)?; Ok::<_, anyhow::Error>(pu) }) .transpose()?; let backdrop = details .backdrop_path .map(|p| { let pu = path.join("backdrop.jpeg"); let mut f = File::create(&pu)?; tmdb_image(&p, &mut f)?; Ok::<_, anyhow::Error>(pu) }) .transpose()?; if title.is_some() { info!("is this correct? [y/n]"); if stdin().lines().next().unwrap().unwrap() != "y" { exit(0) } } let kind; let media; let source; if let Some(input) = input { let source_path = path.join(format!("source.mkv")); // std::fs::rename(&input, &source_path)?; // std::os::unix::fs::symlink(&input, &source_path)?; std::fs::copy(&input, &source_path)?; let input = File::open(&source_path).unwrap(); let mut input = EbmlReader::new(input); let (tracks, local_tracks, seek_index) = import_read(&source_path.to_path_buf(), &mut input)?; for (tn, index) in seek_index { info!("writing index {tn} with {} blocks", index.blocks.len()); bincode::encode_into_std_write( index, &mut File::create(source_path.with_extension(&format!("si.{tn}")))?, bincode::config::standard(), )?; } kind = NodeKind::Movie; media = Some(MediaInfo { tracks, duration: 0., }); source = Some(jellycommon::MediaSource::Local { tracks: local_tracks, }); } else { kind = NodeKind::Series; media = None; source = None; }; let node = Node { private: NodePrivate { backdrop: backdrop.clone(), poster: poster.clone(), source, }, public: NodePublic { parent: None, description: Some(details.overview), tagline: details.tagline, title: details.title.clone().or(details.name.clone()).unwrap(), index: None, kind, children: Vec::new(), media, }, }; if args.dry { println!("{node:?}") } else { let f = File::create(path.join(if series { "directory.json".to_string() } else { format!("{ident}.jelly") }))?; serde_json::to_writer_pretty(f, &node)?; } Ok(()) } Action::Remote { host, config, remote_id, id, } => { let config: GlobalConfig = serde_json::from_reader(File::open(config).unwrap()).unwrap(); let (username, password, tls) = &config.remote_credentials[&id]; Ok(()) } Action::Set { .. } => { // let mut iteminfo: ItemInfo = match File::open(item.clone()) { // Ok(f) => serde_json::from_reader(f)?, // Err(e) => { // warn!("could not load item info: {e}"); // warn!("using the default instead"); // ItemInfo { // common: CommmonInfo { // poster: None, // backdrop: None, // tagline: None, // description: None, // title: item.to_str().unwrap().to_string(), // index: None, // }, // duration: 0.0, // tracks: Default::default(), // } // } // }; // if let Some(title) = title { // iteminfo.title = title; // } // if let Some(poster) = poster { // iteminfo.poster = Some(poster); // } // if let Some(d) = description { // iteminfo.description = Some(d); // } // if let Some(d) = tagline { // iteminfo.tagline = Some(d); // } // if clear_inputs { // iteminfo.tracks = Default::default() // } // // for input_path in input { // // let input = File::open(input_path.clone()).unwrap(); // // let mut input = EbmlReader::new(input); // // import_read(&input_path, &mut input, &mut iteminfo)?; // // } // let k = serde_json::to_string_pretty(&iteminfo)?; // if args.dry { // println!("{k}") // } else { // let mut f = File::create(item)?; // f.write_all(k.as_bytes())?; // } Ok(()) } } } fn make_ident(s: &str) -> String { let mut out = String::new(); for s in s.chars() { match s { 'a'..='z' => out.push(s), 'A'..='Z' => out.push(s.to_ascii_lowercase()), '-' | ' ' | '_' => out.push('-'), _ => (), } } out }