diff options
author | metamuffin <metamuffin@disroot.org> | 2023-08-03 20:45:31 +0200 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2023-08-03 20:45:31 +0200 |
commit | d52233a7a304b7dadda383128eaa42aea02b3b74 (patch) | |
tree | 254ab9e7ca527b16afaa6fa1a7ad76fef220f9f6 /import/src | |
parent | 5b71ccaf2bbe34f1d39d4f38f2b5c2090a9761b1 (diff) | |
download | jellything-d52233a7a304b7dadda383128eaa42aea02b3b74.tar jellything-d52233a7a304b7dadda383128eaa42aea02b3b74.tar.bz2 jellything-d52233a7a304b7dadda383128eaa42aea02b3b74.tar.zst |
rename tools crate
Diffstat (limited to 'import/src')
-rw-r--r-- | import/src/main.rs | 324 | ||||
-rw-r--r-- | import/src/tmdb.rs | 95 |
2 files changed, 419 insertions, 0 deletions
diff --git a/import/src/main.rs b/import/src/main.rs new file mode 100644 index 0000000..6cf2ef3 --- /dev/null +++ b/import/src/main.rs @@ -0,0 +1,324 @@ +/* + 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 <metamuffin.org> +*/ +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<String>, + #[arg(short = 'T', long)] + tmdb_id: Option<String>, + #[arg(long)] + copy: bool, + #[arg(long)] + r#move: bool, + #[arg(short, long)] + input: Option<PathBuf>, + #[arg(short, long)] + series: bool, + }, + Set { + #[arg(short = 'I', long)] + item: PathBuf, + #[arg(short, long)] + poster: Option<PathBuf>, + #[arg(short, long)] + title: Option<String>, + #[arg(short = 'D', long)] + tagline: Option<String>, + #[arg(short = 'd', long)] + description: Option<String>, + #[arg(short = 'c', long)] + clear_inputs: bool, + #[arg(short = 'i', long, num_args(0..))] + input: Vec<PathBuf>, + }, +} + +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::<String>() + ); + } + let res_index = if results.results.len() > 1 { + stdin() + .lines() + .next() + .unwrap() + .unwrap() + .parse::<usize>() + .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 +} diff --git a/import/src/tmdb.rs b/import/src/tmdb.rs new file mode 100644 index 0000000..5f21afd --- /dev/null +++ b/import/src/tmdb.rs @@ -0,0 +1,95 @@ +/* + 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 <metamuffin.org> +*/ +use log::info; +use serde::Deserialize; +use std::io::Write; + +#[derive(Debug, Clone, Deserialize)] +pub struct TmdbQuery { + pub page: usize, + pub results: Vec<TmdbQueryResult>, + pub total_pages: usize, + pub total_results: usize, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct TmdbQueryResult { + pub adult: bool, + pub backdrop_path: Option<String>, + pub genre_ids: Vec<u64>, + pub id: u64, + pub original_language: Option<String>, + pub original_title: Option<String>, + pub overview: String, + pub popularity: f64, + pub poster_path: Option<String>, + pub release_date: Option<String>, + pub title: Option<String>, + pub name: Option<String>, + pub vote_average: f64, + pub vote_count: usize, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct TmdbDetails { + pub adult: bool, + pub backdrop_path: Option<String>, + pub genres: Vec<TmdbGenre>, + pub id: u64, + pub original_language: Option<String>, + pub original_title: Option<String>, + pub overview: String, + pub popularity: f64, + pub poster_path: Option<String>, + pub release_date: Option<String>, + pub title: Option<String>, + pub name: Option<String>, + pub vote_average: f64, + pub vote_count: usize, + pub budget: Option<usize>, + pub homepage: Option<String>, + pub imdb_id: Option<String>, + pub production_companies: Vec<TmdbProductionCompany>, + pub revenue: Option<usize>, + pub tagline: Option<String>, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct TmdbGenre { + pub id: u64, + pub name: String, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct TmdbProductionCompany { + pub id: u64, + pub name: String, + pub logo_path: Option<String>, +} + +pub fn tmdb_search(kind: &str, query: &str, key: &str) -> anyhow::Result<TmdbQuery> { + info!("searching tmdb: {query:?}"); + Ok(reqwest::blocking::get(&format!( + "https://api.themoviedb.org/3/search/{kind}?query={}&api_key={key}", + query.replace(" ", "+") + ))? + .json::<TmdbQuery>()?) +} + +pub fn tmdb_details(kind: &str, id: u64, key: &str) -> anyhow::Result<TmdbDetails> { + info!("fetching details: {id:?}"); + Ok(reqwest::blocking::get(&format!( + "https://api.themoviedb.org/3/{kind}/{id}?api_key={key}" + ))? + .json()?) +} + +pub fn tmdb_image(path: &str, out: &mut impl Write) -> anyhow::Result<()> { + info!("downloading image {path:?}"); + let mut res = reqwest::blocking::get(&format!("https://image.tmdb.org/t/p/original{path}"))?; + res.copy_to(out)?; + Ok(()) +} |