Update for the "full flow"
This commit is contained in:
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user