From 5957d69e7dc2ec5d6c35b335544d28d87a0a8831 Mon Sep 17 00:00:00 2001 From: Connor Johnstone Date: Wed, 18 Mar 2026 15:36:54 -0400 Subject: [PATCH] Formatting --- src/cleaning.rs | 9 ++++-- src/file_tags.rs | 4 +-- src/main.rs | 5 ++- src/matcher.rs | 10 ++---- src/musicbrainz.rs | 52 ++++++++++++------------------ src/tagger.rs | 26 ++++++++------- tests/integration.rs | 76 ++++++++++++++++++++++++++++++++++---------- 7 files changed, 107 insertions(+), 75 deletions(-) diff --git a/src/cleaning.rs b/src/cleaning.rs index 5831b9b..80df29a 100644 --- a/src/cleaning.rs +++ b/src/cleaning.rs @@ -45,8 +45,8 @@ pub fn normalize(s: &str) -> String { /// Escape special characters for MusicBrainz Lucene query syntax. pub fn escape_lucene(s: &str) -> String { let special = [ - '+', '-', '&', '|', '!', '(', ')', '{', '}', '[', ']', '^', '"', '~', '*', '?', ':', - '\\', '/', + '+', '-', '&', '|', '!', '(', ')', '{', '}', '[', ']', '^', '"', '~', '*', '?', ':', '\\', + '/', ]; let mut result = String::with_capacity(s.len()); for c in s.chars() { @@ -72,7 +72,10 @@ mod tests { fn test_normalize_strips_official_video() { assert_eq!(normalize("Time (Official Video)"), "time"); assert_eq!(normalize("Money (Official Music Video)"), "money"); - assert_eq!(normalize("Comfortably Numb (Official Audio)"), "comfortably numb"); + assert_eq!( + normalize("Comfortably Numb (Official Audio)"), + "comfortably numb" + ); } #[test] diff --git a/src/file_tags.rs b/src/file_tags.rs index 3a9d3cb..ed80785 100644 --- a/src/file_tags.rs +++ b/src/file_tags.rs @@ -31,9 +31,7 @@ pub fn write_tags( ) -> TagResult<()> { let path = Path::new(file_path); - let tagged_file = Probe::open(path)? - .options(ParseOptions::default()) - .read()?; + let tagged_file = Probe::open(path)?.options(ParseOptions::default()).read()?; // Determine the tag type to use let tag_type = tagged_file diff --git a/src/main.rs b/src/main.rs index 9f4c816..1c575b4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,10 @@ use shanty_db::Database; use shanty_tag::{MusicBrainzClient, TagConfig, run_tagging}; #[derive(Parser)] -#[command(name = "shanty-tag", about = "Fill in missing metadata on music files via MusicBrainz")] +#[command( + name = "shanty-tag", + about = "Fill in missing metadata on music files via MusicBrainz" +)] struct Cli { /// Database URL. Defaults to sqlite:///shanty/shanty.db?mode=rwc #[arg(long, env = "SHANTY_DATABASE_URL")] diff --git a/src/matcher.rs b/src/matcher.rs index 4e55e35..b8d3909 100644 --- a/src/matcher.rs +++ b/src/matcher.rs @@ -30,9 +30,7 @@ pub fn build_query(track: &track::Model) -> Option<(String, String)> { /// Parse "Artist - Title" from a filename, stripping extension and path. pub fn parse_filename(file_path: &str) -> Option<(String, String)> { - let filename = std::path::Path::new(file_path) - .file_stem()? - .to_str()?; + let filename = std::path::Path::new(file_path).file_stem()?.to_str()?; // Try common "Artist - Title" pattern if let Some((artist, title)) = filename.split_once(" - ") { @@ -55,11 +53,7 @@ pub fn parse_filename(file_path: &str) -> Option<(String, String)> { /// Score a candidate recording against the track's known metadata. /// Returns a confidence value from 0.0 to 1.0. pub fn score_match(track: &track::Model, candidate: &RecordingMatch) -> f64 { - let track_title = track - .title - .as_deref() - .map(normalize) - .unwrap_or_default(); + let track_title = track.title.as_deref().map(normalize).unwrap_or_default(); let candidate_title = normalize(&candidate.title); let track_artist = track diff --git a/src/musicbrainz.rs b/src/musicbrainz.rs index a36ea3b..54654bb 100644 --- a/src/musicbrainz.rs +++ b/src/musicbrainz.rs @@ -64,11 +64,7 @@ impl MusicBrainzClient { } impl MetadataProvider for MusicBrainzClient { - async fn search_recording( - &self, - artist: &str, - title: &str, - ) -> TagResult> { + async fn search_recording(&self, artist: &str, title: &str) -> TagResult> { let query = if artist.is_empty() { format!("recording:{}", escape_lucene(title)) } else { @@ -78,7 +74,10 @@ impl MetadataProvider for MusicBrainzClient { escape_lucene(title) ) }; - let url = format!("{BASE_URL}/recording/?query={}&fmt=json&limit=5", urlencoded(&query)); + let url = format!( + "{BASE_URL}/recording/?query={}&fmt=json&limit=5", + urlencoded(&query) + ); let resp: MbRecordingSearchResponse = self.get_json(&url).await?; Ok(resp @@ -108,11 +107,7 @@ impl MetadataProvider for MusicBrainzClient { .collect()) } - async fn search_release( - &self, - artist: &str, - album: &str, - ) -> TagResult> { + async fn search_release(&self, artist: &str, album: &str) -> TagResult> { let query = if artist.is_empty() { format!("release:{}", escape_lucene(album)) } else { @@ -122,7 +117,10 @@ impl MetadataProvider for MusicBrainzClient { escape_lucene(album) ) }; - let url = format!("{BASE_URL}/release/?query={}&fmt=json&limit=5", urlencoded(&query)); + let url = format!( + "{BASE_URL}/release/?query={}&fmt=json&limit=5", + urlencoded(&query) + ); let resp: MbReleaseSearchResponse = self.get_json(&url).await?; Ok(resp @@ -144,9 +142,7 @@ impl MetadataProvider for MusicBrainzClient { } async fn get_recording(&self, mbid: &str) -> TagResult { - let url = format!( - "{BASE_URL}/recording/{mbid}?inc=artists+releases+genres&fmt=json" - ); + let url = format!("{BASE_URL}/recording/{mbid}?inc=artists+releases+genres&fmt=json"); let r: MbRecordingDetail = self.get_json(&url).await?; let (artist_name, artist_mbid) = extract_artist_credit(&r.artist_credit); @@ -177,11 +173,7 @@ impl MetadataProvider for MusicBrainzClient { secondary_artists, }) } - async fn search_artist( - &self, - query: &str, - limit: u32, - ) -> TagResult> { + async fn search_artist(&self, query: &str, limit: u32) -> TagResult> { let url = format!( "{BASE_URL}/artist/?query={}&fmt=json&limit={limit}", urlencoded(&escape_lucene(query)) @@ -207,9 +199,7 @@ impl MetadataProvider for MusicBrainzClient { artist_mbid: &str, limit: u32, ) -> TagResult> { - let url = format!( - "{BASE_URL}/release/?artist={artist_mbid}&fmt=json&limit={limit}" - ); + let url = format!("{BASE_URL}/release/?artist={artist_mbid}&fmt=json&limit={limit}"); let resp: MbReleaseSearchResponse = self.get_json(&url).await?; Ok(resp @@ -225,13 +215,8 @@ impl MetadataProvider for MusicBrainzClient { .collect()) } - async fn get_release_tracks( - &self, - release_mbid: &str, - ) -> TagResult> { - let url = format!( - "{BASE_URL}/release/{release_mbid}?inc=recordings&fmt=json" - ); + async fn get_release_tracks(&self, release_mbid: &str) -> TagResult> { + 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(); @@ -270,7 +255,8 @@ impl MetadataProvider for MusicBrainzClient { primary_type: rg.primary_type, secondary_types: rg.secondary_types.unwrap_or_default(), first_release_date: rg.first_release_date, - first_release_mbid: rg.releases + first_release_mbid: rg + .releases .and_then(|r| r.into_iter().next().map(|rel| rel.id)), }) .collect()) @@ -294,7 +280,9 @@ fn extract_artist_credit(credits: &Option>) -> (String, Opti /// Extract non-featuring secondary artists from MusicBrainz artist credits. /// Returns (name, mbid) pairs for collaborators that aren't "featuring" credits. fn extract_secondary_artists(credits: &Option>) -> Vec<(String, String)> { - let Some(credits) = credits else { return vec![] }; + let Some(credits) = credits else { + return vec![]; + }; if credits.len() <= 1 { return vec![]; } diff --git a/src/tagger.rs b/src/tagger.rs index 33df805..ec8f29b 100644 --- a/src/tagger.rs +++ b/src/tagger.rs @@ -107,12 +107,16 @@ pub async fn tag_track( // Upsert primary artist with MusicBrainz ID let artist_id = match &details.artist_mbid { - Some(mbid) => { - Some(queries::artists::upsert(conn, &details.artist, Some(mbid)).await?.id) - } - None => { - Some(queries::artists::upsert(conn, &details.artist, None).await?.id) - } + Some(mbid) => Some( + queries::artists::upsert(conn, &details.artist, Some(mbid)) + .await? + .id, + ), + None => Some( + queries::artists::upsert(conn, &details.artist, None) + .await? + .id, + ), }; // Upsert secondary collaborator artists so they exist as separate library entries @@ -174,16 +178,16 @@ pub async fn tag_track( queries::tracks::update_metadata(conn, track.id, active).await?; // Optionally write tags to file - if config.write_tags { - if let Err(e) = file_tags::write_tags( + if config.write_tags + && let Err(e) = file_tags::write_tags( &track.file_path, &details, best_release.as_ref(), year, genre.as_deref(), - ) { - tracing::warn!(id = track.id, path = %track.file_path, "failed to write file tags: {e}"); - } + ) + { + tracing::warn!(id = track.id, path = %track.file_path, "failed to write file tags: {e}"); } Ok(true) diff --git a/tests/integration.rs b/tests/integration.rs index 6fd09cc..f366d85 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -2,11 +2,11 @@ use chrono::Utc; use sea_orm::ActiveValue::Set; use shanty_db::{Database, queries}; +use shanty_tag::error::TagResult; use shanty_tag::provider::{ ArtistSearchResult, DiscographyEntry, MetadataProvider, RecordingDetails, RecordingMatch, ReleaseMatch, ReleaseRef, }; -use shanty_tag::error::TagResult; use shanty_tag::{TagConfig, run_tagging}; /// A mock metadata provider for testing without hitting MusicBrainz. @@ -64,15 +64,25 @@ impl MetadataProvider for MockProvider { Ok(vec![]) } - async fn get_artist_releases(&self, _artist_mbid: &str, _limit: u32) -> TagResult> { + async fn get_artist_releases( + &self, + _artist_mbid: &str, + _limit: u32, + ) -> TagResult> { Ok(vec![]) } - async fn get_release_tracks(&self, _release_mbid: &str) -> TagResult> { + async fn get_release_tracks( + &self, + _release_mbid: &str, + ) -> TagResult> { Ok(vec![]) } - async fn get_artist_release_groups(&self, _artist_mbid: &str) -> TagResult> { + async fn get_artist_release_groups( + &self, + _artist_mbid: &str, + ) -> TagResult> { Ok(vec![]) } } @@ -83,7 +93,12 @@ async fn test_db() -> Database { .expect("failed to create test database") } -async fn insert_untagged_track(db: &Database, file_path: &str, title: Option<&str>, artist: Option<&str>) -> i32 { +async fn insert_untagged_track( + db: &Database, + file_path: &str, + title: Option<&str>, + artist: Option<&str>, +) -> i32 { let now = Utc::now().naive_utc(); let active = shanty_db::entities::track::ActiveModel { file_path: Set(file_path.to_string()), @@ -103,7 +118,8 @@ async fn test_tag_track_with_match() { let db = test_db().await; let provider = MockProvider; - let track_id = insert_untagged_track(&db, "/music/time.mp3", Some("Time"), Some("Pink Floyd")).await; + let track_id = + insert_untagged_track(&db, "/music/time.mp3", Some("Time"), Some("Pink Floyd")).await; let config = TagConfig { dry_run: false, @@ -111,13 +127,17 @@ async fn test_tag_track_with_match() { confidence: 0.8, }; - let stats = run_tagging(db.conn(), &provider, &config, Some(track_id)).await.unwrap(); + let stats = run_tagging(db.conn(), &provider, &config, Some(track_id)) + .await + .unwrap(); assert_eq!(stats.tracks_processed, 1); assert_eq!(stats.tracks_matched, 1); assert_eq!(stats.tracks_updated, 1); // Verify the track was updated - let track = queries::tracks::get_by_id(db.conn(), track_id).await.unwrap(); + let track = queries::tracks::get_by_id(db.conn(), track_id) + .await + .unwrap(); assert_eq!(track.musicbrainz_id.as_deref(), Some("rec-123")); assert_eq!(track.title.as_deref(), Some("Time")); assert_eq!(track.artist.as_deref(), Some("Pink Floyd")); @@ -126,9 +146,14 @@ async fn test_tag_track_with_match() { assert_eq!(track.genre.as_deref(), Some("Progressive Rock")); // Verify artist was created with MusicBrainz ID - let artist = queries::artists::find_by_name(db.conn(), "Pink Floyd").await.unwrap(); + let artist = queries::artists::find_by_name(db.conn(), "Pink Floyd") + .await + .unwrap(); assert!(artist.is_some()); - assert_eq!(artist.unwrap().musicbrainz_id.as_deref(), Some("artist-456")); + assert_eq!( + artist.unwrap().musicbrainz_id.as_deref(), + Some("artist-456") + ); } #[tokio::test] @@ -136,7 +161,13 @@ async fn test_tag_track_no_match() { let db = test_db().await; let provider = MockProvider; - let track_id = insert_untagged_track(&db, "/music/unknown.mp3", Some("Unknown Song"), Some("Nobody")).await; + let track_id = insert_untagged_track( + &db, + "/music/unknown.mp3", + Some("Unknown Song"), + Some("Nobody"), + ) + .await; let config = TagConfig { dry_run: false, @@ -144,12 +175,16 @@ async fn test_tag_track_no_match() { confidence: 0.8, }; - let stats = run_tagging(db.conn(), &provider, &config, Some(track_id)).await.unwrap(); + let stats = run_tagging(db.conn(), &provider, &config, Some(track_id)) + .await + .unwrap(); assert_eq!(stats.tracks_processed, 1); assert_eq!(stats.tracks_skipped, 1); // Track should be unchanged - let track = queries::tracks::get_by_id(db.conn(), track_id).await.unwrap(); + let track = queries::tracks::get_by_id(db.conn(), track_id) + .await + .unwrap(); assert!(track.musicbrainz_id.is_none()); } @@ -158,7 +193,8 @@ async fn test_dry_run_does_not_update() { let db = test_db().await; let provider = MockProvider; - let track_id = insert_untagged_track(&db, "/music/time.mp3", Some("Time"), Some("Pink Floyd")).await; + let track_id = + insert_untagged_track(&db, "/music/time.mp3", Some("Time"), Some("Pink Floyd")).await; let config = TagConfig { dry_run: true, @@ -166,12 +202,16 @@ async fn test_dry_run_does_not_update() { confidence: 0.8, }; - let stats = run_tagging(db.conn(), &provider, &config, Some(track_id)).await.unwrap(); + let stats = run_tagging(db.conn(), &provider, &config, Some(track_id)) + .await + .unwrap(); assert_eq!(stats.tracks_matched, 1); assert_eq!(stats.tracks_updated, 0); // dry run // Track should be unchanged - let track = queries::tracks::get_by_id(db.conn(), track_id).await.unwrap(); + let track = queries::tracks::get_by_id(db.conn(), track_id) + .await + .unwrap(); assert!(track.musicbrainz_id.is_none()); } @@ -189,7 +229,9 @@ async fn test_tag_all_untagged() { confidence: 0.8, }; - let stats = run_tagging(db.conn(), &provider, &config, None).await.unwrap(); + let stats = run_tagging(db.conn(), &provider, &config, None) + .await + .unwrap(); assert_eq!(stats.tracks_processed, 2); assert_eq!(stats.tracks_matched, 1); // only Pink Floyd matched assert_eq!(stats.tracks_skipped, 1);