/* 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 infojson; pub mod tmdb; use crate::{make_ident, ok_or_warn, Action}; use anyhow::Context; use infojson::YVideo; use jellycommon::{ AssetLocation, LocalTrack, MediaInfo, MediaSource, Node, NodeKind, NodePrivate, NodePublic, Rating, }; 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, 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::() ); } let res_index = if results.results.len() > 1 { stdin() .lines() .next() .unwrap() .unwrap() .parse::() .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::(ij).context("parsing info.json")?); } kind = if video { NodeKind::Video } else { NodeKind::Movie }; } let title = 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()), import: None, 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, index: None, kind, children: Vec::new(), media: file_meta.as_ref().map(|m| MediaInfo { 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!(), } }