Provide a keyboard shortcut to pull all release groups of an artist #233

Merged
wojtek merged 14 commits from 160---provide-a-keyboard-shortcut-to-pull-all-release-groups-of-an-artist into main 2024-12-30 23:42:20 +01:00
11 changed files with 449 additions and 173 deletions

View File

@ -144,19 +144,18 @@ impl AlbumStatus {
}
impl Album {
pub fn new<Id: Into<AlbumId>, Date: Into<AlbumDate>>(
id: Id,
date: Date,
primary_type: Option<AlbumPrimaryType>,
secondary_types: Vec<AlbumSecondaryType>,
) -> Self {
let info = AlbumInfo::new(MbRefOption::None, primary_type, secondary_types);
pub fn new<Id: Into<AlbumId>>(id: Id) -> Self {
Album {
meta: AlbumMeta::new(id, date, info),
meta: AlbumMeta::new(id),
tracks: vec![],
}
}
pub fn with_date<Date: Into<AlbumDate>>(mut self, date: Date) -> Self {
self.meta.date = date.into();
self
}
pub fn get_status(&self) -> AlbumStatus {
AlbumStatus::from_tracks(&self.tracks)
}
@ -183,19 +182,25 @@ impl Merge for Album {
}
impl AlbumMeta {
pub fn new<Id: Into<AlbumId>, Date: Into<AlbumDate>>(
id: Id,
date: Date,
info: AlbumInfo,
) -> Self {
pub fn new<Id: Into<AlbumId>>(id: Id) -> Self {
AlbumMeta {
id: id.into(),
date: date.into(),
date: AlbumDate::default(),
seq: AlbumSeq::default(),
info,
info: AlbumInfo::default(),
}
}
pub fn with_date<Date: Into<AlbumDate>>(mut self, date: Date) -> Self {
self.date = date.into();
self
}
pub fn with_info(mut self, info: AlbumInfo) -> Self {
self.info = info;
self
}
pub fn get_sort_key(&self) -> (&AlbumDate, &AlbumSeq, &AlbumId) {
(&self.date, &self.seq, &self.id)
}
@ -258,7 +263,9 @@ impl Merge for AlbumInfo {
fn merge_in_place(&mut self, other: Self) {
self.musicbrainz = self.musicbrainz.take().or(other.musicbrainz);
self.primary_type = self.primary_type.take().or(other.primary_type);
self.secondary_types.merge_in_place(other.secondary_types);
if self.secondary_types.is_empty() {
self.secondary_types = other.secondary_types;
}
}
}
@ -311,13 +318,13 @@ mod tests {
let album_id_1 = AlbumId {
title: String::from("album z"),
};
let mut album_1 = Album::new(album_id_1, date.clone(), None, vec![]);
let mut album_1 = Album::new(album_id_1).with_date(date.clone());
album_1.meta.set_seq(AlbumSeq(1));
let album_id_2 = AlbumId {
title: String::from("album a"),
};
let mut album_2 = Album::new(album_id_2, date.clone(), None, vec![]);
let mut album_2 = Album::new(album_id_2).with_date(date.clone());
album_2.meta.set_seq(AlbumSeq(2));
assert_ne!(album_1, album_2);
@ -327,7 +334,7 @@ mod tests {
#[test]
fn set_clear_seq() {
let mut album = Album::new("An album", AlbumDate::default(), None, vec![]);
let mut album = Album::new("An album");
assert_eq!(album.meta.seq, AlbumSeq(0));

View File

@ -46,6 +46,14 @@ pub enum MbRefOption<T> {
}
impl<T> MbRefOption<T> {
pub fn is_some(&self) -> bool {
matches!(self, MbRefOption::Some(_))
}
pub fn is_none(&self) -> bool {
matches!(self, MbRefOption::None)
}
pub fn or(self, optb: MbRefOption<T>) -> MbRefOption<T> {
match (&self, &optb) {
(MbRefOption::Some(_), _) | (MbRefOption::CannotHaveMbid, MbRefOption::None) => self,

View File

@ -34,6 +34,7 @@ pub trait IMusicHoardBasePrivate {
artist_id: &ArtistId,
) -> Result<&'a mut Artist, Error>;
fn get_album<'a>(artist: &'a mut Artist, album_id: &AlbumId) -> Option<&'a Album>;
fn get_album_mut<'a>(artist: &'a mut Artist, album_id: &AlbumId) -> Option<&'a mut Album>;
fn get_album_mut_or_err<'a>(
artist: &'a mut Artist,
@ -79,6 +80,10 @@ impl<Database, Library> IMusicHoardBasePrivate for MusicHoard<Database, Library>
})
}
fn get_album<'a>(artist: &'a mut Artist, album_id: &AlbumId) -> Option<&'a Album> {
artist.albums.iter().find(|a| &a.meta.id == album_id)
}
fn get_album_mut<'a>(artist: &'a mut Artist, album_id: &AlbumId) -> Option<&'a mut Album> {
artist.albums.iter_mut().find(|a| &a.meta.id == album_id)
}

View File

@ -1,7 +1,11 @@
use std::mem;
use crate::{
collection::{album::AlbumInfo, artist::ArtistInfo, merge::Merge},
collection::{
album::{AlbumInfo, AlbumMeta},
artist::ArtistInfo,
merge::Merge,
},
core::{
collection::{
album::{Album, AlbumId, AlbumSeq},
@ -57,6 +61,17 @@ pub trait IMusicHoardDatabase {
property: S,
) -> Result<(), Error>;
fn add_album<ArtistIdRef: AsRef<ArtistId>>(
&mut self,
artist_id: ArtistIdRef,
album_meta: AlbumMeta,
) -> Result<(), Error>;
fn remove_album<ArtistIdRef: AsRef<ArtistId>, AlbumIdRef: AsRef<AlbumId>>(
&mut self,
artist_id: ArtistIdRef,
album_id: AlbumIdRef,
) -> Result<(), Error>;
fn set_album_seq<ArtistIdRef: AsRef<ArtistId>, AlbumIdRef: AsRef<AlbumId>>(
&mut self,
artist_id: ArtistIdRef,
@ -68,15 +83,15 @@ pub trait IMusicHoardDatabase {
artist_id: ArtistIdRef,
album_id: AlbumIdRef,
) -> Result<(), Error>;
fn merge_album_info<Id: AsRef<ArtistId>, AlbumIdRef: AsRef<AlbumId>>(
fn merge_album_info<ArtistIdRef: AsRef<ArtistId>, AlbumIdRef: AsRef<AlbumId>>(
&mut self,
artist_id: Id,
artist_id: ArtistIdRef,
album_id: AlbumIdRef,
info: AlbumInfo,
) -> Result<(), Error>;
fn clear_album_info<Id: AsRef<ArtistId>, AlbumIdRef: AsRef<AlbumId>>(
fn clear_album_info<ArtistIdRef: AsRef<ArtistId>, AlbumIdRef: AsRef<AlbumId>>(
&mut self,
artist_id: Id,
artist_id: ArtistIdRef,
album_id: AlbumIdRef,
) -> Result<(), Error>;
}
@ -194,6 +209,39 @@ impl<Database: IDatabase, Library> IMusicHoardDatabase for MusicHoard<Database,
})
}
fn add_album<ArtistIdRef: AsRef<ArtistId>>(
&mut self,
artist_id: ArtistIdRef,
album_meta: AlbumMeta,
) -> Result<(), Error> {
let album = Album {
meta: album_meta,
tracks: vec![],
};
self.update_artist(artist_id.as_ref(), |artist| {
if Self::get_album(artist, &album.meta.id).is_none() {
artist.albums.push(album);
artist.albums.sort_unstable();
}
})
}
fn remove_album<ArtistIdRef: AsRef<ArtistId>, AlbumIdRef: AsRef<AlbumId>>(
&mut self,
artist_id: ArtistIdRef,
album_id: AlbumIdRef,
) -> Result<(), Error> {
self.update_artist(artist_id.as_ref(), |artist| {
let index_opt = artist
.albums
.iter()
.position(|a| &a.meta.id == album_id.as_ref());
if let Some(index) = index_opt {
artist.albums.remove(index);
}
})
}
fn set_album_seq<ArtistIdRef: AsRef<ArtistId>, AlbumIdRef: AsRef<AlbumId>>(
&mut self,
artist_id: ArtistIdRef,
@ -344,7 +392,7 @@ mod tests {
musicbrainz::{MbArtistRef, MbRefOption},
},
core::{
collection::{album::AlbumDate, artist::ArtistId},
collection::artist::ArtistId,
interface::database::{self, MockIDatabase},
musichoard::{base::IMusicHoardBase, NoLibrary},
testmod::FULL_COLLECTION,
@ -604,6 +652,62 @@ mod tests {
assert!(music_hoard.collection[0].meta.info.properties.is_empty());
}
#[test]
fn album_new_delete() {
let album_id = AlbumId::new("an album");
let album_meta = AlbumMeta::new(album_id.clone());
let album_id_2 = AlbumId::new("another album");
let album_meta_2 = AlbumMeta::new(album_id_2);
let collection = FULL_COLLECTION.to_owned();
let artist_id = collection[0].meta.id.clone();
let mut with_album = collection.clone();
with_album[0].albums.push(Album::new(album_id));
with_album[0].albums.sort_unstable();
let mut database = MockIDatabase::new();
let mut seq = Sequence::new();
database
.expect_load()
.times(1)
.times(1)
.in_sequence(&mut seq)
.returning(|| Ok(FULL_COLLECTION.to_owned()));
database
.expect_save()
.times(1)
.in_sequence(&mut seq)
.with(predicate::eq(with_album.clone()))
.returning(|_| Ok(()));
database
.expect_save()
.times(1)
.in_sequence(&mut seq)
.with(predicate::eq(collection.clone()))
.returning(|_| Ok(()));
let mut music_hoard = MusicHoard::database(database).unwrap();
assert_eq!(music_hoard.collection, collection);
assert!(music_hoard
.add_album(&artist_id, album_meta.clone())
.is_ok());
assert_eq!(music_hoard.collection, with_album);
assert!(music_hoard
.add_album(&artist_id, album_meta.clone())
.is_ok());
assert_eq!(music_hoard.collection, with_album);
assert!(music_hoard
.remove_album(&artist_id, &album_meta_2.id)
.is_ok());
assert_eq!(music_hoard.collection, with_album);
assert!(music_hoard.remove_album(&artist_id, &album_meta.id).is_ok());
assert_eq!(music_hoard.collection, collection);
}
#[test]
fn set_clear_album_seq() {
let mut database = MockIDatabase::new();
@ -613,12 +717,7 @@ mod tests {
let album_id_2 = AlbumId::new("another album");
let mut database_result = vec![Artist::new(artist_id.clone())];
database_result[0].albums.push(Album::new(
album_id.clone(),
AlbumDate::default(),
None,
vec![],
));
database_result[0].albums.push(Album::new(album_id.clone()));
database
.expect_load()
@ -658,12 +757,7 @@ mod tests {
let album_id_2 = AlbumId::new("another album");
let mut database_result = vec![Artist::new(artist_id.clone())];
database_result[0].albums.push(Album::new(
album_id.clone(),
AlbumDate::default(),
None,
vec![],
));
database_result[0].albums.push(Album::new(album_id.clone()));
database
.expect_load()

View File

@ -109,7 +109,7 @@ impl<Database, Library: ILibrary> MusicHoard<Database, Library> {
{
Some(album) => album.tracks.push(track),
None => {
let mut album = Album::new(album_id, album_date, None, vec![]);
let mut album = Album::new(album_id).with_date(album_date);
album.tracks.push(track);
artist.albums.push(album);
}

View File

@ -5,7 +5,7 @@ use std::{
};
use musichoard::collection::{
album::{Album, AlbumId},
album::{Album, AlbumId, AlbumMeta},
artist::{Artist, ArtistId, ArtistMeta},
musicbrainz::{IMusicBrainzRef, MbArtistRef, MbRefOption, Mbid},
};
@ -13,48 +13,65 @@ use musichoard::collection::{
use crate::tui::{
app::{
machine::{match_state::MatchState, App, AppInner, AppMachine},
selection::KeySelection,
AppPublicState, AppState, Category, IAppEventFetch, IAppInteractFetch,
},
lib::interface::musicbrainz::daemon::{
Error as DaemonError, IMbJobSender, MbApiResult, MbParams, MbReturn, ResultSender,
EntityList, Error as DaemonError, IMbJobSender, MbApiResult, MbParams, MbReturn,
ResultSender,
},
};
pub type FetchReceiver = mpsc::Receiver<MbApiResult>;
pub type MbApiReceiver = mpsc::Receiver<MbApiResult>;
pub struct FetchState {
fetch_rx: FetchReceiver,
lookup_rx: Option<FetchReceiver>,
search_rx: Option<MbApiReceiver>,
lookup_rx: Option<MbApiReceiver>,
fetch_rx: Option<MbApiReceiver>,
}
struct SubmitJob {
fetch: FetchState,
requests: VecDeque<MbParams>,
}
macro_rules! try_recv_mb_api_receiver {
($rx:expr) => {
if let Some(rx) = &($rx) {
match rx.try_recv() {
x @ Ok(_) | x @ Err(TryRecvError::Empty) => return x,
Err(TryRecvError::Disconnected) => {
($rx).take();
}
}
}
};
}
impl FetchState {
pub fn new(fetch_rx: FetchReceiver) -> Self {
pub fn search(search_rx: MbApiReceiver) -> Self {
FetchState {
fetch_rx,
search_rx: Some(search_rx),
lookup_rx: None,
fetch_rx: None,
}
}
pub fn fetch(fetch_rx: MbApiReceiver) -> Self {
FetchState {
search_rx: None,
lookup_rx: None,
fetch_rx: Some(fetch_rx),
}
}
fn try_recv(&mut self) -> Result<MbApiResult, TryRecvError> {
if let Some(lookup_rx) = &self.lookup_rx {
match lookup_rx.try_recv() {
x @ Ok(_) | x @ Err(TryRecvError::Empty) => return x,
Err(TryRecvError::Disconnected) => {
self.lookup_rx.take();
}
}
}
self.fetch_rx.try_recv()
}
}
try_recv_mb_api_receiver!(self.lookup_rx);
try_recv_mb_api_receiver!(self.search_rx);
enum FetchError {
NothingToFetch,
SubmitError(DaemonError),
}
impl From<DaemonError> for FetchError {
fn from(value: DaemonError) -> Self {
FetchError::SubmitError(value)
match &self.fetch_rx {
Some(fetch_rx) => fetch_rx.try_recv(),
None => Err(TryRecvError::Disconnected),
}
}
}
@ -68,73 +85,135 @@ impl AppMachine<FetchState> {
}
fn app_fetch_new(inner: AppInner) -> App {
let (tx, rx) = mpsc::channel::<MbApiResult>();
let job = match Self::fetch_job(&inner, rx) {
Ok(job) => job,
Err(err) => return AppMachine::error_state(inner, err.to_string()).into(),
};
if job.requests.is_empty() {
return AppMachine::browse_state(inner).into();
}
match inner.musicbrainz.submit_background_job(tx, job.requests) {
Ok(()) => AppMachine::fetch_state(inner, job.fetch).into(),
Err(err) => AppMachine::error_state(inner, err.to_string()).into(),
}
}
fn fetch_job(inner: &AppInner, rx: MbApiReceiver) -> Result<SubmitJob, &'static str> {
let coll = inner.music_hoard.get_collection();
let artist = match inner.selection.state_artist(coll) {
Some(artist_state) => &coll[artist_state.index],
None => {
let err = "cannot fetch artist: no artist selected";
return AppMachine::error_state(inner, err).into();
}
None => return Err("cannot fetch: no artist selected"),
};
let (fetch_tx, fetch_rx) = mpsc::channel::<MbApiResult>();
let fetch = FetchState::new(fetch_rx);
let mb = &*inner.musicbrainz;
let result = match inner.selection.category() {
Category::Artist => Self::submit_search_artist_job(mb, fetch_tx, artist),
let requests = match inner.selection.category() {
Category::Artist => {
let fetch: FetchState;
let mut requests = Self::search_artist_job(artist);
if requests.is_empty() {
fetch = FetchState::fetch(rx);
requests = Self::browse_release_group_job(&artist.meta.info.musicbrainz);
} else {
fetch = FetchState::search(rx);
}
SubmitJob { fetch, requests }
}
_ => {
let arid = match artist.meta.info.musicbrainz {
MbRefOption::Some(ref mbref) => mbref,
_ => {
let err = "cannot fetch album: artist has no MBID";
return AppMachine::error_state(inner, err).into();
}
_ => return Err("cannot fetch album: artist has no MBID"),
};
let album = match inner.selection.state_album(coll) {
Some(album_state) => &artist.albums[album_state.index],
None => {
let err = "cannot fetch album: no album selected";
return AppMachine::error_state(inner, err).into();
}
None => return Err("cannot fetch album: no album selected"),
};
let artist_id = &artist.meta.id;
Self::submit_search_release_group_job(mb, fetch_tx, artist_id, arid, album)
SubmitJob {
fetch: FetchState::search(rx),
requests: Self::search_release_group_job(artist_id, arid, album),
}
}
};
match result {
Ok(()) => AppMachine::fetch_state(inner, fetch).into(),
Err(FetchError::NothingToFetch) => AppMachine::browse_state(inner).into(),
Err(FetchError::SubmitError(daemon_err)) => {
AppMachine::error_state(inner, daemon_err.to_string()).into()
}
}
Ok(requests)
}
pub fn app_fetch_next(inner: AppInner, mut fetch: FetchState) -> App {
match fetch.try_recv() {
pub fn app_fetch_next(mut inner: AppInner, mut fetch: FetchState) -> App {
loop {
let app: App = match fetch.try_recv() {
Ok(fetch_result) => match fetch_result {
Ok(retval) => Self::handle_mb_api_return(inner, fetch, retval),
Ok(MbReturn::Match(next_match)) => {
AppMachine::match_state(inner, MatchState::new(next_match, fetch)).into()
}
Ok(MbReturn::Fetch(list)) => {
match Self::apply_fetch_results(&mut inner, list) {
Ok(()) => continue,
Err(err) => AppMachine::error_state(inner, err.to_string()).into(),
}
}
Err(fetch_err) => {
AppMachine::error_state(inner, format!("fetch failed: {fetch_err}")).into()
}
},
Err(recv_err) => match recv_err {
TryRecvError::Empty => AppMachine::fetch_state(inner, fetch).into(),
TryRecvError::Disconnected => Self::app_fetch_new(inner),
},
Err(TryRecvError::Empty) => AppMachine::fetch_state(inner, fetch).into(),
Err(TryRecvError::Disconnected) => {
if fetch.fetch_rx.is_some() {
AppMachine::browse_state(inner).into()
} else {
Self::app_fetch_new(inner)
}
}
};
return app;
}
}
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()
fn apply_fetch_results(
inner: &mut AppInner,
list: EntityList,
) -> Result<(), musichoard::Error> {
match list {
EntityList::Album(fetch_albums) => Self::apply_album_results(inner, fetch_albums),
}
_ => unimplemented!(),
}
fn apply_album_results(
inner: &mut AppInner,
fetch_albums: Vec<AlbumMeta>,
) -> Result<(), musichoard::Error> {
let coll = inner.music_hoard.get_collection();
let artist_state = inner.selection.state_artist(coll).unwrap();
let artist = &coll[artist_state.index];
let selection = KeySelection::get(coll, &inner.selection);
let artist_id = &artist.meta.id.clone();
for new in Self::new_albums(fetch_albums, &artist.albums).into_iter() {
inner.music_hoard.add_album(artist_id, new)?;
}
let coll = inner.music_hoard.get_collection();
inner.selection.select_by_id(coll, selection);
Ok(())
}
fn new_albums(fetch_albums: Vec<AlbumMeta>, albums: &[Album]) -> Vec<AlbumMeta> {
let mut new_albums = vec![];
for alb in fetch_albums.into_iter() {
let existing = albums.iter().find(|old| Self::album_match(&old.meta, &alb));
if existing.is_none() {
new_albums.push(alb);
}
}
new_albums
}
fn album_match(old: &AlbumMeta, new: &AlbumMeta) -> bool {
old.info.musicbrainz.is_some() && (old.info.musicbrainz == new.info.musicbrainz)
}
pub fn app_lookup_artist(
@ -178,36 +257,26 @@ impl AppMachine<FetchState> {
Self::app_fetch_next(inner, fetch)
}
fn submit_search_artist_job(
musicbrainz: &dyn IMbJobSender,
result_sender: ResultSender,
artist: &Artist,
) -> Result<(), FetchError> {
let requests = match artist.meta.info.musicbrainz {
fn search_artist_job(artist: &Artist) -> VecDeque<MbParams> {
match artist.meta.info.musicbrainz {
MbRefOption::Some(ref arid) => {
Self::search_albums_requests(&artist.meta.id, arid, &artist.albums)
}
MbRefOption::CannotHaveMbid => VecDeque::new(),
MbRefOption::None => Self::search_artist_request(&artist.meta),
};
if requests.is_empty() {
return Err(FetchError::NothingToFetch);
}
Ok(musicbrainz.submit_background_job(result_sender, requests)?)
}
fn submit_search_release_group_job(
musicbrainz: &dyn IMbJobSender,
result_sender: ResultSender,
fn search_release_group_job(
artist_id: &ArtistId,
artist_mbid: &MbArtistRef,
album: &Album,
) -> Result<(), FetchError> {
if !matches!(album.meta.info.musicbrainz, MbRefOption::None) {
return Err(FetchError::NothingToFetch);
) -> VecDeque<MbParams> {
Self::search_albums_requests(artist_id, artist_mbid, slice::from_ref(album))
}
let requests = Self::search_albums_requests(artist_id, artist_mbid, slice::from_ref(album));
Ok(musicbrainz.submit_background_job(result_sender, requests)?)
fn search_artist_request(meta: &ArtistMeta) -> VecDeque<MbParams> {
VecDeque::from([MbParams::search_artist(meta.clone())])
}
fn search_albums_requests(
@ -218,15 +287,22 @@ impl AppMachine<FetchState> {
let arid = arid.mbid();
albums
.iter()
.filter(|album| matches!(album.meta.info.musicbrainz, MbRefOption::None))
.filter(|album| album.meta.info.musicbrainz.is_none())
.map(|album| {
MbParams::search_release_group(artist.clone(), arid.clone(), album.meta.clone())
})
.collect()
}
fn search_artist_request(meta: &ArtistMeta) -> VecDeque<MbParams> {
VecDeque::from([MbParams::search_artist(meta.clone())])
fn browse_release_group_job(mbopt: &MbRefOption<MbArtistRef>) -> VecDeque<MbParams> {
match mbopt {
MbRefOption::Some(mbref) => Self::browse_release_group_request(mbref),
_ => VecDeque::new(),
}
}
fn browse_release_group_request(mbref: &MbArtistRef) -> VecDeque<MbParams> {
VecDeque::from([MbParams::browse_release_group(mbref.mbid().clone())])
}
fn submit_lookup_artist_job(
@ -294,7 +370,7 @@ mod tests {
use crate::tui::{
app::{
machine::tests::{inner, music_hoard},
machine::tests::{inner, inner_with_mb, music_hoard},
Delta, EntityMatches, IApp, IAppAccess, IAppInteractBrowse, MatchOption,
},
lib::interface::musicbrainz::{self, api::Entity, daemon::MockIMbJobSender},
@ -312,7 +388,7 @@ mod tests {
let (fetch_tx, fetch_rx) = mpsc::channel();
let (lookup_tx, lookup_rx) = mpsc::channel();
let mut fetch = FetchState::new(fetch_rx);
let mut fetch = FetchState::search(fetch_rx);
fetch.lookup_rx.replace(lookup_rx);
let artist = COLLECTION[3].meta.clone();
@ -494,7 +570,7 @@ mod tests {
let inner = AppInner::new(music_hoard, mb_job_sender);
let (_fetch_tx, fetch_rx) = mpsc::channel();
let fetch = FetchState::new(fetch_rx);
let fetch = FetchState::search(fetch_rx);
AppMachine::app_lookup_album(inner, fetch, &artist_id, &album_id, mbid());
}
@ -548,7 +624,7 @@ mod tests {
let inner = AppInner::new(music_hoard, mb_job_sender);
let (_fetch_tx, fetch_rx) = mpsc::channel();
let fetch = FetchState::new(fetch_rx);
let fetch = FetchState::search(fetch_rx);
AppMachine::app_lookup_artist(inner, fetch, &artist, mbid());
}
@ -597,14 +673,14 @@ mod tests {
let inner = AppInner::new(music_hoard, mb_job_sender);
let (_fetch_tx, fetch_rx) = mpsc::channel();
let fetch = FetchState::new(fetch_rx);
let fetch = FetchState::search(fetch_rx);
let app = AppMachine::app_lookup_artist(inner, fetch, &artist, mbid());
assert!(matches!(app, AppState::Error(_)));
}
#[test]
fn recv_ok_fetch_ok() {
fn recv_ok_match_ok() {
let (tx, rx) = mpsc::channel::<MbApiResult>();
let artist = COLLECTION[3].meta.clone();
@ -615,7 +691,7 @@ mod tests {
tx.send(fetch_result).unwrap();
let inner = inner(music_hoard(COLLECTION.clone()));
let fetch = FetchState::new(rx);
let fetch = FetchState::search(rx);
let mut app = AppMachine::app_fetch_next(inner, fetch);
assert!(matches!(app, AppState::Match(_)));
@ -631,35 +707,102 @@ mod tests {
}
#[test]
fn recv_ok_fetch_err() {
fn recv_ok_search_err() {
let (tx, rx) = mpsc::channel::<MbApiResult>();
let fetch_result = Err(musicbrainz::api::Error::RateLimit);
tx.send(fetch_result).unwrap();
let inner = inner(music_hoard(COLLECTION.clone()));
let fetch = FetchState::new(rx);
let fetch = FetchState::search(rx);
let app = AppMachine::app_fetch_next(inner, fetch);
assert!(matches!(app, AppState::Error(_)));
}
#[test]
fn recv_ok_fetch_ok() {
let collection = COLLECTION.clone();
let (tx, rx) = mpsc::channel::<MbApiResult>();
let fetch = FetchState::fetch(rx);
let artist_id = collection[0].meta.id.clone();
let old_album = collection[0].albums[0].meta.clone();
let new_album = AlbumMeta::new(AlbumId::new("some new album"));
let release_group_fetch = EntityList::Album(vec![old_album.clone(), new_album.clone()]);
let fetch_result = Ok(MbReturn::Fetch(release_group_fetch));
tx.send(fetch_result).unwrap();
drop(tx);
let mut music_hoard = music_hoard(collection);
music_hoard
.expect_add_album()
.with(predicate::eq(artist_id), predicate::eq(new_album))
.times(1)
.return_once(|_, _| Ok(()));
let app = AppMachine::app_fetch_next(inner(music_hoard), fetch);
assert!(matches!(app, AppState::Browse(_)));
}
#[test]
fn recv_ok_fetch_ok_add_album_err() {
let collection = COLLECTION.clone();
let (tx, rx) = mpsc::channel::<MbApiResult>();
let fetch = FetchState::fetch(rx);
let artist_id = collection[0].meta.id.clone();
let old_album = collection[0].albums[0].meta.clone();
let new_album = AlbumMeta::new(AlbumId::new("some new album"));
let release_group_fetch = EntityList::Album(vec![old_album.clone(), new_album.clone()]);
let fetch_result = Ok(MbReturn::Fetch(release_group_fetch));
tx.send(fetch_result).unwrap();
drop(tx);
let mut music_hoard = music_hoard(collection);
music_hoard
.expect_add_album()
.with(predicate::eq(artist_id), predicate::eq(new_album))
.times(1)
.return_once(|_, _| Err(musichoard::Error::CollectionError(String::from("get rekt"))));
let app = AppMachine::app_fetch_next(inner(music_hoard), fetch);
assert!(matches!(app, AppState::Error(_)));
}
#[test]
fn recv_err_empty() {
let (_tx, rx) = mpsc::channel::<MbApiResult>();
let inner = inner(music_hoard(COLLECTION.clone()));
let fetch = FetchState::new(rx);
let fetch = FetchState::search(rx);
let app = AppMachine::app_fetch_next(inner, fetch);
assert!(matches!(app, AppState::Fetch(_)));
}
fn browse_release_group_expectation(artist: &Artist) -> MockIMbJobSender {
let requests = AppMachine::browse_release_group_job(&artist.meta.info.musicbrainz);
let mut mb_job_sender = MockIMbJobSender::new();
mb_job_sender
.expect_submit_background_job()
.with(predicate::always(), predicate::eq(requests))
.times(1)
.return_once(|_, _| Ok(()));
mb_job_sender
}
#[test]
fn recv_err_empty_first() {
let mut collection = COLLECTION.clone();
collection[0].albums.clear();
let app = AppMachine::app_fetch_first(inner(music_hoard(collection)));
assert!(matches!(app, AppState::Browse(_)));
let mb_job_sender = browse_release_group_expectation(&collection[0]);
let inner = inner_with_mb(music_hoard(collection), mb_job_sender);
let app = AppMachine::app_fetch_first(inner);
assert!(matches!(app, AppState::Fetch(_)));
}
#[test]
@ -667,11 +810,25 @@ mod tests {
let mut collection = COLLECTION.clone();
collection[0].albums.clear();
let (_, rx) = mpsc::channel::<MbApiResult>();
let fetch = FetchState::new(rx);
let (_tx, rx) = mpsc::channel::<MbApiResult>();
let fetch = FetchState::search(rx);
let app = AppMachine::app_fetch_next(inner(music_hoard(collection)), fetch);
assert!(matches!(app, AppState::Browse(_)));
assert!(matches!(app, AppState::Fetch(_)));
}
#[test]
fn recv_err_disconnected_search_next() {
let mut collection = COLLECTION.clone();
collection[0].albums.clear();
let (_, rx) = mpsc::channel::<MbApiResult>();
let fetch = FetchState::search(rx);
let mb_job_sender = browse_release_group_expectation(&collection[0]);
let inner = inner_with_mb(music_hoard(collection), mb_job_sender);
let app = AppMachine::app_fetch_next(inner, fetch);
assert!(matches!(app, AppState::Fetch(_)));
}
#[test]
@ -679,7 +836,7 @@ mod tests {
let (tx, rx) = mpsc::channel::<MbApiResult>();
let inner = inner(music_hoard(COLLECTION.clone()));
let fetch = FetchState::new(rx);
let fetch = FetchState::search(rx);
let app = AppMachine::app_fetch_next(inner, fetch);
assert!(matches!(app, AppState::Fetch(_)));
@ -696,7 +853,7 @@ mod tests {
fn abort() {
let (_, rx) = mpsc::channel::<MbApiResult>();
let fetch = FetchState::new(rx);
let fetch = FetchState::search(rx);
let app = AppMachine::fetch_state(inner(music_hoard(COLLECTION.clone())), fetch);
let app = app.abort();

View File

@ -319,15 +319,13 @@ mod tests {
}
fn album_meta(id: AlbumId) -> AlbumMeta {
AlbumMeta::new(
id,
AlbumDate::new(Some(1990), Some(5), None),
AlbumInfo::new(
AlbumMeta::new(id)
.with_date(AlbumDate::new(Some(1990), Some(5), None))
.with_info(AlbumInfo::new(
MbRefOption::Some(mbid().into()),
Some(AlbumPrimaryType::Album),
vec![AlbumSecondaryType::Live, AlbumSecondaryType::Compilation],
),
)
))
}
fn album_match() -> EntityMatches {
@ -357,7 +355,7 @@ mod tests {
fn fetch_state() -> FetchState {
let (_, rx) = mpsc::channel();
FetchState::new(rx)
FetchState::search(rx)
}
fn match_state(match_state_info: EntityMatches) -> MatchState {
@ -388,7 +386,7 @@ mod tests {
fn match_state_flow(mut matches_info: EntityMatches, len: usize) {
// tx must exist for rx to return Empty rather than Disconnected.
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::search(rx));
let mut music_hoard = music_hoard(vec![]);
let artist_id = ArtistId::new("Artist");
@ -487,7 +485,7 @@ mod tests {
let matches_info = artist_match();
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::search(rx));
let mut music_hoard = music_hoard(vec![]);
match matches_info {
@ -511,7 +509,7 @@ mod tests {
let matches_info = album_match();
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::search(rx));
let mut music_hoard = music_hoard(vec![]);
match matches_info {
@ -535,7 +533,7 @@ mod tests {
let matches_info = artist_match();
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::search(rx));
let mut music_hoard = music_hoard(vec![]);
match matches_info {
@ -625,7 +623,7 @@ mod tests {
fn select_manual_input_album() {
let mut mb_job_sender = MockIMbJobSender::new();
let artist_id = ArtistId::new("Artist");
let album = AlbumMeta::new("Album", 1990, AlbumInfo::default());
let album = AlbumMeta::new("Album").with_date(1990);
let requests = VecDeque::from([MbParams::lookup_release_group(
artist_id.clone(),
album.id.clone(),

View File

@ -493,7 +493,7 @@ mod tests {
let (_, rx) = mpsc::channel();
let inner = app.unwrap_browse().inner;
let state = FetchState::new(rx);
let state = FetchState::search(rx);
app = AppMachine::new(inner, state).into();
let state = app.state();
@ -518,7 +518,7 @@ mod tests {
assert!(app.is_running());
let (_, rx) = mpsc::channel();
let fetch = FetchState::new(rx);
let fetch = FetchState::search(rx);
let artist = ArtistMeta::new(ArtistId::new("Artist"));
let info = EntityMatches::artist_lookup(artist.clone(), Entity::new(artist.clone()));
app =

View File

@ -59,7 +59,6 @@ pub trait IMbJobSender {
pub enum MbParams {
Lookup(LookupParams),
Search(SearchParams),
#[allow(dead_code)] // TODO: remove with completion of #160
Browse(BrowseParams),
}
@ -102,7 +101,6 @@ pub struct SearchReleaseGroupParams {
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum BrowseParams {
#[allow(dead_code)] // TODO: remove with completion of #160
ReleaseGroup(BrowseReleaseGroupParams),
}
@ -136,7 +134,6 @@ impl MbParams {
}))
}
#[allow(dead_code)] // TODO: to be removed by completion of #160
pub fn browse_release_group(artist: Mbid) -> Self {
MbParams::Browse(BrowseParams::ReleaseGroup(BrowseReleaseGroupParams {
artist,

View File

@ -3,7 +3,7 @@ pub mod interface;
use musichoard::{
collection::{
album::{AlbumId, AlbumInfo},
album::{AlbumId, AlbumInfo, AlbumMeta},
artist::{ArtistId, ArtistInfo},
Collection,
},
@ -20,6 +20,12 @@ pub trait IMusicHoard {
fn reload_database(&mut self) -> Result<(), musichoard::Error>;
fn get_collection(&self) -> &Collection;
fn add_album(
&mut self,
artist_id: &ArtistId,
album_meta: AlbumMeta,
) -> Result<(), musichoard::Error>;
fn merge_artist_info(
&mut self,
id: &ArtistId,
@ -47,6 +53,14 @@ impl<Database: IDatabase, Library: ILibrary> IMusicHoard for MusicHoard<Database
<Self as IMusicHoardBase>::get_collection(self)
}
fn add_album(
&mut self,
artist_id: &ArtistId,
album_meta: AlbumMeta,
) -> Result<(), musichoard::Error> {
<Self as IMusicHoardDatabase>::add_album(self, artist_id, album_meta)
}
fn merge_artist_info(
&mut self,
id: &ArtistId,

View File

@ -289,9 +289,7 @@ mod tests {
#[test]
fn empty_album() {
let mut artists: Vec<Artist> = vec![Artist::new(ArtistId::new("An artist"))];
artists[0]
.albums
.push(Album::new("An album", AlbumDate::default(), None, vec![]));
artists[0].albums.push(Album::new("An album"));
let mut selection = Selection::new(&artists);
draw_test_suite(&artists, &mut selection);
@ -361,15 +359,13 @@ mod tests {
}
fn album_meta(id: AlbumId) -> AlbumMeta {
AlbumMeta::new(
id,
AlbumDate::new(Some(1990), Some(5), None),
AlbumInfo::new(
AlbumMeta::new(id)
.with_date(AlbumDate::new(Some(1990), Some(5), None))
.with_info(AlbumInfo::new(
MbRefOption::None,
Some(AlbumPrimaryType::Album),
vec![AlbumSecondaryType::Live, AlbumSecondaryType::Compilation],
),
)
))
}
fn album_matches() -> EntityMatches {