Compare commits

...

5 Commits

Author SHA1 Message Date
Connor Johnstone
aef4708439 Sped up artist enrichment at least somewhat 2026-03-21 15:08:28 -04:00
Connor Johnstone
85e24671a3 Added the watch and scheduler systems 2026-03-20 16:28:15 -04:00
Connor Johnstone
15a4efe1e9 Format 2026-03-19 14:05:58 -04:00
Connor Johnstone
0f066d5708 Added auth 2026-03-19 14:02:24 -04:00
Connor Johnstone
0b336789da Formatting 2026-03-18 15:37:02 -04:00
6 changed files with 263 additions and 110 deletions

View File

@@ -24,4 +24,5 @@ chrono = { version = "0.4", features = ["serde"] }
dirs = "6" dirs = "6"
[dev-dependencies] [dev-dependencies]
shanty-data = { path = "../shanty-data" }
tokio = { version = "1", features = ["full", "test-util"] } tokio = { version = "1", features = ["full", "test-util"] }

View File

@@ -9,6 +9,6 @@ pub mod matching;
pub use error::{WatchError, WatchResult}; pub use error::{WatchError, WatchResult};
pub use library::{ pub use library::{
AddSummary, LibrarySummary, WatchListEntry, add_album, add_artist, add_track, AddSummary, LibrarySummary, WatchListEntry, add_album, add_artist, add_track, library_summary,
library_summary, list_items, remove_item, list_items, remove_item,
}; };

View File

