diff --git a/examples/musicbrainz_api/lookup_artist.rs b/examples/musicbrainz_api/lookup_artist.rs index f072d72..3b65167 100644 --- a/examples/musicbrainz_api/lookup_artist.rs +++ b/examples/musicbrainz_api/lookup_artist.rs @@ -34,9 +34,9 @@ fn main() { let mut request = LookupArtistRequest::new(&mbid); request.include_release_groups(); - let albums = client + let response = client .lookup_artist(request) .expect("failed to make API call"); - println!("{albums:#?}"); + println!("{response:#?}"); } diff --git a/src/core/collection/album.rs b/src/core/collection/album.rs index 2ca28b6..6669720 100644 --- a/src/core/collection/album.rs +++ b/src/core/collection/album.rs @@ -205,11 +205,11 @@ impl AlbumMeta { } pub fn set_musicbrainz_ref(&mut self, mbref: MbAlbumRef) { - _ = self.musicbrainz.insert(mbref); + self.musicbrainz.replace(mbref); } pub fn clear_musicbrainz_ref(&mut self) { - _ = self.musicbrainz.take(); + self.musicbrainz.take(); } } @@ -371,7 +371,7 @@ mod tests { album .meta .set_musicbrainz_ref(MbAlbumRef::from_url_str(MUSICBRAINZ).unwrap()); - _ = expected.insert(MbAlbumRef::from_url_str(MUSICBRAINZ).unwrap()); + expected.replace(MbAlbumRef::from_url_str(MUSICBRAINZ).unwrap()); assert_eq!(album.meta.musicbrainz, expected); album @@ -382,12 +382,12 @@ mod tests { album .meta .set_musicbrainz_ref(MbAlbumRef::from_url_str(MUSICBRAINZ_2).unwrap()); - _ = expected.insert(MbAlbumRef::from_url_str(MUSICBRAINZ_2).unwrap()); + expected.replace(MbAlbumRef::from_url_str(MUSICBRAINZ_2).unwrap()); assert_eq!(album.meta.musicbrainz, expected); // Clearing URLs. album.meta.clear_musicbrainz_ref(); - _ = expected.take(); + expected.take(); assert_eq!(album.meta.musicbrainz, expected); } } diff --git a/src/core/collection/artist.rs b/src/core/collection/artist.rs index 93e4a1a..f08869d 100644 --- a/src/core/collection/artist.rs +++ b/src/core/collection/artist.rs @@ -89,15 +89,15 @@ impl ArtistMeta { } pub fn clear_sort_key(&mut self) { - _ = self.sort.take(); + self.sort.take(); } pub fn set_musicbrainz_ref(&mut self, mbref: MbArtistRef) { - _ = self.musicbrainz.insert(mbref); + self.musicbrainz.replace(mbref); } pub fn clear_musicbrainz_ref(&mut self) { - _ = self.musicbrainz.take(); + self.musicbrainz.take(); } // In the functions below, it would be better to use `contains` instead of `iter().any`, but for @@ -262,7 +262,7 @@ mod tests { artist .meta .set_musicbrainz_ref(MbArtistRef::from_url_str(MUSICBRAINZ).unwrap()); - _ = expected.insert(MbArtistRef::from_url_str(MUSICBRAINZ).unwrap()); + expected.replace(MbArtistRef::from_url_str(MUSICBRAINZ).unwrap()); assert_eq!(artist.meta.musicbrainz, expected); artist @@ -273,12 +273,12 @@ mod tests { artist .meta .set_musicbrainz_ref(MbArtistRef::from_url_str(MUSICBRAINZ_2).unwrap()); - _ = expected.insert(MbArtistRef::from_url_str(MUSICBRAINZ_2).unwrap()); + expected.replace(MbArtistRef::from_url_str(MUSICBRAINZ_2).unwrap()); assert_eq!(artist.meta.musicbrainz, expected); // Clearing URLs. artist.meta.clear_musicbrainz_ref(); - _ = expected.take(); + expected.take(); assert_eq!(artist.meta.musicbrainz, expected); } diff --git a/src/core/musichoard/database.rs b/src/core/musichoard/database.rs index 25287ac..0dc4f97 100644 --- a/src/core/musichoard/database.rs +++ b/src/core/musichoard/database.rs @@ -454,7 +454,7 @@ mod tests { assert!(music_hoard .set_artist_musicbrainz(&artist_id, MUSICBRAINZ) .is_ok()); - _ = expected.insert(MbArtistRef::from_url_str(MUSICBRAINZ).unwrap()); + expected.replace(MbArtistRef::from_url_str(MUSICBRAINZ).unwrap()); assert_eq!(music_hoard.collection[0].meta.musicbrainz, expected); // Clearing URLs on an artist that does not exist is an error. @@ -463,7 +463,7 @@ mod tests { // Clearing URLs. assert!(music_hoard.clear_artist_musicbrainz(&artist_id).is_ok()); - _ = expected.take(); + expected.take(); assert_eq!(music_hoard.collection[0].meta.musicbrainz, expected); } diff --git a/src/external/musicbrainz/api/lookup.rs b/src/external/musicbrainz/api/lookup.rs index a4df16a..46be188 100644 --- a/src/external/musicbrainz/api/lookup.rs +++ b/src/external/musicbrainz/api/lookup.rs @@ -2,19 +2,15 @@ use serde::Deserialize; use url::form_urlencoded; use crate::{ - collection::{ - album::{AlbumDate, AlbumPrimaryType, AlbumSecondaryType}, - musicbrainz::Mbid, - }, + collection::musicbrainz::Mbid, external::musicbrainz::{ - api::{ - Error, MusicBrainzClient, SerdeAlbumDate, SerdeAlbumPrimaryType, - SerdeAlbumSecondaryType, SerdeMbid, MB_BASE_URL, - }, + api::{Error, MusicBrainzClient, MB_BASE_URL}, IMusicBrainzHttp, }, }; +use super::{MbArtistMeta, MbReleaseGroupMeta, SerdeMbArtistMeta, SerdeMbReleaseGroupMeta}; + impl MusicBrainzClient { pub fn lookup_artist( &mut self, @@ -36,6 +32,19 @@ impl MusicBrainzClient { let response: DeserializeLookupArtistResponse = self.http.get(&url)?; Ok(response.into()) } + + pub fn lookup_release_group( + &mut self, + request: LookupReleaseGroupRequest, + ) -> Result { + let url = format!( + "{MB_BASE_URL}/release-group/{mbid}", + mbid = request.mbid.uuid().as_hyphenated() + ); + + let response: DeserializeLookupReleaseGroupResponse = self.http.get(&url)?; + Ok(response.into()) + } } pub struct LookupArtistRequest<'a> { @@ -59,50 +68,56 @@ impl<'a> LookupArtistRequest<'a> { #[derive(Debug, PartialEq, Eq)] pub struct LookupArtistResponse { - pub release_groups: Vec, + pub meta: MbArtistMeta, + pub release_groups: Vec, } #[derive(Deserialize)] #[serde(rename_all(deserialize = "kebab-case"))] struct DeserializeLookupArtistResponse { - release_groups: Vec, + #[serde(flatten)] + meta: SerdeMbArtistMeta, + release_groups: Option>, } impl From for LookupArtistResponse { fn from(value: DeserializeLookupArtistResponse) -> Self { LookupArtistResponse { - release_groups: value.release_groups.into_iter().map(Into::into).collect(), + meta: value.meta.into(), + release_groups: value + .release_groups + .map(|rgs| rgs.into_iter().map(Into::into).collect()) + .unwrap_or_default(), } } } +pub struct LookupReleaseGroupRequest<'a> { + mbid: &'a Mbid, +} + +impl<'a> LookupReleaseGroupRequest<'a> { + pub fn new(mbid: &'a Mbid) -> Self { + LookupReleaseGroupRequest { mbid } + } +} + #[derive(Debug, PartialEq, Eq)] -pub struct LookupArtistResponseReleaseGroup { - pub id: Mbid, - pub title: String, - pub first_release_date: AlbumDate, - pub primary_type: AlbumPrimaryType, - pub secondary_types: Vec, +pub struct LookupReleaseGroupResponse { + pub meta: MbReleaseGroupMeta, } -#[derive(Clone, Deserialize)] +#[derive(Deserialize)] #[serde(rename_all(deserialize = "kebab-case"))] -struct DeserializeLookupArtistResponseReleaseGroup { - id: SerdeMbid, - title: String, - first_release_date: SerdeAlbumDate, - primary_type: SerdeAlbumPrimaryType, - secondary_types: Vec, +struct DeserializeLookupReleaseGroupResponse { + #[serde(flatten)] + meta: SerdeMbReleaseGroupMeta, } -impl From for LookupArtistResponseReleaseGroup { - fn from(value: DeserializeLookupArtistResponseReleaseGroup) -> Self { - LookupArtistResponseReleaseGroup { - id: value.id.into(), - title: value.title, - first_release_date: value.first_release_date.into(), - primary_type: value.primary_type.into(), - secondary_types: value.secondary_types.into_iter().map(Into::into).collect(), +impl From for LookupReleaseGroupResponse { + fn from(value: DeserializeLookupReleaseGroupResponse) -> Self { + LookupReleaseGroupResponse { + meta: value.meta.into(), } } } @@ -111,42 +126,45 @@ impl From for LookupArtistResponseR mod tests { use mockall::predicate; - use crate::external::musicbrainz::MockIMusicBrainzHttp; + use crate::{ + collection::album::{AlbumPrimaryType, AlbumSecondaryType}, + external::musicbrainz::{ + api::{SerdeAlbumDate, SerdeAlbumPrimaryType, SerdeAlbumSecondaryType, SerdeMbid}, + MockIMusicBrainzHttp, + }, + }; use super::*; #[test] fn lookup_artist() { + let mbid = "00000000-0000-0000-0000-000000000000"; let mut http = MockIMusicBrainzHttp::new(); - let url = format!( - "https://musicbrainz.org/ws/2/artist/{mbid}?inc=release-groups", - mbid = "00000000-0000-0000-0000-000000000000", - ); + let url = format!("https://musicbrainz.org/ws/2/artist/{mbid}?inc=release-groups",); - let de_release_group = DeserializeLookupArtistResponseReleaseGroup { + let de_meta = SerdeMbArtistMeta { + id: SerdeMbid(mbid.try_into().unwrap()), + name: String::from("the artist"), + sort_name: String::from("artist, the"), + disambiguation: Some(String::from("disambig")), + }; + let de_release_group = SerdeMbReleaseGroupMeta { id: SerdeMbid("11111111-1111-1111-1111-111111111111".try_into().unwrap()), title: String::from("an album"), first_release_date: SerdeAlbumDate((1986, 4).into()), primary_type: SerdeAlbumPrimaryType(AlbumPrimaryType::Album), - secondary_types: vec![SerdeAlbumSecondaryType(AlbumSecondaryType::Compilation)], + secondary_types: Some(vec![SerdeAlbumSecondaryType( + AlbumSecondaryType::Compilation, + )]), }; let de_response = DeserializeLookupArtistResponse { - release_groups: vec![de_release_group.clone()], + meta: de_meta.clone(), + release_groups: Some(vec![de_release_group.clone()]), }; - let release_group = LookupArtistResponseReleaseGroup { - id: de_release_group.id.0, - title: de_release_group.title, - first_release_date: de_release_group.first_release_date.0, - primary_type: de_release_group.primary_type.0, - secondary_types: de_release_group - .secondary_types - .into_iter() - .map(|st| st.0) - .collect(), - }; let response = LookupArtistResponse { - release_groups: vec![release_group], + meta: de_meta.into(), + release_groups: vec![de_release_group.into()], }; http.expect_get() @@ -163,4 +181,41 @@ mod tests { assert_eq!(result, response); } + + #[test] + fn lookup_release_group() { + let mbid = "00000000-0000-0000-0000-000000000000"; + let mut http = MockIMusicBrainzHttp::new(); + let url = format!("https://musicbrainz.org/ws/2/release-group/{mbid}",); + + let de_meta = SerdeMbReleaseGroupMeta { + id: SerdeMbid("11111111-1111-1111-1111-111111111111".try_into().unwrap()), + title: String::from("an album"), + first_release_date: SerdeAlbumDate((1986, 4).into()), + primary_type: SerdeAlbumPrimaryType(AlbumPrimaryType::Album), + secondary_types: Some(vec![SerdeAlbumSecondaryType( + AlbumSecondaryType::Compilation, + )]), + }; + let de_response = DeserializeLookupReleaseGroupResponse { + meta: de_meta.clone(), + }; + + let response = LookupReleaseGroupResponse { + meta: de_meta.into(), + }; + + http.expect_get() + .times(1) + .with(predicate::eq(url)) + .return_once(|_| Ok(de_response)); + + let mut client = MusicBrainzClient::new(http); + + let mbid: Mbid = "00000000-0000-0000-0000-000000000000".try_into().unwrap(); + let request = LookupReleaseGroupRequest::new(&mbid); + let result = client.lookup_release_group(request).unwrap(); + + assert_eq!(result, response); + } } diff --git a/src/external/musicbrainz/api/mod.rs b/src/external/musicbrainz/api/mod.rs index 16173cd..2bb9f43 100644 --- a/src/external/musicbrainz/api/mod.rs +++ b/src/external/musicbrainz/api/mod.rs @@ -4,7 +4,8 @@ use serde::{de::Visitor, Deserialize, Deserializer}; use crate::{ collection::{ - album::{AlbumDate, AlbumPrimaryType, AlbumSecondaryType}, + album::{AlbumDate, AlbumId, AlbumPrimaryType, AlbumSecondaryType}, + artist::ArtistId, musicbrainz::Mbid, Error as CollectionError, }, @@ -58,6 +59,67 @@ impl MusicBrainzClient { } } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MbArtistMeta { + pub id: Mbid, + pub name: ArtistId, + pub sort_name: ArtistId, + pub disambiguation: Option, +} + +#[derive(Clone, Deserialize)] +#[serde(rename_all(deserialize = "kebab-case"))] +struct SerdeMbArtistMeta { + id: SerdeMbid, + name: String, + sort_name: String, + disambiguation: Option, +} + +impl From for MbArtistMeta { + fn from(value: SerdeMbArtistMeta) -> Self { + MbArtistMeta { + id: value.id.into(), + name: value.name.into(), + sort_name: value.sort_name.into(), + disambiguation: value.disambiguation, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MbReleaseGroupMeta { + pub id: Mbid, + pub title: AlbumId, + pub first_release_date: AlbumDate, + pub primary_type: AlbumPrimaryType, + pub secondary_types: Option>, +} + +#[derive(Clone, Deserialize)] +#[serde(rename_all(deserialize = "kebab-case"))] +pub struct SerdeMbReleaseGroupMeta { + id: SerdeMbid, + title: String, + first_release_date: SerdeAlbumDate, + primary_type: SerdeAlbumPrimaryType, + secondary_types: Option>, +} + +impl From for MbReleaseGroupMeta { + fn from(value: SerdeMbReleaseGroupMeta) -> Self { + MbReleaseGroupMeta { + id: value.id.into(), + title: value.title.into(), + first_release_date: value.first_release_date.into(), + primary_type: value.primary_type.into(), + secondary_types: value + .secondary_types + .map(|v| v.into_iter().map(Into::into).collect()), + } + } +} + pub struct ApiDisplay; impl ApiDisplay { diff --git a/src/external/musicbrainz/api/search/artist.rs b/src/external/musicbrainz/api/search/artist.rs index 7d0f805..12d1bf1 100644 --- a/src/external/musicbrainz/api/search/artist.rs +++ b/src/external/musicbrainz/api/search/artist.rs @@ -2,12 +2,9 @@ use std::fmt; use serde::Deserialize; -use crate::{ - collection::{artist::ArtistId, musicbrainz::Mbid}, - external::musicbrainz::api::{ - search::query::{impl_term, EmptyQuery, EmptyQueryJoin, Query, QueryJoin}, - SerdeMbid, - }, +use crate::external::musicbrainz::api::{ + search::query::{impl_term, EmptyQuery, EmptyQueryJoin, Query, QueryJoin}, + MbArtistMeta, SerdeMbArtistMeta, }; pub enum SearchArtist<'a> { @@ -48,33 +45,22 @@ impl From for SearchArtistResponse { #[derive(Clone, Debug, PartialEq, Eq)] pub struct SearchArtistResponseArtist { pub score: u8, - pub id: Mbid, - pub name: ArtistId, - pub sort: Option, - pub disambiguation: Option, + pub meta: MbArtistMeta, } #[derive(Clone, Deserialize)] #[serde(rename_all(deserialize = "kebab-case"))] struct DeserializeSearchArtistResponseArtist { score: u8, - id: SerdeMbid, - name: String, - sort_name: String, - disambiguation: Option, + #[serde(flatten)] + meta: SerdeMbArtistMeta, } impl From for SearchArtistResponseArtist { fn from(value: DeserializeSearchArtistResponseArtist) -> Self { - let sort: Option = Some(value.sort_name) - .filter(|s| s != &value.name) - .map(Into::into); SearchArtistResponseArtist { score: value.score, - id: value.id.into(), - name: value.name.into(), - sort, - disambiguation: value.disambiguation, + meta: value.meta.into(), } } } @@ -83,17 +69,22 @@ impl From for SearchArtistResponseArtist mod tests { use mockall::predicate; - use crate::external::musicbrainz::{api::MusicBrainzClient, MockIMusicBrainzHttp}; + use crate::external::musicbrainz::{ + api::{MusicBrainzClient, SerdeMbid}, + MockIMusicBrainzHttp, + }; use super::*; fn de_response() -> DeserializeSearchArtistResponse { let de_artist = DeserializeSearchArtistResponseArtist { score: 67, - id: SerdeMbid("11111111-1111-1111-1111-111111111111".try_into().unwrap()), - name: String::from("an artist"), - sort_name: String::from("artist, an"), - disambiguation: None, + meta: SerdeMbArtistMeta { + id: SerdeMbid("11111111-1111-1111-1111-111111111111".try_into().unwrap()), + name: String::from("an artist"), + sort_name: String::from("artist, an"), + disambiguation: None, + }, }; DeserializeSearchArtistResponse { artists: vec![de_artist.clone()], @@ -107,10 +98,7 @@ mod tests { .into_iter() .map(|a| SearchArtistResponseArtist { score: 67, - id: a.id.0, - name: a.name.clone().into(), - sort: Some(a.sort_name).filter(|sn| sn != &a.name).map(Into::into), - disambiguation: a.disambiguation, + meta: a.meta.into(), }) .collect(), } diff --git a/src/external/musicbrainz/api/search/release_group.rs b/src/external/musicbrainz/api/search/release_group.rs index ae9394e..d33d558 100644 --- a/src/external/musicbrainz/api/search/release_group.rs +++ b/src/external/musicbrainz/api/search/release_group.rs @@ -3,13 +3,10 @@ use std::fmt; use serde::Deserialize; use crate::{ - collection::{ - album::{AlbumDate, AlbumId, AlbumPrimaryType, AlbumSecondaryType}, - musicbrainz::Mbid, - }, + collection::{album::AlbumDate, musicbrainz::Mbid}, external::musicbrainz::api::{ search::query::{impl_term, EmptyQuery, EmptyQueryJoin, Query, QueryJoin}, - ApiDisplay, SerdeAlbumDate, SerdeAlbumPrimaryType, SerdeAlbumSecondaryType, SerdeMbid, + ApiDisplay, MbReleaseGroupMeta, SerdeMbReleaseGroupMeta, }, }; @@ -72,22 +69,15 @@ impl From for SearchReleaseGroupResponse #[derive(Clone, Debug, PartialEq, Eq)] pub struct SearchReleaseGroupResponseReleaseGroup { pub score: u8, - pub id: Mbid, - pub title: AlbumId, - pub first_release_date: AlbumDate, - pub primary_type: AlbumPrimaryType, - pub secondary_types: Option>, + pub meta: MbReleaseGroupMeta, } #[derive(Clone, Deserialize)] #[serde(rename_all(deserialize = "kebab-case"))] pub struct DeserializeSearchReleaseGroupResponseReleaseGroup { score: u8, - id: SerdeMbid, - title: String, - first_release_date: SerdeAlbumDate, - primary_type: SerdeAlbumPrimaryType, - secondary_types: Option>, + #[serde(flatten)] + meta: SerdeMbReleaseGroupMeta, } impl From @@ -96,13 +86,7 @@ impl From fn from(value: DeserializeSearchReleaseGroupResponseReleaseGroup) -> Self { SearchReleaseGroupResponseReleaseGroup { score: value.score, - id: value.id.into(), - title: value.title.into(), - first_release_date: value.first_release_date.into(), - primary_type: value.primary_type.into(), - secondary_types: value - .secondary_types - .map(|v| v.into_iter().map(Into::into).collect()), + meta: value.meta.into(), } } } @@ -111,18 +95,29 @@ impl From mod tests { use mockall::predicate; - use crate::external::musicbrainz::{api::MusicBrainzClient, MockIMusicBrainzHttp}; + use crate::{ + collection::album::{AlbumPrimaryType, AlbumSecondaryType}, + external::musicbrainz::{ + api::{ + MusicBrainzClient, SerdeAlbumDate, SerdeAlbumPrimaryType, SerdeAlbumSecondaryType, + SerdeMbid, + }, + MockIMusicBrainzHttp, + }, + }; use super::*; fn de_response() -> DeserializeSearchReleaseGroupResponse { let de_release_group = DeserializeSearchReleaseGroupResponseReleaseGroup { score: 67, - id: SerdeMbid("11111111-1111-1111-1111-111111111111".try_into().unwrap()), - title: String::from("an album"), - first_release_date: SerdeAlbumDate((1986, 4).into()), - primary_type: SerdeAlbumPrimaryType(AlbumPrimaryType::Album), - secondary_types: Some(vec![SerdeAlbumSecondaryType(AlbumSecondaryType::Live)]), + meta: SerdeMbReleaseGroupMeta { + id: SerdeMbid("11111111-1111-1111-1111-111111111111".try_into().unwrap()), + title: String::from("an album"), + first_release_date: SerdeAlbumDate((1986, 4).into()), + primary_type: SerdeAlbumPrimaryType(AlbumPrimaryType::Album), + secondary_types: Some(vec![SerdeAlbumSecondaryType(AlbumSecondaryType::Live)]), + }, }; DeserializeSearchReleaseGroupResponse { release_groups: vec![de_release_group.clone()], @@ -136,13 +131,7 @@ mod tests { .into_iter() .map(|rg| SearchReleaseGroupResponseReleaseGroup { score: 67, - id: rg.id.0, - title: rg.title.into(), - first_release_date: rg.first_release_date.0, - primary_type: rg.primary_type.0, - secondary_types: rg - .secondary_types - .map(|v| v.into_iter().map(|st| st.0).collect()), + meta: rg.meta.into(), }) .collect(), } diff --git a/src/tui/app/machine/fetch_state.rs b/src/tui/app/machine/fetch_state.rs index 73e5964..4073a04 100644 --- a/src/tui/app/machine/fetch_state.rs +++ b/src/tui/app/machine/fetch_state.rs @@ -3,7 +3,11 @@ use std::{ sync::mpsc::{self, TryRecvError}, }; -use musichoard::collection::{artist::Artist, musicbrainz::IMusicBrainzRef}; +use musichoard::collection::{ + album::AlbumMeta, + artist::{Artist, ArtistMeta}, + musicbrainz::{IMusicBrainzRef, Mbid}, +}; use crate::tui::{ app::{ @@ -15,14 +19,31 @@ use crate::tui::{ }, }; +pub type FetchReceiver = mpsc::Receiver; pub struct FetchState { fetch_rx: FetchReceiver, + lookup_rx: Option, } -pub type FetchReceiver = mpsc::Receiver; impl FetchState { pub fn new(fetch_rx: FetchReceiver) -> Self { - FetchState { fetch_rx } + FetchState { + fetch_rx, + lookup_rx: None, + } + } + + fn try_recv(&mut self) -> Result { + if let Some(lookup_rx) = &self.lookup_rx { + let result = lookup_rx.try_recv(); + match result { + Ok(_) | Err(TryRecvError::Empty) => return result, + Err(TryRecvError::Disconnected) => { + self.lookup_rx.take(); + } + } + } + self.fetch_rx.try_recv() } } @@ -53,8 +74,46 @@ impl AppMachine { Self::app_fetch(inner, fetch, false) } - fn app_fetch(inner: AppInner, fetch: FetchState, first: bool) -> App { - match fetch.fetch_rx.try_recv() { + pub fn app_lookup_artist( + inner: AppInner, + fetch: FetchState, + artist: &ArtistMeta, + mbid: Mbid, + ) -> App { + let f = Self::submit_lookup_artist_job; + Self::app_lookup(f, inner, fetch, artist, mbid) + } + + pub fn app_lookup_album( + inner: AppInner, + fetch: FetchState, + album: &AlbumMeta, + mbid: Mbid, + ) -> App { + let f = Self::submit_lookup_release_group_job; + Self::app_lookup(f, inner, fetch, album, mbid) + } + + fn app_lookup( + submit: F, + inner: AppInner, + mut fetch: FetchState, + meta: Meta, + mbid: Mbid, + ) -> App + where + F: FnOnce(&dyn IMbJobSender, ResultSender, Meta, Mbid) -> Result<(), DaemonError>, + { + let (lookup_tx, lookup_rx) = mpsc::channel::(); + if let Err(err) = submit(&*inner.musicbrainz, lookup_tx, meta, mbid) { + return AppMachine::error_state(inner, err.to_string()).into(); + } + fetch.lookup_rx.replace(lookup_rx); + Self::app_fetch_next(inner, fetch) + } + + fn app_fetch(inner: AppInner, mut fetch: FetchState, first: bool) -> App { + match fetch.try_recv() { Ok(fetch_result) => match fetch_result { Ok(next_match) => { let current = Some(next_match); @@ -95,6 +154,26 @@ impl AppMachine { }; musicbrainz.submit_background_job(result_sender, requests) } + + fn submit_lookup_artist_job( + musicbrainz: &dyn IMbJobSender, + result_sender: ResultSender, + artist: &ArtistMeta, + mbid: Mbid, + ) -> Result<(), DaemonError> { + let requests = VecDeque::from([MbParams::lookup_artist(artist.clone(), mbid)]); + musicbrainz.submit_foreground_job(result_sender, requests) + } + + fn submit_lookup_release_group_job( + musicbrainz: &dyn IMbJobSender, + result_sender: ResultSender, + album: &AlbumMeta, + mbid: Mbid, + ) -> Result<(), DaemonError> { + let requests = VecDeque::from([MbParams::lookup_release_group(album.clone(), mbid)]); + musicbrainz.submit_foreground_job(result_sender, requests) + } } impl From> for App { @@ -133,14 +212,49 @@ mod tests { use crate::tui::{ app::{ machine::tests::{inner, music_hoard}, - Delta, IApp, IAppAccess, IAppInteractBrowse, MatchOption, MatchStateInfo, + Delta, IApp, IAppAccess, IAppInteractBrowse, MatchStateInfo, MissOption, SearchOption, + }, + lib::interface::musicbrainz::{ + self, + api::{Lookup, Match}, + daemon::MockIMbJobSender, }, - lib::interface::musicbrainz::{self, api::Match, daemon::MockIMbJobSender}, testmod::COLLECTION, }; use super::*; + fn mbid() -> Mbid { + "00000000-0000-0000-0000-000000000000".try_into().unwrap() + } + + #[test] + fn try_recv() { + let (fetch_tx, fetch_rx) = mpsc::channel(); + let (lookup_tx, lookup_rx) = mpsc::channel(); + + let mut fetch = FetchState::new(fetch_rx); + fetch.lookup_rx.replace(lookup_rx); + + let artist = COLLECTION[3].meta.clone(); + + let matches: Vec> = vec![]; + let fetch_result = MatchStateInfo::artist_search(artist.clone(), matches); + fetch_tx.send(Ok(fetch_result.clone())).unwrap(); + + assert_eq!(fetch.try_recv(), Err(TryRecvError::Empty)); + + let lookup = Lookup::new(artist.clone()); + let lookup_result = MatchStateInfo::artist_lookup(artist.clone(), lookup); + lookup_tx.send(Ok(lookup_result.clone())).unwrap(); + + assert_eq!(fetch.try_recv(), Ok(Ok(lookup_result))); + + assert_eq!(fetch.try_recv(), Err(TryRecvError::Empty)); + drop(lookup_tx); + assert_eq!(fetch.try_recv(), Ok(Ok(fetch_result))); + } + #[test] fn fetch_no_artist() { let app = AppMachine::app_fetch_new(inner(music_hoard(vec![]))); @@ -186,6 +300,31 @@ mod tests { assert!(matches!(app, AppState::Match(_))); } + fn lookup_album_expectation(job_sender: &mut MockIMbJobSender, album: &AlbumMeta) { + let requests = VecDeque::from([MbParams::lookup_release_group(album.clone(), mbid())]); + job_sender + .expect_submit_foreground_job() + .with(predicate::always(), predicate::eq(requests)) + .times(1) + .return_once(|_, _| Ok(())); + } + + #[test] + fn lookup_album() { + let mut mb_job_sender = MockIMbJobSender::new(); + + let album = COLLECTION[1].albums[0].meta.clone(); + lookup_album_expectation(&mut mb_job_sender, &album); + + let music_hoard = music_hoard(COLLECTION.to_owned()); + let inner = AppInner::new(music_hoard, mb_job_sender); + + let (_fetch_tx, fetch_rx) = mpsc::channel(); + let fetch = FetchState::new(fetch_rx); + + AppMachine::app_lookup_album(inner, fetch, &album, mbid()); + } + fn search_artist_expectation(job_sender: &mut MockIMbJobSender, artist: &ArtistMeta) { let requests = VecDeque::from([MbParams::search_artist(artist.clone())]); job_sender @@ -215,6 +354,31 @@ mod tests { assert!(matches!(app, AppState::Match(_))); } + fn lookup_artist_expectation(job_sender: &mut MockIMbJobSender, artist: &ArtistMeta) { + let requests = VecDeque::from([MbParams::lookup_artist(artist.clone(), mbid())]); + job_sender + .expect_submit_foreground_job() + .with(predicate::always(), predicate::eq(requests)) + .times(1) + .return_once(|_, _| Ok(())); + } + + #[test] + fn lookup_artist() { + let mut mb_job_sender = MockIMbJobSender::new(); + + let artist = COLLECTION[3].meta.clone(); + lookup_artist_expectation(&mut mb_job_sender, &artist); + + let music_hoard = music_hoard(COLLECTION.to_owned()); + let inner = AppInner::new(music_hoard, mb_job_sender); + + let (_fetch_tx, fetch_rx) = mpsc::channel(); + let fetch = FetchState::new(fetch_rx); + + AppMachine::app_lookup_artist(inner, fetch, &artist, mbid()); + } + #[test] fn fetch_artist_job_sender_err() { let mut mb_job_sender = MockIMbJobSender::new(); @@ -231,13 +395,34 @@ mod tests { assert!(matches!(app, AppState::Error(_))); } + #[test] + fn lookup_artist_job_sender_err() { + let mut mb_job_sender = MockIMbJobSender::new(); + + mb_job_sender + .expect_submit_foreground_job() + .return_once(|_, _| Err(DaemonError::JobChannelDisconnected)); + + let artist = COLLECTION[3].meta.clone(); + + let music_hoard = music_hoard(COLLECTION.to_owned()); + let inner = AppInner::new(music_hoard, mb_job_sender); + + let (_fetch_tx, fetch_rx) = mpsc::channel(); + let fetch = FetchState::new(fetch_rx); + + let app = AppMachine::app_lookup_artist(inner, fetch, &artist, mbid()); + assert!(matches!(app, AppState::Error(_))); + } + #[test] fn recv_ok_fetch_ok() { let (tx, rx) = mpsc::channel::(); let artist = COLLECTION[3].meta.clone(); let artist_match = Match::new(80, COLLECTION[2].meta.clone()); - let artist_match_info = MatchStateInfo::artist(artist.clone(), vec![artist_match.clone()]); + let artist_match_info = + MatchStateInfo::artist_search(artist.clone(), vec![artist_match.clone()]); let fetch_result = Ok(artist_match_info); tx.send(fetch_result).unwrap(); @@ -250,10 +435,10 @@ mod tests { let match_state = public.state.unwrap_match(); let match_options = vec![ artist_match.into(), - MatchOption::CannotHaveMbid, - MatchOption::ManualInputMbid, + SearchOption::None(MissOption::CannotHaveMbid), + SearchOption::None(MissOption::ManualInputMbid), ]; - let expected = MatchStateInfo::artist(artist, match_options); + let expected = MatchStateInfo::artist_search(artist, match_options); assert_eq!(match_state.info, Some(expected).as_ref()); } @@ -309,7 +494,8 @@ mod tests { assert!(matches!(app, AppState::Fetch(_))); let artist = COLLECTION[3].meta.clone(); - let fetch_result = Ok(MatchStateInfo::artist::>(artist, vec![])); + let match_info = MatchStateInfo::artist_search::>(artist, vec![]); + let fetch_result = Ok(match_info); tx.send(fetch_result).unwrap(); let app = app.unwrap_fetch().fetch_result_ready(); diff --git a/src/tui/app/machine/input.rs b/src/tui/app/machine/input.rs index 17f22a1..32ca237 100644 --- a/src/tui/app/machine/input.rs +++ b/src/tui/app/machine/input.rs @@ -11,6 +11,12 @@ impl<'app> From<&'app Input> for InputPublic<'app> { } } +impl Input { + pub fn value(&self) -> &str { + self.0.value() + } +} + impl From for AppMode { fn from(mut app: App) -> Self { if let Some(input) = app.input_mut().take() { @@ -43,9 +49,9 @@ impl IAppInput for AppInputMode { self.app } - fn confirm(mut self) -> Self::APP { - if let AppState::Match(state) = &mut self.app { - state.submit_input(self.input); + fn confirm(self) -> Self::APP { + if let AppState::Match(state) = self.app { + return state.submit_input(self.input); } self.app } @@ -58,20 +64,12 @@ impl IAppInput for AppInputMode { #[cfg(test)] mod tests { use crate::tui::app::{ - machine::tests::{mb_job_sender, music_hoard_init}, + machine::tests::{input_event, mb_job_sender, music_hoard_init}, IApp, }; use super::*; - fn input_event(c: char) -> InputEvent { - crossterm::event::KeyEvent::new( - crossterm::event::KeyCode::Char(c), - crossterm::event::KeyModifiers::empty(), - ) - .into() - } - #[test] fn handle_input() { let mut app = App::new(music_hoard_init(vec![]), mb_job_sender()); diff --git a/src/tui/app/machine/match_state.rs b/src/tui/app/machine/match_state.rs index 362fa96..d633319 100644 --- a/src/tui/app/machine/match_state.rs +++ b/src/tui/app/machine/match_state.rs @@ -1,26 +1,62 @@ use std::cmp; +use musichoard::collection::musicbrainz::Mbid; + use crate::tui::app::{ machine::{fetch_state::FetchState, input::Input, App, AppInner, AppMachine}, - AlbumMatches, AppPublicState, AppState, ArtistMatches, IAppInteractMatch, MatchOption, - MatchStateInfo, MatchStatePublic, WidgetState, + AlbumMatches, AppPublicState, AppState, ArtistMatches, IAppInteractMatch, ListOption, + LookupOption, MatchStateInfo, MatchStatePublic, MissOption, SearchOption, WidgetState, }; +impl ListOption { + fn len(&self) -> usize { + match self { + ListOption::Lookup(list) => list.len(), + ListOption::Search(list) => list.len(), + } + } + + fn push_cannot_have_mbid(&mut self) { + match self { + ListOption::Lookup(list) => list.push(LookupOption::None(MissOption::CannotHaveMbid)), + ListOption::Search(list) => list.push(SearchOption::None(MissOption::CannotHaveMbid)), + } + } + + fn push_manual_input_mbid(&mut self) { + match self { + ListOption::Lookup(list) => list.push(LookupOption::None(MissOption::ManualInputMbid)), + ListOption::Search(list) => list.push(SearchOption::None(MissOption::ManualInputMbid)), + } + } + + fn is_manual_input_mbid(&self, index: usize) -> bool { + match self { + ListOption::Lookup(list) => { + list.get(index) == Some(&LookupOption::None(MissOption::ManualInputMbid)) + } + ListOption::Search(list) => { + list.get(index) == Some(&SearchOption::None(MissOption::ManualInputMbid)) + } + } + } +} + impl ArtistMatches { fn len(&self) -> usize { self.list.len() } fn push_cannot_have_mbid(&mut self) { - self.list.push(MatchOption::CannotHaveMbid) + self.list.push_cannot_have_mbid(); } fn push_manual_input_mbid(&mut self) { - self.list.push(MatchOption::ManualInputMbid) + self.list.push_manual_input_mbid(); } fn is_manual_input_mbid(&self, index: usize) -> bool { - self.list.get(index) == Some(&MatchOption::ManualInputMbid) + self.list.is_manual_input_mbid(index) } } @@ -30,15 +66,15 @@ impl AlbumMatches { } fn push_cannot_have_mbid(&mut self) { - self.list.push(MatchOption::CannotHaveMbid) + self.list.push_cannot_have_mbid(); } fn push_manual_input_mbid(&mut self) { - self.list.push(MatchOption::ManualInputMbid) + self.list.push_manual_input_mbid(); } fn is_manual_input_mbid(&self, index: usize) -> bool { - self.list.get(index) == Some(&MatchOption::ManualInputMbid) + self.list.is_manual_input_mbid(index) } } @@ -50,14 +86,14 @@ impl MatchStateInfo { } } - fn push_cannot_have_mbid(&mut self) { + pub fn push_cannot_have_mbid(&mut self) { match self { Self::Artist(a) => a.push_cannot_have_mbid(), Self::Album(a) => a.push_cannot_have_mbid(), } } - fn push_manual_input_mbid(&mut self) { + pub fn push_manual_input_mbid(&mut self) { match self { Self::Artist(a) => a.push_manual_input_mbid(), Self::Album(a) => a.push_manual_input_mbid(), @@ -99,7 +135,22 @@ impl AppMachine { AppMachine::new(inner, state) } - pub fn submit_input(&mut self, _input: Input) {} + pub fn submit_input(self, input: Input) -> App { + let mbid: Mbid = match input.value().try_into() { + Ok(mbid) => mbid, + Err(err) => return AppMachine::error_state(self.inner, err.to_string()).into(), + }; + match self.state.current.as_ref().unwrap() { + MatchStateInfo::Artist(artist_matches) => { + let matching = &artist_matches.matching; + AppMachine::app_lookup_artist(self.inner, self.state.fetch, matching, mbid) + } + MatchStateInfo::Album(album_matches) => { + let matching = &album_matches.matching; + AppMachine::app_lookup_album(self.inner, self.state.fetch, matching, mbid) + } + } + } } impl From> for App { @@ -167,8 +218,9 @@ impl IAppInteractMatch for AppMachine { #[cfg(test)] mod tests { - use std::sync::mpsc; + use std::{collections::VecDeque, sync::mpsc}; + use mockall::predicate; use musichoard::collection::{ album::{AlbumDate, AlbumId, AlbumMeta, AlbumPrimaryType, AlbumSecondaryType}, artist::{ArtistId, ArtistMeta}, @@ -176,10 +228,13 @@ mod tests { use crate::tui::{ app::{ - machine::tests::{inner, music_hoard}, + machine::tests::{inner, inner_with_mb, input_event, music_hoard}, IApp, IAppAccess, IAppInput, }, - lib::interface::musicbrainz::api::Match, + lib::interface::musicbrainz::{ + api::{Lookup, Match}, + daemon::{MbParams, MockIMbJobSender}, + }, }; use super::*; @@ -194,6 +249,15 @@ mod tests { } } + impl Lookup { + pub fn new(item: T) -> Self { + Lookup { + item, + disambiguation: None, + } + } + } + fn artist_match() -> MatchStateInfo { let artist = ArtistMeta::new(ArtistId::new("Artist")); @@ -205,7 +269,13 @@ mod tests { artist_match_2.disambiguation = Some(String::from("some disambiguation")); let list = vec![artist_match_1.clone(), artist_match_2.clone()]; - MatchStateInfo::artist(artist, list) + MatchStateInfo::artist_search(artist, list) + } + + fn artist_lookup() -> MatchStateInfo { + let artist = ArtistMeta::new(ArtistId::new("Artist")); + let lookup = Lookup::new(artist.clone()); + MatchStateInfo::artist_lookup(artist, lookup) } fn album_match() -> MatchStateInfo { @@ -225,7 +295,18 @@ mod tests { let album_match_2 = Match::new(100, album_2); let list = vec![album_match_1.clone(), album_match_2.clone()]; - MatchStateInfo::album(album, list) + MatchStateInfo::album_search(album, list) + } + + fn album_lookup() -> MatchStateInfo { + let album = AlbumMeta::new( + AlbumId::new("Album"), + AlbumDate::new(Some(1990), Some(5), None), + Some(AlbumPrimaryType::Album), + vec![AlbumSecondaryType::Live, AlbumSecondaryType::Compilation], + ); + let lookup = Lookup::new(album.clone()); + MatchStateInfo::album_lookup(album, lookup) } fn fetch_state() -> FetchState { @@ -233,8 +314,8 @@ mod tests { FetchState::new(rx) } - fn match_state(matches_info: Option) -> MatchState { - MatchState::new(matches_info, fetch_state()) + fn match_state(match_state_info: Option) -> MatchState { + MatchState::new(match_state_info, fetch_state()) } #[test] @@ -278,7 +359,7 @@ mod tests { assert_eq!(public_matches.state, &widget_state); } - fn match_state_flow(mut matches_info: MatchStateInfo) { + fn match_state_flow(mut matches_info: MatchStateInfo, len: usize) { // tx must exist for rx to return Empty rather than Disconnected. #[allow(unused_variables)] let (tx, rx) = mpsc::channel(); @@ -299,27 +380,30 @@ mod tests { assert_eq!(matches.state.current.as_ref(), Some(&matches_info)); assert_eq!(matches.state.state.list.selected(), Some(0)); - let matches = matches.next_match().unwrap_match(); + let mut matches = matches; + for ii in 1..len { + matches = matches.next_match().unwrap_match(); - assert_eq!(matches.state.current.as_ref(), Some(&matches_info)); - assert_eq!(matches.state.state.list.selected(), Some(1)); + assert_eq!(matches.state.current.as_ref(), Some(&matches_info)); + assert_eq!(matches.state.state.list.selected(), Some(ii)); + } // Next is CannotHaveMBID let matches = matches.next_match().unwrap_match(); assert_eq!(matches.state.current.as_ref(), Some(&matches_info)); - assert_eq!(matches.state.state.list.selected(), Some(2)); + assert_eq!(matches.state.state.list.selected(), Some(len)); // Next is ManualInputMbid let matches = matches.next_match().unwrap_match(); assert_eq!(matches.state.current.as_ref(), Some(&matches_info)); - assert_eq!(matches.state.state.list.selected(), Some(3)); + assert_eq!(matches.state.state.list.selected(), Some(len + 1)); let matches = matches.next_match().unwrap_match(); assert_eq!(matches.state.current.as_ref(), Some(&matches_info)); - assert_eq!(matches.state.state.list.selected(), Some(3)); + assert_eq!(matches.state.state.list.selected(), Some(len + 1)); // Go prev_match first as selecting on manual input does not go back to fetch. let matches = matches.prev_match().unwrap_match(); @@ -328,12 +412,22 @@ mod tests { #[test] fn artist_matches_flow() { - match_state_flow(artist_match()); + match_state_flow(artist_match(), 2); + } + + #[test] + fn artist_lookup_flow() { + match_state_flow(artist_lookup(), 1); } #[test] fn album_matches_flow() { - match_state_flow(album_match()); + match_state_flow(album_match(), 2); + } + + #[test] + fn album_lookup_flow() { + match_state_flow(album_lookup(), 1); } #[test] @@ -364,7 +458,7 @@ mod tests { } #[test] - fn select_manual_input() { + fn select_manual_input_empty() { let matches = AppMachine::match_state(inner(music_hoard(vec![])), match_state(Some(album_match()))); @@ -377,6 +471,75 @@ mod tests { let app = matches.select(); let input = app.mode().unwrap_input(); - input.confirm().unwrap_match(); + input.confirm().unwrap_error(); + } + + fn mbid() -> Mbid { + "00000000-0000-0000-0000-000000000000".try_into().unwrap() + } + + fn input_mbid(mut app: App) -> App { + let mbid = mbid().uuid().to_string(); + for c in mbid.chars() { + let input = app.mode().unwrap_input(); + app = input.input(input_event(c)); + } + app + } + + #[test] + fn select_manual_input_artist() { + let mut mb_job_sender = MockIMbJobSender::new(); + let artist = ArtistMeta::new(ArtistId::new("Artist")); + let requests = VecDeque::from([MbParams::lookup_artist(artist.clone(), mbid())]); + mb_job_sender + .expect_submit_foreground_job() + .with(predicate::always(), predicate::eq(requests)) + .return_once(|_, _| Ok(())); + + let matches_vec: Vec> = vec![]; + let artist_match = MatchStateInfo::artist_search(artist.clone(), matches_vec); + let matches = AppMachine::match_state( + inner_with_mb(music_hoard(vec![]), mb_job_sender), + match_state(Some(artist_match)), + ); + + // There are no matches which means that the second option should be manual input. + let matches = matches.next_match().unwrap_match(); + let matches = matches.next_match().unwrap_match(); + + let mut app = matches.select(); + app = input_mbid(app); + + let input = app.mode().unwrap_input(); + input.confirm(); + } + + #[test] + fn select_manual_input_album() { + let mut mb_job_sender = MockIMbJobSender::new(); + let album = AlbumMeta::new("Album", 1990, None, vec![]); + let requests = VecDeque::from([MbParams::lookup_release_group(album.clone(), mbid())]); + mb_job_sender + .expect_submit_foreground_job() + .with(predicate::always(), predicate::eq(requests)) + .return_once(|_, _| Ok(())); + + let matches_vec: Vec> = vec![]; + let album_match = MatchStateInfo::album_search(album.clone(), matches_vec); + let matches = AppMachine::match_state( + inner_with_mb(music_hoard(vec![]), mb_job_sender), + match_state(Some(album_match)), + ); + + // There are no matches which means that the second option should be manual input. + let matches = matches.next_match().unwrap_match(); + let matches = matches.next_match().unwrap_match(); + + let mut app = matches.select(); + app = input_mbid(app); + + let input = app.mode().unwrap_input(); + input.confirm(); } } diff --git a/src/tui/app/machine/mod.rs b/src/tui/app/machine/mod.rs index 35b6542..87dfea8 100644 --- a/src/tui/app/machine/mod.rs +++ b/src/tui/app/machine/mod.rs @@ -222,7 +222,7 @@ mod tests { use musichoard::collection::Collection; use crate::tui::{ - app::{AppState, IApp, IAppInput, IAppInteractBrowse}, + app::{AppState, IApp, IAppInput, IAppInteractBrowse, InputEvent}, lib::{interface::musicbrainz::daemon::MockIMbJobSender, MockIMusicHoard}, }; @@ -355,6 +355,14 @@ mod tests { AppInner::new(music_hoard, mb_job_sender) } + pub fn input_event(c: char) -> InputEvent { + crossterm::event::KeyEvent::new( + crossterm::event::KeyCode::Char(c), + crossterm::event::KeyModifiers::empty(), + ) + .into() + } + #[test] fn input_mode() { let app = App::new(music_hoard_init(vec![]), mb_job_sender()); diff --git a/src/tui/app/mod.rs b/src/tui/app/mod.rs index 39ec665..67c8a35 100644 --- a/src/tui/app/mod.rs +++ b/src/tui/app/mod.rs @@ -32,6 +32,8 @@ macro_rules! IAppState { } use IAppState; +use super::lib::interface::musicbrainz::api::Lookup; + pub trait IApp { type BrowseState: IAppBase + IAppInteractBrowse; type InfoState: IAppBase + IAppInteractInfo; @@ -175,28 +177,51 @@ pub struct AppPublicInner<'app> { pub type InputPublic<'app> = &'app tui_input::Input; #[derive(Clone, Debug, PartialEq, Eq)] -pub enum MatchOption { - Match(Match), +pub enum MissOption { CannotHaveMbid, ManualInputMbid, } -impl From> for MatchOption { +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum SearchOption { + Match(Match), + None(MissOption), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum LookupOption { + Match(Lookup), + None(MissOption), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ListOption { + Search(Vec>), + Lookup(Vec>), +} + +impl From> for SearchOption { fn from(value: Match) -> Self { - MatchOption::Match(value) + SearchOption::Match(value) + } +} + +impl From> for LookupOption { + fn from(value: Lookup) -> Self { + LookupOption::Match(value) } } #[derive(Clone, Debug, PartialEq, Eq)] pub struct ArtistMatches { pub matching: ArtistMeta, - pub list: Vec>, + pub list: ListOption, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct AlbumMatches { pub matching: AlbumMeta, - pub list: Vec>, + pub list: ListOption, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -206,13 +231,29 @@ pub enum MatchStateInfo { } impl MatchStateInfo { - pub fn artist>>(matching: ArtistMeta, list: Vec) -> Self { - let list: Vec> = list.into_iter().map(Into::into).collect(); + pub fn artist_search>>( + matching: ArtistMeta, + list: Vec, + ) -> Self { + let list = ListOption::Search(list.into_iter().map(Into::into).collect()); MatchStateInfo::Artist(ArtistMatches { matching, list }) } - pub fn album>>(matching: AlbumMeta, list: Vec) -> Self { - let list: Vec> = list.into_iter().map(Into::into).collect(); + pub fn album_search>>( + matching: AlbumMeta, + list: Vec, + ) -> Self { + let list = ListOption::Search(list.into_iter().map(Into::into).collect()); + MatchStateInfo::Album(AlbumMatches { matching, list }) + } + + pub fn artist_lookup>>(matching: ArtistMeta, item: M) -> Self { + let list = ListOption::Lookup(vec![item.into()]); + MatchStateInfo::Artist(ArtistMatches { matching, list }) + } + + pub fn album_lookup>>(matching: AlbumMeta, item: M) -> Self { + let list = ListOption::Lookup(vec![item.into()]); MatchStateInfo::Album(AlbumMatches { matching, list }) } } diff --git a/src/tui/lib/external/musicbrainz/api/mod.rs b/src/tui/lib/external/musicbrainz/api/mod.rs index 3730382..4b54c22 100644 --- a/src/tui/lib/external/musicbrainz/api/mod.rs +++ b/src/tui/lib/external/musicbrainz/api/mod.rs @@ -5,11 +5,15 @@ use std::collections::HashMap; use musichoard::{ collection::{ album::{AlbumDate, AlbumMeta, AlbumSeq}, - artist::ArtistMeta, + artist::{ArtistId, ArtistMeta}, musicbrainz::Mbid, }, external::musicbrainz::{ api::{ + lookup::{ + LookupArtistRequest, LookupArtistResponse, LookupReleaseGroupRequest, + LookupReleaseGroupResponse, + }, search::{ SearchArtistRequest, SearchArtistResponseArtist, SearchReleaseGroupRequest, SearchReleaseGroupResponseReleaseGroup, @@ -20,7 +24,7 @@ use musichoard::{ }, }; -use crate::tui::lib::interface::musicbrainz::api::{Error, IMusicBrainz, Match}; +use crate::tui::lib::interface::musicbrainz::api::{Error, IMusicBrainz, Lookup, Match}; // GRCOV_EXCL_START pub struct MusicBrainz { @@ -34,6 +38,22 @@ impl MusicBrainz { } impl IMusicBrainz for MusicBrainz { + fn lookup_artist(&mut self, mbid: &Mbid) -> Result, Error> { + let request = LookupArtistRequest::new(mbid); + + let mb_response = self.client.lookup_artist(request)?; + + Ok(from_lookup_artist_response(mb_response)) + } + + fn lookup_release_group(&mut self, mbid: &Mbid) -> Result, Error> { + let request = LookupReleaseGroupRequest::new(mbid); + + let mb_response = self.client.lookup_release_group(request)?; + + Ok(from_lookup_release_group_response(mb_response)) + } + fn search_artist(&mut self, artist: &ArtistMeta) -> Result>, Error> { let query = SearchArtistRequest::new().string(&artist.id.name); @@ -72,16 +92,48 @@ impl IMusicBrainz for MusicBrainz { } } +fn from_lookup_artist_response(entity: LookupArtistResponse) -> Lookup { + let sort: Option = Some(entity.meta.sort_name) + .filter(|s| s != &entity.meta.name) + .map(Into::into); + Lookup { + item: ArtistMeta { + id: entity.meta.name, + sort, + musicbrainz: Some(entity.meta.id.into()), + properties: HashMap::new(), + }, + disambiguation: entity.meta.disambiguation, + } +} + +fn from_lookup_release_group_response(entity: LookupReleaseGroupResponse) -> Lookup { + Lookup { + item: AlbumMeta { + id: entity.meta.title, + date: entity.meta.first_release_date, + seq: AlbumSeq::default(), + musicbrainz: Some(entity.meta.id.into()), + primary_type: Some(entity.meta.primary_type), + secondary_types: entity.meta.secondary_types.unwrap_or_default(), + }, + disambiguation: None, + } +} + fn from_search_artist_response_artist(entity: SearchArtistResponseArtist) -> Match { + let sort: Option = Some(entity.meta.sort_name) + .filter(|s| s != &entity.meta.name) + .map(Into::into); Match { score: entity.score, item: ArtistMeta { - id: entity.name, - sort: entity.sort.map(Into::into), - musicbrainz: Some(entity.id.into()), + id: entity.meta.name, + sort, + musicbrainz: Some(entity.meta.id.into()), properties: HashMap::new(), }, - disambiguation: entity.disambiguation, + disambiguation: entity.meta.disambiguation, } } @@ -91,12 +143,12 @@ fn from_search_release_group_response_release_group( Match { score: entity.score, item: AlbumMeta { - id: entity.title, - date: entity.first_release_date, + id: entity.meta.title, + date: entity.meta.first_release_date, seq: AlbumSeq::default(), - musicbrainz: Some(entity.id.into()), - primary_type: Some(entity.primary_type), - secondary_types: entity.secondary_types.unwrap_or_default(), + musicbrainz: Some(entity.meta.id.into()), + primary_type: Some(entity.meta.primary_type), + secondary_types: entity.meta.secondary_types.unwrap_or_default(), }, disambiguation: None, } diff --git a/src/tui/lib/external/musicbrainz/daemon/mod.rs b/src/tui/lib/external/musicbrainz/daemon/mod.rs index 686f3ff..8d0f9e9 100644 --- a/src/tui/lib/external/musicbrainz/daemon/mod.rs +++ b/src/tui/lib/external/musicbrainz/daemon/mod.rs @@ -5,7 +5,7 @@ use crate::tui::{ event::IFetchCompleteEventSender, lib::interface::musicbrainz::{ api::{Error as ApiError, IMusicBrainz}, - daemon::{Error, IMbJobSender, MbParams, ResultSender, SearchParams}, + daemon::{Error, IMbJobSender, LookupParams, MbParams, ResultSender, SearchParams}, }, }; @@ -35,7 +35,6 @@ impl Job { #[derive(Debug)] enum JobPriority { - #[cfg(test)] Foreground, Background, } @@ -106,24 +105,24 @@ impl JobChannel { } impl IMbJobSender for JobSender { - fn submit_background_job( + fn submit_foreground_job( &self, result_sender: ResultSender, requests: VecDeque, ) -> Result<(), Error> { - self.send_background_job(result_sender, requests) + self.send_job(JobPriority::Foreground, result_sender, requests) } -} -impl JobSender { - fn send_background_job( + fn submit_background_job( &self, result_sender: ResultSender, requests: VecDeque, ) -> Result<(), Error> { self.send_job(JobPriority::Background, result_sender, requests) } +} +impl JobSender { fn send_job( &self, priority: JobPriority, @@ -239,20 +238,25 @@ impl JobInstance { event_sender: &mut dyn IFetchCompleteEventSender, api_params: MbParams, ) -> Result<(), JobInstanceError> { - match api_params { - MbParams::Search(search) => match search { - SearchParams::Artist(params) => { - let result = musicbrainz.search_artist(¶ms.artist); - let result = result.map(|list| MatchStateInfo::artist(params.artist, list)); - self.return_result(event_sender, result) - } - SearchParams::ReleaseGroup(params) => { - let result = musicbrainz.search_release_group(¶ms.arid, ¶ms.album); - let result = result.map(|list| MatchStateInfo::album(params.album, list)); - self.return_result(event_sender, result) - } + let result = match api_params { + MbParams::Lookup(lookup) => match lookup { + LookupParams::Artist(params) => musicbrainz + .lookup_artist(¶ms.mbid) + .map(|rv| MatchStateInfo::artist_lookup(params.artist, rv)), + LookupParams::ReleaseGroup(params) => musicbrainz + .lookup_release_group(¶ms.mbid) + .map(|rv| MatchStateInfo::album_lookup(params.album, rv)), }, - } + MbParams::Search(search) => match search { + SearchParams::Artist(params) => musicbrainz + .search_artist(¶ms.artist) + .map(|rv| MatchStateInfo::artist_search(params.artist, rv)), + SearchParams::ReleaseGroup(params) => musicbrainz + .search_release_group(¶ms.arid, ¶ms.album) + .map(|rv| MatchStateInfo::album_search(params.album, rv)), + }, + }; + self.return_result(event_sender, result) } fn return_result( @@ -298,22 +302,12 @@ impl JobQueue { .or_else(|| self.background_queue.pop_front()) } - #[cfg(test)] fn push_back(&mut self, job: Job) { match job.priority { JobPriority::Foreground => self.foreground_queue.push_back(job.instance), JobPriority::Background => self.background_queue.push_back(job.instance), } } - - // GRCOV_EXCL_START - #[cfg(not(test))] - fn push_back(&mut self, job: Job) { - match job.priority { - JobPriority::Background => self.background_queue.push_back(job.instance), - } - } - // GRCOV_EXCL_STOP } #[cfg(test)] @@ -327,7 +321,7 @@ mod tests { use crate::tui::{ event::{Event, EventError, MockIFetchCompleteEventSender}, - lib::interface::musicbrainz::api::{Match, MockIMusicBrainz}, + lib::interface::musicbrainz::api::{Lookup, Match, MockIMusicBrainz}, testmod::COLLECTION, }; @@ -377,12 +371,28 @@ mod tests { } } + fn mbid() -> Mbid { + "00000000-0000-0000-0000-000000000000".try_into().unwrap() + } + + fn lookup_artist_requests() -> VecDeque { + let artist = COLLECTION[3].meta.clone(); + let mbid = mbid(); + VecDeque::from([MbParams::lookup_artist(artist, mbid)]) + } + + fn lookup_release_group_requests() -> VecDeque { + let album = COLLECTION[1].albums[0].meta.clone(); + let mbid = mbid(); + VecDeque::from([MbParams::lookup_release_group(album, mbid)]) + } + fn search_artist_requests() -> VecDeque { let artist = COLLECTION[3].meta.clone(); VecDeque::from([MbParams::search_artist(artist)]) } - fn artist_expectations() -> (ArtistMeta, Vec>) { + fn search_artist_expectations() -> (ArtistMeta, Vec>) { let artist = COLLECTION[3].meta.clone(); let artist_match_1 = Match::new(100, artist.clone()); @@ -410,7 +420,7 @@ mod tests { mbref.unwrap().mbid().clone() } - fn album_expectations_1() -> (AlbumMeta, Vec>) { + fn search_album_expectations_1() -> (AlbumMeta, Vec>) { let album_1 = COLLECTION[1].albums[0].meta.clone(); let album_4 = COLLECTION[1].albums[3].meta.clone(); @@ -421,7 +431,7 @@ mod tests { (album_1, matches_1) } - fn album_expectations_4() -> (AlbumMeta, Vec>) { + fn search_album_expectations_4() -> (AlbumMeta, Vec>) { let album_1 = COLLECTION[1].albums[0].meta.clone(); let album_4 = COLLECTION[1].albums[3].meta.clone(); @@ -505,6 +515,90 @@ mod tests { assert_eq!(result, Err(JobError::JobQueueEmpty)); } + fn lookup_artist_expectation( + musicbrainz: &mut MockIMusicBrainz, + mbid: &Mbid, + lookup: &Lookup, + ) { + let result = Ok(lookup.clone()); + musicbrainz + .expect_lookup_artist() + .with(predicate::eq(mbid.clone())) + .times(1) + .return_once(|_| result); + } + + #[test] + fn execute_lookup_artist() { + let mut musicbrainz = musicbrainz(); + let mbid = mbid(); + let artist = COLLECTION[3].meta.clone(); + let lookup = Lookup::new(artist.clone()); + lookup_artist_expectation(&mut musicbrainz, &mbid, &lookup); + + let mut event_sender = event_sender(); + fetch_complete_expectation(&mut event_sender, 1); + + let (job_sender, job_receiver) = job_channel(); + let mut daemon = daemon_with(musicbrainz, job_receiver, event_sender); + + let requests = lookup_artist_requests(); + let (result_sender, result_receiver) = mpsc::channel(); + let result = job_sender.submit_foreground_job(result_sender, requests); + assert_eq!(result, Ok(())); + + let result = daemon.enqueue_all_pending_jobs(); + assert_eq!(result, Ok(())); + + let result = daemon.execute_next_job(); + assert_eq!(result, Ok(())); + + let result = result_receiver.try_recv().unwrap(); + assert_eq!(result, Ok(MatchStateInfo::artist_lookup(artist, lookup))); + } + + fn lookup_release_group_expectation( + musicbrainz: &mut MockIMusicBrainz, + mbid: &Mbid, + lookup: &Lookup, + ) { + let result = Ok(lookup.clone()); + musicbrainz + .expect_lookup_release_group() + .with(predicate::eq(mbid.clone())) + .times(1) + .return_once(|_| result); + } + + #[test] + fn execute_lookup_release_group() { + let mut musicbrainz = musicbrainz(); + let mbid = mbid(); + let album = COLLECTION[1].albums[0].meta.clone(); + let lookup = Lookup::new(album.clone()); + lookup_release_group_expectation(&mut musicbrainz, &mbid, &lookup); + + let mut event_sender = event_sender(); + fetch_complete_expectation(&mut event_sender, 1); + + let (job_sender, job_receiver) = job_channel(); + let mut daemon = daemon_with(musicbrainz, job_receiver, event_sender); + + let requests = lookup_release_group_requests(); + let (result_sender, result_receiver) = mpsc::channel(); + let result = job_sender.submit_foreground_job(result_sender, requests); + assert_eq!(result, Ok(())); + + let result = daemon.enqueue_all_pending_jobs(); + assert_eq!(result, Ok(())); + + let result = daemon.execute_next_job(); + assert_eq!(result, Ok(())); + + let result = result_receiver.try_recv().unwrap(); + assert_eq!(result, Ok(MatchStateInfo::album_lookup(album, lookup))); + } + fn search_artist_expectation( musicbrainz: &mut MockIMusicBrainz, artist: &ArtistMeta, @@ -521,7 +615,7 @@ mod tests { #[test] fn execute_search_artist() { let mut musicbrainz = musicbrainz(); - let (artist, matches) = artist_expectations(); + let (artist, matches) = search_artist_expectations(); search_artist_expectation(&mut musicbrainz, &artist, &matches); let mut event_sender = event_sender(); @@ -542,7 +636,7 @@ mod tests { assert_eq!(result, Ok(())); let result = result_receiver.try_recv().unwrap(); - assert_eq!(result, Ok(MatchStateInfo::artist(artist, matches))); + assert_eq!(result, Ok(MatchStateInfo::artist_search(artist, matches))); } fn search_release_group_expectation( @@ -565,8 +659,8 @@ mod tests { fn execute_search_release_groups() { let mut musicbrainz = musicbrainz(); let arid = album_arid_expectation(); - let (album_1, matches_1) = album_expectations_1(); - let (album_4, matches_4) = album_expectations_4(); + let (album_1, matches_1) = search_album_expectations_1(); + let (album_4, matches_4) = search_album_expectations_4(); let mut seq = Sequence::new(); search_release_group_expectation(&mut musicbrainz, &mut seq, &arid, &album_1, &matches_1); @@ -593,17 +687,17 @@ mod tests { assert_eq!(result, Ok(())); let result = result_receiver.try_recv().unwrap(); - assert_eq!(result, Ok(MatchStateInfo::album(album_1, matches_1))); + assert_eq!(result, Ok(MatchStateInfo::album_search(album_1, matches_1))); let result = result_receiver.try_recv().unwrap(); - assert_eq!(result, Ok(MatchStateInfo::album(album_4, matches_4))); + assert_eq!(result, Ok(MatchStateInfo::album_search(album_4, matches_4))); } #[test] fn execute_search_release_groups_result_disconnect() { let mut musicbrainz = musicbrainz(); let arid = album_arid_expectation(); - let (album_1, matches_1) = album_expectations_1(); + let (album_1, matches_1) = search_album_expectations_1(); let mut seq = Sequence::new(); search_release_group_expectation(&mut musicbrainz, &mut seq, &arid, &album_1, &matches_1); @@ -633,7 +727,7 @@ mod tests { #[test] fn execute_search_artist_event_disconnect() { let mut musicbrainz = musicbrainz(); - let (artist, matches) = artist_expectations(); + let (artist, matches) = search_artist_expectations(); search_artist_expectation(&mut musicbrainz, &artist, &matches); let mut event_sender = event_sender(); diff --git a/src/tui/lib/interface/musicbrainz/api/mod.rs b/src/tui/lib/interface/musicbrainz/api/mod.rs index 3c674e7..62917c0 100644 --- a/src/tui/lib/interface/musicbrainz/api/mod.rs +++ b/src/tui/lib/interface/musicbrainz/api/mod.rs @@ -8,6 +8,8 @@ use musichoard::collection::{album::AlbumMeta, artist::ArtistMeta, musicbrainz:: /// Trait for interacting with the MusicBrainz API. #[cfg_attr(test, automock)] pub trait IMusicBrainz { + fn lookup_artist(&mut self, mbid: &Mbid) -> Result, Error>; + fn lookup_release_group(&mut self, mbid: &Mbid) -> Result, Error>; fn search_artist(&mut self, artist: &ArtistMeta) -> Result>, Error>; fn search_release_group( &mut self, @@ -23,4 +25,10 @@ pub struct Match { pub disambiguation: Option, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Lookup { + pub item: T, + pub disambiguation: Option, +} + pub type Error = musichoard::external::musicbrainz::api::Error; diff --git a/src/tui/lib/interface/musicbrainz/daemon/mod.rs b/src/tui/lib/interface/musicbrainz/daemon/mod.rs index 7647f99..964ec56 100644 --- a/src/tui/lib/interface/musicbrainz/daemon/mod.rs +++ b/src/tui/lib/interface/musicbrainz/daemon/mod.rs @@ -27,6 +27,12 @@ pub type ResultSender = mpsc::Sender; #[cfg_attr(test, automock)] pub trait IMbJobSender { + fn submit_foreground_job( + &self, + result_sender: ResultSender, + requests: VecDeque, + ) -> Result<(), Error>; + fn submit_background_job( &self, result_sender: ResultSender, @@ -36,9 +42,28 @@ pub trait IMbJobSender { #[derive(Clone, Debug, PartialEq, Eq)] pub enum MbParams { + Lookup(LookupParams), Search(SearchParams), } +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum LookupParams { + Artist(LookupArtistParams), + ReleaseGroup(LookupReleaseGroupParams), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LookupArtistParams { + pub artist: ArtistMeta, + pub mbid: Mbid, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LookupReleaseGroupParams { + pub album: AlbumMeta, + pub mbid: Mbid, +} + #[derive(Clone, Debug, PartialEq, Eq)] pub enum SearchParams { Artist(SearchArtistParams), @@ -57,6 +82,17 @@ pub struct SearchReleaseGroupParams { } impl MbParams { + pub fn lookup_artist(artist: ArtistMeta, mbid: Mbid) -> Self { + MbParams::Lookup(LookupParams::Artist(LookupArtistParams { artist, mbid })) + } + + pub fn lookup_release_group(album: AlbumMeta, mbid: Mbid) -> Self { + MbParams::Lookup(LookupParams::ReleaseGroup(LookupReleaseGroupParams { + album, + mbid, + })) + } + pub fn search_artist(artist: ArtistMeta) -> Self { MbParams::Search(SearchParams::Artist(SearchArtistParams { artist })) } diff --git a/src/tui/ui/display.rs b/src/tui/ui/display.rs index 1e8fc1c..e0eba10 100644 --- a/src/tui/ui/display.rs +++ b/src/tui/ui/display.rs @@ -4,7 +4,7 @@ use musichoard::collection::{ track::{TrackFormat, TrackQuality}, }; -use crate::tui::app::{MatchOption, MatchStateInfo}; +use crate::tui::app::{LookupOption, MatchStateInfo, MissOption, SearchOption}; pub struct UiDisplay; @@ -124,37 +124,69 @@ impl UiDisplay { } } - pub fn display_artist_match(match_option: &MatchOption) -> String { + pub fn display_search_option_artist(match_option: &SearchOption) -> String { match match_option { - MatchOption::Match(match_artist) => format!( - "{}{} ({}%)", - &match_artist.item.id.name, - &match_artist - .disambiguation - .as_ref() - .map(|d| format!(" ({d})")) - .unwrap_or_default(), + SearchOption::Match(match_artist) => format!( + "{} ({}%)", + Self::display_option_artist(&match_artist.item, &match_artist.disambiguation), match_artist.score, ), - MatchOption::CannotHaveMbid => Self::display_cannot_have_mbid().to_string(), - MatchOption::ManualInputMbid => Self::display_manual_input_mbid().to_string(), + SearchOption::None(miss) => Self::display_miss_option(miss).to_string(), } } - pub fn display_album_match(match_option: &MatchOption) -> String { + pub fn display_lookup_option_artist(lookup_option: &LookupOption) -> String { + match lookup_option { + LookupOption::Match(match_artist) => { + Self::display_option_artist(&match_artist.item, &match_artist.disambiguation) + } + LookupOption::None(miss) => Self::display_miss_option(miss).to_string(), + } + } + + fn display_option_artist(artist: &ArtistMeta, disambiguation: &Option) -> String { + format!( + "{}{}", + artist.id.name, + disambiguation + .as_ref() + .filter(|s| !s.is_empty()) + .map(|d| format!(" ({d})")) + .unwrap_or_default(), + ) + } + + pub fn display_search_option_album(match_option: &SearchOption) -> String { match match_option { - MatchOption::Match(match_album) => format!( - "{:010} | {} [{}] ({}%)", - UiDisplay::display_album_date(&match_album.item.date), - &match_album.item.id.title, - UiDisplay::display_type( - &match_album.item.primary_type, - &match_album.item.secondary_types - ), + SearchOption::Match(match_album) => format!( + "{} ({}%)", + Self::display_option_album(&match_album.item), match_album.score, ), - MatchOption::CannotHaveMbid => Self::display_cannot_have_mbid().to_string(), - MatchOption::ManualInputMbid => Self::display_manual_input_mbid().to_string(), + SearchOption::None(miss) => Self::display_miss_option(miss).to_string(), + } + } + + pub fn display_lookup_option_album(lookup_option: &LookupOption) -> String { + match lookup_option { + LookupOption::Match(match_album) => Self::display_option_album(&match_album.item), + LookupOption::None(miss) => Self::display_miss_option(miss).to_string(), + } + } + + fn display_option_album(album: &AlbumMeta) -> String { + format!( + "{:010} | {} [{}]", + UiDisplay::display_album_date(&album.date), + album.id.title, + UiDisplay::display_type(&album.primary_type, &album.secondary_types), + ) + } + + fn display_miss_option(miss_option: &MissOption) -> &'static str { + match miss_option { + MissOption::CannotHaveMbid => Self::display_cannot_have_mbid(), + MissOption::ManualInputMbid => Self::display_manual_input_mbid(), } } diff --git a/src/tui/ui/match_state.rs b/src/tui/ui/match_state.rs index 29e367c..2b6528c 100644 --- a/src/tui/ui/match_state.rs +++ b/src/tui/ui/match_state.rs @@ -2,7 +2,7 @@ use musichoard::collection::{album::AlbumMeta, artist::ArtistMeta}; use ratatui::widgets::{List, ListItem}; use crate::tui::{ - app::{MatchOption, MatchStateInfo, WidgetState}, + app::{ListOption, MatchStateInfo, WidgetState}, ui::display::UiDisplay, }; @@ -13,7 +13,7 @@ pub struct MatchOverlay<'a, 'b> { } impl<'a, 'b> MatchOverlay<'a, 'b> { - pub fn new(info: Option<&MatchStateInfo>, state: &'b mut WidgetState) -> Self { + pub fn new(info: Option<&'a MatchStateInfo>, state: &'b mut WidgetState) -> Self { match info { Some(info) => match info { MatchStateInfo::Artist(m) => Self::artists(&m.matching, &m.list, state), @@ -33,18 +33,19 @@ impl<'a, 'b> MatchOverlay<'a, 'b> { fn artists( matching: &ArtistMeta, - matches: &[MatchOption], + matches: &'a ListOption, state: &'b mut WidgetState, ) -> Self { let matching = UiDisplay::display_artist_matching(matching); - let list = List::new( - matches - .iter() - .map(UiDisplay::display_artist_match) - .map(ListItem::new) - .collect::>(), - ); + let list = match matches { + ListOption::Search(matches) => { + Self::display_list(UiDisplay::display_search_option_artist, matches) + } + ListOption::Lookup(matches) => { + Self::display_list(UiDisplay::display_lookup_option_artist, matches) + } + }; MatchOverlay { matching, @@ -55,18 +56,19 @@ impl<'a, 'b> MatchOverlay<'a, 'b> { fn albums( matching: &AlbumMeta, - matches: &[MatchOption], + matches: &'a ListOption, state: &'b mut WidgetState, ) -> Self { let matching = UiDisplay::display_album_matching(matching); - let list = List::new( - matches - .iter() - .map(UiDisplay::display_album_match) - .map(ListItem::new) - .collect::>(), - ); + let list = match matches { + ListOption::Search(matches) => { + Self::display_list(UiDisplay::display_search_option_album, matches) + } + ListOption::Lookup(matches) => { + Self::display_list(UiDisplay::display_lookup_option_album, matches) + } + }; MatchOverlay { matching, @@ -74,4 +76,17 @@ impl<'a, 'b> MatchOverlay<'a, 'b> { state, } } + + fn display_list(display: F, options: &[T]) -> List + where + F: FnMut(&T) -> String, + { + List::new( + options + .iter() + .map(display) + .map(ListItem::new) + .collect::>(), + ) + } } diff --git a/src/tui/ui/mod.rs b/src/tui/ui/mod.rs index fbce47c..4e3f5cf 100644 --- a/src/tui/ui/mod.rs +++ b/src/tui/ui/mod.rs @@ -206,8 +206,8 @@ mod tests { }; use crate::tui::{ - app::{AppPublic, AppPublicInner, Delta, MatchOption, MatchStatePublic}, - lib::interface::musicbrainz::api::Match, + app::{AppPublic, AppPublicInner, Delta, MatchStatePublic}, + lib::interface::musicbrainz::api::{Lookup, Match}, testmod::COLLECTION, tests::terminal, }; @@ -250,20 +250,6 @@ mod tests { } } - fn artist_matches(matching: ArtistMeta, list: Vec>) -> MatchStateInfo { - let mut list: Vec> = list.into_iter().map(Into::into).collect(); - list.push(MatchOption::CannotHaveMbid); - list.push(MatchOption::ManualInputMbid); - MatchStateInfo::artist(matching, list) - } - - fn album_matches(matching: AlbumMeta, list: Vec>) -> MatchStateInfo { - let mut list: Vec> = list.into_iter().map(Into::into).collect(); - list.push(MatchOption::CannotHaveMbid); - list.push(MatchOption::ManualInputMbid); - MatchStateInfo::album(matching, list) - } - fn draw_test_suite(collection: &Collection, selection: &mut Selection) { let mut terminal = terminal(); @@ -354,76 +340,92 @@ mod tests { terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); } - #[test] - fn draw_artist_matches() { - let collection = &COLLECTION; - let mut selection = Selection::new(collection); - - let mut terminal = terminal(); - - let artist = ArtistMeta::new(ArtistId::new("an artist")); - let artist_match = Match { - score: 80, - item: artist.clone(), - disambiguation: None, - }; - let list = vec![artist_match.clone(), artist_match.clone()]; - let artist_matches = artist_matches(artist, list); - - let mut widget_state = WidgetState::default(); - widget_state.list.select(Some(0)); - - let mut app = AppPublic { - inner: public_inner(collection, &mut selection), - state: AppState::Match(MatchStatePublic { - info: Some(&artist_matches), - state: &mut widget_state, - }), - input: None, - }; - terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); - - let input = tui_input::Input::default(); - app.input = Some(&input); - terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); + fn artist_meta() -> ArtistMeta { + ArtistMeta::new(ArtistId::new("an artist")) } - #[test] - fn draw_album_matches() { - let collection = &COLLECTION; - let mut selection = Selection::new(collection); + fn artist_matches() -> MatchStateInfo { + let artist = artist_meta(); + let artist_match = Match::new(80, artist.clone()); + let list = vec![artist_match.clone(), artist_match.clone()]; - let mut terminal = terminal(); + let mut info = MatchStateInfo::artist_search(artist, list); + info.push_cannot_have_mbid(); + info.push_manual_input_mbid(); + info + } - let album = AlbumMeta::new( + fn artist_lookup() -> MatchStateInfo { + let artist = artist_meta(); + let artist_lookup = Lookup::new(artist.clone()); + + let mut info = MatchStateInfo::artist_lookup(artist, artist_lookup); + info.push_cannot_have_mbid(); + info.push_manual_input_mbid(); + info + } + + fn album_meta() -> AlbumMeta { + AlbumMeta::new( AlbumId::new("An Album"), AlbumDate::new(Some(1990), Some(5), None), Some(AlbumPrimaryType::Album), vec![AlbumSecondaryType::Live, AlbumSecondaryType::Compilation], - ); - let album_match = Match { - score: 80, - item: album.clone(), - disambiguation: None, - }; + ) + } + + fn album_matches() -> MatchStateInfo { + let album = album_meta(); + let album_match = Match::new(80, album.clone()); let list = vec![album_match.clone(), album_match.clone()]; - let album_matches = album_matches(album, list); - let mut widget_state = WidgetState::default(); - widget_state.list.select(Some(0)); + let mut info = MatchStateInfo::album_search(album, list); + info.push_cannot_have_mbid(); + info.push_manual_input_mbid(); + info + } - let mut app = AppPublic { - inner: public_inner(collection, &mut selection), - state: AppState::Match(MatchStatePublic { - info: Some(&album_matches), - state: &mut widget_state, - }), - input: None, - }; - terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); + fn album_lookup() -> MatchStateInfo { + let album = album_meta(); + let album_lookup = Lookup::new(album.clone()); - let input = tui_input::Input::default(); - app.input = Some(&input); - terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); + let mut info = MatchStateInfo::album_lookup(album, album_lookup); + info.push_cannot_have_mbid(); + info.push_manual_input_mbid(); + info + } + + #[test] + fn draw_matche_state_suite() { + let collection = &COLLECTION; + let mut selection = Selection::new(collection); + + let mut terminal = terminal(); + + let match_state_infos = vec![ + artist_matches(), + album_matches(), + artist_lookup(), + album_lookup(), + ]; + + for info in match_state_infos.iter() { + let mut widget_state = WidgetState::default(); + widget_state.list.select(Some(0)); + + let mut app = AppPublic { + inner: public_inner(collection, &mut selection), + state: AppState::Match(MatchStatePublic { + info: Some(info), + state: &mut widget_state, + }), + input: None, + }; + terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); + + let input = tui_input::Input::default(); + app.input = Some(&input); + terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); + } } }