diff options
| author | metamuffin <metamuffin@disroot.org> | 2025-12-11 01:20:17 +0100 |
|---|---|---|
| committer | metamuffin <metamuffin@disroot.org> | 2025-12-11 01:20:17 +0100 |
| commit | 6e5f6d9b9c6fedb4ab80190c156595d321d33bbf (patch) | |
| tree | b6c2140e744fc3018ad08975afefad40386ebbc6 /import/src | |
| parent | e4f865e9da9d6660399e22a6fbeb5b84a749b07a (diff) | |
| download | jellything-6e5f6d9b9c6fedb4ab80190c156595d321d33bbf.tar jellything-6e5f6d9b9c6fedb4ab80190c156595d321d33bbf.tar.bz2 jellything-6e5f6d9b9c6fedb4ab80190c156595d321d33bbf.tar.zst | |
refactor import plugins part 3
Diffstat (limited to 'import/src')
| -rw-r--r-- | import/src/lib.rs | 747 | ||||
| -rw-r--r-- | import/src/plugins/acoustid.rs | 3 | ||||
| -rw-r--r-- | import/src/plugins/misc.rs | 37 | ||||
| -rw-r--r-- | import/src/plugins/mod.rs | 20 | ||||
| -rw-r--r-- | import/src/plugins/trakt.rs | 2 | ||||
| -rw-r--r-- | import/src/reporting.rs | 2 |
6 files changed, 428 insertions, 383 deletions
diff --git a/import/src/lib.rs b/import/src/lib.rs index 8ad6790..561a5c9 100644 --- a/import/src/lib.rs +++ b/import/src/lib.rs @@ -8,35 +8,24 @@ pub mod plugins; pub mod reporting; -use crate::plugins::{ - acoustid::AcoustID, - infojson::is_info_json, - misc::is_cover, - musicbrainz::{self, MusicBrainz}, - tmdb::{self, Tmdb, TmdbKind}, - trakt::{Trakt, TraktKind}, - vgmdb::Vgmdb, - wikidata::Wikidata, - wikimedia_commons::WikimediaCommons, +use crate::{ + plugins::{ + ImportContext, ImportPlugin, infojson::is_info_json, init_plugins, misc::is_cover, + trakt::Trakt, + }, + reporting::IMPORT_PROGRESS, }; use anyhow::{Context, Result, anyhow}; use jellycache::{HashKey, cache_memory, cache_store}; -use jellycommon::{ - Appearance, Asset, CreditCategory, IdentifierType, NodeID, NodeKind, PictureSlot, RatingType, - Visibility, -}; +use jellycommon::{NodeID, Visibility}; use jellydb::Database; -use jellyimport_fallback_generator::generate_fallback; use jellyremuxer::{ demuxers::create_demuxer_autodetect, matroska::{self, AttachedFile, Segment}, }; -use log::info; use rayon::iter::{IntoParallelIterator, ParallelIterator}; -use regex::Regex; use serde::{Deserialize, Serialize}; use std::{ - collections::BTreeMap, fs::{File, read_to_string}, path::{Path, PathBuf}, sync::{Arc, LazyLock, Mutex}, @@ -79,19 +68,6 @@ pub const USER_AGENT: &str = concat!( static IMPORT_SEM: LazyLock<Semaphore> = LazyLock::new(|| Semaphore::new(1)); -static RE_EPISODE_FILENAME: LazyLock<Regex> = - LazyLock::new(|| Regex::new(r#"([sS](?<season>\d+))?([eE](?<episode>\d+))( (.+))?"#).unwrap()); - -struct Apis { - trakt: Option<Trakt>, - tmdb: Option<Tmdb>, - acoustid: Option<AcoustID>, - musicbrainz: MusicBrainz, - wikidata: Wikidata, - wikimedia_commons: WikimediaCommons, - vgmdb: Vgmdb, -} - pub fn is_importing() -> bool { IMPORT_SEM.available_permits() == 0 } @@ -120,73 +96,59 @@ pub async fn import_wrap(db: Database, incremental: bool) -> Result<()> { } fn import(db: &Database, incremental: bool) -> Result<()> { - let apis = Apis { - trakt: CONF.api.trakt.as_ref().map(|key| Trakt::new(key)), - tmdb: CONF.api.tmdb.as_ref().map(|key| Tmdb::new(key)), - acoustid: CONF.api.acoustid.as_ref().map(|key| AcoustID::new(key)), - musicbrainz: MusicBrainz::new(), - wikidata: Wikidata::new(), - wikimedia_commons: WikimediaCommons::new(), - vgmdb: Vgmdb::new(), - }; - - let rthandle = Handle::current(); + let plugins = init_plugins(&CONF.api); let mut files = Vec::new(); - import_traverse( &CONF.media_path, db, incremental, NodeID::MIN, - "", - InheritedFlags { - visibility: Visibility::Visible, - use_acoustid: false, - }, + InheritedFlags::default(), &mut files, )?; + let rt = Handle::current(); + files.into_par_iter().for_each(|(path, parent, iflags)| { - import_file(db, &apis, &rthandle, &path, parent, iflags); + import_file(db, &rt, &plugins, &path, parent, iflags); + IMPORT_PROGRESS + .blocking_write() + .as_mut() + .unwrap() + .finished_items += 1; }); - // let meta = path.metadata()?; - // let mtime = meta.modified()?.duration_since(UNIX_EPOCH)?.as_secs(); - // db.set_import_file_mtime(path, mtime)?; - Ok(()) } #[derive(Debug, Clone, Copy)] -struct InheritedFlags { +pub struct InheritedFlags { visibility: Visibility, use_acoustid: bool, } +impl Default for InheritedFlags { + fn default() -> Self { + Self { + visibility: Visibility::Visible, + use_acoustid: false, + } + } +} fn import_traverse( path: &Path, db: &Database, incremental: bool, parent: NodeID, - parent_slug_fragment: &str, mut iflags: InheritedFlags, out: &mut Vec<(PathBuf, NodeID, InheritedFlags)>, ) -> Result<()> { if path.is_dir() { reporting::set_task(format!("indexing {path:?}")); - let slug_fragment = if path == CONF.media_path { - "library".to_string() - } else { - path.file_name().unwrap().to_string_lossy().to_string() - }; - let slug = if parent_slug_fragment.is_empty() { - slug_fragment.clone() - } else { - format!("{parent_slug_fragment}-{slug_fragment}") - }; - let id = NodeID::from_slug(&slug); + let slug = get_node_slug(path).unwrap(); + let node = NodeID::from_slug(&slug); // Some flags need to applied immediatly because they are inherited if let Ok(content) = read_to_string(path.join("flags")) { @@ -200,7 +162,7 @@ fn import_traverse( } } - db.update_node_init(id, |n| { + db.update_node_init(node, |n| { if parent != NodeID::MIN { n.parents.insert(parent); } @@ -212,8 +174,8 @@ fn import_traverse( for e in path.read_dir()? { let path = e?.path(); reporting::catch( - import_traverse(&path, db, incremental, id, &slug_fragment, iflags, out) - .context(anyhow!("index {slug_fragment:?}")), + import_traverse(&path, db, incremental, node, iflags, out) + .context(anyhow!("index {:?}", path.file_name().unwrap())), ); } return Ok(()); @@ -231,6 +193,11 @@ fn import_traverse( } } + IMPORT_PROGRESS + .blocking_write() + .as_mut() + .unwrap() + .total_items += 1; out.push((path.to_owned(), parent, iflags)); } Ok(()) @@ -238,12 +205,13 @@ fn import_traverse( fn import_file( db: &Database, - apis: &Apis, - rthandle: &Handle, + rt: &Handle, + plugins: &[Box<dyn ImportPlugin>], path: &Path, parent: NodeID, iflags: InheritedFlags, ) { + let mut all_ok = true; let filename = path.file_name().unwrap().to_string_lossy(); if filename == "flags" { let Some(content) = @@ -251,10 +219,78 @@ fn import_file( else { return; }; - for flag in content.lines() {} + for line in content.lines() { + for p in plugins { + let inf = p.info(); + if inf.handle_instruction { + reporting::set_task(format!("{}(inst): {path:?}", inf.name)); + all_ok &= reporting::catch( + p.instruction(&ImportContext { db, rt, iflags }, parent, line) + .context(anyhow!("{}(inst) {path:?}", inf.name)), + ) + .is_some(); + } + } + } } + if filename.ends_with("mkv") || filename.ends_with("mka") || filename.ends_with("mks") { - import_media_file(db, apis, rthandle, path, parent, iflags).context("media file"); + let slug = get_node_slug(path).unwrap(); + let node = NodeID::from_slug(&slug); + + all_ok &= reporting::catch(db.update_node_init(node, |node| { + node.slug = slug; + if parent != NodeID::MIN { + node.parents.insert(parent); + } + node.visibility = iflags.visibility; + Ok(()) + })) + .is_some(); + + let Some(seg) = + reporting::catch(read_media_metadata(path).context(anyhow!("media {path:?}"))) + else { + return; + }; + for p in plugins { + let inf = p.info(); + if inf.handle_media { + reporting::set_task(format!("{}(media): {path:?}", inf.name)); + all_ok &= reporting::catch( + p.media(&ImportContext { db, rt, iflags }, node, path, &seg) + .context(anyhow!("{}(media) {path:?}", inf.name)), + ) + .is_some(); + } + } + reporting::set_task("idle".to_owned()); + } + + if all_ok { + reporting::catch(update_mtime(db, path).context("updating mtime")); + } +} + +fn update_mtime(db: &Database, path: &Path) -> Result<()> { + let meta = path.metadata()?; + let mtime = meta.modified()?.duration_since(UNIX_EPOCH)?.as_secs(); + db.set_import_file_mtime(path, mtime)?; + Ok(()) +} + +fn get_node_slug(path: &Path) -> Option<String> { + if path == CONF.media_path { + return Some("library".to_string()); + } + let filename = path.file_name()?.to_string_lossy(); + let filestem = filename.split_once(".").unwrap_or((&filename, "")).0; + if path.parent()? == &CONF.media_path { + Some(format!("{filestem}")) + } else { + let parent_filename = path.parent()?.file_name()?.to_string_lossy(); + let parent_filestem = parent_filename.split_once(".").unwrap_or((&filename, "")).0; + Some(format!("{parent_filestem}-{filestem}")) } } @@ -297,6 +333,7 @@ pub fn read_media_metadata(path: &Path) -> Result<Arc<matroska::Segment>> { }) }, ) + .context("reading media metadata") } pub fn is_useful_attachment(a: &AttachedFile) -> Option<&'static str> { @@ -307,323 +344,291 @@ pub fn is_useful_attachment(a: &AttachedFile) -> Option<&'static str> { } } -fn import_media_file( - db: &Database, - apis: &Apis, - rthandle: &Handle, - path: &Path, - parent: NodeID, - iflags: InheritedFlags, -) -> Result<()> { - info!("media file {path:?}"); - let m = read_media_metadata(path)?; - - let filename = path.file_name().unwrap().to_string_lossy().to_string(); - - let mut episode_index = None; - if let Some(cap) = RE_EPISODE_FILENAME.captures(&filename) { - if let Some(episode) = cap.name("episode").map(|m| m.as_str()) { - let season = cap.name("season").map(|m| m.as_str()); - let episode = episode.parse::<usize>().context("parse episode num")?; - let season = season - .unwrap_or("1") - .parse::<usize>() - .context("parse season num")?; - episode_index = Some((season, episode)) - } - } - - let mut filename_toks = filename.split("."); - let filepath_stem = filename_toks.next().unwrap(); +// let slug = if let Some((s, e)) = episode_index { +// format!( +// "{}-s{s}e{e}", +// make_kebab( +// &path +// .parent() +// .unwrap() +// .file_name() +// .unwrap_or_default() +// .to_string_lossy() +// ) +// ) +// } else { +// make_kebab(filepath_stem) +// }; - let slug = if let Some((s, e)) = episode_index { - format!( - "{}-s{s}e{e}", - make_kebab( - &path - .parent() - .unwrap() - .file_name() - .unwrap_or_default() - .to_string_lossy() - ) - ) - } else { - make_kebab(filepath_stem) - }; +// let node = NodeID::from_slug(&slug); - let node = NodeID::from_slug(&slug); +// db.update_node_init(node, |node| { +// node.slug = slug; +// node.visibility = iflags.visibility; +// node.parents.insert(parent); +// Ok(()) +// })?; - db.update_node_init(node, |node| { - node.slug = slug; - node.visibility = iflags.visibility; - node.parents.insert(parent); - Ok(()) - })?; +// if let Some((season, episode)) = episode_index { +// let mut trakt_id = None; +// let flagspath = path.parent().unwrap().join("flags"); +// if flagspath.exists() { +// for flag in read_to_string(flagspath)?.lines() { +// if let Some(value) = flag.strip_prefix("trakt-").or(flag.strip_prefix("trakt=")) { +// let (kind, id) = value.split_once(":").unwrap_or(("", value)); +// if kind == "show" { +// trakt_id = Some(id.parse::<u64>()?); +// } +// } +// } +// } +// if let Some(trakt_id) = trakt_id { +// let trakt = apis.trakt.as_ref().ok_or(anyhow!("trakt required"))?; +// let seasons = trakt.show_seasons(trakt_id, rthandle)?; +// if seasons.iter().any(|x| x.number == season) { +// let episodes = trakt.show_season_episodes(trakt_id, season, rthandle)?; +// let mut poster = None; +// if let Some(tmdb) = &apis.tmdb { +// let trakt_details = trakt.lookup(TraktKind::Show, trakt_id, rthandle)?; +// if let Some(tmdb_id) = trakt_details.ids.tmdb { +// let tmdb_details = +// tmdb.episode_details(tmdb_id, season, episode, rthandle)?; +// if let Some(still) = &tmdb_details.still_path { +// poster = Some(tmdb.image(still, rthandle)?) +// } +// } +// } +// if let Some(episode) = episodes.get(episode.saturating_sub(1)) { +// db.update_node_init(node, |node| { +// node.kind = NodeKind::Episode; +// node.index = Some(episode.number); +// node.title = Some(episode.title.clone()); +// if let Some(poster) = poster { +// node.pictures.insert(PictureSlot::Cover, poster); +// } +// node.description = episode.overview.clone().or(node.description.clone()); +// node.ratings.insert(RatingType::Trakt, episode.rating); +// Ok(()) +// })? +// } +// } +// } +// } - if let Some((season, episode)) = episode_index { - let mut trakt_id = None; - let flagspath = path.parent().unwrap().join("flags"); - if flagspath.exists() { - for flag in read_to_string(flagspath)?.lines() { - if let Some(value) = flag.strip_prefix("trakt-").or(flag.strip_prefix("trakt=")) { - let (kind, id) = value.split_once(":").unwrap_or(("", value)); - if kind == "show" { - trakt_id = Some(id.parse::<u64>()?); - } - } - } - } - if let Some(trakt_id) = trakt_id { - let trakt = apis.trakt.as_ref().ok_or(anyhow!("trakt required"))?; - let seasons = trakt.show_seasons(trakt_id, rthandle)?; - if seasons.iter().any(|x| x.number == season) { - let episodes = trakt.show_season_episodes(trakt_id, season, rthandle)?; - let mut poster = None; - if let Some(tmdb) = &apis.tmdb { - let trakt_details = trakt.lookup(TraktKind::Show, trakt_id, rthandle)?; - if let Some(tmdb_id) = trakt_details.ids.tmdb { - let tmdb_details = - tmdb.episode_details(tmdb_id, season, episode, rthandle)?; - if let Some(still) = &tmdb_details.still_path { - poster = Some(tmdb.image(still, rthandle)?) - } - } - } - if let Some(episode) = episodes.get(episode.saturating_sub(1)) { - db.update_node_init(node, |node| { - node.kind = NodeKind::Episode; - node.index = Some(episode.number); - node.title = Some(episode.title.clone()); - if let Some(poster) = poster { - node.pictures.insert(PictureSlot::Cover, poster); - } - node.description = episode.overview.clone().or(node.description.clone()); - node.ratings.insert(RatingType::Trakt, episode.rating); - Ok(()) - })? - } - } - } - } +// for tok in filename_toks { +// apply_node_flag(db, rthandle, apis, node, tok)?; +// } - // for tok in filename_toks { - // apply_node_flag(db, rthandle, apis, node, tok)?; - // } +// fn apply_musicbrainz_recording( +// db: &Database, +// rthandle: &Handle, +// apis: &Apis, +// node: NodeID, +// mbid: String, +// ) -> Result<()> { +// let rec = apis.musicbrainz.lookup_recording(mbid, rthandle)?; - Ok(()) -} +// db.update_node_init(node, |node| { +// node.title = Some(rec.title.clone()); +// node.identifiers +// .insert(IdentifierType::MusicbrainzRecording, rec.id.to_string()); +// if let Some(a) = rec.artist_credit.first() { +// node.subtitle = Some(a.artist.name.clone()); +// node.identifiers +// .insert(IdentifierType::MusicbrainzArtist, a.artist.id.to_string()); +// } -fn apply_musicbrainz_recording( - db: &Database, - rthandle: &Handle, - apis: &Apis, - node: NodeID, - mbid: String, -) -> Result<()> { - let rec = apis.musicbrainz.lookup_recording(mbid, rthandle)?; +// // // TODO proper dedup +// // node.people.clear(); - db.update_node_init(node, |node| { - node.title = Some(rec.title.clone()); - node.identifiers - .insert(IdentifierType::MusicbrainzRecording, rec.id.to_string()); - if let Some(a) = rec.artist_credit.first() { - node.subtitle = Some(a.artist.name.clone()); - node.identifiers - .insert(IdentifierType::MusicbrainzArtist, a.artist.id.to_string()); - } +// for rel in &rec.relations { +// use musicbrainz::reltypes::*; +// let a = match rel.type_id.as_str() { +// INSTRUMENT => Some(("", CreditCategory::Instrument)), +// VOCAL => Some(("", CreditCategory::Vocal)), +// PRODUCER => Some(("", CreditCategory::Producer)), +// MIX => Some(("mix ", CreditCategory::Engineer)), +// PHONOGRAPHIC_COPYRIGHT => { +// Some(("phonographic copyright ", CreditCategory::Engineer)) +// } +// PROGRAMMING => Some(("programming ", CreditCategory::Engineer)), +// _ => None, +// }; - // // TODO proper dedup - // node.people.clear(); +// if let Some((note, group)) = a { +// let artist = rel.artist.as_ref().unwrap(); - for rel in &rec.relations { - use musicbrainz::reltypes::*; - let a = match rel.type_id.as_str() { - INSTRUMENT => Some(("", CreditCategory::Instrument)), - VOCAL => Some(("", CreditCategory::Vocal)), - PRODUCER => Some(("", CreditCategory::Producer)), - MIX => Some(("mix ", CreditCategory::Engineer)), - PHONOGRAPHIC_COPYRIGHT => { - Some(("phonographic copyright ", CreditCategory::Engineer)) - } - PROGRAMMING => Some(("programming ", CreditCategory::Engineer)), - _ => None, - }; +// let artist = apis +// .musicbrainz +// .lookup_artist(artist.id.clone(), rthandle)?; - if let Some((note, group)) = a { - let artist = rel.artist.as_ref().unwrap(); +// let mut image_1 = None; +// let mut image_2 = None; - let artist = apis - .musicbrainz - .lookup_artist(artist.id.clone(), rthandle)?; +// for rel in &artist.relations { +// match rel.type_id.as_str() { +// WIKIDATA => { +// let url = rel.url.as_ref().unwrap().resource.clone(); +// if let Some(id) = url.strip_prefix("https://www.wikidata.org/wiki/") { +// if let Some(filename) = +// apis.wikidata.query_image_path(id.to_owned(), rthandle)? +// { +// image_1 = Some( +// apis.wikimedia_commons +// .image_by_filename(filename, rthandle)?, +// ); +// } +// } +// } +// VGMDB => { +// let url = rel.url.as_ref().unwrap().resource.clone(); +// if let Some(id) = url.strip_prefix("https://vgmdb.net/artist/") { +// let id = id.parse::<u64>().context("parse vgmdb id")?; +// if let Some(path) = apis.vgmdb.get_artist_image(id, rthandle)? { +// image_2 = Some(path); +// } +// } +// } +// _ => (), +// } +// } +// let mut jobs = vec![]; +// if !note.is_empty() { +// jobs.push(note.to_string()); +// } +// jobs.extend(rel.attributes.clone()); - let mut image_1 = None; - let mut image_2 = None; +// let _headshot = match image_1.or(image_2) { +// Some(x) => x, +// None => Asset(cache_store( +// format!("fallback/{}.image", HashKey(&artist.sort_name)), +// || generate_fallback(&artist.sort_name), +// )?), +// }; - for rel in &artist.relations { - match rel.type_id.as_str() { - WIKIDATA => { - let url = rel.url.as_ref().unwrap().resource.clone(); - if let Some(id) = url.strip_prefix("https://www.wikidata.org/wiki/") { - if let Some(filename) = - apis.wikidata.query_image_path(id.to_owned(), rthandle)? - { - image_1 = Some( - apis.wikimedia_commons - .image_by_filename(filename, rthandle)?, - ); - } - } - } - VGMDB => { - let url = rel.url.as_ref().unwrap().resource.clone(); - if let Some(id) = url.strip_prefix("https://vgmdb.net/artist/") { - let id = id.parse::<u64>().context("parse vgmdb id")?; - if let Some(path) = apis.vgmdb.get_artist_image(id, rthandle)? { - image_2 = Some(path); - } - } - } - _ => (), - } - } - let mut jobs = vec![]; - if !note.is_empty() { - jobs.push(note.to_string()); - } - jobs.extend(rel.attributes.clone()); +// node.credits.entry(group).or_default().push(Appearance { +// jobs, +// characters: vec![], +// node: NodeID([0; 32]), // TODO +// }); +// } +// } - let _headshot = match image_1.or(image_2) { - Some(x) => x, - None => Asset(cache_store( - format!("fallback/{}.image", HashKey(&artist.sort_name)), - || generate_fallback(&artist.sort_name), - )?), - }; +// for isrc in &rec.isrcs { +// node.identifiers +// .insert(IdentifierType::Isrc, isrc.to_string()); +// } +// Ok(()) +// })?; +// Ok(()) +// } - node.credits.entry(group).or_default().push(Appearance { - jobs, - characters: vec![], - node: NodeID([0; 32]), // TODO - }); - } - } +// 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 = trakt.lookup(trakt_kind, trakt_id, rthandle)?; +// let people = trakt.people(trakt_kind, trakt_id, rthandle)?; - for isrc in &rec.isrcs { - node.identifiers - .insert(IdentifierType::Isrc, isrc.to_string()); - } - Ok(()) - })?; - Ok(()) -} +// let mut people_map = BTreeMap::<CreditCategory, Vec<Appearance>>::new(); +// for p in people.cast.iter() { +// people_map +// .entry(CreditCategory::Cast) +// .or_default() +// .push(p.a()) +// } +// for (group, people) in people.crew.iter() { +// for p in people { +// people_map +// .entry(group.as_credit_category()) +// .or_default() +// .push(p.a()) +// } +// } -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 = trakt.lookup(trakt_kind, trakt_id, rthandle)?; - let people = trakt.people(trakt_kind, trakt_id, rthandle)?; +// let mut tmdb_data = None; +// let mut backdrop = None; +// let mut poster = None; +// if let Some(tmdb_id) = data.ids.tmdb { +// let data = tmdb.details( +// match trakt_kind { +// TraktKind::Movie => TmdbKind::Movie, +// TraktKind::Show => TmdbKind::Tv, +// _ => TmdbKind::Movie, +// }, +// tmdb_id, +// rthandle, +// )?; +// tmdb_data = Some(data.clone()); - let mut people_map = BTreeMap::<CreditCategory, Vec<Appearance>>::new(); - for p in people.cast.iter() { - people_map - .entry(CreditCategory::Cast) - .or_default() - .push(p.a()) - } - for (group, people) in people.crew.iter() { - for p in people { - people_map - .entry(group.as_credit_category()) - .or_default() - .push(p.a()) - } - } +// if let Some(path) = &data.backdrop_path { +// backdrop = Some(tmdb.image(path, rthandle).context("tmdb backdrop image")?); +// } +// if let Some(path) = &data.poster_path { +// poster = Some(tmdb.image(path, rthandle).context("tmdb poster image")?); +// } - let mut tmdb_data = None; - let mut backdrop = None; - let mut poster = None; - if let Some(tmdb_id) = data.ids.tmdb { - let data = tmdb.details( - match trakt_kind { - TraktKind::Movie => TmdbKind::Movie, - TraktKind::Show => TmdbKind::Tv, - _ => TmdbKind::Movie, - }, - tmdb_id, - rthandle, - )?; - tmdb_data = Some(data.clone()); +// // 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()); +// // } +// // } +// // } +// } - if let Some(path) = &data.backdrop_path { - backdrop = Some(tmdb.image(path, rthandle).context("tmdb backdrop image")?); - } - if let Some(path) = &data.poster_path { - poster = Some(tmdb.image(path, rthandle).context("tmdb poster image")?); - } +// db.update_node_init(node, |node| { +// node.title = Some(data.title.clone()); +// node.credits.extend(people_map); +// node.kind = trakt_kind.as_node_kind(); +// 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(RatingType::Trakt, *rating); +// } +// if let Some(poster) = poster { +// node.pictures.insert(PictureSlot::Cover, poster); +// } +// if let Some(backdrop) = backdrop { +// node.pictures.insert(PictureSlot::Backdrop, 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(RatingType::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(()) +// } - // 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.credits.extend(people_map); - node.kind = trakt_kind.as_node_kind(); - 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(RatingType::Trakt, *rating); - } - if let Some(poster) = poster { - node.pictures.insert(PictureSlot::Cover, poster); - } - if let Some(backdrop) = backdrop { - node.pictures.insert(PictureSlot::Backdrop, 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(RatingType::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(()) -} - -fn make_kebab(i: &str) -> String { - let mut o = String::with_capacity(i.len()); - for c in i.chars() { - o.extend(match c { - 'A'..='Z' | 'a'..='z' | '0'..='9' | '_' | '-' => Some(c), - ' ' => Some('-'), - _ => None, - }); - } - o -} +// fn make_kebab(i: &str) -> String { +// let mut o = String::with_capacity(i.len()); +// for c in i.chars() { +// o.extend(match c { +// 'A'..='Z' | 'a'..='z' | '0'..='9' | '_' | '-' => Some(c), +// ' ' => Some('-'), +// _ => None, +// }); +// } +// o +// } diff --git a/import/src/plugins/acoustid.rs b/import/src/plugins/acoustid.rs index bf07f90..38e818c 100644 --- a/import/src/plugins/acoustid.rs +++ b/import/src/plugins/acoustid.rs @@ -167,6 +167,9 @@ impl ImportPlugin for AcoustID { } } fn media(&self, ct: &ImportContext, node: NodeID, path: &Path, _seg: &Segment) -> Result<()> { + if !ct.iflags.use_acoustid { + return Ok(()); + } let fp = acoustid_fingerprint(path)?; if let Some((atid, mbid)) = self.get_atid_mbid(&fp, &ct.rt)? { ct.db.update_node_init(node, |n| { diff --git a/import/src/plugins/misc.rs b/import/src/plugins/misc.rs index 6f2c18e..8d7028c 100644 --- a/import/src/plugins/misc.rs +++ b/import/src/plugins/misc.rs @@ -4,15 +4,17 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ use crate::plugins::{ImportContext, ImportPlugin, PluginInfo}; -use anyhow::{Result, bail}; +use anyhow::{Context, Result, bail}; use jellycache::{HashKey, cache_store}; use jellycommon::{Asset, NodeID, NodeKind, PictureSlot, Visibility}; use jellyremuxer::matroska::{AttachedFile, Segment}; use log::info; +use regex::Regex; use std::{ fs::{File, read_to_string}, io::Read, path::Path, + sync::LazyLock, }; pub struct ImageFiles; @@ -152,3 +154,36 @@ impl ImportPlugin for Children { Ok(()) } } + +static RE_EPISODE_FILENAME: LazyLock<Regex> = + LazyLock::new(|| Regex::new(r#"([sS](?<season>\d+))?([eE](?<episode>\d+))( (.+))?"#).unwrap()); + +pub struct EpisodeIndex; +impl ImportPlugin for EpisodeIndex { + fn info(&self) -> PluginInfo { + PluginInfo { + name: "episode-info", + handle_media: true, + ..Default::default() + } + } + fn media(&self, ct: &ImportContext, node: NodeID, path: &Path, _seg: &Segment) -> Result<()> { + let filename = path.file_name().unwrap().to_string_lossy(); + if let Some(cap) = RE_EPISODE_FILENAME.captures(&filename) { + if let Some(episode) = cap.name("episode").map(|m| m.as_str()) { + let season = cap.name("season").map(|m| m.as_str()); + let episode = episode.parse::<usize>().context("parse episode num")?; + let season = season + .unwrap_or("1") + .parse::<usize>() + .context("parse season num")?; + + ct.db.update_node_init(node, |node| { + node.index = Some(episode); + Ok(()) + })?; + } + } + Ok(()) + } +} diff --git a/import/src/plugins/mod.rs b/import/src/plugins/mod.rs index a5cc3dc..cf0da1c 100644 --- a/import/src/plugins/mod.rs +++ b/import/src/plugins/mod.rs @@ -15,6 +15,7 @@ pub mod vgmdb; pub mod wikidata; pub mod wikimedia_commons; +use crate::{ApiSecrets, InheritedFlags}; use anyhow::Result; use jellycommon::NodeID; use jellydb::Database; @@ -22,20 +23,19 @@ use jellyremuxer::matroska::Segment; use std::path::Path; use tokio::runtime::Handle; -use crate::ApiSecrets; - -pub struct ImportContext { - pub db: Database, - pub rt: Handle, +pub struct ImportContext<'a> { + pub db: &'a Database, + pub rt: &'a Handle, + pub iflags: InheritedFlags, } #[derive(Default, Clone, Copy)] pub struct PluginInfo { - name: &'static str, - handle_file: bool, - handle_media: bool, - handle_instruction: bool, - handle_process: bool, + pub name: &'static str, + pub handle_file: bool, + pub handle_media: bool, + pub handle_instruction: bool, + pub handle_process: bool, } pub trait ImportPlugin: Send + Sync { diff --git a/import/src/plugins/trakt.rs b/import/src/plugins/trakt.rs index 6d5b007..c062b01 100644 --- a/import/src/plugins/trakt.rs +++ b/import/src/plugins/trakt.rs @@ -385,7 +385,7 @@ impl Display for TraktKind { impl ImportPlugin for Trakt { fn info(&self) -> PluginInfo { PluginInfo { - name: "takt", + name: "trakt", handle_instruction: true, ..Default::default() } diff --git a/import/src/reporting.rs b/import/src/reporting.rs index 3105b59..92f38be 100644 --- a/import/src/reporting.rs +++ b/import/src/reporting.rs @@ -6,11 +6,13 @@ use anyhow::Result; use rayon::{current_num_threads, current_thread_index}; +use serde::Serialize; use tokio::sync::RwLock; pub static IMPORT_ERRORS: RwLock<Vec<String>> = RwLock::const_new(Vec::new()); pub static IMPORT_PROGRESS: RwLock<Option<ImportProgress>> = RwLock::const_new(None); +#[derive(Debug, Serialize, Clone)] pub struct ImportProgress { pub total_items: usize, pub finished_items: usize, |