use chrono::Utc; use sea_orm::ActiveValue::Set; use shanty_db::{Database, queries}; 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. struct MockProvider; impl MetadataProvider for MockProvider { async fn search_recording(&self, artist: &str, title: &str) -> TagResult> { // Return a match for "Pink Floyd - Time" if artist.contains("Pink Floyd") && title.contains("Time") { Ok(vec![RecordingMatch { mbid: "rec-123".into(), title: "Time".into(), artist: "Pink Floyd".into(), artist_mbid: Some("artist-456".into()), releases: vec![ReleaseRef { mbid: "release-789".into(), title: "The Dark Side of the Moon".into(), date: Some("1973-03-01".into()), track_number: Some(4), }], score: 100, }]) } else { Ok(vec![]) } } async fn search_release(&self, _artist: &str, _album: &str) -> TagResult> { Ok(vec![]) } async fn get_recording(&self, mbid: &str) -> TagResult { if mbid == "rec-123" { Ok(RecordingDetails { mbid: "rec-123".into(), title: "Time".into(), artist: "Pink Floyd".into(), artist_mbid: Some("artist-456".into()), releases: vec![ReleaseRef { mbid: "release-789".into(), title: "The Dark Side of the Moon".into(), date: Some("1973-03-01".into()), track_number: Some(4), }], duration_ms: Some(413_000), genres: vec!["Progressive Rock".into()], }) } else { Err(shanty_tag::TagError::Other("not found".into())) } } async fn search_artist(&self, _query: &str, _limit: u32) -> TagResult> { Ok(vec![]) } async fn get_artist_releases(&self, _artist_mbid: &str, _limit: u32) -> TagResult> { Ok(vec![]) } } async fn test_db() -> Database { Database::new("sqlite::memory:") .await .expect("failed to create test database") } 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()), title: Set(title.map(String::from)), artist: Set(artist.map(String::from)), file_size: Set(1_000_000), added_at: Set(now), updated_at: Set(now), ..Default::default() }; let track = queries::tracks::upsert(db.conn(), active).await.unwrap(); track.id } #[tokio::test] 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 config = TagConfig { dry_run: false, write_tags: false, confidence: 0.8, }; 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(); 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")); assert_eq!(track.album.as_deref(), Some("The Dark Side of the Moon")); assert_eq!(track.year, Some(1973)); 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(); assert!(artist.is_some()); assert_eq!(artist.unwrap().musicbrainz_id.as_deref(), Some("artist-456")); } #[tokio::test] 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 config = TagConfig { dry_run: false, write_tags: false, confidence: 0.8, }; 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(); assert!(track.musicbrainz_id.is_none()); } #[tokio::test] 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 config = TagConfig { dry_run: true, write_tags: false, confidence: 0.8, }; 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(); assert!(track.musicbrainz_id.is_none()); } #[tokio::test] async fn test_tag_all_untagged() { let db = test_db().await; let provider = MockProvider; insert_untagged_track(&db, "/music/time.mp3", Some("Time"), Some("Pink Floyd")).await; insert_untagged_track(&db, "/music/unknown.mp3", Some("Unknown"), Some("Nobody")).await; let config = TagConfig { dry_run: false, write_tags: false, confidence: 0.8, }; 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); }