aboutsummaryrefslogtreecommitdiff
path: root/import/src
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2025-12-11 01:20:17 +0100
committermetamuffin <metamuffin@disroot.org>2025-12-11 01:20:17 +0100
commit6e5f6d9b9c6fedb4ab80190c156595d321d33bbf (patch)
treeb6c2140e744fc3018ad08975afefad40386ebbc6 /import/src
parente4f865e9da9d6660399e22a6fbeb5b84a749b07a (diff)
downloadjellything-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.rs747
-rw-r--r--import/src/plugins/acoustid.rs3
-rw-r--r--import/src/plugins/misc.rs37
-rw-r--r--import/src/plugins/mod.rs20
-rw-r--r--import/src/plugins/trakt.rs2
-rw-r--r--import/src/reporting.rs2
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,