/* 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 */ pub mod tmdb; use anyhow::Context; use clap::{Parser, Subcommand}; use jellycommon::{AssetLocation, MediaInfo, MediaSource, Node, NodeKind, NodePrivate, NodePublic}; use jellymatroska::read::EbmlReader; use jellyremuxer::import::{import_metadata, seek_index::import_seek_index}; use log::info; use std::{ collections::BTreeMap, fs::{remove_file, File}, io::stdin, path::PathBuf, process::exit, }; use tmdb::{tmdb_details, tmdb_image, tmdb_search}; #[derive(Parser)] struct Args { #[arg(short = 'N', long)] dry: bool, #[clap(subcommand)] action: Action, } #[derive(Subcommand)] enum Action { Create { path: PathBuf, #[arg(short = 't', long)] tmdb_search: Option, #[arg(short = 'T', long)] tmdb_id: Option, #[arg(long)] copy: bool, #[arg(long)] r#move: bool, #[arg(short, long)] input: Option, #[arg(short, long)] series: bool, }, 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, tmdb_id, tmdb_search, input, series, copy, r#move, } => { let tmdb_kind = if series { "tv" } else { "movie" }; let tmdb_key = std::env::var("TMDB_API_KEY").context("tmdb api key required")?; let tmdb_id = if let Some(id) = tmdb_id { Some(id.parse().unwrap()) } else { let title = tmdb_search.as_ref().unwrap(); let results = crate::tmdb_search(tmdb_kind, title, &tmdb_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 }; Some(results.results[res_index].id) }; let tmdb_details = tmdb_id.map(|id| { let td = tmdb_details(tmdb_kind, id, &tmdb_key) .context("fetching details") .unwrap(); if td.title.is_some() { info!("is this correct? [y/n]"); if stdin().lines().next().unwrap().unwrap() != "y" { exit(0) } } td }); let title = tmdb_details .as_ref() .map(|d| d.title.clone().or(d.name.clone())) .flatten() .unwrap(); let ident = make_ident(&title); let path = path.join(&ident); let poster = tmdb_details .as_ref() .map(|d| { d.poster_path .as_ref() .map(|p| { let pu = path.join("poster.jpeg"); let mut f = File::create(&pu)?; tmdb_image(&p, &mut f)?; Ok::<_, anyhow::Error>(pu) }) .transpose() }) .transpose()? .flatten(); let backdrop = tmdb_details .as_ref() .map(|d| { d.backdrop_path .as_ref() .map(|p| { let pu = path.join("backdrop.jpeg"); let mut f = File::create(&pu)?; tmdb_image(&p, &mut f)?; Ok::<_, anyhow::Error>(pu) }) .transpose() }) .transpose()? .flatten(); let kind; let media; let source; let mut seek_index = BTreeMap::new(); let mut source_path_e = None; if let Some(input) = input { let source_path = path.join(format!("source.mkv")); if r#move { std::fs::rename(&input, &source_path)?; } else if copy { std::fs::copy(&input, &source_path)?; } else { if source_path.is_symlink() { remove_file(&source_path)?; } std::os::unix::fs::symlink(&input, &source_path)?; } let (tracks, local_tracks, duration) = { let input = File::open(&source_path).unwrap(); let mut input = EbmlReader::new(input); import_metadata(&source_path.to_path_buf(), &mut input)? }; seek_index = { let input = File::open(&source_path).unwrap(); let mut input = EbmlReader::new(input); import_seek_index(&mut input)? }; kind = NodeKind::Movie; media = Some(MediaInfo { duration, tracks }); source = Some(MediaSource::Local { tracks: local_tracks, }); source_path_e = Some(source_path) } else { kind = NodeKind::Series; media = None; source = None; }; let node = Node { private: NodePrivate { import: None, backdrop: backdrop.clone().map(AssetLocation::Library), poster: poster.clone().map(AssetLocation::Library), source, }, public: NodePublic { parent: None, federated: None, description: tmdb_details.as_ref().map(|d| d.overview.to_owned()), tagline: tmdb_details .as_ref() .map(|d| d.tagline.to_owned()) .flatten(), title, index: None, kind, children: Vec::new(), media, }, }; if args.dry { println!("{node:?}") } else { std::fs::create_dir_all(&path)?; 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_e .as_ref() .unwrap() .with_extension(&format!("si.{tn}")), )?, bincode::config::standard(), )?; } 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::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 }