From 1c70f3d967b79cc4d9a8ee645921c53e95b096b1 Mon Sep 17 00:00:00 2001 From: metamuffin Date: Wed, 5 Feb 2025 16:23:09 +0100 Subject: generic node flags and show meta import --- import/src/lib.rs | 249 +++++++++++++++++++++++++++++++++++------------------- tool/src/add.rs | 105 ++++++++++++++--------- 2 files changed, 226 insertions(+), 128 deletions(-) diff --git a/import/src/lib.rs b/import/src/lib.rs index 125b20b..eee7a42 100644 --- a/import/src/lib.rs +++ b/import/src/lib.rs @@ -3,7 +3,7 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin */ -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, bail, Context, Result}; use infojson::YVideo; use jellybase::{ assetfed::AssetInner, @@ -15,7 +15,7 @@ use jellybase::{ CONF, SECRETS, }; use jellyclient::{Appearance, PeopleGroup, TmdbKind, TraktKind, Visibility}; -use log::{info, warn}; +use log::info; use matroska::matroska_metadata; use rayon::iter::{ParallelBridge, ParallelIterator}; use std::{ @@ -114,12 +114,13 @@ fn import_traverse( }; let id = NodeID::from_slug(&slug); + // Some flags need to applied immediatly because they are inherited if let Ok(content) = read_to_string(path.join("flags")) { for flag in content.lines() { match flag.trim() { "hidden" => visibility = visibility.min(Visibility::Hidden), "reduced" => visibility = visibility.min(Visibility::Reduced), - _ => warn!("unknown flag {flag:?}"), + _ => (), } } } @@ -213,6 +214,12 @@ fn import_file( Ok(()) })?; } + "flags" => { + let content = read_to_string(path)?; + for flag in content.lines() { + apply_node_flag(db, rthandle, apis, parent, flag.trim())?; + } + } "children" => { info!("import children at {path:?}"); for line in read_to_string(path)?.lines() { @@ -295,67 +302,8 @@ fn import_media_file( .to_string_lossy() .to_string(); - let mut backdrop = None; - let mut poster = None; - let mut trakt_data = None; - let mut tmdb_data = None; - let mut filename_toks = filename.split("."); let filepath_stem = filename_toks.next().unwrap(); - for tok in filename_toks { - if let Some(trakt_id) = tok.strip_prefix("trakt-") { - let trakt_id: u64 = trakt_id.parse().context("parse trakt id")?; - if let (Some(trakt), Some(tmdb)) = (&apis.trakt, &apis.tmdb) { - let data = rthandle - .block_on(trakt.lookup(TraktKind::Movie, trakt_id, true)) - .context("trakt lookup")?; - let people = rthandle - .block_on(trakt.people(TraktKind::Movie, trakt_id, true)) - .context("trakt people lookup")?; - - let mut people_map = BTreeMap::>::new(); - for p in people.cast.iter() { - people_map.entry(PeopleGroup::Cast).or_default().push(p.a()) - } - for (group, people) in people.crew.iter() { - for p in people { - people_map.entry(group.a()).or_default().push(p.a()) - } - } - - if let Some(tmdb_id) = data.ids.tmdb { - let data = rthandle - .block_on(tmdb.details(TmdbKind::Movie, tmdb_id)) - .context("tmdb details")?; - tmdb_data = Some(data.clone()); - - if let Some(path) = &data.backdrop_path { - let im = rthandle - .block_on(tmdb.image(path)) - .context("tmdb backdrop image")?; - backdrop = Some(AssetInner::Cache(im).ser()); - } - if let Some(path) = &data.poster_path { - let im = rthandle - .block_on(tmdb.image(path)) - .context("tmdb poster image")?; - poster = Some(AssetInner::Cache(im).ser()); - } - - for p in people_map.values_mut().flatten() { - if let Some(id) = p.person.ids.tmdb { - let k = rthandle.block_on(tmdb.person_image(id))?; - if let Some(prof) = k.profiles.first() { - let im = rthandle.block_on(tmdb.image(&prof.file_path))?; - p.person.headshot = Some(AssetInner::Cache(im).ser()); - } - } - } - } - trakt_data = Some((data.clone(), people_map)); - } - } - } let slug = m .infojson @@ -363,12 +311,13 @@ fn import_media_file( .map(|ij| format!("youtube-{}", ij.id)) .unwrap_or(make_kebab(&filepath_stem)); - db.update_node_init(NodeID::from_slug(&slug), |node| { + let node = NodeID::from_slug(&slug); + + db.update_node_init(node, |node| { node.slug = slug; node.title = info.title; node.visibility = visibility; - node.poster = m.cover.clone().or(poster); - node.backdrop = backdrop; + node.poster = m.cover.clone(); node.description = tags.remove("DESCRIPTION").or(tags.remove("SYNOPSIS")); node.tagline = tags.remove("COMMENT"); node.parents.insert(parent); @@ -381,30 +330,6 @@ fn import_media_file( } } - if let Some(data) = tmdb_data { - node.title = data.title.clone(); - node.tagline = data.tagline.clone(); - node.description = Some(data.overview.clone()); - node.ratings.insert(Rating::Tmdb, data.vote_average); - if let Some(date) = data.release_date.clone() { - node.release_date = tmdb::parse_release_date(&date)?; - } - } - if let Some((data, people)) = trakt_data { - node.title = Some(data.title.clone()); - node.kind = NodeKind::Movie; // TODO - if let Some(overview) = &data.overview { - node.description = Some(overview.clone()) - } - if let Some(tagline) = &data.tagline { - node.tagline = Some(tagline.clone()) - } - if let Some(rating) = &data.rating { - node.ratings.insert(Rating::Trakt, *rating); - } - node.people.extend(people); - } - let tracks = tracks .entries .into_iter() @@ -515,6 +440,152 @@ fn import_media_file( Ok(()) })?; + for tok in filename_toks { + apply_node_flag(db, rthandle, apis, node, tok)?; + } + + Ok(()) +} + +fn apply_node_flag( + db: &Database, + rthandle: &Handle, + apis: &Apis, + node: NodeID, + flag: &str, +) -> Result<()> { + if let Some(value) = flag.strip_prefix("trakt-").or(flag.strip_prefix("trakt=")) { + let (kind, id) = value.split_once(":").unwrap_or(("", value)); + let kind = match kind { + "movie" | "" => TraktKind::Movie, + "show" => TraktKind::Show, + "season" => TraktKind::Season, + _ => bail!("unknown trakt kind"), + }; + apply_trakt_tmdb(db, rthandle, apis, node, kind, id)?; + } + if flag == "hidden" { + db.update_node_init(node, |node| { + node.visibility = node.visibility.min(Visibility::Hidden); + Ok(()) + })?; + } + if flag == "reduced" { + db.update_node_init(node, |node| { + node.visibility = node.visibility.min(Visibility::Reduced); + Ok(()) + })?; + } + Ok(()) +} + +fn apply_trakt_tmdb( + db: &Database, + rthandle: &Handle, + apis: &Apis, + node: NodeID, + trakt_kind: TraktKind, + trakt_id: &str, +) -> Result<()> { + let trakt_id: u64 = trakt_id.parse().context("parse trakt id")?; + if let (Some(trakt), Some(tmdb)) = (&apis.trakt, &apis.tmdb) { + let data = rthandle + .block_on(trakt.lookup(trakt_kind, trakt_id, true)) + .context("trakt lookup")?; + let people = rthandle + .block_on(trakt.people(trakt_kind, trakt_id, true)) + .context("trakt people lookup")?; + + let mut people_map = BTreeMap::>::new(); + for p in people.cast.iter() { + people_map.entry(PeopleGroup::Cast).or_default().push(p.a()) + } + for (group, people) in people.crew.iter() { + for p in people { + people_map.entry(group.a()).or_default().push(p.a()) + } + } + + let mut tmdb_data = None; + let mut backdrop = None; + let mut poster = None; + if let Some(tmdb_id) = data.ids.tmdb { + let data = rthandle + .block_on(tmdb.details( + match trakt_kind { + TraktKind::Movie => TmdbKind::Movie, + TraktKind::Show => TmdbKind::Tv, + _ => TmdbKind::Movie, + }, + tmdb_id, + )) + .context("tmdb details")?; + tmdb_data = Some(data.clone()); + + if let Some(path) = &data.backdrop_path { + let im = rthandle + .block_on(tmdb.image(path)) + .context("tmdb backdrop image")?; + backdrop = Some(AssetInner::Cache(im).ser()); + } + if let Some(path) = &data.poster_path { + let im = rthandle + .block_on(tmdb.image(path)) + .context("tmdb poster image")?; + poster = Some(AssetInner::Cache(im).ser()); + } + + for p in people_map.values_mut().flatten() { + if let Some(id) = p.person.ids.tmdb { + let k = rthandle.block_on(tmdb.person_image(id))?; + if let Some(prof) = k.profiles.first() { + let im = rthandle.block_on(tmdb.image(&prof.file_path))?; + p.person.headshot = Some(AssetInner::Cache(im).ser()); + } + } + } + } + + db.update_node_init(node, |node| { + node.title = Some(data.title.clone()); + node.people.extend(people_map); + node.kind = match trakt_kind { + TraktKind::Movie => NodeKind::Movie, + TraktKind::Show => NodeKind::Show, + TraktKind::Season => NodeKind::Season, + TraktKind::Episode => NodeKind::Episode, + TraktKind::Person => NodeKind::Channel, + TraktKind::User => NodeKind::Channel, + }; + if let Some(overview) = &data.overview { + node.description = Some(overview.clone()) + } + if let Some(tagline) = &data.tagline { + node.tagline = Some(tagline.clone()) + } + if let Some(rating) = &data.rating { + node.ratings.insert(Rating::Trakt, *rating); + } + if let Some(poster) = poster { + node.poster = Some(poster); + } + if let Some(backdrop) = backdrop { + node.backdrop = Some(backdrop); + } + if let Some(data) = tmdb_data { + node.title = data.title.clone().or(node.title.clone()); + node.tagline = data.tagline.clone().or(node.tagline.clone()); + node.description = Some(data.overview.clone()); + node.ratings.insert(Rating::Tmdb, data.vote_average); + if let Some(date) = data.release_date.clone() { + if let Ok(date) = tmdb::parse_release_date(&date) { + node.release_date = date; + } + } + } + Ok(()) + })?; + } Ok(()) } diff --git a/tool/src/add.rs b/tool/src/add.rs index 6e79381..06487c9 100644 --- a/tool/src/add.rs +++ b/tool/src/add.rs @@ -14,30 +14,21 @@ use std::{ fmt::Display, path::{Path, PathBuf}, }; -use tokio::fs::rename; +use tokio::{ + fs::{rename, OpenOptions}, + io::AsyncWriteExt, +}; pub async fn add(action: Action) -> anyhow::Result<()> { match action { Action::Add { media } => { let theme = ColorfulTheme::default(); - // let possible_kinds = [ - // TraktKind::Movie, - // TraktKind::Season, - // TraktKind::Show, - // TraktKind::Episode, - // ]; - // let trakt_kind: Vec = MultiSelect::with_theme(&theme) - // .with_prompt("Media Kind") - // .items(&possible_kinds) - // .defaults(&[true, false, false, false]) - // .interact() - // .unwrap(); - // let search_kinds = trakt_kind - // .iter() - // .map(|&i| possible_kinds[i]) - // .collect::>(); - let search_kinds = [TraktKind::Show, TraktKind::Season, TraktKind::Movie]; + let search_kinds = if media.is_dir() { + &[TraktKind::Show, TraktKind::Season] + } else { + &[TraktKind::Movie, TraktKind::Episode] + }; let (trakt_object, trakt_kind) = loop { let name: String = Input::with_theme(&theme) @@ -54,7 +45,7 @@ pub async fn add(action: Action) -> anyhow::Result<()> { .ok_or(anyhow!("no trakt api key configured"))?, ); - let results = trakt.search(&search_kinds, &name, false).await?; + let results = trakt.search(search_kinds, &name, false).await?; if results.is_empty() { warn!("no search results"); @@ -73,27 +64,63 @@ pub async fn add(action: Action) -> anyhow::Result<()> { } }; - assert_eq!(trakt_kind, TraktKind::Movie); - - let stem = media.file_name().unwrap().to_string_lossy().to_string(); - let stem = stem.split_once(".").unwrap_or((stem.as_str(), "")).0; - let mut newpath = media.parent().unwrap().join(format!( - "{stem}.trakt-{}.mkv", - trakt_object.ids.trakt.unwrap() - )); - let mut n = 1; - while newpath.exists() { - newpath = media.parent().unwrap().join(format!("{stem}.alt-{n}.mkv",)); - n += 1; - } + if media.is_dir() { + let flagspath = media.join("flags"); + let flag = format!( + "trakt={}:{}\n", + match trakt_kind { + TraktKind::Movie => "movie", + TraktKind::Show => "show", + TraktKind::Season => "season", + _ => unreachable!(), + }, + trakt_object.ids.trakt.unwrap() + ); - if Confirm::with_theme(&theme) - .with_prompt(format!("Rename {media:?} -> {newpath:?}?")) - .default(true) - .interact() - .unwrap() - { - rename(media, newpath).await?; + if Confirm::with_theme(&theme) + .with_prompt(format!("Append {flag:?} to {flagspath:?}?")) + .default(true) + .interact() + .unwrap() + { + OpenOptions::new() + .append(true) + .write(true) + .create(true) + .open(flagspath) + .await? + .write_all(flag.as_bytes()) + .await?; + } + } else { + let ext = media + .extension() + .map(|e| format!(".{}", e.to_string_lossy())) + .unwrap_or("mkv".to_string()); + + let stem = media.file_name().unwrap().to_string_lossy().to_string(); + let stem = stem.split_once(".").unwrap_or((stem.as_str(), "")).0; + let mut newpath = media.parent().unwrap().join(format!( + "{stem}.trakt-{}{ext}", + trakt_object.ids.trakt.unwrap() + )); + let mut n = 1; + while newpath.exists() { + newpath = media + .parent() + .unwrap() + .join(format!("{stem}.alt-{n}{ext}",)); + n += 1; + } + + if Confirm::with_theme(&theme) + .with_prompt(format!("Rename {media:?} -> {newpath:?}?")) + .default(true) + .interact() + .unwrap() + { + rename(media, newpath).await?; + } } Ok(()) -- cgit v1.2.3-70-g09d2