diff options
Diffstat (limited to 'import/src/mod.rs')
-rw-r--r-- | import/src/mod.rs | 319 |
1 files changed, 319 insertions, 0 deletions
diff --git a/import/src/mod.rs b/import/src/mod.rs new file mode 100644 index 0000000..0c43cde --- /dev/null +++ b/import/src/mod.rs @@ -0,0 +1,319 @@ +/* + 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 infojson; +pub mod tmdb; + +use crate::{make_ident, ok_or_warn, Action}; +use anyhow::Context; +use infojson::YVideo; +use jellycommon::{ + AssetLocation, LocalTrack, MediaInfo, Node, NodeKind, NodePrivate, NodePublic, Rating, + TrackSource, +}; +use jellymatroska::read::EbmlReader; +use jellyremuxer::import::import_metadata; +use log::{debug, info, warn}; +use std::{ + collections::BTreeMap, + fs::{remove_file, File}, + io::{stdin, BufReader, Write}, +}; +use tmdb::{tmdb_details, tmdb_image}; + +pub(crate) fn import(action: Action, dry: bool) -> anyhow::Result<()> { + match action { + Action::New { + path, + tmdb_id, + tmdb_search, + input, + series, + ident_prefix, + ignore_attachments, + copy, + video, + ignore_metadata, + r#move, + title, + skip_existing, + } => { + if std::env::current_dir().unwrap().file_name().unwrap() != "library" { + warn!("new command can only be used in the library directory; what you are doing right now probably wont work.") + } + + if skip_existing { + if let Some(input) = &input { + let guessed_path = path.join(input.file_stem().unwrap_or(input.as_os_str())); + if guessed_path.exists() { + info!("guessed output ({guessed_path:?}) exists, skipping import"); + return Ok(()); + } else { + debug!("guessed output ({guessed_path:?}) missing"); + } + } + } + + let tmdb_kind = if series { "tv" } else { "movie" }; + let tmdb_id = if let Some(id) = tmdb_id { + Some(id.parse().unwrap()) + } else if let Some(title) = tmdb_search { + let tmdb_key = std::env::var("TMDB_API_KEY").context("tmdb api key required")?; + let results = tmdb::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) + } else { + None + }; + + let tmdb_details = tmdb_id + .map(|id| { + let tmdb_key = + std::env::var("TMDB_API_KEY").context("tmdb api key required")?; + let td = tmdb_details(tmdb_kind, id, &tmdb_key) + .context("fetching details") + .unwrap(); + Ok::<_, anyhow::Error>(td) + }) + .transpose()?; + + let mut kind = NodeKind::Series; + let mut file_meta = None; + let mut infojson = None; + + if let Some(input_path) = &input { + file_meta = Some({ + let input = BufReader::new(File::open(&input_path).unwrap()); + let mut input = EbmlReader::new(input); + import_metadata(&mut input)? + }); + if ignore_attachments { + let file_meta = file_meta.as_mut().unwrap(); + file_meta.cover = None; + file_meta.infojson = None; + } + if ignore_metadata { + let file_meta = file_meta.as_mut().unwrap(); + file_meta.description = None; + file_meta.tagline = None; + file_meta.title = None; + } + + if let Some(ij) = &file_meta.as_ref().unwrap().infojson { + infojson = + Some(serde_json::from_str::<YVideo>(ij).context("parsing info.json")?); + } + + kind = if video { + NodeKind::Video + } else { + NodeKind::Movie + }; + } + + let title = title + .or(tmdb_details + .as_ref() + .map(|d| d.title.clone().or(d.name.clone())) + .flatten()) + .or(file_meta.as_ref().map(|m| m.title.clone()).flatten()) + .expect("no title detected"); + + let ident = format!( + "{}{}", + ident_prefix.unwrap_or(String::new()), + make_ident( + &infojson + .as_ref() + .map(|i| i.id.clone()) + .unwrap_or(title.clone()) + ), + ); + let path = path.join(&ident); + let source_path = input.as_ref().map(|_| path.join(format!("source.mkv"))); + + let (mut poster, mut backdrop) = (None, None); + if !dry { + std::fs::create_dir_all(&path)?; + + poster = file_meta + .as_ref() + .map(|m| { + m.cover + .as_ref() + .map(|(mime, data)| { + let pu = path.join(format!( + "cover.{}", + match mime.as_str() { + "image/webp" => "webp", + "image/jpeg" => "jpeg", + "image/png" => "png", + _ => { + warn!("unknown mime, just using webp"); + "webp" + } + } + )); + if !pu.exists() { + let mut f = File::create(&pu)?; + f.write_all(&data)?; + } + Ok::<_, anyhow::Error>(pu) + }) + .transpose() + }) + .transpose()? + .flatten() + .or(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()); + + 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 mut ratings = BTreeMap::new(); + + ratings.extend( + infojson + .as_ref() + .map(|i| (Rating::YoutubeViews, i.view_count as f64)), + ); + ratings.extend( + infojson + .as_ref() + .map(|i| i.like_count.map(|l| (Rating::YoutubeLikes, l as f64))) + .flatten(), + ); + ratings.extend( + tmdb_details + .as_ref() + .map(|d| (Rating::Tmdb, d.vote_average)), + ); + + let node = Node { + private: NodePrivate { + id: Some(ident.clone()), + backdrop: backdrop.clone().map(AssetLocation::Library), + poster: poster.clone().map(AssetLocation::Library), + source: file_meta.as_ref().map(|m| MediaSource::Local { + tracks: m + .track_sources + .clone() + .into_iter() + .map(|t| LocalTrack { + path: source_path.clone().unwrap(), + ..t + }) + .collect(), + }), + }, + public: NodePublic { + federated: None, + ratings, + description: file_meta + .as_ref() + .map(|m| m.description.clone()) + .flatten() + .or(tmdb_details.as_ref().map(|d| d.overview.to_owned())), + tagline: file_meta.as_ref().map(|m| m.tagline.clone()).flatten().or( + tmdb_details + .as_ref() + .map(|d| d.tagline.to_owned()) + .flatten(), + ), + title: Some(title), + index: None, + kind: Some(kind), + children: Vec::new(), + media: file_meta.as_ref().map(|m| MediaInfo { + chapters: m.chapters.clone(), + duration: m.duration, + tracks: m.tracks.clone(), + }), + release_date: tmdb_details + .as_ref() + .map(|d| tmdb::parse_release_date(&d.release_date.clone()?).ok()) + .flatten() + .or(infojson + .as_ref() + .and_then(|j| ok_or_warn(infojson::parse_upload_date(&j.upload_date)))), + ..Default::default() + }, + }; + + if dry { + println!("{}", serde_json::to_string_pretty(&node)?); + } else { + if let Some(source_path) = source_path { + let input = input.clone().unwrap(); + 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 f = File::create(path.join(if series { + "directory.json" + } else { + "item.jelly" + }))?; + serde_json::to_writer_pretty(f, &node)?; + } + + Ok(()) + } + _ => unreachable!(), + } +} |