aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2025-02-05 16:23:09 +0100
committermetamuffin <metamuffin@disroot.org>2025-02-05 16:23:09 +0100
commit1c70f3d967b79cc4d9a8ee645921c53e95b096b1 (patch)
tree08d9ef037f85f95270505f23538d7072407331c2
parentd84a306e86316b845b7fb8dfc971d9a577f00f22 (diff)
downloadjellything-1c70f3d967b79cc4d9a8ee645921c53e95b096b1.tar
jellything-1c70f3d967b79cc4d9a8ee645921c53e95b096b1.tar.bz2
jellything-1c70f3d967b79cc4d9a8ee645921c53e95b096b1.tar.zst
generic node flags and show meta import
-rw-r--r--import/src/lib.rs249
-rw-r--r--tool/src/add.rs103
2 files changed, 225 insertions, 127 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 <metamuffin.org>
*/
-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::<PeopleGroup, Vec<Appearance>>::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::<PeopleGroup, Vec<Appearance>>::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<usize> = 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::<Vec<_>>();
- 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);
+ 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()
+ );
- 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 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?;
+ if Confirm::with_theme(&theme)
+ .with_prompt(format!("Rename {media:?} -> {newpath:?}?"))
+ .default(true)
+ .interact()
+ .unwrap()
+ {
+ rename(media, newpath).await?;
+ }
}
Ok(())