Compare commits

...

1 Commits

Author SHA1 Message Date
Connor Johnstone
0572722184 Update for the "full flow" 2026-03-17 21:39:08 -04:00
4 changed files with 118 additions and 44 deletions

View File

@@ -6,7 +6,7 @@ use crate::cleaning::escape_lucene;
use crate::error::{TagError, TagResult};
use crate::provider::{
ArtistSearchResult, DiscographyEntry, MetadataProvider, RecordingDetails, RecordingMatch,
ReleaseMatch, ReleaseRef,
ReleaseMatch, ReleaseRef, ReleaseTrack,
};
const BASE_URL: &str = "https://musicbrainz.org/ws/2";
@@ -215,6 +215,31 @@ impl MetadataProvider for MusicBrainzClient {
})
.collect())
}
async fn get_release_tracks(
&self,
release_mbid: &str,
) -> TagResult<Vec<ReleaseTrack>> {
let url = format!(
"{BASE_URL}/release/{release_mbid}?inc=recordings&fmt=json"
);
let resp: MbReleaseDetail = self.get_json(&url).await?;
let mut tracks = Vec::new();
for (disc_idx, medium) in resp.media.unwrap_or_default().into_iter().enumerate() {
for track in medium.tracks.unwrap_or_default() {
tracks.push(ReleaseTrack {
recording_mbid: track.recording.map(|r| r.id).unwrap_or_default(),
title: track.title,
track_number: track.position,
disc_number: Some(disc_idx as i32 + 1),
duration_ms: track.length,
});
}
}
Ok(tracks)
}
}
fn extract_artist_credit(credits: &Option<Vec<MbArtistCredit>>) -> (String, Option<String>) {
@@ -321,3 +346,26 @@ struct MbArtist {
struct MbGenre {
name: String,
}
#[derive(Deserialize)]
struct MbReleaseDetail {
media: Option<Vec<MbMedia>>,
}
#[derive(Deserialize)]
struct MbMedia {
tracks: Option<Vec<MbTrackEntry>>,
}
#[derive(Deserialize)]
struct MbTrackEntry {
title: String,
position: Option<i32>,
length: Option<u64>,
recording: Option<MbTrackRecording>,
}
#[derive(Deserialize)]
struct MbTrackRecording {
id: String,
}

View File

@@ -99,4 +99,19 @@ pub trait MetadataProvider: Send + Sync {
artist_mbid: &str,
limit: u32,
) -> impl std::future::Future<Output = TagResult<Vec<DiscographyEntry>>> + Send;
fn get_release_tracks(
&self,
release_mbid: &str,
) -> impl std::future::Future<Output = TagResult<Vec<ReleaseTrack>>> + Send;
}
/// A track within a release.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReleaseTrack {
pub recording_mbid: String,
pub title: String,
pub track_number: Option<i32>,
pub disc_number: Option<i32>,
pub duration_ms: Option<u64>,
}

View File

@@ -51,52 +51,60 @@ pub async fn tag_track(
track: &track::Model,
config: &TagConfig,
) -> TagResult<bool> {
// Build search query
let (artist, title) = match matcher::build_query(track) {
Some(q) => q,
None => {
tracing::debug!(id = track.id, path = %track.file_path, "no query possible, skipping");
// If the track already has an MBID, skip searching and go straight to lookup
let (details, best_release) = if let Some(ref mbid) = track.musicbrainz_id {
tracing::info!(id = track.id, mbid = %mbid, "looking up recording by MBID");
if config.dry_run {
tracing::info!(id = track.id, mbid = %mbid, "DRY RUN: would enrich from MBID");
return Ok(true);
}
let details = provider.get_recording(mbid).await?;
let best_release = details.releases.first().cloned();
(details, best_release)
} else {
// No MBID — search by artist + title
let (artist, title) = match matcher::build_query(track) {
Some(q) => q,
None => {
tracing::debug!(id = track.id, path = %track.file_path, "no query possible, skipping");
return Ok(false);
}
};
tracing::info!(id = track.id, artist = %artist, title = %title, "searching MusicBrainz");
let candidates = provider.search_recording(&artist, &title).await?;
if candidates.is_empty() {
tracing::debug!(id = track.id, "no results from MusicBrainz");
return Ok(false);
}
};
tracing::info!(
id = track.id,
artist = %artist,
title = %title,
"searching MusicBrainz"
);
let best = match matcher::select_best_match(track, candidates, config.confidence) {
Some(m) => m,
None => {
tracing::debug!(
id = track.id,
"no match above confidence threshold {}",
config.confidence
);
return Ok(false);
}
};
// Search for recordings
let candidates = provider.search_recording(&artist, &title).await?;
log_match(track, &best);
if candidates.is_empty() {
tracing::debug!(id = track.id, "no results from MusicBrainz");
return Ok(false);
}
// Score and select best match
let best = match matcher::select_best_match(track, candidates, config.confidence) {
Some(m) => m,
None => {
tracing::debug!(
id = track.id,
"no match above confidence threshold {}",
config.confidence
);
return Ok(false);
if config.dry_run {
return Ok(true);
}
let details = provider.get_recording(&best.recording.mbid).await?;
let best_release = best.best_release;
(details, best_release)
};
log_match(track, &best);
if config.dry_run {
return Ok(true);
}
// Get full details for the best match
let details = provider.get_recording(&best.recording.mbid).await?;
// Upsert artist with MusicBrainz ID
let artist_id = match &details.artist_mbid {
Some(mbid) => {
@@ -108,7 +116,7 @@ pub async fn tag_track(
};
// Upsert album from best release
let (album_id, album_name) = if let Some(ref release) = best.best_release {
let (album_id, album_name) = if let Some(ref release) = best_release {
let album = queries::albums::upsert(
conn,
&release.title,
@@ -123,8 +131,7 @@ pub async fn tag_track(
};
// Parse year from release date
let year = best
.best_release
let year = best_release
.as_ref()
.and_then(|r| r.date.as_deref())
.and_then(|d| d.split('-').next())
@@ -164,7 +171,7 @@ pub async fn tag_track(
if let Err(e) = file_tags::write_tags(
&track.file_path,
&details,
best.best_release.as_ref(),
best_release.as_ref(),
year,
genre.as_deref(),
) {
@@ -196,7 +203,7 @@ pub async fn run_tagging(
let tracks: Vec<track::Model> = if let Some(id) = track_id {
vec![queries::tracks::get_by_id(conn, id).await?]
} else {
queries::tracks::get_untagged(conn).await?
queries::tracks::get_needing_metadata(conn).await?
};
tracing::info!(count = tracks.len(), "tracks to process");

View File

@@ -66,6 +66,10 @@ impl MetadataProvider for MockProvider {
async fn get_artist_releases(&self, _artist_mbid: &str, _limit: u32) -> TagResult<Vec<DiscographyEntry>> {
Ok(vec![])
}
async fn get_release_tracks(&self, _release_mbid: &str) -> TagResult<Vec<shanty_tag::provider::ReleaseTrack>> {
Ok(vec![])
}
}
async fn test_db() -> Database {