aboutsummaryrefslogtreecommitdiff
path: root/import/src/mod.rs
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2023-12-22 09:02:46 +0100
committermetamuffin <metamuffin@disroot.org>2023-12-22 09:02:46 +0100
commitb92983fb0cab2a284301b930d2b15ec0109dd93e (patch)
treef7fb1b900b3fa5ced46de392a47756c3ca5cc398 /import/src/mod.rs
parent9a52852f736692e5319da49478e16bfba30fbd39 (diff)
parent826c61c9612e855b19c3adb0e93d80bbfb4dc903 (diff)
downloadjellything-b92983fb0cab2a284301b930d2b15ec0109dd93e.tar
jellything-b92983fb0cab2a284301b930d2b15ec0109dd93e.tar.bz2
jellything-b92983fb0cab2a284301b930d2b15ec0109dd93e.tar.zst
Merge branch 'master' of codeberg.org:metamuffin/jellything
Diffstat (limited to 'import/src/mod.rs')
-rw-r--r--import/src/mod.rs319
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!(),
+ }
+}