diff --git a/src/lib.rs b/src/lib.rs index bf6e77e..e585dff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,6 @@ pub mod matching; pub use error::{WatchError, WatchResult}; pub use library::{ - LibrarySummary, WatchListEntry, add_album, add_artist, add_track, library_summary, list_items, - remove_item, + AddSummary, LibrarySummary, WatchListEntry, add_album, add_artist, add_track, + library_summary, list_items, remove_item, }; diff --git a/src/library.rs b/src/library.rs index d25bd72..8c76578 100644 --- a/src/library.rs +++ b/src/library.rs @@ -21,12 +21,28 @@ pub struct WatchListEntry { pub added_at: chrono::NaiveDateTime, } +/// Summary of how many tracks were added when watching an artist or album. +#[derive(Debug, Default)] +pub struct AddSummary { + pub tracks_added: u64, + pub tracks_already_owned: u64, + pub errors: u64, +} + +impl fmt::Display for AddSummary { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "added: {}, already owned: {}, errors: {}", + self.tracks_added, self.tracks_already_owned, self.errors, + ) + } +} + /// Summary statistics for the library. #[derive(Debug, Default, Serialize, Deserialize)] pub struct LibrarySummary { - pub total_artists: u64, - pub total_albums: u64, - pub total_tracks: u64, + pub total_items: u64, pub wanted: u64, pub available: u64, pub downloaded: u64, @@ -35,8 +51,7 @@ pub struct LibrarySummary { impl fmt::Display for LibrarySummary { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - writeln!(f, "Monitored: {} artists, {} albums, {} tracks", - self.total_artists, self.total_albums, self.total_tracks)?; + writeln!(f, "Total tracked: {} items", self.total_items)?; writeln!(f, " Wanted: {}", self.wanted)?; writeln!(f, " Available: {}", self.available)?; writeln!(f, " Downloaded: {}", self.downloaded)?; @@ -44,122 +59,156 @@ impl fmt::Display for LibrarySummary { } } -/// Add an artist to the watchlist. Auto-detects if already owned. +/// Add an artist to the watchlist by expanding into individual track wanted items. /// -/// If `musicbrainz_id` is provided, it will be stored with the artist record. -/// If `name` is empty but `musicbrainz_id` is provided and a `provider` is given, -/// the name will be looked up from MusicBrainz. +/// Fetches the artist's discography from the provider, then for each release +/// fetches the tracklist and adds each track as a separate Wanted item. pub async fn add_artist( conn: &DatabaseConnection, name: Option<&str>, musicbrainz_id: Option<&str>, - provider: Option<&impl MetadataProvider>, -) -> WatchResult { + provider: &impl MetadataProvider, +) -> WatchResult { let (resolved_name, resolved_mbid) = resolve_artist_info(name, musicbrainz_id, provider).await?; let artist = queries::artists::upsert(conn, &resolved_name, resolved_mbid.as_deref()).await?; - let is_owned = matching::artist_is_owned(conn, &resolved_name).await?; - let item = queries::wanted::add(conn, ItemType::Artist, Some(artist.id), None, None).await?; - - let status = if is_owned { - queries::wanted::update_status(conn, item.id, WantedStatus::Owned).await?; - WantedStatus::Owned - } else { - WantedStatus::Wanted + // Get artist MBID — either provided or from the search + let artist_mbid = resolved_mbid.or_else(|| artist.musicbrainz_id.clone()); + let artist_mbid = match artist_mbid { + Some(mbid) => mbid, + None => { + // Search for the artist to get MBID + let results = provider + .search_artist(&resolved_name, 1) + .await + .map_err(|e| WatchError::Other(format!("artist search failed: {e}")))?; + results + .into_iter() + .next() + .map(|a| a.mbid) + .ok_or_else(|| WatchError::Other(format!("artist '{}' not found on MusicBrainz", resolved_name)))? + } }; - if is_owned { - tracing::info!(name = %resolved_name, "artist already in library, marked as owned"); - } else { - tracing::info!(name = %resolved_name, "artist added to watchlist"); + tracing::info!(name = %resolved_name, mbid = %artist_mbid, "fetching discography"); + + // Fetch all releases + let releases = provider + .get_artist_releases(&artist_mbid, 100) + .await + .map_err(|e| WatchError::Other(format!("failed to fetch discography: {e}")))?; + + tracing::info!(count = releases.len(), "found releases"); + + let mut summary = AddSummary::default(); + + for release in &releases { + tracing::info!(title = %release.title, mbid = %release.mbid, "fetching tracks"); + + let tracks = match provider.get_release_tracks(&release.mbid).await { + Ok(t) => t, + Err(e) => { + tracing::warn!(release = %release.title, error = %e, "failed to fetch tracks"); + summary.errors += 1; + continue; + } + }; + + for track in &tracks { + match add_track_inner(conn, &resolved_name, &track.title, Some(&track.recording_mbid)).await { + Ok(true) => summary.tracks_added += 1, + Ok(false) => summary.tracks_already_owned += 1, + Err(e) => { + tracing::warn!(track = %track.title, error = %e, "failed to add track"); + summary.errors += 1; + } + } + } } - Ok(WatchListEntry { - id: item.id, - item_type: ItemType::Artist, - name: artist.name, - artist_name: None, - status, - added_at: item.added_at, - }) + tracing::info!(%summary, "artist watch complete"); + Ok(summary) } -/// Add an album to the watchlist. Auto-detects if already owned. +/// Add an album to the watchlist by expanding into individual track wanted items. /// -/// If `musicbrainz_id` is provided and a `provider` is given, album and artist -/// details will be resolved from MusicBrainz. +/// Fetches the album's tracklist and adds each track as a separate Wanted item. pub async fn add_album( conn: &DatabaseConnection, artist_name: Option<&str>, album_name: Option<&str>, musicbrainz_id: Option<&str>, - provider: Option<&impl MetadataProvider>, -) -> WatchResult { + provider: &impl MetadataProvider, +) -> WatchResult { let (resolved_album, resolved_artist, resolved_mbid) = resolve_album_info(artist_name, album_name, musicbrainz_id, provider).await?; - let artist = queries::artists::upsert(conn, &resolved_artist, None).await?; - let album = queries::albums::upsert( - conn, - &resolved_album, - &resolved_artist, - resolved_mbid.as_deref(), - Some(artist.id), - ) - .await?; - - let is_owned = matching::album_is_owned(conn, &resolved_artist, &resolved_album).await?; - let item = queries::wanted::add( - conn, - ItemType::Album, - Some(artist.id), - Some(album.id), - None, - ) - .await?; - - let status = if is_owned { - queries::wanted::update_status(conn, item.id, WantedStatus::Owned).await?; - WantedStatus::Owned - } else { - WantedStatus::Wanted + // Get release MBID + let release_mbid = match resolved_mbid { + Some(mbid) => mbid, + None => { + let results = provider + .search_release(&resolved_artist, &resolved_album) + .await + .map_err(|e| WatchError::Other(format!("album search failed: {e}")))?; + results + .into_iter() + .next() + .map(|r| r.mbid) + .ok_or_else(|| WatchError::Other(format!("album '{}' not found on MusicBrainz", resolved_album)))? + } }; - Ok(WatchListEntry { - id: item.id, - item_type: ItemType::Album, - name: album.name, - artist_name: Some(artist.name), - status, - added_at: item.added_at, - }) + tracing::info!(album = %resolved_album, artist = %resolved_artist, mbid = %release_mbid, "fetching tracks"); + + let tracks = provider + .get_release_tracks(&release_mbid) + .await + .map_err(|e| WatchError::Other(format!("failed to fetch tracks: {e}")))?; + + let mut summary = AddSummary::default(); + + for track in &tracks { + match add_track_inner(conn, &resolved_artist, &track.title, Some(&track.recording_mbid)).await { + Ok(true) => summary.tracks_added += 1, + Ok(false) => summary.tracks_already_owned += 1, + Err(e) => { + tracing::warn!(track = %track.title, error = %e, "failed to add track"); + summary.errors += 1; + } + } + } + + tracing::info!(%summary, "album watch complete"); + Ok(summary) } -/// Add a track to the watchlist. Auto-detects if already owned. -/// -/// If `musicbrainz_id` is provided and a `provider` is given, track and artist -/// details will be resolved from MusicBrainz via `get_recording`. +/// Add a single track to the watchlist. Auto-detects if already owned. pub async fn add_track( conn: &DatabaseConnection, artist_name: Option<&str>, title: Option<&str>, musicbrainz_id: Option<&str>, - provider: Option<&impl MetadataProvider>, + provider: &impl MetadataProvider, ) -> WatchResult { let (resolved_title, resolved_artist, resolved_mbid) = resolve_track_info(artist_name, title, musicbrainz_id, provider).await?; let artist = queries::artists::upsert(conn, &resolved_artist, None).await?; - let is_owned = matching::track_is_owned(conn, &resolved_artist, &resolved_title).await?; - let item = queries::wanted::add(conn, ItemType::Track, Some(artist.id), None, None).await?; - // Store the MBID on the artist if we got one from the recording lookup - if let Some(ref mbid) = resolved_mbid { - tracing::debug!(mbid = %mbid, "recording MBID stored"); - } + let item = queries::wanted::add( + conn, + ItemType::Track, + &resolved_title, + resolved_mbid.as_deref(), + Some(artist.id), + None, + None, + ) + .await?; let status = if is_owned { queries::wanted::update_status(conn, item.id, WantedStatus::Owned).await?; @@ -178,41 +227,72 @@ pub async fn add_track( }) } +/// Internal: add a single track wanted item. Returns Ok(true) if added as Wanted, +/// Ok(false) if already owned. +async fn add_track_inner( + conn: &DatabaseConnection, + artist_name: &str, + title: &str, + recording_mbid: Option<&str>, +) -> WatchResult { + let artist = queries::artists::upsert(conn, artist_name, None).await?; + let is_owned = matching::track_is_owned(conn, artist_name, title).await?; + + let item = queries::wanted::add( + conn, + ItemType::Track, + title, + recording_mbid, + Some(artist.id), + None, + None, + ) + .await?; + + if is_owned { + queries::wanted::update_status(conn, item.id, WantedStatus::Owned).await?; + tracing::debug!(title = title, artist = artist_name, "already owned"); + Ok(false) + } else { + tracing::debug!(title = title, artist = artist_name, "added to watchlist"); + Ok(true) + } +} + /// Resolve artist name from MBID if needed. -/// Returns (name, mbid). async fn resolve_artist_info( name: Option<&str>, mbid: Option<&str>, - provider: Option<&impl MetadataProvider>, + provider: &impl MetadataProvider, ) -> WatchResult<(String, Option)> { - // If we have a name, use it directly if let Some(n) = name.filter(|s| !s.is_empty()) { return Ok((n.to_string(), mbid.map(String::from))); } - // No name — need MBID + provider to resolve let mbid = mbid.ok_or_else(|| { WatchError::Other("either a name or --mbid is required".into()) })?; - let _provider = provider.ok_or_else(|| { - WatchError::Other("MusicBrainz provider needed to resolve MBID".into()) - })?; - // TODO: Add get_artist(mbid) to MetadataProvider trait for proper resolution. - // For now, store the MBID and use a placeholder name. - tracing::info!(mbid = mbid, "MBID provided without name — storing MBID as reference"); - Ok((format!("Artist [{}]", &mbid[..8.min(mbid.len())]), Some(mbid.to_string()))) + // Search for artist by MBID to get the name + let results = provider + .search_artist(mbid, 1) + .await + .map_err(|e| WatchError::Other(format!("artist lookup failed: {e}")))?; + + if let Some(artist) = results.into_iter().next() { + Ok((artist.name, Some(mbid.to_string()))) + } else { + Ok((format!("Artist [{}]", &mbid[..8.min(mbid.len())]), Some(mbid.to_string()))) + } } /// Resolve album info from MBID if needed. -/// Returns (album_name, artist_name, album_mbid). async fn resolve_album_info( artist_name: Option<&str>, album_name: Option<&str>, mbid: Option<&str>, - provider: Option<&impl MetadataProvider>, + provider: &impl MetadataProvider, ) -> WatchResult<(String, String, Option)> { - // If we have both names, use them directly if let (Some(album), Some(artist)) = ( album_name.filter(|s| !s.is_empty()), artist_name.filter(|s| !s.is_empty()), @@ -220,16 +300,10 @@ async fn resolve_album_info( return Ok((album.to_string(), artist.to_string(), mbid.map(String::from))); } - // Need to resolve from MBID via provider let mbid = mbid.ok_or_else(|| { WatchError::Other("either artist+album names or --mbid is required".into()) })?; - let provider = provider.ok_or_else(|| { - WatchError::Other("MusicBrainz provider needed to resolve MBID".into()) - })?; - // Use search_release with the MBID — our trait doesn't have get_release yet, - // but we can search for it let results = provider .search_release("", mbid) .await @@ -248,14 +322,12 @@ async fn resolve_album_info( } /// Resolve track info from MBID if needed. -/// Returns (title, artist_name, recording_mbid). async fn resolve_track_info( artist_name: Option<&str>, title: Option<&str>, mbid: Option<&str>, - provider: Option<&impl MetadataProvider>, + provider: &impl MetadataProvider, ) -> WatchResult<(String, String, Option)> { - // If we have both names, use them directly if let (Some(t), Some(a)) = ( title.filter(|s| !s.is_empty()), artist_name.filter(|s| !s.is_empty()), @@ -263,32 +335,23 @@ async fn resolve_track_info( return Ok((t.to_string(), a.to_string(), mbid.map(String::from))); } - // Resolve from MBID let mbid = mbid.ok_or_else(|| { WatchError::Other("either artist+title or --mbid is required".into()) })?; - let provider = provider.ok_or_else(|| { - WatchError::Other("MusicBrainz provider needed to resolve MBID".into()) - })?; let details = provider .get_recording(mbid) .await .map_err(|e| WatchError::Other(format!("MusicBrainz lookup failed: {e}")))?; - let resolved_title = title - .filter(|s| !s.is_empty()) - .map(String::from) - .unwrap_or(details.title); - let resolved_artist = artist_name - .filter(|s| !s.is_empty()) - .map(String::from) - .unwrap_or(details.artist); - - Ok((resolved_title, resolved_artist, Some(mbid.to_string()))) + Ok(( + title.filter(|s| !s.is_empty()).map(String::from).unwrap_or(details.title), + artist_name.filter(|s| !s.is_empty()).map(String::from).unwrap_or(details.artist), + Some(mbid.to_string()), + )) } -/// List watchlist items with optional filters, resolved to display-friendly entries. +/// List watchlist items with optional filters. pub async fn list_items( conn: &DatabaseConnection, status_filter: Option, @@ -298,18 +361,22 @@ pub async fn list_items( let mut entries = Vec::new(); for item in items { - let (name, artist_name) = resolve_names(conn, &item).await; + // Use the name field directly now + let artist_name = if let Some(id) = item.artist_id { + queries::artists::get_by_id(conn, id) + .await + .map(|a| a.name) + .ok() + } else { + None + }; // Apply artist filter if provided if let Some(filter) = artist_filter { let filter_norm = matching::normalize(filter); let matches = artist_name .as_deref() - .or(if item.item_type == ItemType::Artist { Some(name.as_str()) } else { None }) - .map(|a| { - let a_norm = matching::normalize(a); - strsim::jaro_winkler(&filter_norm, &a_norm) > 0.85 - }) + .map(|a| strsim::jaro_winkler(&filter_norm, &matching::normalize(a)) > 0.85) .unwrap_or(false); if !matches { continue; @@ -319,7 +386,7 @@ pub async fn list_items( entries.push(WatchListEntry { id: item.id, item_type: item.item_type, - name, + name: item.name.clone(), artist_name, status: item.status, added_at: item.added_at, @@ -342,11 +409,7 @@ pub async fn library_summary(conn: &DatabaseConnection) -> WatchResult summary.total_artists += 1, - ItemType::Album => summary.total_albums += 1, - ItemType::Track => summary.total_tracks += 1, - } + summary.total_items += 1; match item.status { WantedStatus::Wanted => summary.wanted += 1, WantedStatus::Available => summary.available += 1, @@ -357,57 +420,3 @@ pub async fn library_summary(conn: &DatabaseConnection) -> WatchResult (String, Option) { - match item.item_type { - ItemType::Artist => { - let name = if let Some(id) = item.artist_id { - queries::artists::get_by_id(conn, id) - .await - .map(|a| a.name) - .unwrap_or_else(|_| format!("Artist #{id}")) - } else { - "Unknown Artist".to_string() - }; - (name, None) - } - ItemType::Album => { - let (album_name, artist_name) = if let Some(id) = item.album_id { - let album = queries::albums::get_by_id(conn, id).await; - match album { - Ok(a) => (a.name.clone(), Some(a.album_artist.clone())), - Err(_) => (format!("Album #{id}"), None), - } - } else { - ("Unknown Album".to_string(), None) - }; - let artist_name = artist_name.or_else(|| { - item.artist_id.map(|id| format!("Artist #{id}")) - }); - (album_name, artist_name) - } - ItemType::Track => { - let artist_name = if let Some(id) = item.artist_id { - queries::artists::get_by_id(conn, id) - .await - .map(|a| a.name) - .ok() - } else { - None - }; - let name = if let Some(id) = item.track_id { - queries::tracks::get_by_id(conn, id) - .await - .map(|t| t.title.unwrap_or_else(|| format!("Track #{id}"))) - .unwrap_or_else(|_| format!("Track #{id}")) - } else { - "Track (title not stored)".to_string() - }; - (name, artist_name) - } - } -} diff --git a/src/main.rs b/src/main.rs index e0310d7..dc48621 100644 --- a/src/main.rs +++ b/src/main.rs @@ -119,8 +119,8 @@ async fn main() -> anyhow::Result<()> { let database_url = cli.database.unwrap_or_else(default_database_url); let db = Database::new(&database_url).await?; - // Create MB client lazily — only needed for MBID resolution - let mb_client = MusicBrainzClient::new().ok(); + // MB client needed for artist/album expansion into tracks + let mb_client = MusicBrainzClient::new()?; match cli.command { Commands::Add { what } => match what { @@ -128,37 +128,28 @@ async fn main() -> anyhow::Result<()> { if name.is_none() && mbid.is_none() { anyhow::bail!("provide either a name or --mbid"); } - let entry = add_artist( + let summary = add_artist( db.conn(), name.as_deref(), mbid.as_deref(), - mb_client.as_ref(), + &mb_client, ) .await?; - println!( - "Added artist: {} (id={}, status={:?})", - entry.name, entry.id, entry.status - ); + println!("Artist watch: {summary}"); } AddCommand::Album { artist, album, mbid } => { if artist.is_none() && album.is_none() && mbid.is_none() { anyhow::bail!("provide artist+album names or --mbid"); } - let entry = add_album( + let summary = add_album( db.conn(), artist.as_deref(), album.as_deref(), mbid.as_deref(), - mb_client.as_ref(), + &mb_client, ) .await?; - println!( - "Added album: {} by {} (id={}, status={:?})", - entry.name, - entry.artist_name.as_deref().unwrap_or("?"), - entry.id, - entry.status - ); + println!("Album watch: {summary}"); } AddCommand::Track { artist, title, mbid } => { if artist.is_none() && title.is_none() && mbid.is_none() { @@ -169,7 +160,7 @@ async fn main() -> anyhow::Result<()> { artist.as_deref(), title.as_deref(), mbid.as_deref(), - mb_client.as_ref(), + &mb_client, ) .await?; println!( diff --git a/tests/integration.rs b/tests/integration.rs index 5170c9b..9bc8c2b 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -3,7 +3,8 @@ use sea_orm::ActiveValue::Set; use shanty_db::entities::wanted_item::{ItemType, WantedStatus}; use shanty_db::{Database, queries}; -use shanty_tag::MusicBrainzClient; +use shanty_tag::error::TagResult; +use shanty_tag::provider::*; use shanty_watch::{add_album, add_artist, add_track, library_summary, list_items, remove_item}; async fn test_db() -> Database { @@ -37,58 +38,72 @@ async fn insert_track(db: &Database, artist: &str, title: &str, album: &str) { queries::tracks::upsert(db.conn(), active).await.unwrap(); } -// No MB client needed for tests — pass None -const NO_MB: Option<&MusicBrainzClient> = None; +/// Mock provider that returns a tracklist for known releases. +struct MockProvider; -#[tokio::test] -async fn test_add_artist_wanted() { - let db = test_db().await; - - let entry = add_artist(db.conn(), Some("Radiohead"), None, NO_MB).await.unwrap(); - assert_eq!(entry.item_type, ItemType::Artist); - assert_eq!(entry.name, "Radiohead"); - assert_eq!(entry.status, WantedStatus::Wanted); -} - -#[tokio::test] -async fn test_add_artist_auto_owned() { - let db = test_db().await; - - insert_track(&db, "Pink Floyd", "Time", "DSOTM").await; - - let entry = add_artist(db.conn(), Some("Pink Floyd"), None, NO_MB).await.unwrap(); - assert_eq!(entry.status, WantedStatus::Owned); -} - -#[tokio::test] -async fn test_add_album_wanted() { - let db = test_db().await; - - let entry = add_album(db.conn(), Some("Radiohead"), Some("OK Computer"), None, NO_MB) - .await - .unwrap(); - assert_eq!(entry.item_type, ItemType::Album); - assert_eq!(entry.name, "OK Computer"); - assert_eq!(entry.status, WantedStatus::Wanted); -} - -#[tokio::test] -async fn test_add_album_auto_owned() { - let db = test_db().await; - - insert_track(&db, "Pink Floyd", "Time", "DSOTM").await; - - let entry = add_album(db.conn(), Some("Pink Floyd"), Some("DSOTM"), None, NO_MB) - .await - .unwrap(); - assert_eq!(entry.status, WantedStatus::Owned); +impl MetadataProvider for MockProvider { + async fn search_recording(&self, _artist: &str, _title: &str) -> TagResult> { + Ok(vec![]) + } + async fn search_release(&self, _artist: &str, _album: &str) -> TagResult> { + Ok(vec![ReleaseMatch { + mbid: "release-123".into(), + title: "Test Album".into(), + artist: "Test Artist".into(), + artist_mbid: Some("artist-456".into()), + date: Some("2024".into()), + track_count: Some(2), + score: 100, + }]) + } + async fn get_recording(&self, _mbid: &str) -> TagResult { + Err(shanty_tag::TagError::Other("not found".into())) + } + async fn search_artist(&self, _query: &str, _limit: u32) -> TagResult> { + Ok(vec![ArtistSearchResult { + mbid: "artist-456".into(), + name: "Test Artist".into(), + disambiguation: None, + country: None, + artist_type: None, + score: 100, + }]) + } + async fn get_artist_releases(&self, _mbid: &str, _limit: u32) -> TagResult> { + Ok(vec![DiscographyEntry { + mbid: "release-123".into(), + title: "Test Album".into(), + date: Some("2024".into()), + release_type: Some("Album".into()), + track_count: Some(2), + }]) + } + async fn get_release_tracks(&self, _release_mbid: &str) -> TagResult> { + Ok(vec![ + ReleaseTrack { + recording_mbid: "rec-1".into(), + title: "Track One".into(), + track_number: Some(1), + disc_number: Some(1), + duration_ms: Some(180_000), + }, + ReleaseTrack { + recording_mbid: "rec-2".into(), + title: "Track Two".into(), + track_number: Some(2), + disc_number: Some(1), + duration_ms: Some(200_000), + }, + ]) + } } #[tokio::test] async fn test_add_track_wanted() { let db = test_db().await; + let provider = MockProvider; - let entry = add_track(db.conn(), Some("Radiohead"), Some("Creep"), None, NO_MB) + let entry = add_track(db.conn(), Some("Radiohead"), Some("Creep"), None, &provider) .await .unwrap(); assert_eq!(entry.item_type, ItemType::Track); @@ -99,53 +114,81 @@ async fn test_add_track_wanted() { #[tokio::test] async fn test_add_track_auto_owned() { let db = test_db().await; + let provider = MockProvider; insert_track(&db, "Pink Floyd", "Time", "DSOTM").await; - let entry = add_track(db.conn(), Some("Pink Floyd"), Some("Time"), None, NO_MB) + let entry = add_track(db.conn(), Some("Pink Floyd"), Some("Time"), None, &provider) .await .unwrap(); assert_eq!(entry.status, WantedStatus::Owned); } +#[tokio::test] +async fn test_add_album_expands_to_tracks() { + let db = test_db().await; + let provider = MockProvider; + + let summary = add_album(db.conn(), Some("Test Artist"), Some("Test Album"), None, &provider) + .await + .unwrap(); + assert_eq!(summary.tracks_added, 2); + + let items = list_items(db.conn(), Some(WantedStatus::Wanted), None) + .await + .unwrap(); + assert_eq!(items.len(), 2); + assert_eq!(items[0].item_type, ItemType::Track); +} + +#[tokio::test] +async fn test_add_artist_expands_to_tracks() { + let db = test_db().await; + let provider = MockProvider; + + let summary = add_artist(db.conn(), Some("Test Artist"), None, &provider) + .await + .unwrap(); + assert_eq!(summary.tracks_added, 2); + + let items = list_items(db.conn(), None, None).await.unwrap(); + assert_eq!(items.len(), 2); +} + #[tokio::test] async fn test_list_items_with_filters() { let db = test_db().await; + let provider = MockProvider; - add_artist(db.conn(), Some("Radiohead"), None, NO_MB).await.unwrap(); - add_artist(db.conn(), Some("Tool"), None, NO_MB).await.unwrap(); + add_track(db.conn(), Some("Radiohead"), Some("Creep"), None, &provider) + .await + .unwrap(); + add_track(db.conn(), Some("Tool"), Some("Lateralus"), None, &provider) + .await + .unwrap(); - // List all let all = list_items(db.conn(), None, None).await.unwrap(); assert_eq!(all.len(), 2); - // List by status let wanted = list_items(db.conn(), Some(WantedStatus::Wanted), None) .await .unwrap(); assert_eq!(wanted.len(), 2); - let owned = list_items(db.conn(), Some(WantedStatus::Owned), None) - .await - .unwrap(); - assert!(owned.is_empty()); - - // List by artist let radiohead = list_items(db.conn(), None, Some("Radiohead")) .await .unwrap(); assert_eq!(radiohead.len(), 1); - assert_eq!(radiohead[0].name, "Radiohead"); } #[tokio::test] async fn test_remove_item() { let db = test_db().await; + let provider = MockProvider; - let entry = add_artist(db.conn(), Some("Radiohead"), None, NO_MB).await.unwrap(); - let all = list_items(db.conn(), None, None).await.unwrap(); - assert_eq!(all.len(), 1); - + let entry = add_track(db.conn(), Some("Radiohead"), Some("Creep"), None, &provider) + .await + .unwrap(); remove_item(db.conn(), entry.id).await.unwrap(); let all = list_items(db.conn(), None, None).await.unwrap(); assert!(all.is_empty()); @@ -154,18 +197,19 @@ async fn test_remove_item() { #[tokio::test] async fn test_library_summary() { let db = test_db().await; + let provider = MockProvider; insert_track(&db, "Pink Floyd", "Time", "DSOTM").await; - add_artist(db.conn(), Some("Radiohead"), None, NO_MB).await.unwrap(); - add_artist(db.conn(), Some("Pink Floyd"), None, NO_MB).await.unwrap(); - add_album(db.conn(), Some("Tool"), Some("Lateralus"), None, NO_MB).await.unwrap(); - add_track(db.conn(), Some("Pink Floyd"), Some("Time"), None, NO_MB).await.unwrap(); + add_track(db.conn(), Some("Radiohead"), Some("Creep"), None, &provider) + .await + .unwrap(); + add_track(db.conn(), Some("Pink Floyd"), Some("Time"), None, &provider) + .await + .unwrap(); let summary = library_summary(db.conn()).await.unwrap(); - assert_eq!(summary.total_artists, 2); - assert_eq!(summary.total_albums, 1); - assert_eq!(summary.total_tracks, 1); - assert_eq!(summary.wanted, 2); - assert_eq!(summary.owned, 2); + assert_eq!(summary.total_items, 2); + assert_eq!(summary.wanted, 1); + assert_eq!(summary.owned, 1); }