@@ -60,64 +60,83 @@ impl fmt::Display for LibrarySummary {
} }
/// Add an artist to the watchlist by expanding into individual track wanted items. /// Add an artist to the watchlist by expanding into individual track wanted items.
///
/// 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( pub async fn add_artist(
conn: &DatabaseConnection, conn: &DatabaseConnection,
name: Option<&str>, name: Option<&str>,
musicbrainz_id: Option<&str>, musicbrainz_id: Option<&str>,
provider: &impl MetadataProvider, provider: &impl MetadataProvider,
user_id: Option<i32>,
) -> WatchResult<AddSummary> { ) -> WatchResult<AddSummary> {
let (resolved_name, resolved_mbid) = let (resolved_name, resolved_mbid) =
resolve_artist_info(name, musicbrainz_id, provider).await?; resolve_artist_info(name, musicbrainz_id, provider).await?;
let artist = queries::artists::upsert(conn, &resolved_name, resolved_mbid.as_deref()).await?; let artist = queries::artists::upsert(conn, &resolved_name, resolved_mbid.as_deref()).await?;
// Get artist MBID — either provided or from the search
let artist_mbid = resolved_mbid.or_else(|| artist.musicbrainz_id.clone()); let artist_mbid = resolved_mbid.or_else(|| artist.musicbrainz_id.clone());
let artist_mbid = match artist_mbid { let artist_mbid = match artist_mbid {
Some(mbid) => mbid, Some(mbid) => mbid,
None => { None => {
// Search for the artist to get MBID
let results = provider let results = provider
.search_artist(&resolved_name, 1) .search_artist(&resolved_name, 1)
.await .await
.map_err(|e| WatchError::Other(format!("artist search failed: {e}")))?; .map_err(|e| WatchError::Other(format!("artist search failed: {e}")))?;
results results.into_iter().next().map(|a| a.mbid).ok_or_else(|| {
.into_iter() WatchError::Other(format!(
.next() "artist '{}' not found on MusicBrainz",
.map(|a| a.mbid) resolved_name
.ok_or_else(|| WatchError::Other(format!("artist '{}' not found on MusicBrainz", resolved_name)))? ))
})?
} }
}; };
tracing::info!(name = %resolved_name, mbid = %artist_mbid, "fetching discography"); tracing::info!(name = %resolved_name, mbid = %artist_mbid, "fetching discography (release groups)");
// Fetch all releases let release_groups = provider
let releases = provider .get_artist_release_groups(&artist_mbid)
.get_artist_releases(&artist_mbid, 100)
.await .await
.map_err(|e| WatchError::Other(format!("failed to fetch discography: {e}")))?; .map_err(|e| WatchError::Other(format!("failed to fetch release groups: {e}")))?;
tracing::info!(count = releases.len(), "found releases"); tracing::info!(count = release_groups.len(), "found release groups");
let mut summary = AddSummary::default(); let mut summary = AddSummary::default();
for release in &releases { for rg in &release_groups {
tracing::info!(title = %release.title, mbid = %release.mbid, "fetching tracks"); // Resolve a concrete release MBID from the release group
let release_mbid = if let Some(ref rid) = rg.first_release_mbid {
rid.clone()
} else {
// Browse releases for this release group to find a concrete release
match provider.resolve_release_from_group(&rg.mbid).await {
Ok(rid) => rid,
Err(e) => {
tracing::warn!(rg = %rg.title, rg_id = %rg.mbid, error = %e, "failed to resolve release from group");
summary.errors += 1;
continue;
}
}
};
let tracks = match provider.get_release_tracks(&release.mbid).await { tracing::info!(title = %rg.title, release_mbid = %release_mbid, "fetching tracks for release group");
let tracks = match provider.get_release_tracks(&release_mbid).await {
Ok(t) => t, Ok(t) => t,
Err(e) => { Err(e) => {
tracing::warn!(release = %release.title, error = %e, "failed to fetch tracks"); tracing::warn!(rg = %rg.title, error = %e, "failed to fetch tracks");
summary.errors += 1; summary.errors += 1;
continue; continue;
} }
}; };
for track in &tracks { for track in &tracks {
match add_track_inner(conn, &resolved_name, &track.title, Some(&track.recording_mbid)).await { match add_track_inner(
conn,
&resolved_name,
&track.title,
Some(&track.recording_mbid),
user_id,
)
.await
{
Ok(true) => summary.tracks_added += 1, Ok(true) => summary.tracks_added += 1,
Ok(false) => summary.tracks_already_owned += 1, Ok(false) => summary.tracks_already_owned += 1,
Err(e) => { Err(e) => {
@@ -133,19 +152,17 @@ pub async fn add_artist(
} }
/// Add an album to the watchlist by expanding into individual track wanted items. /// Add an album to the watchlist by expanding into individual track wanted items.
///
/// Fetches the album's tracklist and adds each track as a separate Wanted item.
pub async fn add_album( pub async fn add_album(
conn: &DatabaseConnection, conn: &DatabaseConnection,
artist_name: Option<&str>, artist_name: Option<&str>,
album_name: Option<&str>, album_name: Option<&str>,
musicbrainz_id: Option<&str>, musicbrainz_id: Option<&str>,
provider: &impl MetadataProvider, provider: &impl MetadataProvider,
user_id: Option<i32>,
) -> WatchResult<AddSummary> { ) -> WatchResult<AddSummary> {
let (resolved_album, resolved_artist, resolved_mbid) = let (resolved_album, resolved_artist, resolved_mbid) =
resolve_album_info(artist_name, album_name, musicbrainz_id, provider).await?; resolve_album_info(artist_name, album_name, musicbrainz_id, provider).await?;
// Get release MBID
let release_mbid = match resolved_mbid { let release_mbid = match resolved_mbid {
Some(mbid) => mbid, Some(mbid) => mbid,
None => { None => {
@@ -153,11 +170,12 @@ pub async fn add_album(
.search_release(&resolved_artist, &resolved_album) .search_release(&resolved_artist, &resolved_album)
.await .await
.map_err(|e| WatchError::Other(format!("album search failed: {e}")))?; .map_err(|e| WatchError::Other(format!("album search failed: {e}")))?;
results results.into_iter().next().map(|r| r.mbid).ok_or_else(|| {
.into_iter() WatchError::Other(format!(
.next() "album '{}' not found on MusicBrainz",
.map(|r| r.mbid) resolved_album
.ok_or_else(|| WatchError::Other(format!("album '{}' not found on MusicBrainz", resolved_album)))? ))
})?
} }
}; };
@@ -171,7 +189,15 @@ pub async fn add_album(
let mut summary = AddSummary::default(); let mut summary = AddSummary::default();
for track in &tracks { for track in &tracks {
match add_track_inner(conn, &resolved_artist, &track.title, Some(&track.recording_mbid)).await { match add_track_inner(
conn,
&resolved_artist,
&track.title,
Some(&track.recording_mbid),
user_id,
)
.await
{
Ok(true) => summary.tracks_added += 1, Ok(true) => summary.tracks_added += 1,
Ok(false) => summary.tracks_already_owned += 1, Ok(false) => summary.tracks_already_owned += 1,
Err(e) => { Err(e) => {
@@ -192,6 +218,7 @@ pub async fn add_track(
title: Option<&str>, title: Option<&str>,
musicbrainz_id: Option<&str>, musicbrainz_id: Option<&str>,
provider: &impl MetadataProvider, provider: &impl MetadataProvider,
user_id: Option<i32>,
) -> WatchResult<WatchListEntry> { ) -> WatchResult<WatchListEntry> {
let (resolved_title, resolved_artist, resolved_mbid) = let (resolved_title, resolved_artist, resolved_mbid) =
resolve_track_info(artist_name, title, musicbrainz_id, provider).await?; resolve_track_info(artist_name, title, musicbrainz_id, provider).await?;
@@ -201,12 +228,15 @@ pub async fn add_track(
let item = queries::wanted::add( let item = queries::wanted::add(
conn, conn,
ItemType::Track, queries::wanted::AddWantedItem {
&resolved_title, item_type: ItemType::Track,
resolved_mbid.as_deref(), name: &resolved_title,
Some(artist.id), musicbrainz_id: resolved_mbid.as_deref(),
None, artist_id: Some(artist.id),
None, album_id: None,
track_id: None,
user_id,
},
) )
.await?; .await?;
@@ -234,18 +264,22 @@ async fn add_track_inner(
artist_name: &str, artist_name: &str,
title: &str, title: &str,
recording_mbid: Option<&str>, recording_mbid: Option<&str>,
user_id: Option<i32>,
) -> WatchResult<bool> { ) -> WatchResult<bool> {
let artist = queries::artists::upsert(conn, artist_name, None).await?; let artist = queries::artists::upsert(conn, artist_name, None).await?;
let is_owned = matching::track_is_owned(conn, artist_name, title).await?; let is_owned = matching::track_is_owned(conn, artist_name, title).await?;
let item = queries::wanted::add( let item = queries::wanted::add(
conn, conn,
ItemType::Track, queries::wanted::AddWantedItem {
title, item_type: ItemType::Track,
recording_mbid, name: title,
Some(artist.id), musicbrainz_id: recording_mbid,
None, artist_id: Some(artist.id),
None, album_id: None,
track_id: None,
user_id,
},
) )
.await?; .await?;
@@ -269,11 +303,9 @@ async fn resolve_artist_info(
return Ok((n.to_string(), mbid.map(String::from))); return Ok((n.to_string(), mbid.map(String::from)));
} }
let mbid = mbid.ok_or_else(|| { let mbid =
WatchError::Other("either a name or --mbid is required".into()) mbid.ok_or_else(|| WatchError::Other("either a name or --mbid is required".into()))?;
})?;
// Search for artist by MBID to get the name
let results = provider let results = provider
.search_artist(mbid, 1) .search_artist(mbid, 1)
.await .await
@@ -282,7 +314,10 @@ async fn resolve_artist_info(
if let Some(artist) = results.into_iter().next() { if let Some(artist) = results.into_iter().next() {
Ok((artist.name, Some(mbid.to_string()))) Ok((artist.name, Some(mbid.to_string())))
} else { } else {
Ok((format!("Artist [{}]", &mbid[..8.min(mbid.len())]), Some(mbid.to_string()))) Ok((
format!("Artist [{}]", &mbid[..8.min(mbid.len())]),
Some(mbid.to_string()),
))
} }
} }
@@ -297,7 +332,11 @@ async fn resolve_album_info(
album_name.filter(|s| !s.is_empty()), album_name.filter(|s| !s.is_empty()),
artist_name.filter(|s| !s.is_empty()), artist_name.filter(|s| !s.is_empty()),
) { ) {
return Ok((album.to_string(), artist.to_string(), mbid.map(String::from))); return Ok((
album.to_string(),
artist.to_string(),
mbid.map(String::from),
));
} }
let mbid = mbid.ok_or_else(|| { let mbid = mbid.ok_or_else(|| {
@@ -317,7 +356,9 @@ async fn resolve_album_info(
.unwrap_or_else(|| release.artist.clone()); .unwrap_or_else(|| release.artist.clone());
Ok((album, artist, Some(mbid.to_string()))) Ok((album, artist, Some(mbid.to_string())))
} else { } else {
Err(WatchError::Other(format!("no release found for MBID {mbid}"))) Err(WatchError::Other(format!(
"no release found for MBID {mbid}"
)))
} }
} }
@@ -335,9 +376,8 @@ async fn resolve_track_info(
return Ok((t.to_string(), a.to_string(), mbid.map(String::from))); return Ok((t.to_string(), a.to_string(), mbid.map(String::from)));
} }
let mbid = mbid.ok_or_else(|| { let mbid =
WatchError::Other("either artist+title or --mbid is required".into()) mbid.ok_or_else(|| WatchError::Other("either artist+title or --mbid is required".into()))?;
})?;
let details = provider let details = provider
.get_recording(mbid) .get_recording(mbid)
@@ -345,8 +385,14 @@ async fn resolve_track_info(
.map_err(|e| WatchError::Other(format!("MusicBrainz lookup failed: {e}")))?; .map_err(|e| WatchError::Other(format!("MusicBrainz lookup failed: {e}")))?;
Ok(( Ok((
title.filter(|s| !s.is_empty()).map(String::from).unwrap_or(details.title), title
artist_name.filter(|s| !s.is_empty()).map(String::from).unwrap_or(details.artist), .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()), Some(mbid.to_string()),
)) ))
} }
@@ -356,12 +402,12 @@ pub async fn list_items(
conn: &DatabaseConnection, conn: &DatabaseConnection,
status_filter: Option<WantedStatus>, status_filter: Option<WantedStatus>,
artist_filter: Option<&str>, artist_filter: Option<&str>,
user_id: Option<i32>,
) -> WatchResult<Vec<WatchListEntry>> { ) -> WatchResult<Vec<WatchListEntry>> {
let items = queries::wanted::list(conn, status_filter).await?; let items = queries::wanted::list(conn, status_filter, user_id).await?;
let mut entries = Vec::new(); let mut entries = Vec::new();
for item in items { for item in items {
// Use the name field directly now
let artist_name = if let Some(id) = item.artist_id { let artist_name = if let Some(id) = item.artist_id {
queries::artists::get_by_id(conn, id) queries::artists::get_by_id(conn, id)
.await .await
@@ -371,7 +417,6 @@ pub async fn list_items(
None None
}; };
// Apply artist filter if provided
if let Some(filter) = artist_filter { if let Some(filter) = artist_filter {
let filter_norm = matching::normalize(filter); let filter_norm = matching::normalize(filter);
let matches = artist_name let matches = artist_name
@@ -405,7 +450,7 @@ pub async fn remove_item(conn: &DatabaseConnection, id: i32) -> WatchResult<()>
/// Get a summary of the library state. /// Get a summary of the library state.
pub async fn library_summary(conn: &DatabaseConnection) -> WatchResult<LibrarySummary> { pub async fn library_summary(conn: &DatabaseConnection) -> WatchResult<LibrarySummary> {
let all = queries::wanted::list(conn, None).await?; let all = queries::wanted::list(conn, None, None).await?;
let mut summary = LibrarySummary::default(); let mut summary = LibrarySummary::default();
for item in &all { for item in &all {

View File

@@ -96,7 +96,9 @@ fn parse_status(s: &str) -> anyhow::Result<WantedStatus> {
"available" => Ok(WantedStatus::Available), "available" => Ok(WantedStatus::Available),
"downloaded" => Ok(WantedStatus::Downloaded), "downloaded" => Ok(WantedStatus::Downloaded),
"owned" => Ok(WantedStatus::Owned), "owned" => Ok(WantedStatus::Owned),
_ => anyhow::bail!("unknown status: {s} (expected wanted, available, downloaded, or owned)"), _ => {
anyhow::bail!("unknown status: {s} (expected wanted, available, downloaded, or owned)")
}
} }
} }
@@ -133,11 +135,16 @@ async fn main() -> anyhow::Result<()> {
name.as_deref(), name.as_deref(),
mbid.as_deref(), mbid.as_deref(),
&mb_client, &mb_client,
None,
) )
.await?; .await?;
println!("Artist watch: {summary}"); println!("Artist watch: {summary}");
} }
AddCommand::Album { artist, album, mbid } => { AddCommand::Album {
artist,
album,
mbid,
} => {
if artist.is_none() && album.is_none() && mbid.is_none() { if artist.is_none() && album.is_none() && mbid.is_none() {
anyhow::bail!("provide artist+album names or --mbid"); anyhow::bail!("provide artist+album names or --mbid");
} }
@@ -147,11 +154,16 @@ async fn main() -> anyhow::Result<()> {
album.as_deref(), album.as_deref(),
mbid.as_deref(), mbid.as_deref(),
&mb_client, &mb_client,
None,
) )
.await?; .await?;
println!("Album watch: {summary}"); println!("Album watch: {summary}");
} }
AddCommand::Track { artist, title, mbid } => { AddCommand::Track {
artist,
title,
mbid,
} => {
if artist.is_none() && title.is_none() && mbid.is_none() { if artist.is_none() && title.is_none() && mbid.is_none() {
anyhow::bail!("provide artist+title or --mbid"); anyhow::bail!("provide artist+title or --mbid");
} }
@@ -161,6 +173,7 @@ async fn main() -> anyhow::Result<()> {
title.as_deref(), title.as_deref(),
mbid.as_deref(), mbid.as_deref(),
&mb_client, &mb_client,
None,
) )
.await?; .await?;
println!( println!(
@@ -174,14 +187,14 @@ async fn main() -> anyhow::Result<()> {
}, },
Commands::List { status, artist } => { Commands::List { status, artist } => {
let status_filter = status.as_deref().map(parse_status).transpose()?; let status_filter = status.as_deref().map(parse_status).transpose()?;
let entries = list_items(db.conn(), status_filter, artist.as_deref()).await?; let entries = list_items(db.conn(), status_filter, artist.as_deref(), None).await?;
if entries.is_empty() { if entries.is_empty() {
println!("Watchlist is empty."); println!("Watchlist is empty.");
} else { } else {
println!( println!(
"{:<5} {:<8} {:<12} {:<30} {}", "{:<5} {:<8} {:<12} {:<30} ARTIST",
"ID", "TYPE", "STATUS", "NAME", "ARTIST" "ID", "TYPE", "STATUS", "NAME"
); );
for e in &entries { for e in &entries {
println!( println!(

View File

@@ -7,7 +7,11 @@ use crate::error::WatchResult;
/// Normalize a string for fuzzy comparison: NFC unicode, lowercase, trim. /// Normalize a string for fuzzy comparison: NFC unicode, lowercase, trim.
pub fn normalize(s: &str) -> String { pub fn normalize(s: &str) -> String {
s.nfc().collect::<String>().to_lowercase().trim().to_string() s.nfc()
.collect::<String>()
.to_lowercase()
.trim()
.to_string()
} }
/// Check if an artist is "owned" — i.e., any tracks by this artist exist in the indexed library. /// Check if an artist is "owned" — i.e., any tracks by this artist exist in the indexed library.
@@ -52,7 +56,9 @@ pub async fn album_is_owned(
let norm_album = normalize(album_name); let norm_album = normalize(album_name);
// Try exact lookup // Try exact lookup
if let Some(album) = queries::albums::find_by_name_and_artist(conn, album_name, artist_name).await? { if let Some(album) =
queries::albums::find_by_name_and_artist(conn, album_name, artist_name).await?
{
let tracks = queries::tracks::get_by_album(conn, album.id).await?; let tracks = queries::tracks::get_by_album(conn, album.id).await?;
if !tracks.is_empty() { if !tracks.is_empty() {
return Ok(true); return Ok(true);

View File

@@ -1,9 +1,9 @@
use chrono::Utc; use chrono::Utc;
use sea_orm::ActiveValue::Set; use sea_orm::ActiveValue::Set;
use shanty_data::DataResult;
use shanty_db::entities::wanted_item::{ItemType, WantedStatus}; use shanty_db::entities::wanted_item::{ItemType, WantedStatus};
use shanty_db::{Database, queries}; use shanty_db::{Database, queries};
use shanty_tag::error::TagResult;
use shanty_tag::provider::*; use shanty_tag::provider::*;
use shanty_watch::{add_album, add_artist, add_track, library_summary, list_items, remove_item}; use shanty_watch::{add_album, add_artist, add_track, library_summary, list_items, remove_item};
@@ -42,10 +42,14 @@ async fn insert_track(db: &Database, artist: &str, title: &str, album: &str) {
struct MockProvider; struct MockProvider;
impl MetadataProvider for MockProvider { impl MetadataProvider for MockProvider {
async fn search_recording(&self, _artist: &str, _title: &str) -> TagResult<Vec<RecordingMatch>> { async fn search_recording(
&self,
_artist: &str,
_title: &str,
) -> DataResult<Vec<RecordingMatch>> {
Ok(vec![]) Ok(vec![])
} }
async fn search_release(&self, _artist: &str, _album: &str) -> TagResult<Vec<ReleaseMatch>> { async fn search_release(&self, _artist: &str, _album: &str) -> DataResult<Vec<ReleaseMatch>> {
Ok(vec![ReleaseMatch { Ok(vec![ReleaseMatch {
mbid: "release-123".into(), mbid: "release-123".into(),
title: "Test Album".into(), title: "Test Album".into(),
@@ -56,10 +60,14 @@ impl MetadataProvider for MockProvider {
score: 100, score: 100,
}]) }])
} }
async fn get_recording(&self, _mbid: &str) -> TagResult<RecordingDetails> { async fn get_recording(&self, _mbid: &str) -> DataResult<RecordingDetails> {
Err(shanty_tag::TagError::Other("not found".into())) Err(shanty_data::DataError::Other("not found".into()))
} }
async fn search_artist(&self, _query: &str, _limit: u32) -> TagResult<Vec<ArtistSearchResult>> { async fn search_artist(
&self,
_query: &str,
_limit: u32,
) -> DataResult<Vec<ArtistSearchResult>> {
Ok(vec![ArtistSearchResult { Ok(vec![ArtistSearchResult {
mbid: "artist-456".into(), mbid: "artist-456".into(),
name: "Test Artist".into(), name: "Test Artist".into(),
@@ -69,7 +77,11 @@ impl MetadataProvider for MockProvider {
score: 100, score: 100,
}]) }])
} }
async fn get_artist_releases(&self, _mbid: &str, _limit: u32) -> TagResult<Vec<DiscographyEntry>> { async fn get_artist_releases(
&self,
_mbid: &str,
_limit: u32,
) -> DataResult<Vec<DiscographyEntry>> {
Ok(vec![DiscographyEntry { Ok(vec![DiscographyEntry {
mbid: "release-123".into(), mbid: "release-123".into(),
title: "Test Album".into(), title: "Test Album".into(),
@@ -78,7 +90,7 @@ impl MetadataProvider for MockProvider {
track_count: Some(2), track_count: Some(2),
}]) }])
} }
async fn get_release_tracks(&self, _release_mbid: &str) -> TagResult<Vec<ReleaseTrack>> { async fn get_release_tracks(&self, _release_mbid: &str) -> DataResult<Vec<ReleaseTrack>> {
Ok(vec![ Ok(vec![
ReleaseTrack { ReleaseTrack {
recording_mbid: "rec-1".into(), recording_mbid: "rec-1".into(),
@@ -97,8 +109,28 @@ impl MetadataProvider for MockProvider {
]) ])
} }
async fn get_artist_release_groups(&self, _artist_mbid: &str) -> TagResult<Vec<shanty_tag::provider::ReleaseGroupEntry>> { async fn get_artist_release_groups(
Ok(vec![]) &self,
_artist_mbid: &str,
) -> DataResult<Vec<shanty_tag::provider::ReleaseGroupEntry>> {
Ok(vec![ReleaseGroupEntry {
mbid: "rg-123".into(),
title: "Test Album".into(),
primary_type: Some("Album".into()),
secondary_types: vec![],
first_release_date: Some("2024".into()),
first_release_mbid: Some("release-123".into()),
}])
}
async fn resolve_release_from_group(&self, release_group_mbid: &str) -> DataResult<String> {
if release_group_mbid == "rg-123" {
Ok("release-123".into())
} else {
Err(shanty_data::DataError::Other(format!(
"no releases for release-group {release_group_mbid}"
)))
}
} }
} }
@@ -107,7 +139,14 @@ async fn test_add_track_wanted() {
let db = test_db().await; let db = test_db().await;
let provider = MockProvider; let provider = MockProvider;
let entry = add_track(db.conn(), Some("Radiohead"), Some("Creep"), None, &provider) let entry = add_track(
db.conn(),
Some("Radiohead"),
Some("Creep"),
None,
&provider,
None,
)
.await .await
.unwrap(); .unwrap();
assert_eq!(entry.item_type, ItemType::Track); assert_eq!(entry.item_type, ItemType::Track);
@@ -122,7 +161,14 @@ async fn test_add_track_auto_owned() {
insert_track(&db, "Pink Floyd", "Time", "DSOTM").await; insert_track(&db, "Pink Floyd", "Time", "DSOTM").await;
let entry = add_track(db.conn(), Some("Pink Floyd"), Some("Time"), None, &provider) let entry = add_track(
db.conn(),
Some("Pink Floyd"),
Some("Time"),
None,
&provider,
None,
)
.await .await
.unwrap(); .unwrap();
assert_eq!(entry.status, WantedStatus::Owned); assert_eq!(entry.status, WantedStatus::Owned);
@@ -133,12 +179,19 @@ async fn test_add_album_expands_to_tracks() {
let db = test_db().await; let db = test_db().await;
let provider = MockProvider; let provider = MockProvider;
let summary = add_album(db.conn(), Some("Test Artist"), Some("Test Album"), None, &provider) let summary = add_album(
db.conn(),
Some("Test Artist"),
Some("Test Album"),
None,
&provider,
None,
)
.await .await
.unwrap(); .unwrap();
assert_eq!(summary.tracks_added, 2); assert_eq!(summary.tracks_added, 2);
let items = list_items(db.conn(), Some(WantedStatus::Wanted), None) let items = list_items(db.conn(), Some(WantedStatus::Wanted), None, None)
.await .await
.unwrap(); .unwrap();
assert_eq!(items.len(), 2); assert_eq!(items.len(), 2);
@@ -150,12 +203,12 @@ async fn test_add_artist_expands_to_tracks() {
let db = test_db().await; let db = test_db().await;
let provider = MockProvider; let provider = MockProvider;
let summary = add_artist(db.conn(), Some("Test Artist"), None, &provider) let summary = add_artist(db.conn(), Some("Test Artist"), None, &provider, None)
.await .await
.unwrap(); .unwrap();
assert_eq!(summary.tracks_added, 2); assert_eq!(summary.tracks_added, 2);
let items = list_items(db.conn(), None, None).await.unwrap(); let items = list_items(db.conn(), None, None, None).await.unwrap();
assert_eq!(items.len(), 2); assert_eq!(items.len(), 2);
} }
@@ -164,22 +217,36 @@ async fn test_list_items_with_filters() {
let db = test_db().await; let db = test_db().await;
let provider = MockProvider; let provider = MockProvider;
add_track(db.conn(), Some("Radiohead"), Some("Creep"), None, &provider) add_track(
db.conn(),
Some("Radiohead"),
Some("Creep"),
None,
&provider,
None,
)
.await .await
.unwrap(); .unwrap();
add_track(db.conn(), Some("Tool"), Some("Lateralus"), None, &provider) add_track(
db.conn(),
Some("Tool"),
Some("Lateralus"),
None,
&provider,
None,
)
.await .await
.unwrap(); .unwrap();
let all = list_items(db.conn(), None, None).await.unwrap(); let all = list_items(db.conn(), None, None, None).await.unwrap();
assert_eq!(all.len(), 2); assert_eq!(all.len(), 2);
let wanted = list_items(db.conn(), Some(WantedStatus::Wanted), None) let wanted = list_items(db.conn(), Some(WantedStatus::Wanted), None, None)
.await .await
.unwrap(); .unwrap();
assert_eq!(wanted.len(), 2); assert_eq!(wanted.len(), 2);
let radiohead = list_items(db.conn(), None, Some("Radiohead")) let radiohead = list_items(db.conn(), None, Some("Radiohead"), None)
.await .await
.unwrap(); .unwrap();
assert_eq!(radiohead.len(), 1); assert_eq!(radiohead.len(), 1);
@@ -190,11 +257,18 @@ async fn test_remove_item() {
let db = test_db().await; let db = test_db().await;
let provider = MockProvider; let provider = MockProvider;
let entry = add_track(db.conn(), Some("Radiohead"), Some("Creep"), None, &provider) let entry = add_track(
db.conn(),
Some("Radiohead"),
Some("Creep"),
None,
&provider,
None,
)
.await .await
.unwrap(); .unwrap();
remove_item(db.conn(), entry.id).await.unwrap(); remove_item(db.conn(), entry.id).await.unwrap();
let all = list_items(db.conn(), None, None).await.unwrap(); let all = list_items(db.conn(), None, None, None).await.unwrap();
assert!(all.is_empty()); assert!(all.is_empty());
} }
@@ -205,10 +279,24 @@ async fn test_library_summary() {
insert_track(&db, "Pink Floyd", "Time", "DSOTM").await; insert_track(&db, "Pink Floyd", "Time", "DSOTM").await;
add_track(db.conn(), Some("Radiohead"), Some("Creep"), None, &provider) add_track(
db.conn(),
Some("Radiohead"),
Some("Creep"),
None,
&provider,
None,
)
.await .await
.unwrap(); .unwrap();
add_track(db.conn(), Some("Pink Floyd"), Some("Time"), None, &provider) add_track(
db.conn(),
Some("Pink Floyd"),
Some("Time"),
None,
&provider,
None,
)
.await .await
.unwrap(); .unwrap();