From c007813421b21f85a29d4bda501b2119c8462a53 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sun, 1 Sep 2024 14:21:25 +0200 Subject: [PATCH 1/3] Channel communication between browse and matches --- src/tui/app/machine/browse.rs | 97 +++++++++++++++++++++------------- src/tui/app/machine/matches.rs | 90 ++++++++++++++++++++----------- src/tui/app/machine/mod.rs | 2 +- 3 files changed, 121 insertions(+), 68 deletions(-) diff --git a/src/tui/app/machine/browse.rs b/src/tui/app/machine/browse.rs index 344e3ea..75e9402 100644 --- a/src/tui/app/machine/browse.rs +++ b/src/tui/app/machine/browse.rs @@ -1,11 +1,18 @@ use std::{sync::mpsc, thread, time}; -use musichoard::collection::musicbrainz::IMusicBrainzRef; +use musichoard::collection::{ + album::AlbumMeta, + artist::ArtistMeta, + musicbrainz::{IMusicBrainzRef, Mbid}, +}; -use crate::tui::app::{ - machine::{App, AppInner, AppMachine}, - selection::{Delta, ListSelection}, - AppMatchesInfo, AppPublic, AppState, IAppInteractBrowse, +use crate::tui::{ + app::{ + machine::{App, AppInner, AppMachine}, + selection::{Delta, ListSelection}, + AppMatchesInfo, AppPublic, AppState, IAppInteractBrowse, + }, + lib::interface::musicbrainz::Error as MbError, }; pub struct AppBrowse; @@ -91,43 +98,20 @@ impl IAppInteractBrowse for AppMachine { } }; - let (matches_tx, matches_rx) = mpsc::channel::(); + let (matches_tx, matches_rx) = mpsc::channel::(); match artist.meta.musicbrainz { - Some(ref mbid) => { - let arid = mbid.mbid(); - - let mut album_iter = artist.albums.iter().peekable(); - while let Some(album) = album_iter.next() { - if album.meta.musicbrainz.is_some() { - continue; - } - - match self - .inner - .musicbrainz - .search_release_group(arid, &album.meta) - { - Ok(list) => matches_tx - .send(AppMatchesInfo::album(album.meta.clone(), list)) - .expect("send fails only if receiver is disconnected"), - Err(err) => return AppMachine::error(self.inner, err.to_string()).into(), - } - - if album_iter.peek().is_some() { - thread::sleep(time::Duration::from_secs(1)); - } - } + Some(ref arid) => { + self.fetch_albums( + matches_tx, + arid.mbid().clone(), + artist.albums.iter().map(|a| &a.meta).cloned().collect(), + ); } - None => match self.inner.musicbrainz.search_artist(&artist.meta) { - Ok(list) => matches_tx - .send(AppMatchesInfo::artist(artist.meta.clone(), list)) - .expect("send fails only if receiver is disconnected"), - Err(err) => return AppMachine::error(self.inner, err.to_string()).into(), - }, + None => self.fetch_artist(matches_tx, artist.meta.clone()), }; - AppMachine::matches(self.inner, matches_rx).into() + AppMachine::app_matches(self.inner, matches_rx) } fn no_op(self) -> Self::APP { @@ -135,6 +119,45 @@ impl IAppInteractBrowse for AppMachine { } } +pub type FetchError = MbError; +pub type FetchResult = Result; +pub type FetchSender = mpsc::Sender; +pub type FetchReceiver = mpsc::Receiver; + +trait IAppInteractBrowsePrivate { + fn fetch_artist(&mut self, matches_tx: FetchSender, artist: ArtistMeta); + fn fetch_albums(&mut self, matches_tx: FetchSender, arid: Mbid, albums: Vec); +} + +impl IAppInteractBrowsePrivate for AppMachine { + fn fetch_artist(&mut self, matches_tx: FetchSender, artist: ArtistMeta) { + let musicbrainz = &mut self.inner.musicbrainz; + let result = musicbrainz + .search_artist(&artist) + .map(|list| AppMatchesInfo::artist(artist, list)); + matches_tx.send(result).expect("receiver is disconnected"); + } + + fn fetch_albums(&mut self, matches_tx: FetchSender, arid: Mbid, albums: Vec) { + let mut album_iter = albums.into_iter().peekable(); + while let Some(album) = album_iter.next() { + if album.musicbrainz.is_some() { + continue; + } + + let musicbrainz = &mut self.inner.musicbrainz; + let result = musicbrainz + .search_release_group(&arid, &album) + .map(|list| AppMatchesInfo::album(album, list)); + matches_tx.send(result).expect("receiver is disconnected"); + + if album_iter.peek().is_some() { + thread::sleep(time::Duration::from_secs(1)); + } + } + } +} + #[cfg(test)] mod tests { use mockall::{predicate, Sequence}; diff --git a/src/tui/app/machine/matches.rs b/src/tui/app/machine/matches.rs index 26c2774..ea4bced 100644 --- a/src/tui/app/machine/matches.rs +++ b/src/tui/app/machine/matches.rs @@ -1,4 +1,4 @@ -use std::{cmp, sync::mpsc}; +use std::cmp; use crate::tui::app::{ machine::{App, AppInner, AppMachine}, @@ -6,6 +6,8 @@ use crate::tui::app::{ IAppInteractMatches, MatchOption, WidgetState, }; +use super::browse::{FetchError, FetchReceiver}; + impl AppArtistMatches { fn len(&self) -> usize { self.list.len() @@ -43,21 +45,32 @@ impl AppMatchesInfo { } pub struct AppMatches { - matches_rx: mpsc::Receiver, + matches_rx: FetchReceiver, current: Option, state: WidgetState, } -impl AppMachine { - pub fn matches(inner: AppInner, matches_rx: mpsc::Receiver) -> Self { - let mut state = AppMatches { +impl AppMatches { + fn new(matches_rx: FetchReceiver) -> Self { + AppMatches { matches_rx, current: None, state: WidgetState::default(), - }; - state.next_matches_info(); + } + } +} + +impl AppMachine { + fn matches(inner: AppInner, state: AppMatches) -> Self { AppMachine { inner, state } } + + pub fn app_matches(inner: AppInner, matches_rx: FetchReceiver) -> App { + match AppMatches::new(matches_rx).next_matches_info() { + Ok(state) => AppMachine::matches(inner, state).into(), + Err(err) => AppMachine::error(inner, format!("fetch failed: {err}")).into(), + } + } } impl From> for App { @@ -105,10 +118,15 @@ impl IAppInteractMatches for AppMachine { } fn select(mut self) -> Self::APP { - self.state.next_matches_info(); - match self.state.current { - Some(_) => self.into(), - None => AppMachine::browse(self.inner).into(), + match self.state.next_matches_info() { + Ok(state) => { + self.state = state; + match self.state.current { + Some(_) => self.into(), + None => AppMachine::browse(self.inner).into(), + } + } + Err(err) => AppMachine::error(self.inner, format!("fetch failed: {err}")).into(), } } @@ -121,28 +139,34 @@ impl IAppInteractMatches for AppMachine { } } -trait IAppInteractMatchesPrivate { - fn next_matches_info(&mut self); +trait IAppInteractMatchesPrivate +where + Self: Sized, +{ + fn next_matches_info(self) -> Result; } impl IAppInteractMatchesPrivate for AppMatches { - fn next_matches_info(&mut self) { - // FIXME: try_recv might not be appropriate for asynchronous version. - (self.current, self.state) = match self.matches_rx.try_recv() { - Ok(mut next_match) => { + fn next_matches_info(mut self) -> Result { + (self.current, self.state) = match self.matches_rx.recv() { + Ok(fetch_result) => { + let mut next_match = fetch_result?; next_match.push_cannot_have_mbid(); let mut state = WidgetState::default(); state.list.select(Some(0)); (Some(next_match), state) } Err(_) => (None, WidgetState::default()), - } + }; + + Ok(self) } } #[cfg(test)] mod tests { - use mpsc::Receiver; + use std::sync::mpsc; + use musichoard::collection::{ album::{AlbumDate, AlbumId, AlbumMeta, AlbumPrimaryType, AlbumSecondaryType}, artist::{ArtistId, ArtistMeta}, @@ -227,10 +251,10 @@ mod tests { vec![matches_info_1, matches_info_2] } - fn receiver(matches_info_vec: Vec) -> Receiver { + fn receiver(matches_info_vec: Vec) -> FetchReceiver { let (tx, rx) = mpsc::channel(); for matches_info in matches_info_vec.into_iter() { - tx.send(matches_info).unwrap(); + tx.send(Ok(matches_info)).unwrap(); } rx } @@ -243,7 +267,8 @@ mod tests { #[test] fn create_empty() { - let matches = AppMachine::matches(inner(music_hoard(vec![])), receiver(vec![])); + let matches = + AppMachine::app_matches(inner(music_hoard(vec![])), receiver(vec![])).unwrap_matches(); let widget_state = WidgetState::default(); @@ -261,10 +286,11 @@ mod tests { #[test] fn create_nonempty() { let mut matches_info_vec = album_matches_info_vec(); - let matches = AppMachine::matches( + let matches = AppMachine::app_matches( inner(music_hoard(vec![])), receiver(matches_info_vec.clone()), - ); + ) + .unwrap_matches(); push_cannot_have_mbid(&mut matches_info_vec); let mut widget_state = WidgetState::default(); @@ -282,10 +308,11 @@ mod tests { } fn matches_flow(mut matches_info_vec: Vec) { - let matches = AppMachine::matches( + let matches = AppMachine::app_matches( inner(music_hoard(vec![])), receiver(matches_info_vec.clone()), - ); + ) + .unwrap_matches(); push_cannot_have_mbid(&mut matches_info_vec); let mut widget_state = WidgetState::default(); @@ -337,10 +364,11 @@ mod tests { #[test] fn matches_abort() { let mut matches_info_vec = album_matches_info_vec(); - let matches = AppMachine::matches( + let matches = AppMachine::app_matches( inner(music_hoard(vec![])), receiver(matches_info_vec.clone()), - ); + ) + .unwrap_matches(); push_cannot_have_mbid(&mut matches_info_vec); let mut widget_state = WidgetState::default(); @@ -354,7 +382,8 @@ mod tests { #[test] fn matches_select_empty() { - let matches = AppMachine::matches(inner(music_hoard(vec![])), receiver(vec![])); + let matches = + AppMachine::app_matches(inner(music_hoard(vec![])), receiver(vec![])).unwrap_matches(); assert_eq!(matches.state.current, None); @@ -363,7 +392,8 @@ mod tests { #[test] fn no_op() { - let matches = AppMachine::matches(inner(music_hoard(vec![])), receiver(vec![])); + let matches = + AppMachine::app_matches(inner(music_hoard(vec![])), receiver(vec![])).unwrap_matches(); let app = matches.no_op(); app.unwrap_matches(); } diff --git a/src/tui/app/machine/mod.rs b/src/tui/app/machine/mod.rs index afdf5e3..5740021 100644 --- a/src/tui/app/machine/mod.rs +++ b/src/tui/app/machine/mod.rs @@ -314,7 +314,7 @@ mod tests { assert!(app.is_running()); let (_, rx) = mpsc::channel(); - app = AppMachine::matches(app.unwrap_browse().inner, rx).into(); + app = AppMachine::app_matches(app.unwrap_browse().inner, rx); let state = app.state(); assert!(matches!(state, AppState::Matches(_))); -- 2.45.2 From f4068e5f779524fd0d73b9732cfd44758e98ae7c Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sun, 1 Sep 2024 14:57:01 +0200 Subject: [PATCH 2/3] Don't repeat yourself --- src/tui/app/machine/matches.rs | 51 +++++++++++++--------------------- 1 file changed, 20 insertions(+), 31 deletions(-) diff --git a/src/tui/app/machine/matches.rs b/src/tui/app/machine/matches.rs index ea4bced..46f240d 100644 --- a/src/tui/app/machine/matches.rs +++ b/src/tui/app/machine/matches.rs @@ -6,7 +6,7 @@ use crate::tui::app::{ IAppInteractMatches, MatchOption, WidgetState, }; -use super::browse::{FetchError, FetchReceiver}; +use super::browse::FetchReceiver; impl AppArtistMatches { fn len(&self) -> usize { @@ -66,10 +66,7 @@ impl AppMachine { } pub fn app_matches(inner: AppInner, matches_rx: FetchReceiver) -> App { - match AppMatches::new(matches_rx).next_matches_info() { - Ok(state) => AppMachine::matches(inner, state).into(), - Err(err) => AppMachine::error(inner, format!("fetch failed: {err}")).into(), - } + AppMachine::matches(inner, AppMatches::new(matches_rx)).fetch() } } @@ -117,17 +114,8 @@ impl IAppInteractMatches for AppMachine { self.into() } - fn select(mut self) -> Self::APP { - match self.state.next_matches_info() { - Ok(state) => { - self.state = state; - match self.state.current { - Some(_) => self.into(), - None => AppMachine::browse(self.inner).into(), - } - } - Err(err) => AppMachine::error(self.inner, format!("fetch failed: {err}")).into(), - } + fn select(self) -> Self::APP { + self.fetch() } fn abort(self) -> Self::APP { @@ -143,23 +131,24 @@ trait IAppInteractMatchesPrivate where Self: Sized, { - fn next_matches_info(self) -> Result; + fn fetch(self) -> App; } -impl IAppInteractMatchesPrivate for AppMatches { - fn next_matches_info(mut self) -> Result { - (self.current, self.state) = match self.matches_rx.recv() { - Ok(fetch_result) => { - let mut next_match = fetch_result?; - next_match.push_cannot_have_mbid(); - let mut state = WidgetState::default(); - state.list.select(Some(0)); - (Some(next_match), state) - } - Err(_) => (None, WidgetState::default()), - }; - - Ok(self) +impl IAppInteractMatchesPrivate for AppMachine { + fn fetch(mut self) -> App { + match self.state.matches_rx.recv() { + Ok(fetch_result) => match fetch_result { + Ok(mut next_match) => { + next_match.push_cannot_have_mbid(); + self.state.current = Some(next_match); + self.state.state.list.select(Some(0)); + AppMachine::matches(self.inner, self.state).into() + } + Err(err) => AppMachine::error(self.inner, format!("fetch failed: {err}")).into(), + }, + // only happens when the sender disconnects which means it finished its job + Err(_) => AppMachine::browse(self.inner).into(), + } } } -- 2.45.2 From eecf5e3f0ad610c8d9c273f02fde3c901b8176f0 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sun, 1 Sep 2024 17:43:14 +0200 Subject: [PATCH 3/3] Move fetch into threads --- src/main.rs | 4 +- src/tui/app/machine/browse.rs | 117 +++++++++++++++++++++++++-------- src/tui/app/machine/matches.rs | 32 ++++++--- src/tui/app/machine/mod.rs | 41 +++++++----- src/tui/mod.rs | 6 +- 5 files changed, 141 insertions(+), 59 deletions(-) diff --git a/src/main.rs b/src/main.rs index 86feb45..73233f3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -73,7 +73,7 @@ struct DbOpt { fn with( builder: MusicHoardBuilder, ) { - let music_hoard = Box::new(builder.build().expect("failed to initialise MusicHoard")); + let music_hoard = builder.build().expect("failed to initialise MusicHoard"); // Initialize the terminal user interface. let backend = CrosstermBackend::new(io::stdout()); @@ -86,7 +86,7 @@ fn with( let http = MusicBrainzHttp::new(MUSICHOARD_HTTP_USER_AGENT).expect("failed to initialise HTTP client"); let client = MusicBrainzClient::new(http); - let musicbrainz = Box::new(MusicBrainz::new(client)); + let musicbrainz = MusicBrainz::new(client); let app = App::new(music_hoard, musicbrainz); let ui = Ui; diff --git a/src/tui/app/machine/browse.rs b/src/tui/app/machine/browse.rs index 75e9402..4b7b6c2 100644 --- a/src/tui/app/machine/browse.rs +++ b/src/tui/app/machine/browse.rs @@ -1,4 +1,7 @@ -use std::{sync::mpsc, thread, time}; +use std::{ + sync::{mpsc, Arc, Mutex}, + thread, time, +}; use musichoard::collection::{ album::AlbumMeta, @@ -12,7 +15,7 @@ use crate::tui::{ selection::{Delta, ListSelection}, AppMatchesInfo, AppPublic, AppState, IAppInteractBrowse, }, - lib::interface::musicbrainz::Error as MbError, + lib::interface::musicbrainz::{Error as MbError, IMusicBrainz}, }; pub struct AppBrowse; @@ -89,7 +92,7 @@ impl IAppInteractBrowse for AppMachine { AppMachine::search(self.inner, orig).into() } - fn fetch_musicbrainz(mut self) -> Self::APP { + fn fetch_musicbrainz(self) -> Self::APP { let coll = self.inner.music_hoard.get_collection(); let artist = match self.inner.selection.state_artist(coll) { Some(artist_state) => &coll[artist_state.index], @@ -102,13 +105,16 @@ impl IAppInteractBrowse for AppMachine { match artist.meta.musicbrainz { Some(ref arid) => { - self.fetch_albums( - matches_tx, - arid.mbid().clone(), - artist.albums.iter().map(|a| &a.meta).cloned().collect(), - ); + let musicbrainz = Arc::clone(&self.inner.musicbrainz); + let arid = arid.mbid().clone(); + let albums = artist.albums.iter().map(|a| &a.meta).cloned().collect(); + thread::spawn(|| Self::fetch_albums(musicbrainz, matches_tx, arid, albums)); + } + None => { + let musicbrainz = Arc::clone(&self.inner.musicbrainz); + let artist = artist.meta.clone(); + thread::spawn(|| Self::fetch_artist(musicbrainz, matches_tx, artist)); } - None => self.fetch_artist(matches_tx, artist.meta.clone()), }; AppMachine::app_matches(self.inner, matches_rx) @@ -125,31 +131,49 @@ pub type FetchSender = mpsc::Sender; pub type FetchReceiver = mpsc::Receiver; trait IAppInteractBrowsePrivate { - fn fetch_artist(&mut self, matches_tx: FetchSender, artist: ArtistMeta); - fn fetch_albums(&mut self, matches_tx: FetchSender, arid: Mbid, albums: Vec); + fn fetch_artist( + musicbrainz: Arc>, + matches_tx: FetchSender, + artist: ArtistMeta, + ); + fn fetch_albums( + musicbrainz: Arc>, + matches_tx: FetchSender, + arid: Mbid, + albums: Vec, + ); } impl IAppInteractBrowsePrivate for AppMachine { - fn fetch_artist(&mut self, matches_tx: FetchSender, artist: ArtistMeta) { - let musicbrainz = &mut self.inner.musicbrainz; - let result = musicbrainz - .search_artist(&artist) - .map(|list| AppMatchesInfo::artist(artist, list)); - matches_tx.send(result).expect("receiver is disconnected"); + fn fetch_artist( + musicbrainz: Arc>, + matches_tx: FetchSender, + artist: ArtistMeta, + ) { + let result = musicbrainz.lock().unwrap().search_artist(&artist); + let result = result.map(|list| AppMatchesInfo::artist(artist, list)); + matches_tx.send(result).ok(); } - fn fetch_albums(&mut self, matches_tx: FetchSender, arid: Mbid, albums: Vec) { + fn fetch_albums( + musicbrainz: Arc>, + matches_tx: FetchSender, + arid: Mbid, + albums: Vec, + ) { + let mut musicbrainz = musicbrainz.lock().unwrap(); let mut album_iter = albums.into_iter().peekable(); while let Some(album) = album_iter.next() { if album.musicbrainz.is_some() { continue; } - let musicbrainz = &mut self.inner.musicbrainz; - let result = musicbrainz - .search_release_group(&arid, &album) - .map(|list| AppMatchesInfo::album(album, list)); - matches_tx.send(result).expect("receiver is disconnected"); + let result = musicbrainz.search_release_group(&arid, &album); + let result = result.map(|list| AppMatchesInfo::album(album, list)); + if matches_tx.send(result).is_err() { + // If receiver disconnects just drop the rest. + return; + } if album_iter.peek().is_some() { thread::sleep(time::Duration::from_secs(1)); @@ -247,7 +271,7 @@ mod tests { #[test] fn fetch_musicbrainz() { - let mut mb_api = Box::new(MockIMusicBrainz::new()); + let mut mb_api = MockIMusicBrainz::new(); let arid: Mbid = "11111111-1111-1111-1111-111111111111".try_into().unwrap(); let album_1 = COLLECTION[1].albums[0].meta.clone(); @@ -328,7 +352,7 @@ mod tests { #[test] fn fetch_musicbrainz_no_artist_mbid() { - let mut mb_api = Box::new(MockIMusicBrainz::new()); + let mut mb_api = MockIMusicBrainz::new(); let artist = COLLECTION[3].meta.clone(); @@ -372,7 +396,7 @@ mod tests { #[test] fn fetch_musicbrainz_artist_api_error() { - let mut mb_api = Box::new(MockIMusicBrainz::new()); + let mut mb_api = MockIMusicBrainz::new(); let error = Err(musicbrainz::Error::RateLimit); @@ -394,7 +418,7 @@ mod tests { #[test] fn fetch_musicbrainz_album_api_error() { - let mut mb_api = Box::new(MockIMusicBrainz::new()); + let mut mb_api = MockIMusicBrainz::new(); let error = Err(musicbrainz::Error::RateLimit); @@ -409,6 +433,45 @@ mod tests { app.unwrap_error(); } + #[test] + fn fetch_musicbrainz_artist_receiver_disconnect() { + let (tx, rx) = mpsc::channel::(); + drop(rx); + + let mut mb_api = MockIMusicBrainz::new(); + + mb_api + .expect_search_artist() + .times(1) + .return_once(|_| Ok(vec![])); + + // We only check it does not panic and that it doesn't call the API more than once. + AppMachine::fetch_artist(Arc::new(Mutex::new(mb_api)), tx, COLLECTION[3].meta.clone()); + } + + #[test] + fn fetch_musicbrainz_albums_receiver_disconnect() { + let (tx, rx) = mpsc::channel::(); + drop(rx); + + let mut mb_api = MockIMusicBrainz::new(); + + mb_api + .expect_search_release_group() + .times(1) + .return_once(|_, _| Ok(vec![])); + + // We only check it does not panic and that it doesn't call the API more than once. + let mbref = &COLLECTION[1].meta.musicbrainz; + let albums = &COLLECTION[1].albums; + AppMachine::fetch_albums( + Arc::new(Mutex::new(mb_api)), + tx, + mbref.as_ref().unwrap().mbid().clone(), + albums.iter().map(|a| &a.meta).cloned().collect(), + ); + } + #[test] fn no_op() { let browse = AppMachine::browse(inner(music_hoard(vec![]))); diff --git a/src/tui/app/machine/matches.rs b/src/tui/app/machine/matches.rs index 46f240d..d3ff9ab 100644 --- a/src/tui/app/machine/matches.rs +++ b/src/tui/app/machine/matches.rs @@ -1,13 +1,11 @@ use std::cmp; use crate::tui::app::{ - machine::{App, AppInner, AppMachine}, + machine::{browse::FetchReceiver, App, AppInner, AppMachine}, AppAlbumMatches, AppArtistMatches, AppMatchesInfo, AppPublic, AppPublicMatches, AppState, IAppInteractMatches, MatchOption, WidgetState, }; -use super::browse::FetchReceiver; - impl AppArtistMatches { fn len(&self) -> usize { self.list.len() @@ -51,7 +49,7 @@ pub struct AppMatches { } impl AppMatches { - fn new(matches_rx: FetchReceiver) -> Self { + fn empty(matches_rx: FetchReceiver) -> Self { AppMatches { matches_rx, current: None, @@ -66,7 +64,7 @@ impl AppMachine { } pub fn app_matches(inner: AppInner, matches_rx: FetchReceiver) -> App { - AppMachine::matches(inner, AppMatches::new(matches_rx)).fetch() + AppMachine::matches(inner, AppMatches::empty(matches_rx)).fetch_first() } } @@ -115,7 +113,7 @@ impl IAppInteractMatches for AppMachine { } fn select(self) -> Self::APP { - self.fetch() + self.fetch_next() } fn abort(self) -> Self::APP { @@ -131,11 +129,21 @@ trait IAppInteractMatchesPrivate where Self: Sized, { - fn fetch(self) -> App; + fn fetch_first(self) -> App; + fn fetch_next(self) -> App; + fn fetch(self, first: bool) -> App; } impl IAppInteractMatchesPrivate for AppMachine { - fn fetch(mut self) -> App { + fn fetch_first(self) -> App { + self.fetch(true) + } + + fn fetch_next(self) -> App { + self.fetch(false) + } + + fn fetch(mut self, first: bool) -> App { match self.state.matches_rx.recv() { Ok(fetch_result) => match fetch_result { Ok(mut next_match) => { @@ -147,7 +155,13 @@ impl IAppInteractMatchesPrivate for AppMachine { Err(err) => AppMachine::error(self.inner, format!("fetch failed: {err}")).into(), }, // only happens when the sender disconnects which means it finished its job - Err(_) => AppMachine::browse(self.inner).into(), + Err(_) => { + if first { + AppMachine::matches(self.inner, AppMatches::empty(self.state.matches_rx)).into() + } else { + AppMachine::browse(self.inner).into() + } + } } } } diff --git a/src/tui/app/machine/mod.rs b/src/tui/app/machine/mod.rs index 5740021..6aa186f 100644 --- a/src/tui/app/machine/mod.rs +++ b/src/tui/app/machine/mod.rs @@ -6,6 +6,8 @@ mod matches; mod reload; mod search; +use std::sync::{Arc, Mutex}; + use crate::tui::{ app::{selection::Selection, AppPublic, AppPublicInner, AppState, IAppAccess, IAppInteract}, lib::{interface::musicbrainz::IMusicBrainz, IMusicHoard}, @@ -37,21 +39,24 @@ pub struct AppMachine { pub struct AppInner { running: bool, music_hoard: Box, - musicbrainz: Box, + musicbrainz: Arc>, selection: Selection, } impl App { - pub fn new(mut music_hoard: Box, mb_api: Box) -> Self { + pub fn new( + mut music_hoard: MH, + musicbrainz: MB, + ) -> Self { let init_result = Self::init(&mut music_hoard); - let inner = AppInner::new(music_hoard, mb_api); + let inner = AppInner::new(music_hoard, musicbrainz); match init_result { Ok(()) => AppMachine::browse(inner).into(), Err(err) => AppMachine::critical(inner, err.to_string()).into(), } } - fn init(music_hoard: &mut Box) -> Result<(), musichoard::Error> { + fn init(music_hoard: &mut MH) -> Result<(), musichoard::Error> { music_hoard.rescan_library()?; Ok(()) } @@ -121,12 +126,15 @@ impl IAppAccess for App { } impl AppInner { - pub fn new(music_hoard: Box, musicbrainz: Box) -> Self { + pub fn new( + music_hoard: MH, + musicbrainz: MB, + ) -> Self { let selection = Selection::new(music_hoard.get_collection()); AppInner { running: true, - music_hoard, - musicbrainz, + music_hoard: Box::new(music_hoard), + musicbrainz: Arc::new(Mutex::new(musicbrainz)), selection, } } @@ -205,14 +213,14 @@ mod tests { } } - pub fn music_hoard(collection: Collection) -> Box { - let mut music_hoard = Box::new(MockIMusicHoard::new()); + pub fn music_hoard(collection: Collection) -> MockIMusicHoard { + let mut music_hoard = MockIMusicHoard::new(); music_hoard.expect_get_collection().return_const(collection); music_hoard } - fn music_hoard_init(collection: Collection) -> Box { + fn music_hoard_init(collection: Collection) -> MockIMusicHoard { let mut music_hoard = music_hoard(collection); music_hoard @@ -223,18 +231,15 @@ mod tests { music_hoard } - fn mb_api() -> Box { - Box::new(MockIMusicBrainz::new()) + fn mb_api() -> MockIMusicBrainz { + MockIMusicBrainz::new() } - pub fn inner(music_hoard: Box) -> AppInner { + pub fn inner(music_hoard: MockIMusicHoard) -> AppInner { AppInner::new(music_hoard, mb_api()) } - pub fn inner_with_mb( - music_hoard: Box, - mb_api: Box, - ) -> AppInner { + pub fn inner_with_mb(music_hoard: MockIMusicHoard, mb_api: MockIMusicBrainz) -> AppInner { AppInner::new(music_hoard, mb_api) } @@ -365,7 +370,7 @@ mod tests { #[test] fn init_error() { - let mut music_hoard = Box::new(MockIMusicHoard::new()); + let mut music_hoard = MockIMusicHoard::new(); music_hoard .expect_rescan_library() diff --git a/src/tui/mod.rs b/src/tui/mod.rs index f25baa2..131ebb7 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -192,8 +192,8 @@ mod tests { Terminal::new(backend).unwrap() } - fn music_hoard(collection: Collection) -> Box { - let mut music_hoard = Box::new(MockIMusicHoard::new()); + fn music_hoard(collection: Collection) -> MockIMusicHoard { + let mut music_hoard = MockIMusicHoard::new(); music_hoard.expect_reload_database().returning(|| Ok(())); music_hoard.expect_rescan_library().returning(|| Ok(())); @@ -203,7 +203,7 @@ mod tests { } fn app(collection: Collection) -> App { - App::new(music_hoard(collection), Box::new(MockIMusicBrainz::new())) + App::new(music_hoard(collection), MockIMusicBrainz::new()) } fn listener() -> MockIEventListener { -- 2.45.2