aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2026-02-20 21:02:33 +0100
committermetamuffin <metamuffin@disroot.org>2026-02-20 21:02:33 +0100
commit8b2cf84bc7a80f9a45aa350a2c98949bfed4b7c1 (patch)
tree1611e9f7e221427cd0ff5c18da2dec87c19720de
parent5caf1f1db721d6dee2ddb5d0613e8c9914ccf879 (diff)
downloadjellything-8b2cf84bc7a80f9a45aa350a2c98949bfed4b7c1.tar
jellything-8b2cf84bc7a80f9a45aa350a2c98949bfed4b7c1.tar.bz2
jellything-8b2cf84bc7a80f9a45aa350a2c98949bfed4b7c1.tar.zst
source ranks
-rw-r--r--common/object/src/lib.rs5
-rw-r--r--common/src/node.rs15
-rw-r--r--import/src/lib.rs29
-rw-r--r--import/src/plugins/acoustid.rs33
-rw-r--r--import/src/plugins/infojson.rs32
-rw-r--r--import/src/plugins/media_info.rs16
-rw-r--r--import/src/plugins/misc.rs42
-rw-r--r--import/src/plugins/mod.rs5
-rw-r--r--import/src/plugins/musicbrainz.rs11
-rw-r--r--import/src/plugins/omdb.rs10
-rw-r--r--import/src/plugins/tags.rs13
-rw-r--r--import/src/plugins/tmdb.rs40
-rw-r--r--import/src/plugins/trakt.rs26
-rw-r--r--import/src/plugins/vgmdb.rs4
-rw-r--r--import/src/plugins/wikidata.rs4
-rw-r--r--import/src/source_rank.rs85
16 files changed, 271 insertions, 99 deletions
diff --git a/common/object/src/lib.rs b/common/object/src/lib.rs
index c057163..a73de08 100644
--- a/common/object/src/lib.rs
+++ b/common/object/src/lib.rs
@@ -26,6 +26,11 @@ use std::{collections::BTreeSet, fmt::Display, hash::Hash, marker::PhantomData};
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Tag(pub u32);
+impl Default for Tag {
+ fn default() -> Self {
+ Self::new(b"1111")
+ }
+}
impl Tag {
pub const fn new(fourcc: &[u8; 4]) -> Self {
Self(u32::from_le_bytes(*fourcc))
diff --git a/common/src/node.rs b/common/src/node.rs
index b02f8e8..92b5a5b 100644
--- a/common/src/node.rs
+++ b/common/src/node.rs
@@ -29,6 +29,7 @@ fields! {
NO_STORAGE_SIZE: u64 = b"stsz";
NO_CREDIT: Object = b"crdt"; // multi
NO_SLUG: &str = b"slug";
+ NO_METASOURCE: Object = b"msrc";
CR_NODE: u64 = b"node";
CR_KIND: Tag = b"kind";
@@ -89,9 +90,23 @@ fields! {
IDENT_OMDB: &str = b"omdb";
IDENT_VGMDB_ARTIST: &str = b"vgar";
IDENT_WIKIDATA: &str = b"wkdt";
+
}
enums! {
+ MSOURCE_TRAKT = b"trkt";
+ MSOURCE_INFOJSON = b"infj";
+ MSOURCE_TMDB = b"tmdb";
+ MSOURCE_OMDB = b"omdb";
+ MSOURCE_VGMDB = b"vgmd";
+ MSOURCE_WIKIDATA = b"wkdt";
+ MSOURCE_MUSICBRAINZ = b"mbrz";
+ MSOURCE_TAGS = b"tags";
+ MSOURCE_IMAGE_ATT = b"iatt";
+ MSOURCE_ACOUSTID = b"acid";
+ MSOURCE_MEDIA = b"medi";
+ MSOURCE_EXPLICIT = b"expl";
+
VISI_HIDDEN = b"hidn";
VISI_REDUCED = b"rdcd";
VISI_VISIBLE = b"visi";
diff --git a/import/src/lib.rs b/import/src/lib.rs
index b31d356..7e402be 100644
--- a/import/src/lib.rs
+++ b/import/src/lib.rs
@@ -8,15 +8,17 @@
pub mod helpers;
pub mod plugins;
pub mod reporting;
+pub mod source_rank;
-use crate::plugins::{
- ImportPlugin, PluginContext, infojson::is_info_json, init_plugins, misc::is_cover,
+use crate::{
+ plugins::{ImportPlugin, PluginContext, infojson::is_info_json, init_plugins, misc::is_cover},
+ source_rank::{ImportSource, SourceRanks},
};
use anyhow::{Context, Result, anyhow};
use jellycache::{Cache, HashKey};
use jellycommon::{
internal::{IM_MTIME, IM_PATH},
- jellyobject::{self, ObjectBuffer, Path as TagPath},
+ jellyobject::{self, ObjectBuffer, Path as TagPath, Tag},
*,
};
use jellydb::{Database, Filter, Query, RowNum, Sort};
@@ -153,6 +155,7 @@ pub async fn import_wrap(ic: ImportConfig, incremental: bool) -> Result<()> {
fn import(ic: ImportConfig, rt: &Handle, incremental: bool) -> Result<()> {
reporting::set_stage(format!("Initializing Plugins"), 0);
let plugins = init_plugins(&ic.config.api);
+ let ranks = SourceRanks::new();
reporting::set_stage(format!("Indexing files"), 0);
let files = Mutex::new(Vec::new());
@@ -171,7 +174,7 @@ fn import(ic: ImportConfig, rt: &Handle, incremental: bool) -> Result<()> {
reporting::set_stage(format!("Importing files"), files.len());
files.into_par_iter().for_each(|(path, parent, iflags)| {
reporting::set_task(format!("unknown: {path:?}"));
- import_file(&ic, &rt, &nodes, &plugins, &path, parent, iflags);
+ import_file(&ic, &rt, &ranks, &nodes, &plugins, &path, parent, iflags);
reporting::inc_finished();
reporting::set_task("idle".to_owned());
});
@@ -187,7 +190,7 @@ fn import(ic: ImportConfig, rt: &Handle, incremental: bool) -> Result<()> {
swap(nodes.get_mut().unwrap(), &mut cur_nodes);
cur_nodes.into_par_iter().for_each(|node| {
reporting::set_task(format!("unknown: {node}"));
- process_node(&ic, &rt, &plugins, &nodes, node);
+ process_node(&ic, &rt, &ranks, &plugins, &nodes, node);
reporting::inc_finished();
reporting::set_task("idle".to_owned());
});
@@ -274,6 +277,7 @@ fn import_traverse(
fn import_file(
ic: &ImportConfig,
rt: &Handle,
+ ranks: &SourceRanks,
pending_nodes: &Mutex<HashSet<RowNum>>,
plugins: &[Box<dyn ImportPlugin>],
path: &Path,
@@ -281,9 +285,13 @@ fn import_file(
iflags: InheritedFlags,
) {
let mut all_ok = true;
- let ct = PluginContext {
+ let mut ct = PluginContext {
ic,
rt,
+ is: ImportSource {
+ tag: Tag::new(b"xxxx"),
+ ranks,
+ },
iflags,
pending_nodes,
};
@@ -299,6 +307,7 @@ fn import_file(
for p in plugins {
let inf = p.info();
if inf.handle_instruction {
+ ct.is.tag = inf.tag;
reporting::set_task(format!("{}(inst): {path:?}", inf.name));
all_ok &= reporting::catch(
p.instruction(&ct, parent, line)
@@ -344,6 +353,7 @@ fn import_file(
for p in plugins {
let inf = p.info();
if inf.handle_instruction {
+ ct.is.tag = inf.tag;
reporting::set_task(format!("{}(inst): {path:?}", inf.name));
all_ok &= reporting::catch(
p.instruction(&ct, row, line)
@@ -363,6 +373,7 @@ fn import_file(
for p in plugins {
let inf = p.info();
if inf.handle_media {
+ ct.is.tag = inf.tag;
reporting::set_task(format!("{}(media): {path:?}", inf.name));
all_ok &= reporting::catch(
p.media(&ct, row, path, &seg)
@@ -375,6 +386,7 @@ fn import_file(
for p in plugins {
let inf = p.info();
if inf.handle_file {
+ ct.is.tag = inf.tag;
reporting::set_task(format!("{}(file): {path:?}", inf.name));
all_ok &= reporting::catch(
p.file(&ct, parent, path)
@@ -393,6 +405,7 @@ fn import_file(
fn process_node(
dba: &ImportConfig,
rt: &Handle,
+ ranks: &SourceRanks,
plugins: &[Box<dyn ImportPlugin>],
pending_nodes: &Mutex<HashSet<RowNum>>,
node: RowNum,
@@ -414,6 +427,10 @@ fn process_node(
p.process(
&PluginContext {
ic: dba,
+ is: ImportSource {
+ tag: inf.tag,
+ ranks,
+ },
rt,
iflags: InheritedFlags::default(),
pending_nodes,
diff --git a/import/src/plugins/acoustid.rs b/import/src/plugins/acoustid.rs
index 8e502b0..c829aac 100644
--- a/import/src/plugins/acoustid.rs
+++ b/import/src/plugins/acoustid.rs
@@ -6,12 +6,11 @@
use crate::{
USER_AGENT,
plugins::{ImportPlugin, PluginContext, PluginInfo},
+ source_rank::ObjectImportSourceExt,
};
use anyhow::{Context, Result};
use jellycache::{Cache, HashKey};
-use jellycommon::{
- IDENT_ACOUST_ID_TRACK, IDENT_MUSICBRAINZ_RECORDING, NO_IDENTIFIERS, jellyobject::Object,
-};
+use jellycommon::*;
use jellydb::RowNum;
use jellyremuxer::matroska::Segment;
use log::info;
@@ -176,6 +175,7 @@ impl ImportPlugin for AcoustID {
fn info(&self) -> PluginInfo {
PluginInfo {
name: "acoustid",
+ tag: MSOURCE_ACOUSTID,
handle_media: true,
..Default::default()
}
@@ -193,23 +193,18 @@ impl ImportPlugin for AcoustID {
let fp = acoustid_fingerprint(&ct.ic.cache, path)?;
- if let Some((atid, mbid)) = self.get_atid_mbid(&ct.ic.cache, &fp, &ct.rt)? {
- ct.ic.db.transaction(&mut |txn| {
- let ob = txn.get(node)?.unwrap();
- let ob = ob.as_object();
- let ob = ob.insert(
- NO_IDENTIFIERS,
- ob.get(NO_IDENTIFIERS)
- .unwrap_or(Object::EMPTY)
- .insert(IDENT_ACOUST_ID_TRACK, &atid)
- .as_object()
- .insert(IDENT_MUSICBRAINZ_RECORDING, &mbid)
- .as_object(),
- );
- txn.update(node, ob)?;
- Ok(())
- })?;
+ let Some((atid, mbid)) = self.get_atid_mbid(&ct.ic.cache, &fp, &ct.rt)? else {
+ return Ok(());
};
+
+ ct.ic.update_node(node, |node| {
+ node.as_object().update(NO_IDENTIFIERS, |ids| {
+ ids.insert(IDENT_ACOUST_ID_TRACK, &atid)
+ .as_object()
+ .insert_s(ct.is, IDENT_MUSICBRAINZ_RECORDING, &mbid)
+ })
+ })?;
+
Ok(())
}
}
diff --git a/import/src/plugins/infojson.rs b/import/src/plugins/infojson.rs
index 3928e7a..6905e57 100644
--- a/import/src/plugins/infojson.rs
+++ b/import/src/plugins/infojson.rs
@@ -3,7 +3,10 @@
which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
Copyright (C) 2026 metamuffin <metamuffin.org>
*/
-use crate::plugins::{ImportPlugin, PluginContext, PluginInfo};
+use crate::{
+ plugins::{ImportPlugin, PluginContext, PluginInfo},
+ source_rank::ObjectImportSourceExt,
+};
use anyhow::{Context, Result, anyhow};
use chrono::{Utc, format::Parsed};
use jellycommon::*;
@@ -160,6 +163,7 @@ impl ImportPlugin for Infojson {
fn info(&self) -> PluginInfo {
PluginInfo {
name: "infojson",
+ tag: MSOURCE_INFOJSON,
handle_file: true,
handle_media: true,
..Default::default()
@@ -177,20 +181,20 @@ impl ImportPlugin for Infojson {
ct.ic.db.transaction(&mut |txn| {
let mut node = txn.get(parent)?.unwrap();
- node = node.as_object().insert(NO_KIND, KIND_CHANNEL);
- node = node.as_object().insert(NO_TITLE, title);
+ node = node.as_object().insert_s(ct.is, NO_KIND, KIND_CHANNEL);
+ node = node.as_object().insert_s(ct.is, NO_TITLE, title);
if let Some(cid) = &data.channel_id {
node = node.as_object().update(NO_IDENTIFIERS, |ids| {
- ids.insert(IDENT_YOUTUBE_CHANNEL, &cid)
+ ids.insert_s(ct.is, IDENT_YOUTUBE_CHANNEL, &cid)
});
}
if let Some(uid) = &data.uploader_id {
node = node.as_object().update(NO_IDENTIFIERS, |ids| {
- ids.insert(IDENT_YOUTUBE_CHANNEL_HANDLE, &uid)
+ ids.insert_s(ct.is, IDENT_YOUTUBE_CHANNEL_HANDLE, &uid)
})
}
if let Some(desc) = &data.description {
- node = node.as_object().insert(NO_DESCRIPTION, &desc);
+ node = node.as_object().insert_s(ct.is, NO_DESCRIPTION, &desc);
}
if let Some(followers) = data.channel_follower_count {
node = node.as_object().update(NO_RATINGS, |rat| {
@@ -238,23 +242,23 @@ impl ImportPlugin for Infojson {
ct.ic.db.transaction(&mut |txn| {
let mut node = txn.get(row)?.unwrap();
- node = node.as_object().insert(NO_KIND, kind);
- node = node.as_object().insert(NO_TITLE, &infojson.title);
+ node = node.as_object().insert_s(ct.is, NO_KIND, kind);
+ node = node.as_object().insert_s(ct.is, NO_TITLE, &infojson.title);
if let Some(title) = &infojson.alt_title
&& title != &infojson.title
&& !node.as_object().has(NO_SUBTITLE.0)
{
- node = node.as_object().insert(NO_SUBTITLE, &title);
+ node = node.as_object().insert_s(ct.is, NO_SUBTITLE, &title);
}
if let Some(up) = &infojson.uploader
&& !node.as_object().has(NO_SUBTITLE.0)
{
node = node
.as_object()
- .insert(NO_SUBTITLE, &clean_uploader_name(&up));
+ .insert_s(ct.is, NO_SUBTITLE, &clean_uploader_name(&up));
}
if let Some(desc) = &infojson.description {
- node = node.as_object().insert(NO_DESCRIPTION, &desc);
+ node = node.as_object().insert_s(ct.is, NO_DESCRIPTION, &desc);
}
if let Some(tag) = infojson.tags.clone() {
node = node
@@ -262,12 +266,12 @@ impl ImportPlugin for Infojson {
.extend(NO_TAG, tag.iter().map(String::as_str));
}
if let Some(rd) = release_date {
- node = node.as_object().insert(NO_RELEASEDATE, rd);
+ node = node.as_object().insert_s(ct.is, NO_RELEASEDATE, rd);
}
match infojson.extractor.as_str() {
"youtube" => {
node = node.as_object().update(NO_IDENTIFIERS, |rat| {
- rat.insert(IDENT_YOUTUBE_VIDEO, &infojson.id)
+ rat.insert_s(ct.is, IDENT_YOUTUBE_VIDEO, &infojson.id)
});
node = node.as_object().update(NO_RATINGS, |rat| {
rat.insert(
@@ -284,7 +288,7 @@ impl ImportPlugin for Infojson {
}
"Bandcamp" => {
node = node.as_object().update(NO_IDENTIFIERS, |rat| {
- rat.insert(IDENT_BANDCAMP, &infojson.id)
+ rat.insert_s(ct.is, IDENT_BANDCAMP, &infojson.id)
});
}
_ => (),
diff --git a/import/src/plugins/media_info.rs b/import/src/plugins/media_info.rs
index 11da365..f21386e 100644
--- a/import/src/plugins/media_info.rs
+++ b/import/src/plugins/media_info.rs
@@ -4,7 +4,10 @@
Copyright (C) 2026 metamuffin <metamuffin.org>
*/
-use crate::plugins::{ImportPlugin, PluginContext, PluginInfo};
+use crate::{
+ plugins::{ImportPlugin, PluginContext, PluginInfo},
+ source_rank::ObjectImportSourceExt,
+};
use anyhow::Result;
use jellycommon::{
jellyobject::{Object, ObjectBuffer},
@@ -19,6 +22,7 @@ impl ImportPlugin for MediaInfo {
fn info(&self) -> PluginInfo {
PluginInfo {
name: "media-info",
+ tag: MSOURCE_MEDIA,
handle_media: true,
..Default::default()
}
@@ -101,13 +105,11 @@ impl ImportPlugin for MediaInfo {
);
}
- node = node.as_object().insert(
- NO_DURATION,
- fix_invalid_runtime(
- seg.info.duration.unwrap_or_default() * seg.info.timestamp_scale as f64 * 1e-9,
- ),
+ let runtime = fix_invalid_runtime(
+ seg.info.duration.unwrap_or_default() * seg.info.timestamp_scale as f64 * 1e-9,
);
- node = node.as_object().insert(NO_STORAGE_SIZE, size);
+ node = node.as_object().insert_s(ct.is, NO_DURATION, runtime);
+ node = node.as_object().insert_s(ct.is, NO_STORAGE_SIZE, size);
txn.update(row, node)?;
diff --git a/import/src/plugins/misc.rs b/import/src/plugins/misc.rs
index 1699f6d..2c56dae 100644
--- a/import/src/plugins/misc.rs
+++ b/import/src/plugins/misc.rs
@@ -3,7 +3,10 @@
which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
Copyright (C) 2026 metamuffin <metamuffin.org>
*/
-use crate::plugins::{ImportPlugin, PluginContext, PluginInfo};
+use crate::{
+ plugins::{ImportPlugin, PluginContext, PluginInfo},
+ source_rank::ObjectImportSourceExt,
+};
use anyhow::{Context, Result, bail};
use jellycache::HashKey;
use jellycommon::*;
@@ -23,6 +26,7 @@ impl ImportPlugin for ImageFiles {
fn info(&self) -> PluginInfo {
PluginInfo {
name: "image-files",
+ tag: MSOURCE_EXPLICIT,
handle_file: true,
..Default::default()
}
@@ -47,7 +51,7 @@ impl ImportPlugin for ImageFiles {
let mut node = txn.get(row)?.unwrap();
node = node
.as_object()
- .update(NO_PICTURES, |picts| picts.insert(slot, &asset));
+ .update(NO_PICTURES, |picts| picts.insert_s(ct.is, slot, &asset));
txn.update(row, node)?;
Ok(())
})?;
@@ -63,6 +67,7 @@ impl ImportPlugin for ImageAttachments {
fn info(&self) -> PluginInfo {
PluginInfo {
name: "image-attachments",
+ tag: MSOURCE_IMAGE_ATT,
handle_media: true,
..Default::default()
}
@@ -79,8 +84,9 @@ impl ImportPlugin for ImageAttachments {
};
ct.ic.update_node(row, |node| {
- node.as_object()
- .update(NO_PICTURES, |picts| picts.insert(PICT_COVER, &cover))
+ node.as_object().update(NO_PICTURES, |picts| {
+ picts.insert_s(ct.is, PICT_COVER, &cover)
+ })
})?;
Ok(())
}
@@ -91,6 +97,7 @@ impl ImportPlugin for General {
fn info(&self) -> PluginInfo {
PluginInfo {
name: "general",
+ tag: MSOURCE_EXPLICIT,
handle_instruction: true,
..Default::default()
}
@@ -98,12 +105,13 @@ impl ImportPlugin for General {
fn instruction(&self, ct: &PluginContext, node: RowNum, line: &str) -> Result<()> {
if line == "hidden" {
ct.ic.update_node(node, |node| {
- node.as_object().insert(NO_VISIBILITY, VISI_HIDDEN)
+ node.as_object().insert_s(ct.is, NO_VISIBILITY, VISI_HIDDEN)
})?;
}
if line == "reduced" {
ct.ic.update_node(node, |node| {
- node.as_object().insert(NO_VISIBILITY, VISI_REDUCED)
+ node.as_object()
+ .insert_s(ct.is, NO_VISIBILITY, VISI_REDUCED)
})?;
}
if let Some(kind) = line.strip_prefix("kind-").or(line.strip_prefix("kind=")) {
@@ -121,16 +129,18 @@ impl ImportPlugin for General {
_ => bail!("unknown node kind"),
};
ct.ic
- .update_node(node, |node| node.as_object().insert(NO_KIND, kind))?;
+ .update_node(node, |node| node.as_object().insert_s(ct.is, NO_KIND, kind))?;
}
if let Some(title) = line.strip_prefix("title=") {
- ct.ic
- .update_node(node, |node| node.as_object().insert(NO_TITLE, title))?;
+ ct.ic.update_node(node, |node| {
+ node.as_object().insert_s(ct.is, NO_TITLE, title)
+ })?;
}
if let Some(index) = line.strip_prefix("index=") {
let index = index.parse().context("parse index")?;
- ct.ic
- .update_node(node, |node| node.as_object().insert(NO_INDEX, index))?;
+ ct.ic.update_node(node, |node| {
+ node.as_object().insert_s(ct.is, NO_INDEX, index)
+ })?;
}
Ok(())
}
@@ -141,6 +151,7 @@ impl ImportPlugin for Children {
fn info(&self) -> PluginInfo {
PluginInfo {
name: "children",
+ tag: MSOURCE_EXPLICIT,
handle_file: true,
..Default::default()
}
@@ -155,7 +166,7 @@ impl ImportPlugin for Children {
continue;
}
ct.ic
- .update_node_slug(line, |n| n.as_object().insert(NO_PARENT, parent))?;
+ .update_node_slug(line, |n| n.as_object().extend(NO_PARENT, [parent]))?;
}
}
Ok(())
@@ -170,6 +181,7 @@ impl ImportPlugin for EpisodeIndex {
fn info(&self) -> PluginInfo {
PluginInfo {
name: "episode-info",
+ tag: MSOURCE_IMAGE_ATT,
handle_media: true,
..Default::default()
}
@@ -186,9 +198,9 @@ impl ImportPlugin for EpisodeIndex {
.context("parse season num")?;
ct.ic.update_node(node, |mut node| {
- node = node.as_object().insert(NO_SEASON_INDEX, season);
- node = node.as_object().insert(NO_INDEX, episode);
- node = node.as_object().insert(NO_KIND, KIND_EPISODE);
+ node = node.as_object().insert_s(ct.is, NO_SEASON_INDEX, season);
+ node = node.as_object().insert_s(ct.is, NO_INDEX, episode);
+ node = node.as_object().insert_s(ct.is, NO_KIND, KIND_EPISODE);
node
})?;
}
diff --git a/import/src/plugins/mod.rs b/import/src/plugins/mod.rs
index 60bf09b..8fd1e67 100644
--- a/import/src/plugins/mod.rs
+++ b/import/src/plugins/mod.rs
@@ -15,8 +15,9 @@ pub mod trakt;
pub mod vgmdb;
pub mod wikidata;
-use crate::{ApiSecrets, ImportConfig, InheritedFlags};
+use crate::{ApiSecrets, ImportConfig, InheritedFlags, source_rank::ImportSource};
use anyhow::Result;
+use jellycommon::jellyobject::Tag;
use jellydb::RowNum;
use jellyremuxer::matroska::Segment;
use std::{collections::HashSet, path::Path, sync::Mutex};
@@ -25,6 +26,7 @@ use tokio::runtime::Handle;
pub struct PluginContext<'a> {
pub ic: &'a ImportConfig,
pub rt: &'a Handle,
+ pub is: ImportSource<'a>,
pub iflags: InheritedFlags,
pub pending_nodes: &'a Mutex<HashSet<RowNum>>,
}
@@ -32,6 +34,7 @@ pub struct PluginContext<'a> {
#[derive(Default, Clone, Copy)]
pub struct PluginInfo {
pub name: &'static str,
+ pub tag: Tag,
pub handle_file: bool,
pub handle_media: bool,
pub handle_instruction: bool,
diff --git a/import/src/plugins/musicbrainz.rs b/import/src/plugins/musicbrainz.rs
index 4dfd974..454c562 100644
--- a/import/src/plugins/musicbrainz.rs
+++ b/import/src/plugins/musicbrainz.rs
@@ -11,6 +11,7 @@ use crate::{
ImportPlugin, PluginContext, PluginInfo,
musicbrainz::reltypes::{VGMDB, WIKIDATA},
},
+ source_rank::ObjectImportSourceExt,
};
use anyhow::{Context, Result};
use jellycache::Cache;
@@ -339,6 +340,7 @@ impl ImportPlugin for MusicBrainz {
fn info(&self) -> PluginInfo {
PluginInfo {
name: "musicbrainz",
+ tag: MSOURCE_MUSICBRAINZ,
handle_process: true,
..Default::default()
}
@@ -367,9 +369,11 @@ impl MusicBrainz {
ct.ic.db.transaction(&mut |txn| {
let mut node = txn.get(node_row)?.unwrap();
- node = node.as_object().insert(NO_TITLE, &rec.title);
+ node = node.as_object().insert_s(ct.is, NO_TITLE, &rec.title);
if let Some(a) = rec.artist_credit.first() {
- node = node.as_object().insert(NO_SUBTITLE, &a.artist.name);
+ node = node
+ .as_object()
+ .insert_s(ct.is, NO_SUBTITLE, &a.artist.name);
}
node = node.as_object().update(NO_IDENTIFIERS, |ids| {
ids.insert_multi(
@@ -436,8 +440,7 @@ impl MusicBrainz {
ct.ic.db.transaction(&mut |txn| {
let mut node = txn.get(node_row)?.unwrap();
-
- node = node.as_object().insert(NO_TITLE, &artist.name);
+ node = node.as_object().insert_s(ct.is, NO_TITLE, &artist.name);
for rel in &artist.relations {
let url = rel.url.as_ref().map(|u| u.resource.clone());
diff --git a/import/src/plugins/omdb.rs b/import/src/plugins/omdb.rs
index bb58c6b..20fb933 100644
--- a/import/src/plugins/omdb.rs
+++ b/import/src/plugins/omdb.rs
@@ -9,7 +9,8 @@ use std::sync::Arc;
use anyhow::{Context, Result, anyhow};
use jellycache::Cache;
use jellycommon::{
- IDENT_IMDB, NO_IDENTIFIERS, NO_RATINGS, RTYP_IMDB, RTYP_METACRITIC, RTYP_ROTTEN_TOMATOES,
+ IDENT_IMDB, MSOURCE_OMDB, NO_DESCRIPTION, NO_IDENTIFIERS, NO_RATINGS, NO_TITLE, RTYP_IMDB,
+ RTYP_METACRITIC, RTYP_ROTTEN_TOMATOES,
};
use jellydb::RowNum;
use log::info;
@@ -23,6 +24,7 @@ use tokio::runtime::Handle;
use crate::{
USER_AGENT,
plugins::{ImportPlugin, PluginContext, PluginInfo},
+ source_rank::ObjectImportSourceExt,
};
pub struct Omdb {
@@ -115,6 +117,7 @@ impl ImportPlugin for Omdb {
fn info(&self) -> PluginInfo {
PluginInfo {
name: "omdb",
+ tag: MSOURCE_OMDB,
handle_process: true,
..Default::default()
}
@@ -151,6 +154,11 @@ impl ImportPlugin for Omdb {
.transpose()?;
ct.ic.update_node(node, |mut node| {
+ node = node.as_object().insert_s(ct.is, NO_TITLE, &entry.title);
+ node = node
+ .as_object()
+ .insert_s(ct.is, NO_DESCRIPTION, &entry.plot);
+
for (typ, val) in [
(RTYP_METACRITIC, metascore),
(RTYP_IMDB, imdb),
diff --git a/import/src/plugins/tags.rs b/import/src/plugins/tags.rs
index bd7d0bc..b60ef37 100644
--- a/import/src/plugins/tags.rs
+++ b/import/src/plugins/tags.rs
@@ -4,7 +4,10 @@
Copyright (C) 2026 metamuffin <metamuffin.org>
*/
-use crate::plugins::{PluginContext, ImportPlugin, PluginInfo};
+use crate::{
+ plugins::{ImportPlugin, PluginContext, PluginInfo},
+ source_rank::ObjectImportSourceExt,
+};
use anyhow::Result;
use jellycommon::*;
use jellydb::RowNum;
@@ -16,6 +19,7 @@ impl ImportPlugin for Tags {
fn info(&self) -> PluginInfo {
PluginInfo {
name: "tags",
+ tag: MSOURCE_TAGS,
handle_media: true,
..Default::default()
}
@@ -35,16 +39,17 @@ impl ImportPlugin for Tags {
ct.ic.update_node(node, |mut node| {
if let Some(title) = &seg.info.title {
- node = node.as_object().insert(NO_TITLE, title);
+ node = node.as_object().insert_s(ct.is, NO_TITLE, title);
}
for (key, value) in &tags {
match key.as_str() {
"DESCRIPTION" | "SYNOPSIS" => {
- node = node.as_object().insert(NO_DESCRIPTION, &value)
+ node = node.as_object().insert_s(ct.is, NO_DESCRIPTION, &value)
}
"COMMENT" => node = node.as_object().insert(NO_TAGLINE, &value),
"CONTENT_TYPE" => {
- node = node.as_object().insert(
+ node = node.as_object().insert_s(
+ ct.is,
NO_KIND,
match value.to_lowercase().trim() {
"movie" | "documentary" | "film" => KIND_MOVIE,
diff --git a/import/src/plugins/tmdb.rs b/import/src/plugins/tmdb.rs
index ab0a679..db336fe 100644
--- a/import/src/plugins/tmdb.rs
+++ b/import/src/plugins/tmdb.rs
@@ -6,6 +6,7 @@
use crate::{
USER_AGENT,
plugins::{ImportPlugin, PluginContext, PluginInfo},
+ source_rank::ObjectImportSourceExt,
};
use anyhow::{Context, Result, anyhow, bail};
use chrono::{Utc, format::Parsed};
@@ -180,6 +181,7 @@ impl ImportPlugin for Tmdb {
fn info(&self) -> PluginInfo {
PluginInfo {
name: "tmdb",
+ tag: MSOURCE_TMDB,
handle_process: true,
..Default::default()
}
@@ -235,27 +237,31 @@ impl Tmdb {
ct.ic.update_node(node, |mut node| {
if let Some(title) = &details.title {
- node = node.as_object().insert(NO_TITLE, &title);
+ node = node.as_object().insert_s(ct.is, NO_TITLE, &title);
}
if let Some(tagline) = &details.tagline {
- node = node.as_object().insert(NO_TAGLINE, &tagline);
+ node = node.as_object().insert_s(ct.is, NO_TAGLINE, &tagline);
}
- node = node.as_object().insert(NO_DESCRIPTION, &details.overview);
+ node = node
+ .as_object()
+ .insert_s(ct.is, NO_DESCRIPTION, &details.overview);
node = node.as_object().update(NO_RATINGS, |rat| {
rat.insert(RTYP_TMDB, details.vote_average)
});
if let Some(poster) = &poster {
node = node
.as_object()
- .update(NO_PICTURES, |rat| rat.insert(PICT_COVER, &poster));
+ .update(NO_PICTURES, |rat| rat.insert_s(ct.is, PICT_COVER, &poster));
}
if let Some(backdrop) = &backdrop {
- node = node
- .as_object()
- .update(NO_PICTURES, |rat| rat.insert(PICT_BACKDROP, &backdrop));
+ node = node.as_object().update(NO_PICTURES, |rat| {
+ rat.insert_s(ct.is, PICT_BACKDROP, &backdrop)
+ });
}
if let Some(releasedate) = release_date {
- node = node.as_object().insert(NO_RELEASEDATE, releasedate);
+ node = node
+ .as_object()
+ .insert_s(ct.is, NO_RELEASEDATE, releasedate);
}
node
})?;
@@ -296,18 +302,22 @@ impl Tmdb {
.context("still image download")?;
let release_date = parse_release_date(&details.air_date)?;
ct.ic.update_node(node, |mut node| {
- node = node.as_object().insert(NO_TITLE, &details.name);
- node = node.as_object().insert(NO_DESCRIPTION, &details.overview);
+ node = node.as_object().insert_s(ct.is, NO_TITLE, &details.name);
+ node = node
+ .as_object()
+ .insert_s(ct.is, NO_DESCRIPTION, &details.overview);
if let Some(release_date) = release_date {
- node = node.as_object().insert(NO_RELEASEDATE, release_date)
+ node = node
+ .as_object()
+ .insert_s(ct.is, NO_RELEASEDATE, release_date)
}
node = node.as_object().update(NO_RATINGS, |rat| {
rat.insert(RTYP_TMDB, details.vote_average)
});
if let Some(cover) = &cover {
- node = node
- .as_object()
- .update(NO_PICTURES, |picts| picts.insert(PICT_COVER, &cover));
+ node = node.as_object().update(NO_PICTURES, |picts| {
+ picts.insert_s(ct.is, PICT_COVER, &cover)
+ });
}
node
})
@@ -335,7 +345,7 @@ impl Tmdb {
ct.ic.update_node(node, |node| {
node.as_object()
- .update(NO_PICTURES, |pict| pict.insert(PICT_COVER, &image))
+ .update(NO_PICTURES, |pict| pict.insert_s(ct.is, PICT_COVER, &image))
})?;
Ok(())
diff --git a/import/src/plugins/trakt.rs b/import/src/plugins/trakt.rs
index 1d01436..cc3b119 100644
--- a/import/src/plugins/trakt.rs
+++ b/import/src/plugins/trakt.rs
@@ -7,6 +7,7 @@ use crate::{
USER_AGENT,
helpers::get_or_insert_slug,
plugins::{ImportPlugin, PluginContext, PluginInfo},
+ source_rank::ObjectImportSourceExt,
};
use anyhow::{Context, Result, anyhow, bail};
use jellycache::{Cache, HashKey};
@@ -404,6 +405,7 @@ impl ImportPlugin for Trakt {
fn info(&self) -> PluginInfo {
PluginInfo {
name: "trakt",
+ tag: MSOURCE_TRAKT,
handle_instruction: true,
handle_process: true,
..Default::default()
@@ -470,13 +472,15 @@ impl Trakt {
ct.ic.db.transaction(&mut |txn| {
let mut node = txn.get(node_row)?.unwrap();
- node = node.as_object().insert(NO_KIND, trakt_kind.as_node_kind());
- node = node.as_object().insert(NO_TITLE, &details.title);
+ node = node
+ .as_object()
+ .insert_s(ct.is, NO_KIND, trakt_kind.as_node_kind());
+ node = node.as_object().insert_s(ct.is, NO_TITLE, &details.title);
if let Some(overview) = &details.overview {
- node = node.as_object().insert(NO_DESCRIPTION, &overview);
+ node = node.as_object().insert_s(ct.is, NO_DESCRIPTION, &overview);
}
if let Some(tagline) = &details.tagline {
- node = node.as_object().insert(NO_TAGLINE, &tagline);
+ node = node.as_object().insert_s(ct.is, NO_TAGLINE, &tagline);
}
if let Some(x) = details.ids.imdb.clone() {
node = node
@@ -520,9 +524,9 @@ impl Trakt {
let row = get_or_insert_slug(txn, &slug)?;
let mut c = txn.get(row)?.unwrap();
- c = c.as_object().insert(NO_KIND, KIND_PERSON);
- c = c.as_object().insert(NO_VISIBILITY, VISI_VISIBLE);
- c = c.as_object().insert(NO_TITLE, &ap.person.name);
+ c = c.as_object().insert_s(ct.is, NO_KIND, KIND_PERSON);
+ c = c.as_object().insert_s(ct.is, NO_VISIBILITY, VISI_VISIBLE);
+ c = c.as_object().insert_s(ct.is, NO_TITLE, &ap.person.name);
c = c.as_object().update(NO_IDENTIFIERS, |ids| {
let mut ids = ids.insert(IDENT_TRAKT_PERSON, &traktid.to_string());
if let Some(tmdbid) = ap.person.ids.tmdb {
@@ -591,11 +595,11 @@ impl Trakt {
let episodes = self.show_season_episodes(&ct.ic.cache, show_id, season, ct.rt)?;
if let Some(episode) = episodes.get(episode.saturating_sub(1) as usize) {
ct.ic.update_node(node, |mut node| {
- node = node.as_object().insert(NO_KIND, KIND_EPISODE);
- node = node.as_object().insert(NO_INDEX, episode.number);
- node = node.as_object().insert(NO_TITLE, &episode.title);
+ node = node.as_object().insert_s(ct.is, NO_KIND, KIND_EPISODE);
+ node = node.as_object().insert_s(ct.is, NO_INDEX, episode.number);
+ node = node.as_object().insert_s(ct.is, NO_TITLE, &episode.title);
if let Some(overview) = &episode.overview {
- node = node.as_object().insert(NO_DESCRIPTION, &overview);
+ node = node.as_object().insert_s(ct.is, NO_DESCRIPTION, &overview);
}
if let Some(r) = episode.rating {
node = node
diff --git a/import/src/plugins/vgmdb.rs b/import/src/plugins/vgmdb.rs
index b7630d2..4e1b273 100644
--- a/import/src/plugins/vgmdb.rs
+++ b/import/src/plugins/vgmdb.rs
@@ -7,6 +7,7 @@
use crate::{
USER_AGENT,
plugins::{ImportPlugin, PluginContext, PluginInfo},
+ source_rank::ObjectImportSourceExt,
};
use anyhow::{Context, Result};
use jellycache::{Cache, HashKey};
@@ -140,6 +141,7 @@ impl ImportPlugin for Vgmdb {
fn info(&self) -> PluginInfo {
PluginInfo {
name: "vgmdb",
+ tag: MSOURCE_VGMDB,
handle_process: true,
..Default::default()
}
@@ -164,7 +166,7 @@ impl ImportPlugin for Vgmdb {
ct.ic.update_node(node, |node| {
node.as_object()
- .update(NO_PICTURES, |pics| pics.insert(PICT_COVER, &image))
+ .update(NO_PICTURES, |pics| pics.insert_s(ct.is, PICT_COVER, &image))
})?;
Ok(())
diff --git a/import/src/plugins/wikidata.rs b/import/src/plugins/wikidata.rs
index 2286f8d..b92fdbb 100644
--- a/import/src/plugins/wikidata.rs
+++ b/import/src/plugins/wikidata.rs
@@ -7,6 +7,7 @@
use crate::{
USER_AGENT,
plugins::{ImportPlugin, PluginContext, PluginInfo},
+ source_rank::ObjectImportSourceExt,
};
use anyhow::{Context, Result, bail};
use jellycache::{Cache, EscapeKey};
@@ -195,6 +196,7 @@ impl ImportPlugin for Wikidata {
fn info(&self) -> PluginInfo {
PluginInfo {
name: "wikidata",
+ tag: MSOURCE_WIKIDATA,
handle_process: true,
..Default::default()
}
@@ -221,7 +223,7 @@ impl ImportPlugin for Wikidata {
ct.ic.update_node(node, |node| {
node.as_object()
- .update(NO_PICTURES, |pics| pics.insert(PICT_COVER, &image))
+ .update(NO_PICTURES, |pics| pics.insert_s(ct.is, PICT_COVER, &image))
})?;
Ok(())
diff --git a/import/src/source_rank.rs b/import/src/source_rank.rs
new file mode 100644
index 0000000..28ab4f7
--- /dev/null
+++ b/import/src/source_rank.rs
@@ -0,0 +1,85 @@
+/*
+ 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) 2026 metamuffin <metamuffin.org>
+*/
+
+use jellycommon::{
+ jellyobject::{Object, ObjectBuffer, Tag, TypedTag, ValueStore},
+ *,
+};
+use std::marker::PhantomData;
+
+pub struct SourceRanks {
+ list: Vec<Tag>,
+}
+
+#[derive(Clone, Copy)]
+pub struct ImportSource<'a> {
+ pub tag: Tag,
+ pub ranks: &'a SourceRanks,
+}
+
+pub trait ObjectImportSourceExt {
+ fn insert_s<T: ValueStore>(&self, is: ImportSource, key: TypedTag<T>, value: T)
+ -> ObjectBuffer;
+}
+impl<'a> ObjectImportSourceExt for Object<'a> {
+ fn insert_s<T: ValueStore>(
+ &self,
+ is: ImportSource,
+ key: TypedTag<T>,
+ value: T,
+ ) -> ObjectBuffer {
+ let ms = self.get(NO_METASOURCE).unwrap_or_default();
+ let ms_key = TypedTag::<Tag>(key.0, PhantomData);
+
+ if let Some(current_source) = ms.get(ms_key) {
+ if !is.ranks.compare(key.0, current_source, is.tag) {
+ return self.dump();
+ }
+ }
+
+ self.insert(key, value)
+ .as_object()
+ .update(NO_METASOURCE, |ms| ms.insert(ms_key, is.tag))
+ }
+}
+
+impl SourceRanks {
+ pub fn new() -> Self {
+ Self {
+ list: [
+ MSOURCE_EXPLICIT,
+ MSOURCE_TRAKT,
+ MSOURCE_MUSICBRAINZ,
+ MSOURCE_MEDIA,
+ MSOURCE_TAGS,
+ MSOURCE_IMAGE_ATT,
+ MSOURCE_TMDB,
+ MSOURCE_WIKIDATA,
+ MSOURCE_VGMDB,
+ MSOURCE_ACOUSTID,
+ MSOURCE_INFOJSON,
+ MSOURCE_OMDB,
+ ]
+ .to_vec(),
+ }
+ }
+ pub fn compare(&self, key: Tag, old: Tag, new: Tag) -> bool {
+ let _ = key;
+
+ let old_index = self
+ .list
+ .iter()
+ .position(|e| *e == old)
+ .unwrap_or(usize::MAX);
+ let new_index = self
+ .list
+ .iter()
+ .position(|e| *e == new)
+ .unwrap_or(usize::MAX);
+
+ new_index <= old_index
+ }
+}