Compare commits

...

3 Commits

Author SHA1 Message Date
b01af5ff18 Implement browse in daemon 2024-10-05 23:29:58 +02:00
b51fa34935 Add return type for browse 2024-10-05 21:41:25 +02:00
46d38d9274 Another renaming 2024-10-05 21:29:08 +02:00
13 changed files with 234 additions and 116 deletions

View File

@ -72,7 +72,7 @@ fn main() {
println!("Release groups in this response: {count}"); println!("Release groups in this response: {count}");
match response.page.next_page_offset(count) { match response.page.next_page_offset(count) {
NextPage::Offset(next_offset) => paging.with_offset(next_offset), NextPage::Offset(next_offset) => paging.set_offset(next_offset),
NextPage::Complete => break, NextPage::Complete => break,
} }

View File

@ -61,7 +61,7 @@ impl<Http> MusicBrainzClient<Http> {
} }
} }
#[derive(Default)] #[derive(Debug, Default)]
pub struct PageSettings { pub struct PageSettings {
limit: Option<usize>, limit: Option<usize>,
offset: Option<usize>, offset: Option<usize>,
@ -79,7 +79,12 @@ impl PageSettings {
Self::with_limit(MB_MAX_PAGE_LIMIT) Self::with_limit(MB_MAX_PAGE_LIMIT)
} }
pub fn with_offset(&mut self, offset: usize) { pub fn with_offset(mut self, offset: usize) -> Self {
self.offset = Some(offset);
self
}
pub fn set_offset(&mut self, offset: usize) {
self.offset = Some(offset); self.offset = Some(offset);
} }
} }
@ -91,6 +96,10 @@ pub enum NextPage {
} }
impl NextPage { impl NextPage {
pub fn new() -> NextPage {
NextPage::Offset(0)
}
pub fn next_page_offset(offset: usize, total_count: usize, page_count: usize) -> NextPage { pub fn next_page_offset(offset: usize, total_count: usize, page_count: usize) -> NextPage {
let next_offset = offset + page_count; let next_offset = offset + page_count;
if next_offset < total_count { if next_offset < total_count {
@ -387,14 +396,14 @@ mod tests {
assert_eq!(ApiDisplay::format_page_settings(&paging), "&limit=100"); assert_eq!(ApiDisplay::format_page_settings(&paging), "&limit=100");
let mut paging = PageSettings::with_limit(45); let mut paging = PageSettings::with_limit(45);
paging.with_offset(145); paging.set_offset(145);
assert_eq!( assert_eq!(
ApiDisplay::format_page_settings(&paging), ApiDisplay::format_page_settings(&paging),
"&limit=45&offset=145" "&limit=45&offset=145"
); );
let mut paging = PageSettings::default(); let mut paging = PageSettings::default();
paging.with_offset(26); paging.set_offset(26);
assert_eq!(ApiDisplay::format_page_settings(&paging), "&offset=26"); assert_eq!(ApiDisplay::format_page_settings(&paging), "&offset=26");
} }

View File

@ -16,7 +16,7 @@ use crate::tui::{
AppPublicState, AppState, Category, IAppEventFetch, IAppInteractFetch, AppPublicState, AppState, Category, IAppEventFetch, IAppInteractFetch,
}, },
lib::interface::musicbrainz::daemon::{ lib::interface::musicbrainz::daemon::{
Error as DaemonError, IMbJobSender, MbApiResult, MbParams, ResultSender, Error as DaemonError, IMbJobSender, MbApiResult, MbParams, MbReturn, ResultSender,
}, },
}; };
@ -116,9 +116,7 @@ impl AppMachine<FetchState> {
pub fn app_fetch_next(inner: AppInner, mut fetch: FetchState) -> App { pub fn app_fetch_next(inner: AppInner, mut fetch: FetchState) -> App {
match fetch.try_recv() { match fetch.try_recv() {
Ok(fetch_result) => match fetch_result { Ok(fetch_result) => match fetch_result {
Ok(next_match) => { Ok(retval) => Self::handle_mb_api_return(inner, fetch, retval),
AppMachine::match_state(inner, MatchState::new(next_match, fetch)).into()
}
Err(fetch_err) => { Err(fetch_err) => {
AppMachine::error_state(inner, format!("fetch failed: {fetch_err}")).into() AppMachine::error_state(inner, format!("fetch failed: {fetch_err}")).into()
} }
@ -130,6 +128,15 @@ impl AppMachine<FetchState> {
} }
} }
fn handle_mb_api_return(inner: AppInner, fetch: FetchState, retval: MbReturn) -> App {
match retval {
MbReturn::Match(next_match) => {
AppMachine::match_state(inner, MatchState::new(next_match, fetch)).into()
}
_ => unimplemented!(),
}
}
pub fn app_lookup_artist( pub fn app_lookup_artist(
inner: AppInner, inner: AppInner,
fetch: FetchState, fetch: FetchState,
@ -288,7 +295,7 @@ mod tests {
use crate::tui::{ use crate::tui::{
app::{ app::{
machine::tests::{inner, music_hoard}, machine::tests::{inner, music_hoard},
Delta, IApp, IAppAccess, IAppInteractBrowse, MatchOption, MatchStateInfo, Delta, EntityMatches, IApp, IAppAccess, IAppInteractBrowse, MatchOption,
}, },
lib::interface::musicbrainz::{self, api::Entity, daemon::MockIMbJobSender}, lib::interface::musicbrainz::{self, api::Entity, daemon::MockIMbJobSender},
testmod::COLLECTION, testmod::COLLECTION,
@ -311,13 +318,13 @@ mod tests {
let artist = COLLECTION[3].meta.clone(); let artist = COLLECTION[3].meta.clone();
let matches: Vec<Entity<ArtistMeta>> = vec![]; let matches: Vec<Entity<ArtistMeta>> = vec![];
let fetch_result = MatchStateInfo::artist_search(artist.clone(), matches); let fetch_result = MbReturn::Match(EntityMatches::artist_search(artist.clone(), matches));
fetch_tx.send(Ok(fetch_result.clone())).unwrap(); fetch_tx.send(Ok(fetch_result.clone())).unwrap();
assert_eq!(fetch.try_recv(), Err(TryRecvError::Empty)); assert_eq!(fetch.try_recv(), Err(TryRecvError::Empty));
let lookup = Entity::new(artist.clone()); let lookup = Entity::new(artist.clone());
let lookup_result = MatchStateInfo::artist_lookup(artist.clone(), lookup); let lookup_result = MbReturn::Match(EntityMatches::artist_lookup(artist.clone(), lookup));
lookup_tx.send(Ok(lookup_result.clone())).unwrap(); lookup_tx.send(Ok(lookup_result.clone())).unwrap();
assert_eq!(fetch.try_recv(), Ok(Ok(lookup_result))); assert_eq!(fetch.try_recv(), Ok(Ok(lookup_result)));
@ -603,8 +610,8 @@ mod tests {
let artist = COLLECTION[3].meta.clone(); let artist = COLLECTION[3].meta.clone();
let artist_match = Entity::with_score(COLLECTION[2].meta.clone(), 80); let artist_match = Entity::with_score(COLLECTION[2].meta.clone(), 80);
let artist_match_info = let artist_match_info =
MatchStateInfo::artist_search(artist.clone(), vec![artist_match.clone()]); EntityMatches::artist_search(artist.clone(), vec![artist_match.clone()]);
let fetch_result = Ok(artist_match_info); let fetch_result = Ok(MbReturn::Match(artist_match_info));
tx.send(fetch_result).unwrap(); tx.send(fetch_result).unwrap();
let inner = inner(music_hoard(COLLECTION.clone())); let inner = inner(music_hoard(COLLECTION.clone()));
@ -619,8 +626,8 @@ mod tests {
MatchOption::CannotHaveMbid, MatchOption::CannotHaveMbid,
MatchOption::ManualInputMbid, MatchOption::ManualInputMbid,
]; ];
let expected = MatchStateInfo::artist_search(artist, match_options); let expected = EntityMatches::artist_search(artist, match_options);
assert_eq!(match_state.info, &expected); assert_eq!(match_state.matches, &expected);
} }
#[test] #[test]
@ -677,8 +684,8 @@ mod tests {
assert!(matches!(app, AppState::Fetch(_))); assert!(matches!(app, AppState::Fetch(_)));
let artist = COLLECTION[3].meta.clone(); let artist = COLLECTION[3].meta.clone();
let match_info = MatchStateInfo::artist_search::<Entity<ArtistMeta>>(artist, vec![]); let match_info = EntityMatches::artist_search::<Entity<ArtistMeta>>(artist, vec![]);
let fetch_result = Ok(match_info); let fetch_result = Ok(MbReturn::Match(match_info));
tx.send(fetch_result).unwrap(); tx.send(fetch_result).unwrap();
let app = app.unwrap_fetch().fetch_result_ready(); let app = app.unwrap_fetch().fetch_result_ready();

View File

@ -8,8 +8,8 @@ use musichoard::collection::{
use crate::tui::app::{ use crate::tui::app::{
machine::{fetch_state::FetchState, input::Input, App, AppInner, AppMachine}, machine::{fetch_state::FetchState, input::Input, App, AppInner, AppMachine},
AlbumMatches, AppPublicState, AppState, ArtistMatches, Delta, IAppInteractMatch, MatchOption, AlbumMatches, AppPublicState, AppState, ArtistMatches, Delta, EntityMatches, IAppInteractMatch,
MatchStateInfo, MatchStatePublic, WidgetState, MatchOption, MatchStatePublic, WidgetState,
}; };
trait GetInfoMeta { trait GetInfoMeta {
@ -103,7 +103,7 @@ impl AlbumMatches {
} }
} }
impl MatchStateInfo { impl EntityMatches {
fn len(&self) -> usize { fn len(&self) -> usize {
match self { match self {
Self::Artist(a) => a.len(), Self::Artist(a) => a.len(),
@ -127,13 +127,13 @@ impl MatchStateInfo {
} }
pub struct MatchState { pub struct MatchState {
current: MatchStateInfo, current: EntityMatches,
state: WidgetState, state: WidgetState,
fetch: FetchState, fetch: FetchState,
} }
impl MatchState { impl MatchState {
pub fn new(mut current: MatchStateInfo, fetch: FetchState) -> Self { pub fn new(mut current: EntityMatches, fetch: FetchState) -> Self {
current.push_cannot_have_mbid(); current.push_cannot_have_mbid();
current.push_manual_input_mbid(); current.push_manual_input_mbid();
@ -158,11 +158,11 @@ impl AppMachine<MatchState> {
Err(err) => return AppMachine::error_state(self.inner, err.to_string()).into(), Err(err) => return AppMachine::error_state(self.inner, err.to_string()).into(),
}; };
match self.state.current { match self.state.current {
MatchStateInfo::Artist(artist_matches) => { EntityMatches::Artist(artist_matches) => {
let matching = &artist_matches.matching; let matching = &artist_matches.matching;
AppMachine::app_lookup_artist(self.inner, self.state.fetch, matching, mbid) AppMachine::app_lookup_artist(self.inner, self.state.fetch, matching, mbid)
} }
MatchStateInfo::Album(album_matches) => { EntityMatches::Album(album_matches) => {
let artist_id = &album_matches.artist; let artist_id = &album_matches.artist;
let matching = &album_matches.matching; let matching = &album_matches.matching;
AppMachine::app_lookup_album( AppMachine::app_lookup_album(
@ -191,7 +191,7 @@ impl From<AppMachine<MatchState>> for App {
impl<'a> From<&'a mut MatchState> for AppPublicState<'a> { impl<'a> From<&'a mut MatchState> for AppPublicState<'a> {
fn from(state: &'a mut MatchState) -> Self { fn from(state: &'a mut MatchState) -> Self {
AppState::Match(MatchStatePublic { AppState::Match(MatchStatePublic {
info: &state.current, matches: &state.current,
state: &mut state.state, state: &mut state.state,
}) })
} }
@ -225,14 +225,14 @@ impl IAppInteractMatch for AppMachine<MatchState> {
let mh = &mut self.inner.music_hoard; let mh = &mut self.inner.music_hoard;
let result = match self.state.current { let result = match self.state.current {
MatchStateInfo::Artist(ref mut matches) => { EntityMatches::Artist(ref mut matches) => {
let info: ArtistInfo = matches.matching.info.clone(); let info: ArtistInfo = matches.matching.info.clone();
match matches.list.extract_info(index, info) { match matches.list.extract_info(index, info) {
InfoOption::Info(info) => mh.set_artist_info(&matches.matching.id, info), InfoOption::Info(info) => mh.set_artist_info(&matches.matching.id, info),
InfoOption::NeedInput => return self.get_input(), InfoOption::NeedInput => return self.get_input(),
} }
} }
MatchStateInfo::Album(ref mut matches) => { EntityMatches::Album(ref mut matches) => {
let info: AlbumInfo = matches.matching.info.clone(); let info: AlbumInfo = matches.matching.info.clone();
match matches.list.extract_info(index, info) { match matches.list.extract_info(index, info) {
InfoOption::Info(info) => { InfoOption::Info(info) => {
@ -298,7 +298,7 @@ mod tests {
meta meta
} }
fn artist_match() -> MatchStateInfo { fn artist_match() -> EntityMatches {
let artist = artist_meta(); let artist = artist_meta();
let artist_1 = artist.clone(); let artist_1 = artist.clone();
@ -309,13 +309,13 @@ mod tests {
artist_match_2.disambiguation = Some(String::from("some disambiguation")); artist_match_2.disambiguation = Some(String::from("some disambiguation"));
let list = vec![artist_match_1.clone(), artist_match_2.clone()]; let list = vec![artist_match_1.clone(), artist_match_2.clone()];
MatchStateInfo::artist_search(artist, list) EntityMatches::artist_search(artist, list)
} }
fn artist_lookup() -> MatchStateInfo { fn artist_lookup() -> EntityMatches {
let artist = artist_meta(); let artist = artist_meta();
let lookup = Entity::new(artist.clone()); let lookup = Entity::new(artist.clone());
MatchStateInfo::artist_lookup(artist, lookup) EntityMatches::artist_lookup(artist, lookup)
} }
fn album_meta() -> AlbumMeta { fn album_meta() -> AlbumMeta {
@ -330,7 +330,7 @@ mod tests {
) )
} }
fn album_match() -> MatchStateInfo { fn album_match() -> EntityMatches {
let artist_id = ArtistId::new("Artist"); let artist_id = ArtistId::new("Artist");
let album = album_meta(); let album = album_meta();
@ -343,14 +343,14 @@ mod tests {
let album_match_2 = Entity::with_score(album_2, 100); let album_match_2 = Entity::with_score(album_2, 100);
let list = vec![album_match_1.clone(), album_match_2.clone()]; let list = vec![album_match_1.clone(), album_match_2.clone()];
MatchStateInfo::album_search(artist_id, album, list) EntityMatches::album_search(artist_id, album, list)
} }
fn album_lookup() -> MatchStateInfo { fn album_lookup() -> EntityMatches {
let artist_id = ArtistId::new("Artist"); let artist_id = ArtistId::new("Artist");
let album = album_meta(); let album = album_meta();
let lookup = Entity::new(album.clone()); let lookup = Entity::new(album.clone());
MatchStateInfo::album_lookup(artist_id, album, lookup) EntityMatches::album_lookup(artist_id, album, lookup)
} }
fn fetch_state() -> FetchState { fn fetch_state() -> FetchState {
@ -358,7 +358,7 @@ mod tests {
FetchState::new(rx) FetchState::new(rx)
} }
fn match_state(match_state_info: MatchStateInfo) -> MatchState { fn match_state(match_state_info: EntityMatches) -> MatchState {
MatchState::new(match_state_info, fetch_state()) MatchState::new(match_state_info, fetch_state())
} }
@ -379,11 +379,11 @@ mod tests {
let public = app.get(); let public = app.get();
let public_matches = public.state.unwrap_match(); let public_matches = public.state.unwrap_match();
assert_eq!(public_matches.info, &album_match); assert_eq!(public_matches.matches, &album_match);
assert_eq!(public_matches.state, &widget_state); assert_eq!(public_matches.state, &widget_state);
} }
fn match_state_flow(mut matches_info: MatchStateInfo, len: usize) { fn match_state_flow(mut matches_info: EntityMatches, len: usize) {
// tx must exist for rx to return Empty rather than Disconnected. // tx must exist for rx to return Empty rather than Disconnected.
let (_tx, rx) = mpsc::channel(); let (_tx, rx) = mpsc::channel();
let app_matches = MatchState::new(matches_info.clone(), FetchState::new(rx)); let app_matches = MatchState::new(matches_info.clone(), FetchState::new(rx));
@ -391,7 +391,7 @@ mod tests {
let mut music_hoard = music_hoard(vec![]); let mut music_hoard = music_hoard(vec![]);
let artist_id = ArtistId::new("Artist"); let artist_id = ArtistId::new("Artist");
match matches_info { match matches_info {
MatchStateInfo::Album(_) => { EntityMatches::Album(_) => {
let album_id = AlbumId::new("Album"); let album_id = AlbumId::new("Album");
let mut info = album_meta().info; let mut info = album_meta().info;
info.musicbrainz = MbRefOption::CannotHaveMbid; info.musicbrainz = MbRefOption::CannotHaveMbid;
@ -401,7 +401,7 @@ mod tests {
.times(1) .times(1)
.return_once(|_, _, _| Ok(())); .return_once(|_, _, _| Ok(()));
} }
MatchStateInfo::Artist(_) => { EntityMatches::Artist(_) => {
let mut info = artist_meta().info; let mut info = artist_meta().info;
info.musicbrainz = MbRefOption::CannotHaveMbid; info.musicbrainz = MbRefOption::CannotHaveMbid;
music_hoard music_hoard
@ -485,8 +485,8 @@ mod tests {
let mut music_hoard = music_hoard(vec![]); let mut music_hoard = music_hoard(vec![]);
match matches_info { match matches_info {
MatchStateInfo::Album(_) => panic!(), EntityMatches::Album(_) => panic!(),
MatchStateInfo::Artist(_) => { EntityMatches::Artist(_) => {
let meta = artist_meta(); let meta = artist_meta();
music_hoard music_hoard
.expect_set_artist_info() .expect_set_artist_info()
@ -509,8 +509,8 @@ mod tests {
let mut music_hoard = music_hoard(vec![]); let mut music_hoard = music_hoard(vec![]);
match matches_info { match matches_info {
MatchStateInfo::Artist(_) => panic!(), EntityMatches::Artist(_) => panic!(),
MatchStateInfo::Album(matches) => { EntityMatches::Album(matches) => {
let meta = album_meta(); let meta = album_meta();
music_hoard music_hoard
.expect_set_album_info() .expect_set_album_info()
@ -533,8 +533,8 @@ mod tests {
let mut music_hoard = music_hoard(vec![]); let mut music_hoard = music_hoard(vec![]);
match matches_info { match matches_info {
MatchStateInfo::Album(_) => panic!(), EntityMatches::Album(_) => panic!(),
MatchStateInfo::Artist(_) => { EntityMatches::Artist(_) => {
music_hoard.expect_set_artist_info().return_once(|_, _| { music_hoard.expect_set_artist_info().return_once(|_, _| {
Err(musichoard::Error::DatabaseError(String::from("error"))) Err(musichoard::Error::DatabaseError(String::from("error")))
}); });
@ -598,7 +598,7 @@ mod tests {
.return_once(|_, _| Ok(())); .return_once(|_, _| Ok(()));
let matches_vec: Vec<Entity<ArtistMeta>> = vec![]; let matches_vec: Vec<Entity<ArtistMeta>> = vec![];
let artist_match = MatchStateInfo::artist_search(artist.clone(), matches_vec); let artist_match = EntityMatches::artist_search(artist.clone(), matches_vec);
let matches = AppMachine::match_state( let matches = AppMachine::match_state(
inner_with_mb(music_hoard(vec![]), mb_job_sender), inner_with_mb(music_hoard(vec![]), mb_job_sender),
match_state(artist_match), match_state(artist_match),
@ -632,7 +632,7 @@ mod tests {
let matches_vec: Vec<Entity<AlbumMeta>> = vec![]; let matches_vec: Vec<Entity<AlbumMeta>> = vec![];
let album_match = let album_match =
MatchStateInfo::album_search(artist_id.clone(), album.clone(), matches_vec); EntityMatches::album_search(artist_id.clone(), album.clone(), matches_vec);
let matches = AppMachine::match_state( let matches = AppMachine::match_state(
inner_with_mb(music_hoard(vec![]), mb_job_sender), inner_with_mb(music_hoard(vec![]), mb_job_sender),
match_state(album_match), match_state(album_match),

View File

@ -225,7 +225,7 @@ mod tests {
}; };
use crate::tui::{ use crate::tui::{
app::{AppState, IApp, IAppInput, IAppInteractBrowse, InputEvent, MatchStateInfo}, app::{AppState, EntityMatches, IApp, IAppInput, IAppInteractBrowse, InputEvent},
lib::{ lib::{
interface::musicbrainz::{api::Entity, daemon::MockIMbJobSender}, interface::musicbrainz::{api::Entity, daemon::MockIMbJobSender},
MockIMusicHoard, MockIMusicHoard,
@ -520,7 +520,7 @@ mod tests {
let (_, rx) = mpsc::channel(); let (_, rx) = mpsc::channel();
let fetch = FetchState::new(rx); let fetch = FetchState::new(rx);
let artist = ArtistMeta::new(ArtistId::new("Artist")); let artist = ArtistMeta::new(ArtistId::new("Artist"));
let info = MatchStateInfo::artist_lookup(artist.clone(), Entity::new(artist.clone())); let info = EntityMatches::artist_lookup(artist.clone(), Entity::new(artist.clone()));
app = app =
AppMachine::match_state(app.unwrap_browse().inner, MatchState::new(info, fetch)).into(); AppMachine::match_state(app.unwrap_browse().inner, MatchState::new(info, fetch)).into();

View File

@ -240,18 +240,18 @@ pub struct AlbumMatches {
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub enum MatchStateInfo { pub enum EntityMatches {
Artist(ArtistMatches), Artist(ArtistMatches),
Album(AlbumMatches), Album(AlbumMatches),
} }
impl MatchStateInfo { impl EntityMatches {
pub fn artist_search<M: Into<MatchOption<ArtistMeta>>>( pub fn artist_search<M: Into<MatchOption<ArtistMeta>>>(
matching: ArtistMeta, matching: ArtistMeta,
list: Vec<M>, list: Vec<M>,
) -> Self { ) -> Self {
let list = list.into_iter().map(Into::into).collect(); let list = list.into_iter().map(Into::into).collect();
MatchStateInfo::Artist(ArtistMatches { matching, list }) EntityMatches::Artist(ArtistMatches { matching, list })
} }
pub fn album_search<M: Into<MatchOption<AlbumMeta>>>( pub fn album_search<M: Into<MatchOption<AlbumMeta>>>(
@ -260,7 +260,7 @@ impl MatchStateInfo {
list: Vec<M>, list: Vec<M>,
) -> Self { ) -> Self {
let list = list.into_iter().map(Into::into).collect(); let list = list.into_iter().map(Into::into).collect();
MatchStateInfo::Album(AlbumMatches { EntityMatches::Album(AlbumMatches {
artist, artist,
matching, matching,
list, list,
@ -269,7 +269,7 @@ impl MatchStateInfo {
pub fn artist_lookup<M: Into<MatchOption<ArtistMeta>>>(matching: ArtistMeta, item: M) -> Self { pub fn artist_lookup<M: Into<MatchOption<ArtistMeta>>>(matching: ArtistMeta, item: M) -> Self {
let list = vec![item.into()]; let list = vec![item.into()];
MatchStateInfo::Artist(ArtistMatches { matching, list }) EntityMatches::Artist(ArtistMatches { matching, list })
} }
pub fn album_lookup<M: Into<MatchOption<AlbumMeta>>>( pub fn album_lookup<M: Into<MatchOption<AlbumMeta>>>(
@ -278,7 +278,7 @@ impl MatchStateInfo {
item: M, item: M,
) -> Self { ) -> Self {
let list = vec![item.into()]; let list = vec![item.into()];
MatchStateInfo::Album(AlbumMatches { EntityMatches::Album(AlbumMatches {
artist, artist,
matching, matching,
list, list,
@ -287,7 +287,7 @@ impl MatchStateInfo {
} }
pub struct MatchStatePublic<'app> { pub struct MatchStatePublic<'app> {
pub info: &'app MatchStateInfo, pub matches: &'app EntityMatches,
pub state: &'app mut WidgetState, pub state: &'app mut WidgetState,
} }

View File

@ -1,11 +1,16 @@
use std::{collections::VecDeque, sync::mpsc, thread, time}; use std::{collections::VecDeque, sync::mpsc, thread, time};
use musichoard::external::musicbrainz::api::{NextPage, PageSettings};
use crate::tui::{ use crate::tui::{
app::MatchStateInfo, app::EntityMatches,
event::IFetchCompleteEventSender, event::IFetchCompleteEventSender,
lib::interface::musicbrainz::{ lib::interface::musicbrainz::{
api::{Error as ApiError, IMusicBrainz}, api::{Error as ApiError, IMusicBrainz, Paged},
daemon::{Error, IMbJobSender, LookupParams, MbParams, ResultSender, SearchParams}, daemon::{
BrowseParams, EntityList, Error, IMbJobSender, LookupParams, MbParams, MbReturn,
ResultSender, SearchParams,
},
}, },
}; };
@ -43,6 +48,7 @@ enum JobPriority {
struct JobInstance { struct JobInstance {
result_sender: ResultSender, result_sender: ResultSender,
requests: VecDeque<MbParams>, requests: VecDeque<MbParams>,
paging: Option<PageSettings>,
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
@ -62,6 +68,7 @@ impl JobInstance {
JobInstance { JobInstance {
result_sender, result_sender,
requests, requests,
paging: None,
} }
} }
} }
@ -221,8 +228,17 @@ impl JobInstance {
event_sender: &mut dyn IFetchCompleteEventSender, event_sender: &mut dyn IFetchCompleteEventSender,
) -> Result<JobInstanceStatus, JobInstanceError> { ) -> Result<JobInstanceStatus, JobInstanceError> {
// self.requests can be empty if the caller submits an empty job. // self.requests can be empty if the caller submits an empty job.
if let Some(params) = self.requests.pop_front() { self.paging = match self.requests.front() {
self.execute(musicbrainz, event_sender, params)? Some(params) => {
let result_sender = &mut self.result_sender;
let paging = self.paging.take();
Self::execute(musicbrainz, result_sender, event_sender, &params, paging)?
}
None => None,
};
if self.paging.is_none() {
self.requests.pop_front();
} }
if self.requests.is_empty() { if self.requests.is_empty() {
@ -233,38 +249,64 @@ impl JobInstance {
} }
fn execute( fn execute(
&mut self,
musicbrainz: &mut dyn IMusicBrainz, musicbrainz: &mut dyn IMusicBrainz,
result_sender: &mut ResultSender,
event_sender: &mut dyn IFetchCompleteEventSender, event_sender: &mut dyn IFetchCompleteEventSender,
api_params: MbParams, api_params: &MbParams,
) -> Result<(), JobInstanceError> { paging: Option<PageSettings>,
) -> Result<Option<PageSettings>, JobInstanceError> {
let mut paging = match paging {
Some(paging) => paging,
None => PageSettings::with_max_limit(),
};
let mut next_page = NextPage::Complete;
let result = match api_params { let result = match api_params {
MbParams::Lookup(lookup) => match lookup { MbParams::Lookup(lookup) => match lookup {
LookupParams::Artist(params) => musicbrainz LookupParams::Artist(p) => musicbrainz
.lookup_artist(&params.mbid) .lookup_artist(&p.mbid)
.map(|rv| MatchStateInfo::artist_lookup(params.artist, rv)), .map(|rv| EntityMatches::artist_lookup(p.artist.clone(), rv)),
LookupParams::ReleaseGroup(params) => musicbrainz LookupParams::ReleaseGroup(p) => {
.lookup_release_group(&params.mbid) musicbrainz.lookup_release_group(&p.mbid).map(|rv| {
.map(|rv| MatchStateInfo::album_lookup(params.artist_id, params.album, rv)), EntityMatches::album_lookup(p.artist_id.clone(), p.album.clone(), rv)
}, })
}
}
.map(MbReturn::Match),
MbParams::Search(search) => match search { MbParams::Search(search) => match search {
SearchParams::Artist(params) => musicbrainz SearchParams::Artist(p) => musicbrainz
.search_artist(&params.artist) .search_artist(&p.artist)
.map(|rv| MatchStateInfo::artist_search(params.artist, rv)), .map(|rv| EntityMatches::artist_search(p.artist.clone(), rv)),
SearchParams::ReleaseGroup(params) => musicbrainz SearchParams::ReleaseGroup(p) => musicbrainz
.search_release_group(&params.artist_mbid, &params.album) .search_release_group(&p.artist_mbid, &p.album)
.map(|rv| MatchStateInfo::album_search(params.artist_id, params.album, rv)), .map(|rv| {
}, EntityMatches::album_search(p.artist_id.clone(), p.album.clone(), rv)
}),
}
.map(MbReturn::Match),
MbParams::Browse(browse) => match browse {
BrowseParams::ReleaseGroup(params) => Paged::map_paged_result(
musicbrainz.browse_release_group(&params.artist, &mut paging),
|ents| EntityList::Album(ents.into_iter().map(|rg| rg.entity).collect()),
&mut next_page,
),
}
.map(MbReturn::Fetch),
}; };
self.return_result(event_sender, result) Self::return_result(result_sender, event_sender, result)?;
Ok(match next_page {
NextPage::Offset(offset) => Some(paging.with_offset(offset)),
NextPage::Complete => None,
})
} }
fn return_result( fn return_result(
&mut self, result_sender: &mut ResultSender,
event_sender: &mut dyn IFetchCompleteEventSender, event_sender: &mut dyn IFetchCompleteEventSender,
result: Result<MatchStateInfo, ApiError>, result: Result<MbReturn, ApiError>,
) -> Result<(), JobInstanceError> { ) -> Result<(), JobInstanceError> {
self.result_sender result_sender
.send(result) .send(result)
.map_err(|_| JobInstanceError::ReturnChannelDisconnected)?; .map_err(|_| JobInstanceError::ReturnChannelDisconnected)?;
@ -575,7 +617,12 @@ mod tests {
assert_eq!(result, Ok(())); assert_eq!(result, Ok(()));
let result = result_receiver.try_recv().unwrap(); let result = result_receiver.try_recv().unwrap();
assert_eq!(result, Ok(MatchStateInfo::artist_lookup(artist, lookup))); assert_eq!(
result,
Ok(MbReturn::Match(EntityMatches::artist_lookup(
artist, lookup
)))
);
} }
fn lookup_release_group_expectation( fn lookup_release_group_expectation(
@ -620,7 +667,9 @@ mod tests {
let artist_id = album_artist_id(); let artist_id = album_artist_id();
assert_eq!( assert_eq!(
result, result,
Ok(MatchStateInfo::album_lookup(artist_id, album, lookup)) Ok(MbReturn::Match(EntityMatches::album_lookup(
artist_id, album, lookup
)))
); );
} }
@ -661,7 +710,12 @@ mod tests {
assert_eq!(result, Ok(())); assert_eq!(result, Ok(()));
let result = result_receiver.try_recv().unwrap(); let result = result_receiver.try_recv().unwrap();
assert_eq!(result, Ok(MatchStateInfo::artist_search(artist, matches))); assert_eq!(
result,
Ok(MbReturn::Match(EntityMatches::artist_search(
artist, matches
)))
);
} }
fn search_release_group_expectation( fn search_release_group_expectation(
@ -716,21 +770,21 @@ mod tests {
let result = result_receiver.try_recv().unwrap(); let result = result_receiver.try_recv().unwrap();
assert_eq!( assert_eq!(
result, result,
Ok(MatchStateInfo::album_search( Ok(MbReturn::Match(EntityMatches::album_search(
artist_id.clone(), artist_id.clone(),
album_1, album_1,
matches_1 matches_1
)) )))
); );
let result = result_receiver.try_recv().unwrap(); let result = result_receiver.try_recv().unwrap();
assert_eq!( assert_eq!(
result, result,
Ok(MatchStateInfo::album_search( Ok(MbReturn::Match(EntityMatches::album_search(
artist_id.clone(), artist_id.clone(),
album_4, album_4,
matches_4 matches_4
)) )))
); );
} }

View File

@ -1,5 +1,7 @@
//! Module for accessing MusicBrainz metadata. //! Module for accessing MusicBrainz metadata.
use std::mem;
#[cfg(test)] #[cfg(test)]
use mockall::automock; use mockall::automock;
@ -47,4 +49,26 @@ pub struct Paged<T> {
pub next: NextPage, pub next: NextPage,
} }
// pub fn map<U, F: FnOnce(T) -> U>(self, op: F) -> Result<U, E> {
// match self {
// Ok(t) => Ok(op(t)),
// Err(e) => Err(e),
// }
// }
impl<T> Paged<T> {
pub fn map_paged_result<E, U, F: FnOnce(T) -> U>(
result: Result<Paged<T>, E>,
op: F,
next: &mut NextPage,
) -> Result<U, E> {
match result {
Ok(paged) => {
_ = mem::replace(next, paged.next);
Ok(op(paged.item))
}
Err(err) => Err(err),
}
}
}
pub type Error = musichoard::external::musicbrainz::api::Error; pub type Error = musichoard::external::musicbrainz::api::Error;

View File

@ -6,7 +6,7 @@ use musichoard::collection::{
musicbrainz::Mbid, musicbrainz::Mbid,
}; };
use crate::tui::{app::MatchStateInfo, lib::interface::musicbrainz::api::Error as MbApiError}; use crate::tui::{app::EntityMatches, lib::interface::musicbrainz::api::Error as MbApiError};
#[cfg(test)] #[cfg(test)]
use mockall::automock; use mockall::automock;
@ -26,9 +26,20 @@ impl fmt::Display for Error {
} }
} }
pub type MbApiResult = Result<MatchStateInfo, MbApiError>; pub type MbApiResult = Result<MbReturn, MbApiError>;
pub type ResultSender = mpsc::Sender<MbApiResult>; pub type ResultSender = mpsc::Sender<MbApiResult>;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MbReturn {
Match(EntityMatches),
Fetch(EntityList),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EntityList {
Album(Vec<AlbumMeta>),
}
#[cfg_attr(test, automock)] #[cfg_attr(test, automock)]
pub trait IMbJobSender { pub trait IMbJobSender {
fn submit_foreground_job( fn submit_foreground_job(
@ -48,6 +59,7 @@ pub trait IMbJobSender {
pub enum MbParams { pub enum MbParams {
Lookup(LookupParams), Lookup(LookupParams),
Search(SearchParams), Search(SearchParams),
Browse(BrowseParams),
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
@ -87,6 +99,16 @@ pub struct SearchReleaseGroupParams {
pub album: AlbumMeta, pub album: AlbumMeta,
} }
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum BrowseParams {
ReleaseGroup(BrowseReleaseGroupParams),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BrowseReleaseGroupParams {
pub artist: Mbid,
}
impl MbParams { impl MbParams {
pub fn lookup_artist(artist: ArtistMeta, mbid: Mbid) -> Self { pub fn lookup_artist(artist: ArtistMeta, mbid: Mbid) -> Self {
MbParams::Lookup(LookupParams::Artist(LookupArtistParams { artist, mbid })) MbParams::Lookup(LookupParams::Artist(LookupArtistParams { artist, mbid }))

View File

@ -5,7 +5,7 @@ use musichoard::collection::{
track::{TrackFormat, TrackQuality}, track::{TrackFormat, TrackQuality},
}; };
use crate::tui::app::{MatchOption, MatchStateInfo}; use crate::tui::app::{EntityMatches, MatchOption};
pub struct UiDisplay; pub struct UiDisplay;
@ -119,10 +119,10 @@ impl UiDisplay {
) )
} }
pub fn display_matching_info(info: &MatchStateInfo) -> String { pub fn display_matching_info(info: &EntityMatches) -> String {
match info { match info {
MatchStateInfo::Artist(m) => UiDisplay::display_artist_matching(&m.matching), EntityMatches::Artist(m) => UiDisplay::display_artist_matching(&m.matching),
MatchStateInfo::Album(m) => UiDisplay::display_album_matching(&m.matching), EntityMatches::Album(m) => UiDisplay::display_album_matching(&m.matching),
} }
} }

View File

@ -2,7 +2,7 @@ use musichoard::collection::{album::AlbumMeta, artist::ArtistMeta};
use ratatui::widgets::{List, ListItem}; use ratatui::widgets::{List, ListItem};
use crate::tui::{ use crate::tui::{
app::{MatchOption, MatchStateInfo, WidgetState}, app::{EntityMatches, MatchOption, WidgetState},
ui::display::UiDisplay, ui::display::UiDisplay,
}; };
@ -13,10 +13,10 @@ pub struct MatchOverlay<'a, 'b> {
} }
impl<'a, 'b> MatchOverlay<'a, 'b> { impl<'a, 'b> MatchOverlay<'a, 'b> {
pub fn new(info: &'a MatchStateInfo, state: &'b mut WidgetState) -> Self { pub fn new(info: &'a EntityMatches, state: &'b mut WidgetState) -> Self {
match info { match info {
MatchStateInfo::Artist(m) => Self::artists(&m.matching, &m.list, state), EntityMatches::Artist(m) => Self::artists(&m.matching, &m.list, state),
MatchStateInfo::Album(m) => Self::albums(&m.matching, &m.list, state), EntityMatches::Album(m) => Self::albums(&m.matching, &m.list, state),
} }
} }

View File

@ -65,7 +65,7 @@ impl Minibuffer<'_> {
}, },
AppState::Match(public) => Minibuffer { AppState::Match(public) => Minibuffer {
paragraphs: vec![ paragraphs: vec![
Paragraph::new(UiDisplay::display_matching_info(public.info)), Paragraph::new(UiDisplay::display_matching_info(public.matches)),
Paragraph::new("ctrl+g: abort"), Paragraph::new("ctrl+g: abort"),
], ],
columns: 2, columns: 2,

View File

@ -18,7 +18,7 @@ use musichoard::collection::{album::Album, Collection};
use crate::tui::{ use crate::tui::{
app::{ app::{
AppPublicState, AppState, Category, IAppAccess, InputPublic, MatchStateInfo, Selection, AppPublicState, AppState, Category, EntityMatches, IAppAccess, InputPublic, Selection,
WidgetState, WidgetState,
}, },
ui::{ ui::{
@ -140,7 +140,7 @@ impl Ui {
UiWidget::render_overlay_widget("Fetching", fetch_text, area, false, frame) UiWidget::render_overlay_widget("Fetching", fetch_text, area, false, frame)
} }
fn render_match_overlay(info: &MatchStateInfo, state: &mut WidgetState, frame: &mut Frame) { fn render_match_overlay(info: &EntityMatches, state: &mut WidgetState, frame: &mut Frame) {
let area = OverlayBuilder::default().build(frame.area()); let area = OverlayBuilder::default().build(frame.area());
let st = MatchOverlay::new(info, state); let st = MatchOverlay::new(info, state);
UiWidget::render_overlay_list_widget(&st.matching, st.list, st.state, true, area, frame) UiWidget::render_overlay_list_widget(&st.matching, st.list, st.state, true, area, frame)
@ -182,7 +182,9 @@ impl IUi for Ui {
AppState::Info(()) => Self::render_info_overlay(collection, selection, frame), AppState::Info(()) => Self::render_info_overlay(collection, selection, frame),
AppState::Reload(()) => Self::render_reload_overlay(frame), AppState::Reload(()) => Self::render_reload_overlay(frame),
AppState::Fetch(()) => Self::render_fetch_overlay(frame), AppState::Fetch(()) => Self::render_fetch_overlay(frame),
AppState::Match(public) => Self::render_match_overlay(public.info, public.state, frame), AppState::Match(public) => {
Self::render_match_overlay(public.matches, public.state, frame)
}
AppState::Error(msg) => Self::render_error_overlay("Error", msg, frame), AppState::Error(msg) => Self::render_error_overlay("Error", msg, frame),
AppState::Critical(msg) => Self::render_error_overlay("Critical Error", msg, frame), AppState::Critical(msg) => Self::render_error_overlay("Critical Error", msg, frame),
_ => {} _ => {}
@ -226,7 +228,7 @@ mod tests {
AppState::Search(s) => AppState::Search(s), AppState::Search(s) => AppState::Search(s),
AppState::Fetch(()) => AppState::Fetch(()), AppState::Fetch(()) => AppState::Fetch(()),
AppState::Match(ref mut m) => AppState::Match(MatchStatePublic { AppState::Match(ref mut m) => AppState::Match(MatchStatePublic {
info: m.info, matches: m.matches,
state: m.state, state: m.state,
}), }),
AppState::Error(s) => AppState::Error(s), AppState::Error(s) => AppState::Error(s),
@ -329,22 +331,22 @@ mod tests {
ArtistMeta::new(ArtistId::new("an artist")) ArtistMeta::new(ArtistId::new("an artist"))
} }
fn artist_matches() -> MatchStateInfo { fn artist_matches() -> EntityMatches {
let artist = artist_meta(); let artist = artist_meta();
let artist_match = Entity::with_score(artist.clone(), 80); let artist_match = Entity::with_score(artist.clone(), 80);
let list = vec![artist_match.clone(), artist_match.clone()]; let list = vec![artist_match.clone(), artist_match.clone()];
let mut info = MatchStateInfo::artist_search(artist, list); let mut info = EntityMatches::artist_search(artist, list);
info.push_cannot_have_mbid(); info.push_cannot_have_mbid();
info.push_manual_input_mbid(); info.push_manual_input_mbid();
info info
} }
fn artist_lookup() -> MatchStateInfo { fn artist_lookup() -> EntityMatches {
let artist = artist_meta(); let artist = artist_meta();
let artist_lookup = Entity::new(artist.clone()); let artist_lookup = Entity::new(artist.clone());
let mut info = MatchStateInfo::artist_lookup(artist, artist_lookup); let mut info = EntityMatches::artist_lookup(artist, artist_lookup);
info.push_cannot_have_mbid(); info.push_cannot_have_mbid();
info.push_manual_input_mbid(); info.push_manual_input_mbid();
info info
@ -366,24 +368,24 @@ mod tests {
) )
} }
fn album_matches() -> MatchStateInfo { fn album_matches() -> EntityMatches {
let artist_id = album_artist_id(); let artist_id = album_artist_id();
let album = album_meta(); let album = album_meta();
let album_match = Entity::with_score(album.clone(), 80); let album_match = Entity::with_score(album.clone(), 80);
let list = vec![album_match.clone(), album_match.clone()]; let list = vec![album_match.clone(), album_match.clone()];
let mut info = MatchStateInfo::album_search(artist_id, album, list); let mut info = EntityMatches::album_search(artist_id, album, list);
info.push_cannot_have_mbid(); info.push_cannot_have_mbid();
info.push_manual_input_mbid(); info.push_manual_input_mbid();
info info
} }
fn album_lookup() -> MatchStateInfo { fn album_lookup() -> EntityMatches {
let artist_id = album_artist_id(); let artist_id = album_artist_id();
let album = album_meta(); let album = album_meta();
let album_lookup = Entity::new(album.clone()); let album_lookup = Entity::new(album.clone());
let mut info = MatchStateInfo::album_lookup(artist_id, album, album_lookup); let mut info = EntityMatches::album_lookup(artist_id, album, album_lookup);
info.push_cannot_have_mbid(); info.push_cannot_have_mbid();
info.push_manual_input_mbid(); info.push_manual_input_mbid();
info info
@ -403,13 +405,13 @@ mod tests {
album_lookup(), album_lookup(),
]; ];
for info in match_state_infos.iter() { for matches in match_state_infos.iter() {
let mut widget_state = WidgetState::default().with_selected(Some(0)); let mut widget_state = WidgetState::default().with_selected(Some(0));
let mut app = AppPublic { let mut app = AppPublic {
inner: public_inner(collection, &mut selection), inner: public_inner(collection, &mut selection),
state: AppState::Match(MatchStatePublic { state: AppState::Match(MatchStatePublic {
info, matches,
state: &mut widget_state, state: &mut widget_state,
}), }),
input: None, input: None,