diff options
-rw-r--r-- | import/src/acoustid.rs | 26 | ||||
-rw-r--r-- | import/src/lib.rs | 98 | ||||
-rw-r--r-- | locale/en.ini | 1 | ||||
-rw-r--r-- | server/src/routes/ui/node.rs | 2 |
4 files changed, 77 insertions, 50 deletions
diff --git a/import/src/acoustid.rs b/import/src/acoustid.rs index 8e8a603..49c5e38 100644 --- a/import/src/acoustid.rs +++ b/import/src/acoustid.rs @@ -25,12 +25,18 @@ pub(crate) struct AcoustID { rate_limit: Arc<Semaphore>, } -#[derive(Debug, Hash, Clone, Encode, Decode, Deserialize)] +#[derive(Debug, Hash, Clone, Encode, Decode)] pub(crate) struct Fingerprint { duration: u32, fingerprint: String, } +#[derive(Debug, Deserialize)] +pub(crate) struct FpCalcOutput { + duration: f32, + fingerprint: String, +} + #[derive(Deserialize, Encode, Decode)] pub(crate) struct AcoustIDLookupResultRecording { id: String, @@ -65,8 +71,8 @@ impl AcoustID { } } - pub async fn get_atid_mbid(&self, fp: Fingerprint) -> Result<Option<(String, String)>> { - let res = self.lookup(fp).await?; + pub async fn get_atid_mbid(&self, fp: &Fingerprint) -> Result<Option<(String, String)>> { + let res = self.lookup(fp.to_owned()).await?; for r in &res.results { if let Some(k) = r.recordings.get(0) { return Ok(Some((r.id.clone(), k.id.clone()))); @@ -81,19 +87,19 @@ impl AcoustID { let permit_drop_ts = Instant::now() + Duration::SECOND; let duration = fp.duration; - let fingerprint = &fp.fingerprint; + let fingerprint = fp.fingerprint.replace("=", "%3D"); let client = &self.key; - let meta = "recordingids"; - let body = format!("format=json&client={client}&duration={duration}&fingerprint={fingerprint}&meta={meta}"); + let body = format!("format=json&meta=recordingids&client={client}&duration={duration}&fingerprint={fingerprint}"); let resp = self .client .post(format!("https://api.acoustid.org/v2/lookup")) + // .post(format!("http://127.0.0.1:1234/v2/lookup")) + .header("Content-Type", "application/x-www-form-urlencoded") .body(body) .send() .await?.error_for_status()?.json::<AcoustIDLookupResponse>().await?; - tokio::task::spawn(async move { sleep_until(permit_drop_ts).await; drop(_permit); @@ -116,7 +122,11 @@ pub(crate) async fn acoustid_fingerprint(path: &Path) -> Result<Arc<Fingerprint> let mut buf = Vec::new(); child.stdout.unwrap().read_to_end(&mut buf).await?; - let out: Fingerprint = serde_json::from_slice(&buf)?; + let out: FpCalcOutput = serde_json::from_slice(&buf)?; + let out = Fingerprint { + duration: out.duration as u32, + fingerprint: out.fingerprint, + }; Ok(out) }) diff --git a/import/src/lib.rs b/import/src/lib.rs index 452d9b9..bd01fc9 100644 --- a/import/src/lib.rs +++ b/import/src/lib.rs @@ -4,7 +4,7 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ #![feature(duration_constants)] -use acoustid::AcoustID; +use acoustid::{acoustid_fingerprint, AcoustID}; use anyhow::{anyhow, bail, Context, Result}; use infojson::YVideo; use jellybase::{ @@ -89,12 +89,21 @@ fn import(db: &Database, incremental: bool) -> Result<()> { incremental, NodeID::MIN, "", - Visibility::Visible, + InheritedFlags { + visibility: Visibility::Visible, + use_acoustid: false, + }, )?; Ok(()) } +#[derive(Debug, Clone, Copy)] +struct InheritedFlags { + visibility: Visibility, + use_acoustid: bool, +} + fn import_traverse( path: &Path, db: &Database, @@ -103,7 +112,7 @@ fn import_traverse( incremental: bool, parent: NodeID, parent_slug_fragment: &str, - mut visibility: Visibility, + mut iflags: InheritedFlags, ) -> Result<()> { if path.is_dir() { let slug_fragment = if path == CONF.media_path { @@ -122,8 +131,9 @@ fn import_traverse( 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), + "hidden" => iflags.visibility = iflags.visibility.min(Visibility::Hidden), + "reduced" => iflags.visibility = iflags.visibility.min(Visibility::Reduced), + "use_acoustid" => iflags.use_acoustid = true, _ => (), } } @@ -134,7 +144,7 @@ fn import_traverse( n.parents.insert(parent); } n.slug = slug; - n.visibility = visibility; + n.visibility = iflags.visibility; Ok(()) })?; @@ -148,7 +158,7 @@ fn import_traverse( incremental, id, &slug_fragment, - visibility, + iflags, ) { IMPORT_ERRORS .blocking_write() @@ -170,7 +180,7 @@ fn import_traverse( } } - import_file(db, apis, rthandle, path, parent, visibility)?; + import_file(db, apis, rthandle, path, parent, iflags)?; db.set_import_file_mtime(path, mtime)?; } Ok(()) @@ -182,7 +192,7 @@ fn import_file( rthandle: &Handle, path: &Path, parent: NodeID, - visibility: Visibility, + iflags: InheritedFlags, ) -> Result<()> { let filename = path.file_name().unwrap().to_string_lossy(); match filename.as_ref() { @@ -265,9 +275,7 @@ fn import_file( Ok(()) })?; } - _ => { - import_media_file(db, apis, rthandle, path, parent, visibility).context("media file")? - } + _ => import_media_file(db, apis, rthandle, path, parent, iflags).context("media file")?, } Ok(()) @@ -279,7 +287,7 @@ fn import_media_file( rthandle: &Handle, path: &Path, parent: NodeID, - visibility: Visibility, + iflags: InheritedFlags, ) -> Result<()> { info!("media file {path:?}"); let Some(m) = (*checked_matroska_metadata(path)?).to_owned() else { @@ -348,11 +356,44 @@ fn import_media_file( let node = NodeID::from_slug(&slug); let meta = path.metadata()?; + let mut eids = BTreeMap::new(); + + for (key, value) in &tags { + match key.as_str() { + "MUSICBRAINZ_TRACKID" => eids.insert("musicbrainz.track".to_string(), value.to_owned()), + "MUSICBRAINZ_ARTISTID" => { + eids.insert("musicbrainz.artist".to_string(), value.to_owned()) + } + "MUSICBRAINZ_ALBUMID" => eids.insert("musicbrainz.album".to_string(), value.to_owned()), + "MUSICBRAINZ_ALBUMARTISTID" => { + eids.insert("musicbrainz.albumartist".to_string(), value.to_owned()) + } + "MUSICBRAINZ_RELEASEGROUPID" => { + eids.insert("musicbrainz.releasegroup".to_string(), value.to_owned()) + } + "ISRC" => eids.insert("isrc".to_string(), value.to_owned()), + "BARCODE" => eids.insert("barcode".to_string(), value.to_owned()), + _ => None, + }; + } + + if iflags.use_acoustid { + let fp = rthandle.block_on(acoustid_fingerprint(path))?; + if let Some((atid, mbid)) = rthandle.block_on( + apis.acoustid + .as_ref() + .ok_or(anyhow!("need acoustid"))? + .get_atid_mbid(&fp), + )? { + eids.insert("acoustid.track".to_string(), atid); + eids.insert("musicbrainz.track".to_string(), mbid); + }; + } db.update_node_init(node, |node| { node.slug = slug; node.title = info.title.or(node.title.clone()); - node.visibility = visibility; + node.visibility = iflags.visibility; node.poster = m.cover.or(node.poster.clone()); node.description = tags .remove("DESCRIPTION") @@ -361,6 +402,8 @@ fn import_media_file( node.tagline = tags.remove("COMMENT").or(node.tagline.clone()); node.parents.insert(parent); + node.external_ids.extend(eids); + if let Some(ct) = tags.get("CONTENT_TYPE") { node.kind = match ct.to_lowercase().trim() { "movie" | "documentary" | "film" => NodeKind::Movie, @@ -369,33 +412,6 @@ fn import_media_file( } } - for (key, value) in &tags { - match key.as_str() { - "MUSICBRAINZ_TRACKID" => node - .external_ids - .insert("musicbrainz.track".to_string(), value.to_owned()), - "MUSICBRAINZ_ARTISTID" => node - .external_ids - .insert("musicbrainz.artist".to_string(), value.to_owned()), - "MUSICBRAINZ_ALBUMID" => node - .external_ids - .insert("musicbrainz.album".to_string(), value.to_owned()), - "MUSICBRAINZ_ALBUMARTISTID" => node - .external_ids - .insert("musicbrainz.albumartist".to_string(), value.to_owned()), - "MUSICBRAINZ_RELEASEGROUPID" => node - .external_ids - .insert("musicbrainz.releasegroup".to_string(), value.to_owned()), - "ISRC" => node - .external_ids - .insert("isrc".to_string(), value.to_owned()), - "BARCODE" => node - .external_ids - .insert("barcode".to_string(), value.to_owned()), - _ => None, - }; - } - let tracks = tracks .entries .into_iter() diff --git a/locale/en.ini b/locale/en.ini index cd8e5e9..8705ae1 100644 --- a/locale/en.ini +++ b/locale/en.ini @@ -164,6 +164,7 @@ eid.musicbrainz.albumartist=MusicBrainz Album Artist ID eid.musicbrainz.artist=MusicBrainz Artist ID eid.musicbrainz.releasegroup=MusicBrainz Release Group ID eid.musicbrainz.track=MusicBrainz Track ID +eid.acoustid.track=AcoustID Track ID eid.youtube.channelname=YouTube Channel Handle eid.youtube.channelname=YouTube Channel ID eid.youtube.video=YouTube Video ID diff --git a/server/src/routes/ui/node.rs b/server/src/routes/ui/node.rs index 8d06f36..6d2151f 100644 --- a/server/src/routes/ui/node.rs +++ b/server/src/routes/ui/node.rs @@ -551,7 +551,7 @@ fn external_id_url(key: &str, value: &str) -> Option<String> { "musicbrainz.album" => format!("https://musicbrainz.org/release/{value}"), "musicbrainz.albumartist" => format!("https://musicbrainz.org/artist/{value}"), "musicbrainz.artist" => format!("https://musicbrainz.org/artist/{value}"), - "musicbrainz.releasegroup" => format!("https://musicbrainz.org/releasegroup/{value}"), + "musicbrainz.releasegroup" => format!("https://musicbrainz.org/release-group/{value}"), "musicbrainz.track" => format!("https://musicbrainz.org/recording/{value}"), _ => return None, }) |