/* 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::{CommmonInfo, DirectoryInfo, ItemInfo}; use jellymatroska::read::EbmlReader; use jellyremuxer::import::import_read; use jellytools::tmdb::{tmdb_details, tmdb_image, tmdb_search}; use log::{info, warn}; use std::{ fs::File, io::{stdin, Write}, path::{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, }, Episode { path: PathBuf, index: usize, title: String, input: PathBuf, }, 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.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()?; let common = CommmonInfo { poster, backdrop, description: Some(details.overview), tagline: details.tagline, title: details.title.clone().or(details.name.clone()).unwrap(), index: None, }; info!("is this correct? [y/n]"); if stdin().lines().next().unwrap().unwrap() != "y" { exit(0) } let k = if let Some(input) = input { let mut iteminfo = ItemInfo { common, duration: Default::default(), tracks: Default::default(), }; info!("{iteminfo:#?}"); 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)?; import_source(&mut iteminfo, &source_path)?; serde_json::to_string_pretty(&iteminfo)? } else { serde_json::to_string_pretty(&DirectoryInfo { common, kind: jellycommon::DirectoryKind::Series, })? }; if args.dry { println!("{k}") } else { let mut f = File::create(path.join(if series { "directory.json".to_string() } else { format!("{ident}.jelly",) }))?; f.write_all(k.as_bytes())?; } Ok(()) } Action::Episode { path, index, title, input, } => { let ident = make_ident(&title); let common = CommmonInfo { poster: None, backdrop: None, description: None, tagline: None, title, index: Some(index), }; let mut iteminfo = ItemInfo { common, duration: Default::default(), tracks: Default::default(), }; let path = path.join(&ident); std::fs::create_dir_all(&path)?; 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)?; import_source(&mut iteminfo, &source_path)?; let k = serde_json::to_string_pretty(&iteminfo)?; if args.dry { println!("{k}") } else { let mut f = File::create(path.join(format!("{ident}.jelly",)))?; f.write_all(k.as_bytes())?; } Ok(()) } Action::Set { poster, clear_inputs, description, tagline, input, item, title, } => { 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 } fn import_source(iteminfo: &mut ItemInfo, source_path: &Path) -> anyhow::Result<()> { let input = File::open(&source_path).unwrap(); let mut input = EbmlReader::new(input); import_read(&source_path.to_path_buf(), &mut input, iteminfo)?; Ok(()) }