Make fetch asynchronous #210
@ -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<AppBrowse> {
|
||||
}
|
||||
};
|
||||
|
||||
let (matches_tx, matches_rx) = mpsc::channel::<AppMatchesInfo>();
|
||||
let (matches_tx, matches_rx) = mpsc::channel::<FetchResult>();
|
||||
|
||||
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<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)]
|
||||
mod tests {
|
||||
use mockall::{predicate, Sequence};
|
||||
|
@ -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<AppMatchesInfo>,
|
||||
matches_rx: FetchReceiver,
|
||||
current: Option<AppMatchesInfo>,
|
||||
state: WidgetState,
|
||||
}
|
||||
|
||||
impl AppMachine<AppMatches> {
|
||||
pub fn matches(inner: AppInner, matches_rx: mpsc::Receiver<AppMatchesInfo>) -> 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<AppMatches> {
|
||||
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<AppMachine<AppMatches>> for App {
|
||||
@ -105,10 +118,15 @@ impl IAppInteractMatches for AppMachine<AppMatches> {
|
||||
}
|
||||
|
||||
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<AppMatches> {
|
||||
}
|
||||
}
|
||||
|
||||
trait IAppInteractMatchesPrivate {
|
||||
fn next_matches_info(&mut self);
|
||||
trait IAppInteractMatchesPrivate
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn next_matches_info(self) -> Result<Self, FetchError>;
|
||||
}
|
||||
|
||||
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, FetchError> {
|
||||
(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<AppMatchesInfo>) -> Receiver<AppMatchesInfo> {
|
||||
fn receiver(matches_info_vec: Vec<AppMatchesInfo>) -> 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<AppMatchesInfo>) {
|
||||
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();
|
||||
}
|
||||
|
@ -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(_)));
|
||||
|
Loading…
Reference in New Issue
Block a user