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..6405ad6 100644 --- a/src/external/musicbrainz/api/lookup.rs +++ b/src/external/musicbrainz/api/lookup.rs @@ -3,7 +3,8 @@ use url::form_urlencoded; use crate::{ collection::{ - album::{AlbumDate, AlbumPrimaryType, AlbumSecondaryType}, + album::{AlbumDate, AlbumId, AlbumPrimaryType, AlbumSecondaryType}, + artist::ArtistId, musicbrainz::Mbid, }, external::musicbrainz::{ @@ -36,6 +37,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,19 +73,37 @@ impl<'a> LookupArtistRequest<'a> { #[derive(Debug, PartialEq, Eq)] pub struct LookupArtistResponse { + pub id: Mbid, + pub name: ArtistId, + pub sort: Option, + pub disambiguation: Option, pub release_groups: Vec, } #[derive(Deserialize)] #[serde(rename_all(deserialize = "kebab-case"))] struct DeserializeLookupArtistResponse { - release_groups: Vec, + id: SerdeMbid, + name: String, + sort_name: String, + disambiguation: Option, + release_groups: Option>, } impl From for LookupArtistResponse { fn from(value: DeserializeLookupArtistResponse) -> Self { + let sort: Option = Some(value.sort_name) + .filter(|s| s != &value.name) + .map(Into::into); LookupArtistResponse { - release_groups: value.release_groups.into_iter().map(Into::into).collect(), + id: value.id.into(), + name: value.name.into(), + sort, + disambiguation: value.disambiguation, + release_groups: value + .release_groups + .map(|rgs| rgs.into_iter().map(Into::into).collect()) + .unwrap_or_default(), } } } @@ -107,6 +139,49 @@ impl From for LookupArtistResponseR } } +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 LookupReleaseGroupResponse { + pub id: Mbid, + pub title: AlbumId, + pub first_release_date: AlbumDate, + pub primary_type: AlbumPrimaryType, + pub secondary_types: Option>, +} + +#[derive(Deserialize)] +#[serde(rename_all(deserialize = "kebab-case"))] +struct DeserializeLookupReleaseGroupResponse { + id: SerdeMbid, + title: String, + first_release_date: SerdeAlbumDate, + primary_type: SerdeAlbumPrimaryType, + secondary_types: Option>, +} + +impl From for LookupReleaseGroupResponse { + fn from(value: DeserializeLookupReleaseGroupResponse) -> Self { + LookupReleaseGroupResponse { + 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()), + } + } +} + #[cfg(test)] mod tests { use mockall::predicate; @@ -117,12 +192,14 @@ mod tests { #[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_id = SerdeMbid(mbid.try_into().unwrap()); + let de_name = String::from("the artist"); + let de_sort_name = String::from("artist, the"); + let de_disambiguation = Some(String::from("disambig")); let de_release_group = DeserializeLookupArtistResponseReleaseGroup { id: SerdeMbid("11111111-1111-1111-1111-111111111111".try_into().unwrap()), title: String::from("an album"), @@ -131,7 +208,11 @@ mod tests { secondary_types: vec![SerdeAlbumSecondaryType(AlbumSecondaryType::Compilation)], }; let de_response = DeserializeLookupArtistResponse { - release_groups: vec![de_release_group.clone()], + id: de_id.clone(), + name: de_name.clone(), + sort_name: de_sort_name.clone(), + disambiguation: de_disambiguation.clone(), + release_groups: Some(vec![de_release_group.clone()]), }; let release_group = LookupArtistResponseReleaseGroup { @@ -146,6 +227,10 @@ mod tests { .collect(), }; let response = LookupArtistResponse { + id: de_id.0, + name: de_name.into(), + sort: Some(de_sort_name.into()), + disambiguation: de_disambiguation, release_groups: vec![release_group], }; diff --git a/src/tui/app/machine/fetch_state.rs b/src/tui/app/machine/fetch_state.rs index 73e5964..e2787c5 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::{ @@ -17,12 +21,29 @@ use crate::tui::{ 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, + _ => { + 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,7 +212,7 @@ 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::Match, daemon::MockIMbJobSender}, testmod::COLLECTION, @@ -237,7 +316,8 @@ mod tests { 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 +330,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 +389,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..d508f76 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 } diff --git a/src/tui/app/machine/match_state.rs b/src/tui/app/machine/match_state.rs index 362fa96..eb07445 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) } } @@ -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 { @@ -205,7 +256,7 @@ 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 album_match() -> MatchStateInfo { @@ -225,7 +276,7 @@ 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 fetch_state() -> FetchState { @@ -233,8 +284,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] 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..5f713fd 100644 --- a/src/tui/lib/external/musicbrainz/api/mod.rs +++ b/src/tui/lib/external/musicbrainz/api/mod.rs @@ -10,6 +10,10 @@ use musichoard::{ }, 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,6 +92,32 @@ impl IMusicBrainz for MusicBrainz { } } +fn from_lookup_artist_response(entity: LookupArtistResponse) -> Lookup { + Lookup { + item: ArtistMeta { + id: entity.name, + sort: entity.sort.map(Into::into), + musicbrainz: Some(entity.id.into()), + properties: HashMap::new(), + }, + disambiguation: entity.disambiguation, + } +} + +fn from_lookup_release_group_response(entity: LookupReleaseGroupResponse) -> Lookup { + Lookup { + item: AlbumMeta { + id: entity.title, + date: entity.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(), + }, + disambiguation: None, + } +} + fn from_search_artist_response_artist(entity: SearchArtistResponseArtist) -> Match { Match { score: entity.score, diff --git a/src/tui/lib/external/musicbrainz/daemon/mod.rs b/src/tui/lib/external/musicbrainz/daemon/mod.rs index d2d9597..69fbdfa 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}, }, }; @@ -105,6 +105,14 @@ impl JobChannel { } impl IMbJobSender for JobSender { + fn submit_foreground_job( + &self, + result_sender: ResultSender, + requests: VecDeque, + ) -> Result<(), Error> { + self.send_foreground_job(result_sender, requests) + } + fn submit_background_job( &self, result_sender: ResultSender, @@ -115,6 +123,14 @@ impl IMbJobSender for JobSender { } impl JobSender { + fn send_foreground_job( + &self, + result_sender: ResultSender, + requests: VecDeque, + ) -> Result<(), Error> { + self.send_job(JobPriority::Foreground, result_sender, requests) + } + fn send_background_job( &self, result_sender: ResultSender, @@ -238,20 +254,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( @@ -531,7 +552,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( @@ -582,10 +603,10 @@ 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] 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..ed40510 100644 --- a/src/tui/ui/mod.rs +++ b/src/tui/ui/mod.rs @@ -206,7 +206,7 @@ mod tests { }; use crate::tui::{ - app::{AppPublic, AppPublicInner, Delta, MatchOption, MatchStatePublic}, + app::{AppPublic, AppPublicInner, Delta, MatchStatePublic, MissOption, SearchOption}, lib::interface::musicbrainz::api::Match, testmod::COLLECTION, tests::terminal, @@ -251,17 +251,17 @@ 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) + let mut list: Vec> = list.into_iter().map(Into::into).collect(); + list.push(SearchOption::None(MissOption::CannotHaveMbid)); + list.push(SearchOption::None(MissOption::ManualInputMbid)); + MatchStateInfo::artist_search(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) + let mut list: Vec> = list.into_iter().map(Into::into).collect(); + list.push(SearchOption::None(MissOption::CannotHaveMbid)); + list.push(SearchOption::None(MissOption::ManualInputMbid)); + MatchStateInfo::album_search(matching, list) } fn draw_test_suite(collection: &Collection, selection: &mut Selection) {