Channel communication between browse and matches
Some checks failed
Cargo CI / Build and Test (pull_request) Failing after 2m39s
Cargo CI / Lint (pull_request) Successful in 1m5s

This commit is contained in:
Wojciech Kozlowski 2024-09-01 14:21:25 +02:00
parent fd9d3677ec
commit c007813421
3 changed files with 121 additions and 68 deletions

View File

@ -1,11 +1,18 @@
use std::{sync::mpsc, thread, time}; 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::{ use crate::tui::{
machine::{App, AppInner, AppMachine}, app::{
selection::{Delta, ListSelection}, machine::{App, AppInner, AppMachine},
AppMatchesInfo, AppPublic, AppState, IAppInteractBrowse, selection::{Delta, ListSelection},
AppMatchesInfo, AppPublic, AppState, IAppInteractBrowse,
},
lib::interface::musicbrainz::Error as MbError,
}; };
pub struct AppBrowse; pub struct AppBrowse;
@ -91,43 +98,20 @@ impl IAppInteractBrowse for AppMachine<AppBrowse> {
} }
}; };
let (matches_tx, matches_rx) = mpsc::channel::<AppMatchesInfo>(); let (matches_tx, matches_rx) = mpsc::channel::<FetchResult>();
match artist.meta.musicbrainz { match artist.meta.musicbrainz {
Some(ref mbid) => { Some(ref arid) => {
let arid = mbid.mbid(); self.fetch_albums(
matches_tx,
let mut album_iter = artist.albums.iter().peekable(); arid.mbid().clone(),
while let Some(album) = album_iter.next() { artist.albums.iter().map(|a| &a.meta).cloned().collect(),
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));
}
}
} }
None => match self.inner.musicbrainz.search_artist(&artist.meta) { None => self.fetch_artist(matches_tx, artist.meta.clone()),
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(),
},
}; };
AppMachine::matches(self.inner, matches_rx).into() AppMachine::app_matches(self.inner, matches_rx)
} }
fn no_op(self) -> Self::APP { fn no_op(self) -> Self::APP {
@ -135,6 +119,45 @@ impl IAppInteractBrowse for AppMachine<AppBrowse> {
} }
} }
pub type FetchError = MbError;
pub type FetchResult = Result<AppMatchesInfo, FetchError>;
pub type FetchSender = mpsc::Sender<FetchResult>;
pub type FetchReceiver = mpsc::Receiver<FetchResult>;
trait IAppInteractBrowsePrivate {
fn fetch_artist(&mut self, matches_tx: FetchSender, artist: ArtistMeta);
fn fetch_albums(&mut self, matches_tx: FetchSender, arid: Mbid, albums: Vec<AlbumMeta>);
}
impl IAppInteractBrowsePrivate for AppMachine<AppBrowse> {
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<AlbumMeta>) {
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)] #[cfg(test)]
mod tests { mod tests {
use mockall::{predicate, Sequence}; use mockall::{predicate, Sequence};

View File

@ -1,4 +1,4 @@
use std::{cmp, sync::mpsc}; use std::cmp;
use crate::tui::app::{ use crate::tui::app::{
machine::{App, AppInner, AppMachine}, machine::{App, AppInner, AppMachine},
@ -6,6 +6,8 @@ use crate::tui::app::{
IAppInteractMatches, MatchOption, WidgetState, IAppInteractMatches, MatchOption, WidgetState,
}; };
use super::browse::{FetchError, FetchReceiver};
impl AppArtistMatches { impl AppArtistMatches {
fn len(&self) -> usize { fn len(&self) -> usize {
self.list.len() self.list.len()
@ -43,21 +45,32 @@ impl AppMatchesInfo {
} }
pub struct AppMatches { pub struct AppMatches {
matches_rx: mpsc::Receiver<AppMatchesInfo>, matches_rx: FetchReceiver,
current: Option<AppMatchesInfo>, current: Option<AppMatchesInfo>,
state: WidgetState, state: WidgetState,
} }
impl AppMachine<AppMatches> { impl AppMatches {
pub fn matches(inner: AppInner, matches_rx: mpsc::Receiver<AppMatchesInfo>) -> Self { fn new(matches_rx: FetchReceiver) -> Self {
let mut state = AppMatches { AppMatches {
matches_rx, matches_rx,
current: None, current: None,
state: WidgetState::default(), state: WidgetState::default(),
}; }
state.next_matches_info(); }
}
impl AppMachine<AppMatches> {
fn matches(inner: AppInner, state: AppMatches) -> Self {
AppMachine { inner, state } 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<AppMachine<AppMatches>> for App { impl From<AppMachine<AppMatches>> for App {
@ -105,10 +118,15 @@ impl IAppInteractMatches for AppMachine<AppMatches> {
} }
fn select(mut self) -> Self::APP { fn select(mut self) -> Self::APP {
self.state.next_matches_info(); match self.state.next_matches_info() {
match self.state.current { Ok(state) => {
Some(_) => self.into(), self.state = state;
None => AppMachine::browse(self.inner).into(), 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<AppMatches> {
} }
} }
trait IAppInteractMatchesPrivate { trait IAppInteractMatchesPrivate
fn next_matches_info(&mut self); where
Self: Sized,
{
fn next_matches_info(self) -> Result<Self, FetchError>;
} }
impl IAppInteractMatchesPrivate for AppMatches { impl IAppInteractMatchesPrivate for AppMatches {
fn next_matches_info(&mut self) { fn next_matches_info(mut self) -> Result<Self, FetchError> {
// FIXME: try_recv might not be appropriate for asynchronous version. (self.current, self.state) = match self.matches_rx.recv() {
(self.current, self.state) = match self.matches_rx.try_recv() { Ok(fetch_result) => {
Ok(mut next_match) => { let mut next_match = fetch_result?;
next_match.push_cannot_have_mbid(); next_match.push_cannot_have_mbid();
let mut state = WidgetState::default(); let mut state = WidgetState::default();
state.list.select(Some(0)); state.list.select(Some(0));
(Some(next_match), state) (Some(next_match), state)
} }
Err(_) => (None, WidgetState::default()), Err(_) => (None, WidgetState::default()),
} };
Ok(self)
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use mpsc::Receiver; use std::sync::mpsc;
use musichoard::collection::{ use musichoard::collection::{
album::{AlbumDate, AlbumId, AlbumMeta, AlbumPrimaryType, AlbumSecondaryType}, album::{AlbumDate, AlbumId, AlbumMeta, AlbumPrimaryType, AlbumSecondaryType},
artist::{ArtistId, ArtistMeta}, artist::{ArtistId, ArtistMeta},
@ -227,10 +251,10 @@ mod tests {
vec![matches_info_1, matches_info_2] vec![matches_info_1, matches_info_2]
} }
fn receiver(matches_info_vec: Vec<AppMatchesInfo>) -> Receiver<AppMatchesInfo> { fn receiver(matches_info_vec: Vec<AppMatchesInfo>) -> FetchReceiver {
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
for matches_info in matches_info_vec.into_iter() { for matches_info in matches_info_vec.into_iter() {
tx.send(matches_info).unwrap(); tx.send(Ok(matches_info)).unwrap();
} }
rx rx
} }
@ -243,7 +267,8 @@ mod tests {
#[test] #[test]
fn create_empty() { 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(); let widget_state = WidgetState::default();
@ -261,10 +286,11 @@ mod tests {
#[test] #[test]
fn create_nonempty() { fn create_nonempty() {
let mut matches_info_vec = album_matches_info_vec(); let mut matches_info_vec = album_matches_info_vec();
let matches = AppMachine::matches( let matches = AppMachine::app_matches(
inner(music_hoard(vec![])), inner(music_hoard(vec![])),
receiver(matches_info_vec.clone()), receiver(matches_info_vec.clone()),
); )
.unwrap_matches();
push_cannot_have_mbid(&mut matches_info_vec); push_cannot_have_mbid(&mut matches_info_vec);
let mut widget_state = WidgetState::default(); let mut widget_state = WidgetState::default();
@ -282,10 +308,11 @@ mod tests {
} }
fn matches_flow(mut matches_info_vec: Vec<AppMatchesInfo>) { fn matches_flow(mut matches_info_vec: Vec<AppMatchesInfo>) {
let matches = AppMachine::matches( let matches = AppMachine::app_matches(
inner(music_hoard(vec![])), inner(music_hoard(vec![])),
receiver(matches_info_vec.clone()), receiver(matches_info_vec.clone()),
); )
.unwrap_matches();
push_cannot_have_mbid(&mut matches_info_vec); push_cannot_have_mbid(&mut matches_info_vec);
let mut widget_state = WidgetState::default(); let mut widget_state = WidgetState::default();
@ -337,10 +364,11 @@ mod tests {
#[test] #[test]
fn matches_abort() { fn matches_abort() {
let mut matches_info_vec = album_matches_info_vec(); let mut matches_info_vec = album_matches_info_vec();
let matches = AppMachine::matches( let matches = AppMachine::app_matches(
inner(music_hoard(vec![])), inner(music_hoard(vec![])),
receiver(matches_info_vec.clone()), receiver(matches_info_vec.clone()),
); )
.unwrap_matches();
push_cannot_have_mbid(&mut matches_info_vec); push_cannot_have_mbid(&mut matches_info_vec);
let mut widget_state = WidgetState::default(); let mut widget_state = WidgetState::default();
@ -354,7 +382,8 @@ mod tests {
#[test] #[test]
fn matches_select_empty() { 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); assert_eq!(matches.state.current, None);
@ -363,7 +392,8 @@ mod tests {
#[test] #[test]
fn no_op() { 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(); let app = matches.no_op();
app.unwrap_matches(); app.unwrap_matches();
} }

View File

@ -314,7 +314,7 @@ mod tests {
assert!(app.is_running()); assert!(app.is_running());
let (_, rx) = mpsc::channel(); 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(); let state = app.state();
assert!(matches!(state, AppState::Matches(_))); assert!(matches!(state, AppState::Matches(_)));