Add option for manual input during fetch (#219)
Closes #188 Reviewed-on: #219
This commit is contained in:
parent
d6f4b2b6b7
commit
90db5faae7
@ -34,9 +34,9 @@ fn main() {
|
|||||||
let mut request = LookupArtistRequest::new(&mbid);
|
let mut request = LookupArtistRequest::new(&mbid);
|
||||||
request.include_release_groups();
|
request.include_release_groups();
|
||||||
|
|
||||||
let albums = client
|
let response = client
|
||||||
.lookup_artist(request)
|
.lookup_artist(request)
|
||||||
.expect("failed to make API call");
|
.expect("failed to make API call");
|
||||||
|
|
||||||
println!("{albums:#?}");
|
println!("{response:#?}");
|
||||||
}
|
}
|
||||||
|
@ -205,11 +205,11 @@ impl AlbumMeta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_musicbrainz_ref(&mut self, mbref: MbAlbumRef) {
|
pub fn set_musicbrainz_ref(&mut self, mbref: MbAlbumRef) {
|
||||||
_ = self.musicbrainz.insert(mbref);
|
self.musicbrainz.replace(mbref);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear_musicbrainz_ref(&mut self) {
|
pub fn clear_musicbrainz_ref(&mut self) {
|
||||||
_ = self.musicbrainz.take();
|
self.musicbrainz.take();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -371,7 +371,7 @@ mod tests {
|
|||||||
album
|
album
|
||||||
.meta
|
.meta
|
||||||
.set_musicbrainz_ref(MbAlbumRef::from_url_str(MUSICBRAINZ).unwrap());
|
.set_musicbrainz_ref(MbAlbumRef::from_url_str(MUSICBRAINZ).unwrap());
|
||||||
_ = expected.insert(MbAlbumRef::from_url_str(MUSICBRAINZ).unwrap());
|
expected.replace(MbAlbumRef::from_url_str(MUSICBRAINZ).unwrap());
|
||||||
assert_eq!(album.meta.musicbrainz, expected);
|
assert_eq!(album.meta.musicbrainz, expected);
|
||||||
|
|
||||||
album
|
album
|
||||||
@ -382,12 +382,12 @@ mod tests {
|
|||||||
album
|
album
|
||||||
.meta
|
.meta
|
||||||
.set_musicbrainz_ref(MbAlbumRef::from_url_str(MUSICBRAINZ_2).unwrap());
|
.set_musicbrainz_ref(MbAlbumRef::from_url_str(MUSICBRAINZ_2).unwrap());
|
||||||
_ = expected.insert(MbAlbumRef::from_url_str(MUSICBRAINZ_2).unwrap());
|
expected.replace(MbAlbumRef::from_url_str(MUSICBRAINZ_2).unwrap());
|
||||||
assert_eq!(album.meta.musicbrainz, expected);
|
assert_eq!(album.meta.musicbrainz, expected);
|
||||||
|
|
||||||
// Clearing URLs.
|
// Clearing URLs.
|
||||||
album.meta.clear_musicbrainz_ref();
|
album.meta.clear_musicbrainz_ref();
|
||||||
_ = expected.take();
|
expected.take();
|
||||||
assert_eq!(album.meta.musicbrainz, expected);
|
assert_eq!(album.meta.musicbrainz, expected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -89,15 +89,15 @@ impl ArtistMeta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear_sort_key(&mut self) {
|
pub fn clear_sort_key(&mut self) {
|
||||||
_ = self.sort.take();
|
self.sort.take();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_musicbrainz_ref(&mut self, mbref: MbArtistRef) {
|
pub fn set_musicbrainz_ref(&mut self, mbref: MbArtistRef) {
|
||||||
_ = self.musicbrainz.insert(mbref);
|
self.musicbrainz.replace(mbref);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear_musicbrainz_ref(&mut self) {
|
pub fn clear_musicbrainz_ref(&mut self) {
|
||||||
_ = self.musicbrainz.take();
|
self.musicbrainz.take();
|
||||||
}
|
}
|
||||||
|
|
||||||
// In the functions below, it would be better to use `contains` instead of `iter().any`, but for
|
// In the functions below, it would be better to use `contains` instead of `iter().any`, but for
|
||||||
@ -262,7 +262,7 @@ mod tests {
|
|||||||
artist
|
artist
|
||||||
.meta
|
.meta
|
||||||
.set_musicbrainz_ref(MbArtistRef::from_url_str(MUSICBRAINZ).unwrap());
|
.set_musicbrainz_ref(MbArtistRef::from_url_str(MUSICBRAINZ).unwrap());
|
||||||
_ = expected.insert(MbArtistRef::from_url_str(MUSICBRAINZ).unwrap());
|
expected.replace(MbArtistRef::from_url_str(MUSICBRAINZ).unwrap());
|
||||||
assert_eq!(artist.meta.musicbrainz, expected);
|
assert_eq!(artist.meta.musicbrainz, expected);
|
||||||
|
|
||||||
artist
|
artist
|
||||||
@ -273,12 +273,12 @@ mod tests {
|
|||||||
artist
|
artist
|
||||||
.meta
|
.meta
|
||||||
.set_musicbrainz_ref(MbArtistRef::from_url_str(MUSICBRAINZ_2).unwrap());
|
.set_musicbrainz_ref(MbArtistRef::from_url_str(MUSICBRAINZ_2).unwrap());
|
||||||
_ = expected.insert(MbArtistRef::from_url_str(MUSICBRAINZ_2).unwrap());
|
expected.replace(MbArtistRef::from_url_str(MUSICBRAINZ_2).unwrap());
|
||||||
assert_eq!(artist.meta.musicbrainz, expected);
|
assert_eq!(artist.meta.musicbrainz, expected);
|
||||||
|
|
||||||
// Clearing URLs.
|
// Clearing URLs.
|
||||||
artist.meta.clear_musicbrainz_ref();
|
artist.meta.clear_musicbrainz_ref();
|
||||||
_ = expected.take();
|
expected.take();
|
||||||
assert_eq!(artist.meta.musicbrainz, expected);
|
assert_eq!(artist.meta.musicbrainz, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -454,7 +454,7 @@ mod tests {
|
|||||||
assert!(music_hoard
|
assert!(music_hoard
|
||||||
.set_artist_musicbrainz(&artist_id, MUSICBRAINZ)
|
.set_artist_musicbrainz(&artist_id, MUSICBRAINZ)
|
||||||
.is_ok());
|
.is_ok());
|
||||||
_ = expected.insert(MbArtistRef::from_url_str(MUSICBRAINZ).unwrap());
|
expected.replace(MbArtistRef::from_url_str(MUSICBRAINZ).unwrap());
|
||||||
assert_eq!(music_hoard.collection[0].meta.musicbrainz, expected);
|
assert_eq!(music_hoard.collection[0].meta.musicbrainz, expected);
|
||||||
|
|
||||||
// Clearing URLs on an artist that does not exist is an error.
|
// Clearing URLs on an artist that does not exist is an error.
|
||||||
@ -463,7 +463,7 @@ mod tests {
|
|||||||
|
|
||||||
// Clearing URLs.
|
// Clearing URLs.
|
||||||
assert!(music_hoard.clear_artist_musicbrainz(&artist_id).is_ok());
|
assert!(music_hoard.clear_artist_musicbrainz(&artist_id).is_ok());
|
||||||
_ = expected.take();
|
expected.take();
|
||||||
assert_eq!(music_hoard.collection[0].meta.musicbrainz, expected);
|
assert_eq!(music_hoard.collection[0].meta.musicbrainz, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
159
src/external/musicbrainz/api/lookup.rs
vendored
159
src/external/musicbrainz/api/lookup.rs
vendored
@ -2,19 +2,15 @@ use serde::Deserialize;
|
|||||||
use url::form_urlencoded;
|
use url::form_urlencoded;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
collection::{
|
collection::musicbrainz::Mbid,
|
||||||
album::{AlbumDate, AlbumPrimaryType, AlbumSecondaryType},
|
|
||||||
musicbrainz::Mbid,
|
|
||||||
},
|
|
||||||
external::musicbrainz::{
|
external::musicbrainz::{
|
||||||
api::{
|
api::{Error, MusicBrainzClient, MB_BASE_URL},
|
||||||
Error, MusicBrainzClient, SerdeAlbumDate, SerdeAlbumPrimaryType,
|
|
||||||
SerdeAlbumSecondaryType, SerdeMbid, MB_BASE_URL,
|
|
||||||
},
|
|
||||||
IMusicBrainzHttp,
|
IMusicBrainzHttp,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::{MbArtistMeta, MbReleaseGroupMeta, SerdeMbArtistMeta, SerdeMbReleaseGroupMeta};
|
||||||
|
|
||||||
impl<Http: IMusicBrainzHttp> MusicBrainzClient<Http> {
|
impl<Http: IMusicBrainzHttp> MusicBrainzClient<Http> {
|
||||||
pub fn lookup_artist(
|
pub fn lookup_artist(
|
||||||
&mut self,
|
&mut self,
|
||||||
@ -36,6 +32,19 @@ impl<Http: IMusicBrainzHttp> MusicBrainzClient<Http> {
|
|||||||
let response: DeserializeLookupArtistResponse = self.http.get(&url)?;
|
let response: DeserializeLookupArtistResponse = self.http.get(&url)?;
|
||||||
Ok(response.into())
|
Ok(response.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn lookup_release_group(
|
||||||
|
&mut self,
|
||||||
|
request: LookupReleaseGroupRequest,
|
||||||
|
) -> Result<LookupReleaseGroupResponse, Error> {
|
||||||
|
let url = format!(
|
||||||
|
"{MB_BASE_URL}/release-group/{mbid}",
|
||||||
|
mbid = request.mbid.uuid().as_hyphenated()
|
||||||
|
);
|
||||||
|
|
||||||
|
let response: DeserializeLookupReleaseGroupResponse = self.http.get(&url)?;
|
||||||
|
Ok(response.into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct LookupArtistRequest<'a> {
|
pub struct LookupArtistRequest<'a> {
|
||||||
@ -59,50 +68,56 @@ impl<'a> LookupArtistRequest<'a> {
|
|||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub struct LookupArtistResponse {
|
pub struct LookupArtistResponse {
|
||||||
pub release_groups: Vec<LookupArtistResponseReleaseGroup>,
|
pub meta: MbArtistMeta,
|
||||||
|
pub release_groups: Vec<MbReleaseGroupMeta>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(rename_all(deserialize = "kebab-case"))]
|
#[serde(rename_all(deserialize = "kebab-case"))]
|
||||||
struct DeserializeLookupArtistResponse {
|
struct DeserializeLookupArtistResponse {
|
||||||
release_groups: Vec<DeserializeLookupArtistResponseReleaseGroup>,
|
#[serde(flatten)]
|
||||||
|
meta: SerdeMbArtistMeta,
|
||||||
|
release_groups: Option<Vec<SerdeMbReleaseGroupMeta>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<DeserializeLookupArtistResponse> for LookupArtistResponse {
|
impl From<DeserializeLookupArtistResponse> for LookupArtistResponse {
|
||||||
fn from(value: DeserializeLookupArtistResponse) -> Self {
|
fn from(value: DeserializeLookupArtistResponse) -> Self {
|
||||||
LookupArtistResponse {
|
LookupArtistResponse {
|
||||||
release_groups: value.release_groups.into_iter().map(Into::into).collect(),
|
meta: value.meta.into(),
|
||||||
|
release_groups: value
|
||||||
|
.release_groups
|
||||||
|
.map(|rgs| rgs.into_iter().map(Into::into).collect())
|
||||||
|
.unwrap_or_default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct LookupReleaseGroupRequest<'a> {
|
||||||
|
mbid: &'a Mbid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> LookupReleaseGroupRequest<'a> {
|
||||||
|
pub fn new(mbid: &'a Mbid) -> Self {
|
||||||
|
LookupReleaseGroupRequest { mbid }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub struct LookupArtistResponseReleaseGroup {
|
pub struct LookupReleaseGroupResponse {
|
||||||
pub id: Mbid,
|
pub meta: MbReleaseGroupMeta,
|
||||||
pub title: String,
|
|
||||||
pub first_release_date: AlbumDate,
|
|
||||||
pub primary_type: AlbumPrimaryType,
|
|
||||||
pub secondary_types: Vec<AlbumSecondaryType>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(rename_all(deserialize = "kebab-case"))]
|
#[serde(rename_all(deserialize = "kebab-case"))]
|
||||||
struct DeserializeLookupArtistResponseReleaseGroup {
|
struct DeserializeLookupReleaseGroupResponse {
|
||||||
id: SerdeMbid,
|
#[serde(flatten)]
|
||||||
title: String,
|
meta: SerdeMbReleaseGroupMeta,
|
||||||
first_release_date: SerdeAlbumDate,
|
|
||||||
primary_type: SerdeAlbumPrimaryType,
|
|
||||||
secondary_types: Vec<SerdeAlbumSecondaryType>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<DeserializeLookupArtistResponseReleaseGroup> for LookupArtistResponseReleaseGroup {
|
impl From<DeserializeLookupReleaseGroupResponse> for LookupReleaseGroupResponse {
|
||||||
fn from(value: DeserializeLookupArtistResponseReleaseGroup) -> Self {
|
fn from(value: DeserializeLookupReleaseGroupResponse) -> Self {
|
||||||
LookupArtistResponseReleaseGroup {
|
LookupReleaseGroupResponse {
|
||||||
id: value.id.into(),
|
meta: value.meta.into(),
|
||||||
title: value.title,
|
|
||||||
first_release_date: value.first_release_date.into(),
|
|
||||||
primary_type: value.primary_type.into(),
|
|
||||||
secondary_types: value.secondary_types.into_iter().map(Into::into).collect(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -111,42 +126,45 @@ impl From<DeserializeLookupArtistResponseReleaseGroup> for LookupArtistResponseR
|
|||||||
mod tests {
|
mod tests {
|
||||||
use mockall::predicate;
|
use mockall::predicate;
|
||||||
|
|
||||||
use crate::external::musicbrainz::MockIMusicBrainzHttp;
|
use crate::{
|
||||||
|
collection::album::{AlbumPrimaryType, AlbumSecondaryType},
|
||||||
|
external::musicbrainz::{
|
||||||
|
api::{SerdeAlbumDate, SerdeAlbumPrimaryType, SerdeAlbumSecondaryType, SerdeMbid},
|
||||||
|
MockIMusicBrainzHttp,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn lookup_artist() {
|
fn lookup_artist() {
|
||||||
|
let mbid = "00000000-0000-0000-0000-000000000000";
|
||||||
let mut http = MockIMusicBrainzHttp::new();
|
let mut http = MockIMusicBrainzHttp::new();
|
||||||
let url = format!(
|
let url = format!("https://musicbrainz.org/ws/2/artist/{mbid}?inc=release-groups",);
|
||||||
"https://musicbrainz.org/ws/2/artist/{mbid}?inc=release-groups",
|
|
||||||
mbid = "00000000-0000-0000-0000-000000000000",
|
|
||||||
);
|
|
||||||
|
|
||||||
let de_release_group = DeserializeLookupArtistResponseReleaseGroup {
|
let de_meta = SerdeMbArtistMeta {
|
||||||
|
id: SerdeMbid(mbid.try_into().unwrap()),
|
||||||
|
name: String::from("the artist"),
|
||||||
|
sort_name: String::from("artist, the"),
|
||||||
|
disambiguation: Some(String::from("disambig")),
|
||||||
|
};
|
||||||
|
let de_release_group = SerdeMbReleaseGroupMeta {
|
||||||
id: SerdeMbid("11111111-1111-1111-1111-111111111111".try_into().unwrap()),
|
id: SerdeMbid("11111111-1111-1111-1111-111111111111".try_into().unwrap()),
|
||||||
title: String::from("an album"),
|
title: String::from("an album"),
|
||||||
first_release_date: SerdeAlbumDate((1986, 4).into()),
|
first_release_date: SerdeAlbumDate((1986, 4).into()),
|
||||||
primary_type: SerdeAlbumPrimaryType(AlbumPrimaryType::Album),
|
primary_type: SerdeAlbumPrimaryType(AlbumPrimaryType::Album),
|
||||||
secondary_types: vec![SerdeAlbumSecondaryType(AlbumSecondaryType::Compilation)],
|
secondary_types: Some(vec![SerdeAlbumSecondaryType(
|
||||||
|
AlbumSecondaryType::Compilation,
|
||||||
|
)]),
|
||||||
};
|
};
|
||||||
let de_response = DeserializeLookupArtistResponse {
|
let de_response = DeserializeLookupArtistResponse {
|
||||||
release_groups: vec![de_release_group.clone()],
|
meta: de_meta.clone(),
|
||||||
|
release_groups: Some(vec![de_release_group.clone()]),
|
||||||
};
|
};
|
||||||
|
|
||||||
let release_group = LookupArtistResponseReleaseGroup {
|
|
||||||
id: de_release_group.id.0,
|
|
||||||
title: de_release_group.title,
|
|
||||||
first_release_date: de_release_group.first_release_date.0,
|
|
||||||
primary_type: de_release_group.primary_type.0,
|
|
||||||
secondary_types: de_release_group
|
|
||||||
.secondary_types
|
|
||||||
.into_iter()
|
|
||||||
.map(|st| st.0)
|
|
||||||
.collect(),
|
|
||||||
};
|
|
||||||
let response = LookupArtistResponse {
|
let response = LookupArtistResponse {
|
||||||
release_groups: vec![release_group],
|
meta: de_meta.into(),
|
||||||
|
release_groups: vec![de_release_group.into()],
|
||||||
};
|
};
|
||||||
|
|
||||||
http.expect_get()
|
http.expect_get()
|
||||||
@ -163,4 +181,41 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(result, response);
|
assert_eq!(result, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lookup_release_group() {
|
||||||
|
let mbid = "00000000-0000-0000-0000-000000000000";
|
||||||
|
let mut http = MockIMusicBrainzHttp::new();
|
||||||
|
let url = format!("https://musicbrainz.org/ws/2/release-group/{mbid}",);
|
||||||
|
|
||||||
|
let de_meta = SerdeMbReleaseGroupMeta {
|
||||||
|
id: SerdeMbid("11111111-1111-1111-1111-111111111111".try_into().unwrap()),
|
||||||
|
title: String::from("an album"),
|
||||||
|
first_release_date: SerdeAlbumDate((1986, 4).into()),
|
||||||
|
primary_type: SerdeAlbumPrimaryType(AlbumPrimaryType::Album),
|
||||||
|
secondary_types: Some(vec![SerdeAlbumSecondaryType(
|
||||||
|
AlbumSecondaryType::Compilation,
|
||||||
|
)]),
|
||||||
|
};
|
||||||
|
let de_response = DeserializeLookupReleaseGroupResponse {
|
||||||
|
meta: de_meta.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = LookupReleaseGroupResponse {
|
||||||
|
meta: de_meta.into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
http.expect_get()
|
||||||
|
.times(1)
|
||||||
|
.with(predicate::eq(url))
|
||||||
|
.return_once(|_| Ok(de_response));
|
||||||
|
|
||||||
|
let mut client = MusicBrainzClient::new(http);
|
||||||
|
|
||||||
|
let mbid: Mbid = "00000000-0000-0000-0000-000000000000".try_into().unwrap();
|
||||||
|
let request = LookupReleaseGroupRequest::new(&mbid);
|
||||||
|
let result = client.lookup_release_group(request).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result, response);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
64
src/external/musicbrainz/api/mod.rs
vendored
64
src/external/musicbrainz/api/mod.rs
vendored
@ -4,7 +4,8 @@ use serde::{de::Visitor, Deserialize, Deserializer};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
collection::{
|
collection::{
|
||||||
album::{AlbumDate, AlbumPrimaryType, AlbumSecondaryType},
|
album::{AlbumDate, AlbumId, AlbumPrimaryType, AlbumSecondaryType},
|
||||||
|
artist::ArtistId,
|
||||||
musicbrainz::Mbid,
|
musicbrainz::Mbid,
|
||||||
Error as CollectionError,
|
Error as CollectionError,
|
||||||
},
|
},
|
||||||
@ -58,6 +59,67 @@ impl<Http> MusicBrainzClient<Http> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct MbArtistMeta {
|
||||||
|
pub id: Mbid,
|
||||||
|
pub name: ArtistId,
|
||||||
|
pub sort_name: ArtistId,
|
||||||
|
pub disambiguation: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
#[serde(rename_all(deserialize = "kebab-case"))]
|
||||||
|
struct SerdeMbArtistMeta {
|
||||||
|
id: SerdeMbid,
|
||||||
|
name: String,
|
||||||
|
sort_name: String,
|
||||||
|
disambiguation: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SerdeMbArtistMeta> for MbArtistMeta {
|
||||||
|
fn from(value: SerdeMbArtistMeta) -> Self {
|
||||||
|
MbArtistMeta {
|
||||||
|
id: value.id.into(),
|
||||||
|
name: value.name.into(),
|
||||||
|
sort_name: value.sort_name.into(),
|
||||||
|
disambiguation: value.disambiguation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct MbReleaseGroupMeta {
|
||||||
|
pub id: Mbid,
|
||||||
|
pub title: AlbumId,
|
||||||
|
pub first_release_date: AlbumDate,
|
||||||
|
pub primary_type: AlbumPrimaryType,
|
||||||
|
pub secondary_types: Option<Vec<AlbumSecondaryType>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
#[serde(rename_all(deserialize = "kebab-case"))]
|
||||||
|
pub struct SerdeMbReleaseGroupMeta {
|
||||||
|
id: SerdeMbid,
|
||||||
|
title: String,
|
||||||
|
first_release_date: SerdeAlbumDate,
|
||||||
|
primary_type: SerdeAlbumPrimaryType,
|
||||||
|
secondary_types: Option<Vec<SerdeAlbumSecondaryType>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SerdeMbReleaseGroupMeta> for MbReleaseGroupMeta {
|
||||||
|
fn from(value: SerdeMbReleaseGroupMeta) -> Self {
|
||||||
|
MbReleaseGroupMeta {
|
||||||
|
id: value.id.into(),
|
||||||
|
title: value.title.into(),
|
||||||
|
first_release_date: value.first_release_date.into(),
|
||||||
|
primary_type: value.primary_type.into(),
|
||||||
|
secondary_types: value
|
||||||
|
.secondary_types
|
||||||
|
.map(|v| v.into_iter().map(Into::into).collect()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct ApiDisplay;
|
pub struct ApiDisplay;
|
||||||
|
|
||||||
impl ApiDisplay {
|
impl ApiDisplay {
|
||||||
|
38
src/external/musicbrainz/api/search/artist.rs
vendored
38
src/external/musicbrainz/api/search/artist.rs
vendored
@ -2,12 +2,9 @@ use std::fmt;
|
|||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::{
|
use crate::external::musicbrainz::api::{
|
||||||
collection::{artist::ArtistId, musicbrainz::Mbid},
|
|
||||||
external::musicbrainz::api::{
|
|
||||||
search::query::{impl_term, EmptyQuery, EmptyQueryJoin, Query, QueryJoin},
|
search::query::{impl_term, EmptyQuery, EmptyQueryJoin, Query, QueryJoin},
|
||||||
SerdeMbid,
|
MbArtistMeta, SerdeMbArtistMeta,
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub enum SearchArtist<'a> {
|
pub enum SearchArtist<'a> {
|
||||||
@ -48,33 +45,22 @@ impl From<DeserializeSearchArtistResponse> for SearchArtistResponse {
|
|||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct SearchArtistResponseArtist {
|
pub struct SearchArtistResponseArtist {
|
||||||
pub score: u8,
|
pub score: u8,
|
||||||
pub id: Mbid,
|
pub meta: MbArtistMeta,
|
||||||
pub name: ArtistId,
|
|
||||||
pub sort: Option<ArtistId>,
|
|
||||||
pub disambiguation: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize)]
|
#[derive(Clone, Deserialize)]
|
||||||
#[serde(rename_all(deserialize = "kebab-case"))]
|
#[serde(rename_all(deserialize = "kebab-case"))]
|
||||||
struct DeserializeSearchArtistResponseArtist {
|
struct DeserializeSearchArtistResponseArtist {
|
||||||
score: u8,
|
score: u8,
|
||||||
id: SerdeMbid,
|
#[serde(flatten)]
|
||||||
name: String,
|
meta: SerdeMbArtistMeta,
|
||||||
sort_name: String,
|
|
||||||
disambiguation: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<DeserializeSearchArtistResponseArtist> for SearchArtistResponseArtist {
|
impl From<DeserializeSearchArtistResponseArtist> for SearchArtistResponseArtist {
|
||||||
fn from(value: DeserializeSearchArtistResponseArtist) -> Self {
|
fn from(value: DeserializeSearchArtistResponseArtist) -> Self {
|
||||||
let sort: Option<ArtistId> = Some(value.sort_name)
|
|
||||||
.filter(|s| s != &value.name)
|
|
||||||
.map(Into::into);
|
|
||||||
SearchArtistResponseArtist {
|
SearchArtistResponseArtist {
|
||||||
score: value.score,
|
score: value.score,
|
||||||
id: value.id.into(),
|
meta: value.meta.into(),
|
||||||
name: value.name.into(),
|
|
||||||
sort,
|
|
||||||
disambiguation: value.disambiguation,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -83,17 +69,22 @@ impl From<DeserializeSearchArtistResponseArtist> for SearchArtistResponseArtist
|
|||||||
mod tests {
|
mod tests {
|
||||||
use mockall::predicate;
|
use mockall::predicate;
|
||||||
|
|
||||||
use crate::external::musicbrainz::{api::MusicBrainzClient, MockIMusicBrainzHttp};
|
use crate::external::musicbrainz::{
|
||||||
|
api::{MusicBrainzClient, SerdeMbid},
|
||||||
|
MockIMusicBrainzHttp,
|
||||||
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn de_response() -> DeserializeSearchArtistResponse {
|
fn de_response() -> DeserializeSearchArtistResponse {
|
||||||
let de_artist = DeserializeSearchArtistResponseArtist {
|
let de_artist = DeserializeSearchArtistResponseArtist {
|
||||||
score: 67,
|
score: 67,
|
||||||
|
meta: SerdeMbArtistMeta {
|
||||||
id: SerdeMbid("11111111-1111-1111-1111-111111111111".try_into().unwrap()),
|
id: SerdeMbid("11111111-1111-1111-1111-111111111111".try_into().unwrap()),
|
||||||
name: String::from("an artist"),
|
name: String::from("an artist"),
|
||||||
sort_name: String::from("artist, an"),
|
sort_name: String::from("artist, an"),
|
||||||
disambiguation: None,
|
disambiguation: None,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
DeserializeSearchArtistResponse {
|
DeserializeSearchArtistResponse {
|
||||||
artists: vec![de_artist.clone()],
|
artists: vec![de_artist.clone()],
|
||||||
@ -107,10 +98,7 @@ mod tests {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|a| SearchArtistResponseArtist {
|
.map(|a| SearchArtistResponseArtist {
|
||||||
score: 67,
|
score: 67,
|
||||||
id: a.id.0,
|
meta: a.meta.into(),
|
||||||
name: a.name.clone().into(),
|
|
||||||
sort: Some(a.sort_name).filter(|sn| sn != &a.name).map(Into::into),
|
|
||||||
disambiguation: a.disambiguation,
|
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
}
|
}
|
||||||
|
@ -3,13 +3,10 @@ use std::fmt;
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
collection::{
|
collection::{album::AlbumDate, musicbrainz::Mbid},
|
||||||
album::{AlbumDate, AlbumId, AlbumPrimaryType, AlbumSecondaryType},
|
|
||||||
musicbrainz::Mbid,
|
|
||||||
},
|
|
||||||
external::musicbrainz::api::{
|
external::musicbrainz::api::{
|
||||||
search::query::{impl_term, EmptyQuery, EmptyQueryJoin, Query, QueryJoin},
|
search::query::{impl_term, EmptyQuery, EmptyQueryJoin, Query, QueryJoin},
|
||||||
ApiDisplay, SerdeAlbumDate, SerdeAlbumPrimaryType, SerdeAlbumSecondaryType, SerdeMbid,
|
ApiDisplay, MbReleaseGroupMeta, SerdeMbReleaseGroupMeta,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -72,22 +69,15 @@ impl From<DeserializeSearchReleaseGroupResponse> for SearchReleaseGroupResponse
|
|||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct SearchReleaseGroupResponseReleaseGroup {
|
pub struct SearchReleaseGroupResponseReleaseGroup {
|
||||||
pub score: u8,
|
pub score: u8,
|
||||||
pub id: Mbid,
|
pub meta: MbReleaseGroupMeta,
|
||||||
pub title: AlbumId,
|
|
||||||
pub first_release_date: AlbumDate,
|
|
||||||
pub primary_type: AlbumPrimaryType,
|
|
||||||
pub secondary_types: Option<Vec<AlbumSecondaryType>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize)]
|
#[derive(Clone, Deserialize)]
|
||||||
#[serde(rename_all(deserialize = "kebab-case"))]
|
#[serde(rename_all(deserialize = "kebab-case"))]
|
||||||
pub struct DeserializeSearchReleaseGroupResponseReleaseGroup {
|
pub struct DeserializeSearchReleaseGroupResponseReleaseGroup {
|
||||||
score: u8,
|
score: u8,
|
||||||
id: SerdeMbid,
|
#[serde(flatten)]
|
||||||
title: String,
|
meta: SerdeMbReleaseGroupMeta,
|
||||||
first_release_date: SerdeAlbumDate,
|
|
||||||
primary_type: SerdeAlbumPrimaryType,
|
|
||||||
secondary_types: Option<Vec<SerdeAlbumSecondaryType>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<DeserializeSearchReleaseGroupResponseReleaseGroup>
|
impl From<DeserializeSearchReleaseGroupResponseReleaseGroup>
|
||||||
@ -96,13 +86,7 @@ impl From<DeserializeSearchReleaseGroupResponseReleaseGroup>
|
|||||||
fn from(value: DeserializeSearchReleaseGroupResponseReleaseGroup) -> Self {
|
fn from(value: DeserializeSearchReleaseGroupResponseReleaseGroup) -> Self {
|
||||||
SearchReleaseGroupResponseReleaseGroup {
|
SearchReleaseGroupResponseReleaseGroup {
|
||||||
score: value.score,
|
score: value.score,
|
||||||
id: value.id.into(),
|
meta: value.meta.into(),
|
||||||
title: value.title.into(),
|
|
||||||
first_release_date: value.first_release_date.into(),
|
|
||||||
primary_type: value.primary_type.into(),
|
|
||||||
secondary_types: value
|
|
||||||
.secondary_types
|
|
||||||
.map(|v| v.into_iter().map(Into::into).collect()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -111,18 +95,29 @@ impl From<DeserializeSearchReleaseGroupResponseReleaseGroup>
|
|||||||
mod tests {
|
mod tests {
|
||||||
use mockall::predicate;
|
use mockall::predicate;
|
||||||
|
|
||||||
use crate::external::musicbrainz::{api::MusicBrainzClient, MockIMusicBrainzHttp};
|
use crate::{
|
||||||
|
collection::album::{AlbumPrimaryType, AlbumSecondaryType},
|
||||||
|
external::musicbrainz::{
|
||||||
|
api::{
|
||||||
|
MusicBrainzClient, SerdeAlbumDate, SerdeAlbumPrimaryType, SerdeAlbumSecondaryType,
|
||||||
|
SerdeMbid,
|
||||||
|
},
|
||||||
|
MockIMusicBrainzHttp,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn de_response() -> DeserializeSearchReleaseGroupResponse {
|
fn de_response() -> DeserializeSearchReleaseGroupResponse {
|
||||||
let de_release_group = DeserializeSearchReleaseGroupResponseReleaseGroup {
|
let de_release_group = DeserializeSearchReleaseGroupResponseReleaseGroup {
|
||||||
score: 67,
|
score: 67,
|
||||||
|
meta: SerdeMbReleaseGroupMeta {
|
||||||
id: SerdeMbid("11111111-1111-1111-1111-111111111111".try_into().unwrap()),
|
id: SerdeMbid("11111111-1111-1111-1111-111111111111".try_into().unwrap()),
|
||||||
title: String::from("an album"),
|
title: String::from("an album"),
|
||||||
first_release_date: SerdeAlbumDate((1986, 4).into()),
|
first_release_date: SerdeAlbumDate((1986, 4).into()),
|
||||||
primary_type: SerdeAlbumPrimaryType(AlbumPrimaryType::Album),
|
primary_type: SerdeAlbumPrimaryType(AlbumPrimaryType::Album),
|
||||||
secondary_types: Some(vec![SerdeAlbumSecondaryType(AlbumSecondaryType::Live)]),
|
secondary_types: Some(vec![SerdeAlbumSecondaryType(AlbumSecondaryType::Live)]),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
DeserializeSearchReleaseGroupResponse {
|
DeserializeSearchReleaseGroupResponse {
|
||||||
release_groups: vec![de_release_group.clone()],
|
release_groups: vec![de_release_group.clone()],
|
||||||
@ -136,13 +131,7 @@ mod tests {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|rg| SearchReleaseGroupResponseReleaseGroup {
|
.map(|rg| SearchReleaseGroupResponseReleaseGroup {
|
||||||
score: 67,
|
score: 67,
|
||||||
id: rg.id.0,
|
meta: rg.meta.into(),
|
||||||
title: rg.title.into(),
|
|
||||||
first_release_date: rg.first_release_date.0,
|
|
||||||
primary_type: rg.primary_type.0,
|
|
||||||
secondary_types: rg
|
|
||||||
.secondary_types
|
|
||||||
.map(|v| v.into_iter().map(|st| st.0).collect()),
|
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,11 @@ use std::{
|
|||||||
sync::mpsc::{self, TryRecvError},
|
sync::mpsc::{self, TryRecvError},
|
||||||
};
|
};
|
||||||
|
|
||||||
use musichoard::collection::{artist::Artist, musicbrainz::IMusicBrainzRef};
|
use musichoard::collection::{
|
||||||
|
album::AlbumMeta,
|
||||||
|
artist::{Artist, ArtistMeta},
|
||||||
|
musicbrainz::{IMusicBrainzRef, Mbid},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::tui::{
|
use crate::tui::{
|
||||||
app::{
|
app::{
|
||||||
@ -15,14 +19,31 @@ use crate::tui::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub type FetchReceiver = mpsc::Receiver<MbApiResult>;
|
||||||
pub struct FetchState {
|
pub struct FetchState {
|
||||||
fetch_rx: FetchReceiver,
|
fetch_rx: FetchReceiver,
|
||||||
|
lookup_rx: Option<FetchReceiver>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type FetchReceiver = mpsc::Receiver<MbApiResult>;
|
|
||||||
impl FetchState {
|
impl FetchState {
|
||||||
pub fn new(fetch_rx: FetchReceiver) -> Self {
|
pub fn new(fetch_rx: FetchReceiver) -> Self {
|
||||||
FetchState { fetch_rx }
|
FetchState {
|
||||||
|
fetch_rx,
|
||||||
|
lookup_rx: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_recv(&mut self) -> Result<MbApiResult, TryRecvError> {
|
||||||
|
if let Some(lookup_rx) = &self.lookup_rx {
|
||||||
|
let result = lookup_rx.try_recv();
|
||||||
|
match result {
|
||||||
|
Ok(_) | Err(TryRecvError::Empty) => return result,
|
||||||
|
Err(TryRecvError::Disconnected) => {
|
||||||
|
self.lookup_rx.take();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.fetch_rx.try_recv()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,8 +74,46 @@ impl AppMachine<FetchState> {
|
|||||||
Self::app_fetch(inner, fetch, false)
|
Self::app_fetch(inner, fetch, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn app_fetch(inner: AppInner, fetch: FetchState, first: bool) -> App {
|
pub fn app_lookup_artist(
|
||||||
match fetch.fetch_rx.try_recv() {
|
inner: AppInner,
|
||||||
|
fetch: FetchState,
|
||||||
|
artist: &ArtistMeta,
|
||||||
|
mbid: Mbid,
|
||||||
|
) -> App {
|
||||||
|
let f = Self::submit_lookup_artist_job;
|
||||||
|
Self::app_lookup(f, inner, fetch, artist, mbid)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn app_lookup_album(
|
||||||
|
inner: AppInner,
|
||||||
|
fetch: FetchState,
|
||||||
|
album: &AlbumMeta,
|
||||||
|
mbid: Mbid,
|
||||||
|
) -> App {
|
||||||
|
let f = Self::submit_lookup_release_group_job;
|
||||||
|
Self::app_lookup(f, inner, fetch, album, mbid)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app_lookup<F, Meta>(
|
||||||
|
submit: F,
|
||||||
|
inner: AppInner,
|
||||||
|
mut fetch: FetchState,
|
||||||
|
meta: Meta,
|
||||||
|
mbid: Mbid,
|
||||||
|
) -> App
|
||||||
|
where
|
||||||
|
F: FnOnce(&dyn IMbJobSender, ResultSender, Meta, Mbid) -> Result<(), DaemonError>,
|
||||||
|
{
|
||||||
|
let (lookup_tx, lookup_rx) = mpsc::channel::<MbApiResult>();
|
||||||
|
if let Err(err) = submit(&*inner.musicbrainz, lookup_tx, meta, mbid) {
|
||||||
|
return AppMachine::error_state(inner, err.to_string()).into();
|
||||||
|
}
|
||||||
|
fetch.lookup_rx.replace(lookup_rx);
|
||||||
|
Self::app_fetch_next(inner, fetch)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app_fetch(inner: AppInner, mut fetch: FetchState, first: bool) -> App {
|
||||||
|
match fetch.try_recv() {
|
||||||
Ok(fetch_result) => match fetch_result {
|
Ok(fetch_result) => match fetch_result {
|
||||||
Ok(next_match) => {
|
Ok(next_match) => {
|
||||||
let current = Some(next_match);
|
let current = Some(next_match);
|
||||||
@ -95,6 +154,26 @@ impl AppMachine<FetchState> {
|
|||||||
};
|
};
|
||||||
musicbrainz.submit_background_job(result_sender, requests)
|
musicbrainz.submit_background_job(result_sender, requests)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn submit_lookup_artist_job(
|
||||||
|
musicbrainz: &dyn IMbJobSender,
|
||||||
|
result_sender: ResultSender,
|
||||||
|
artist: &ArtistMeta,
|
||||||
|
mbid: Mbid,
|
||||||
|
) -> Result<(), DaemonError> {
|
||||||
|
let requests = VecDeque::from([MbParams::lookup_artist(artist.clone(), mbid)]);
|
||||||
|
musicbrainz.submit_foreground_job(result_sender, requests)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn submit_lookup_release_group_job(
|
||||||
|
musicbrainz: &dyn IMbJobSender,
|
||||||
|
result_sender: ResultSender,
|
||||||
|
album: &AlbumMeta,
|
||||||
|
mbid: Mbid,
|
||||||
|
) -> Result<(), DaemonError> {
|
||||||
|
let requests = VecDeque::from([MbParams::lookup_release_group(album.clone(), mbid)]);
|
||||||
|
musicbrainz.submit_foreground_job(result_sender, requests)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<AppMachine<FetchState>> for App {
|
impl From<AppMachine<FetchState>> for App {
|
||||||
@ -133,14 +212,49 @@ 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, IApp, IAppAccess, IAppInteractBrowse, MatchStateInfo, MissOption, SearchOption,
|
||||||
|
},
|
||||||
|
lib::interface::musicbrainz::{
|
||||||
|
self,
|
||||||
|
api::{Lookup, Match},
|
||||||
|
daemon::MockIMbJobSender,
|
||||||
},
|
},
|
||||||
lib::interface::musicbrainz::{self, api::Match, daemon::MockIMbJobSender},
|
|
||||||
testmod::COLLECTION,
|
testmod::COLLECTION,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
fn mbid() -> Mbid {
|
||||||
|
"00000000-0000-0000-0000-000000000000".try_into().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn try_recv() {
|
||||||
|
let (fetch_tx, fetch_rx) = mpsc::channel();
|
||||||
|
let (lookup_tx, lookup_rx) = mpsc::channel();
|
||||||
|
|
||||||
|
let mut fetch = FetchState::new(fetch_rx);
|
||||||
|
fetch.lookup_rx.replace(lookup_rx);
|
||||||
|
|
||||||
|
let artist = COLLECTION[3].meta.clone();
|
||||||
|
|
||||||
|
let matches: Vec<Match<ArtistMeta>> = vec![];
|
||||||
|
let fetch_result = MatchStateInfo::artist_search(artist.clone(), matches);
|
||||||
|
fetch_tx.send(Ok(fetch_result.clone())).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(fetch.try_recv(), Err(TryRecvError::Empty));
|
||||||
|
|
||||||
|
let lookup = Lookup::new(artist.clone());
|
||||||
|
let lookup_result = MatchStateInfo::artist_lookup(artist.clone(), lookup);
|
||||||
|
lookup_tx.send(Ok(lookup_result.clone())).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(fetch.try_recv(), Ok(Ok(lookup_result)));
|
||||||
|
|
||||||
|
assert_eq!(fetch.try_recv(), Err(TryRecvError::Empty));
|
||||||
|
drop(lookup_tx);
|
||||||
|
assert_eq!(fetch.try_recv(), Ok(Ok(fetch_result)));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn fetch_no_artist() {
|
fn fetch_no_artist() {
|
||||||
let app = AppMachine::app_fetch_new(inner(music_hoard(vec![])));
|
let app = AppMachine::app_fetch_new(inner(music_hoard(vec![])));
|
||||||
@ -186,6 +300,31 @@ mod tests {
|
|||||||
assert!(matches!(app, AppState::Match(_)));
|
assert!(matches!(app, AppState::Match(_)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn lookup_album_expectation(job_sender: &mut MockIMbJobSender, album: &AlbumMeta) {
|
||||||
|
let requests = VecDeque::from([MbParams::lookup_release_group(album.clone(), mbid())]);
|
||||||
|
job_sender
|
||||||
|
.expect_submit_foreground_job()
|
||||||
|
.with(predicate::always(), predicate::eq(requests))
|
||||||
|
.times(1)
|
||||||
|
.return_once(|_, _| Ok(()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lookup_album() {
|
||||||
|
let mut mb_job_sender = MockIMbJobSender::new();
|
||||||
|
|
||||||
|
let album = COLLECTION[1].albums[0].meta.clone();
|
||||||
|
lookup_album_expectation(&mut mb_job_sender, &album);
|
||||||
|
|
||||||
|
let music_hoard = music_hoard(COLLECTION.to_owned());
|
||||||
|
let inner = AppInner::new(music_hoard, mb_job_sender);
|
||||||
|
|
||||||
|
let (_fetch_tx, fetch_rx) = mpsc::channel();
|
||||||
|
let fetch = FetchState::new(fetch_rx);
|
||||||
|
|
||||||
|
AppMachine::app_lookup_album(inner, fetch, &album, mbid());
|
||||||
|
}
|
||||||
|
|
||||||
fn search_artist_expectation(job_sender: &mut MockIMbJobSender, artist: &ArtistMeta) {
|
fn search_artist_expectation(job_sender: &mut MockIMbJobSender, artist: &ArtistMeta) {
|
||||||
let requests = VecDeque::from([MbParams::search_artist(artist.clone())]);
|
let requests = VecDeque::from([MbParams::search_artist(artist.clone())]);
|
||||||
job_sender
|
job_sender
|
||||||
@ -215,6 +354,31 @@ mod tests {
|
|||||||
assert!(matches!(app, AppState::Match(_)));
|
assert!(matches!(app, AppState::Match(_)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn lookup_artist_expectation(job_sender: &mut MockIMbJobSender, artist: &ArtistMeta) {
|
||||||
|
let requests = VecDeque::from([MbParams::lookup_artist(artist.clone(), mbid())]);
|
||||||
|
job_sender
|
||||||
|
.expect_submit_foreground_job()
|
||||||
|
.with(predicate::always(), predicate::eq(requests))
|
||||||
|
.times(1)
|
||||||
|
.return_once(|_, _| Ok(()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lookup_artist() {
|
||||||
|
let mut mb_job_sender = MockIMbJobSender::new();
|
||||||
|
|
||||||
|
let artist = COLLECTION[3].meta.clone();
|
||||||
|
lookup_artist_expectation(&mut mb_job_sender, &artist);
|
||||||
|
|
||||||
|
let music_hoard = music_hoard(COLLECTION.to_owned());
|
||||||
|
let inner = AppInner::new(music_hoard, mb_job_sender);
|
||||||
|
|
||||||
|
let (_fetch_tx, fetch_rx) = mpsc::channel();
|
||||||
|
let fetch = FetchState::new(fetch_rx);
|
||||||
|
|
||||||
|
AppMachine::app_lookup_artist(inner, fetch, &artist, mbid());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn fetch_artist_job_sender_err() {
|
fn fetch_artist_job_sender_err() {
|
||||||
let mut mb_job_sender = MockIMbJobSender::new();
|
let mut mb_job_sender = MockIMbJobSender::new();
|
||||||
@ -231,13 +395,34 @@ mod tests {
|
|||||||
assert!(matches!(app, AppState::Error(_)));
|
assert!(matches!(app, AppState::Error(_)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lookup_artist_job_sender_err() {
|
||||||
|
let mut mb_job_sender = MockIMbJobSender::new();
|
||||||
|
|
||||||
|
mb_job_sender
|
||||||
|
.expect_submit_foreground_job()
|
||||||
|
.return_once(|_, _| Err(DaemonError::JobChannelDisconnected));
|
||||||
|
|
||||||
|
let artist = COLLECTION[3].meta.clone();
|
||||||
|
|
||||||
|
let music_hoard = music_hoard(COLLECTION.to_owned());
|
||||||
|
let inner = AppInner::new(music_hoard, mb_job_sender);
|
||||||
|
|
||||||
|
let (_fetch_tx, fetch_rx) = mpsc::channel();
|
||||||
|
let fetch = FetchState::new(fetch_rx);
|
||||||
|
|
||||||
|
let app = AppMachine::app_lookup_artist(inner, fetch, &artist, mbid());
|
||||||
|
assert!(matches!(app, AppState::Error(_)));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn recv_ok_fetch_ok() {
|
fn recv_ok_fetch_ok() {
|
||||||
let (tx, rx) = mpsc::channel::<MbApiResult>();
|
let (tx, rx) = mpsc::channel::<MbApiResult>();
|
||||||
|
|
||||||
let artist = COLLECTION[3].meta.clone();
|
let artist = COLLECTION[3].meta.clone();
|
||||||
let artist_match = Match::new(80, COLLECTION[2].meta.clone());
|
let artist_match = Match::new(80, COLLECTION[2].meta.clone());
|
||||||
let artist_match_info = MatchStateInfo::artist(artist.clone(), vec![artist_match.clone()]);
|
let artist_match_info =
|
||||||
|
MatchStateInfo::artist_search(artist.clone(), vec![artist_match.clone()]);
|
||||||
let fetch_result = Ok(artist_match_info);
|
let fetch_result = Ok(artist_match_info);
|
||||||
tx.send(fetch_result).unwrap();
|
tx.send(fetch_result).unwrap();
|
||||||
|
|
||||||
@ -250,10 +435,10 @@ mod tests {
|
|||||||
let match_state = public.state.unwrap_match();
|
let match_state = public.state.unwrap_match();
|
||||||
let match_options = vec![
|
let match_options = vec![
|
||||||
artist_match.into(),
|
artist_match.into(),
|
||||||
MatchOption::CannotHaveMbid,
|
SearchOption::None(MissOption::CannotHaveMbid),
|
||||||
MatchOption::ManualInputMbid,
|
SearchOption::None(MissOption::ManualInputMbid),
|
||||||
];
|
];
|
||||||
let expected = MatchStateInfo::artist(artist, match_options);
|
let expected = MatchStateInfo::artist_search(artist, match_options);
|
||||||
assert_eq!(match_state.info, Some(expected).as_ref());
|
assert_eq!(match_state.info, Some(expected).as_ref());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -309,7 +494,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 fetch_result = Ok(MatchStateInfo::artist::<Match<ArtistMeta>>(artist, vec![]));
|
let match_info = MatchStateInfo::artist_search::<Match<ArtistMeta>>(artist, vec![]);
|
||||||
|
let fetch_result = Ok(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();
|
||||||
|
@ -11,6 +11,12 @@ impl<'app> From<&'app Input> for InputPublic<'app> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Input {
|
||||||
|
pub fn value(&self) -> &str {
|
||||||
|
self.0.value()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<App> for AppMode<App, AppInputMode> {
|
impl From<App> for AppMode<App, AppInputMode> {
|
||||||
fn from(mut app: App) -> Self {
|
fn from(mut app: App) -> Self {
|
||||||
if let Some(input) = app.input_mut().take() {
|
if let Some(input) = app.input_mut().take() {
|
||||||
@ -43,9 +49,9 @@ impl IAppInput for AppInputMode {
|
|||||||
self.app
|
self.app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn confirm(mut self) -> Self::APP {
|
fn confirm(self) -> Self::APP {
|
||||||
if let AppState::Match(state) = &mut self.app {
|
if let AppState::Match(state) = self.app {
|
||||||
state.submit_input(self.input);
|
return state.submit_input(self.input);
|
||||||
}
|
}
|
||||||
self.app
|
self.app
|
||||||
}
|
}
|
||||||
@ -58,20 +64,12 @@ impl IAppInput for AppInputMode {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::tui::app::{
|
use crate::tui::app::{
|
||||||
machine::tests::{mb_job_sender, music_hoard_init},
|
machine::tests::{input_event, mb_job_sender, music_hoard_init},
|
||||||
IApp,
|
IApp,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn input_event(c: char) -> InputEvent {
|
|
||||||
crossterm::event::KeyEvent::new(
|
|
||||||
crossterm::event::KeyCode::Char(c),
|
|
||||||
crossterm::event::KeyModifiers::empty(),
|
|
||||||
)
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn handle_input() {
|
fn handle_input() {
|
||||||
let mut app = App::new(music_hoard_init(vec![]), mb_job_sender());
|
let mut app = App::new(music_hoard_init(vec![]), mb_job_sender());
|
||||||
|
@ -1,26 +1,62 @@
|
|||||||
use std::cmp;
|
use std::cmp;
|
||||||
|
|
||||||
|
use musichoard::collection::musicbrainz::Mbid;
|
||||||
|
|
||||||
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, IAppInteractMatch, MatchOption,
|
AlbumMatches, AppPublicState, AppState, ArtistMatches, IAppInteractMatch, ListOption,
|
||||||
MatchStateInfo, MatchStatePublic, WidgetState,
|
LookupOption, MatchStateInfo, MatchStatePublic, MissOption, SearchOption, WidgetState,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
impl<T: PartialEq> ListOption<T> {
|
||||||
|
fn len(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
ListOption::Lookup(list) => list.len(),
|
||||||
|
ListOption::Search(list) => list.len(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_cannot_have_mbid(&mut self) {
|
||||||
|
match self {
|
||||||
|
ListOption::Lookup(list) => list.push(LookupOption::None(MissOption::CannotHaveMbid)),
|
||||||
|
ListOption::Search(list) => list.push(SearchOption::None(MissOption::CannotHaveMbid)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_manual_input_mbid(&mut self) {
|
||||||
|
match self {
|
||||||
|
ListOption::Lookup(list) => list.push(LookupOption::None(MissOption::ManualInputMbid)),
|
||||||
|
ListOption::Search(list) => list.push(SearchOption::None(MissOption::ManualInputMbid)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_manual_input_mbid(&self, index: usize) -> bool {
|
||||||
|
match self {
|
||||||
|
ListOption::Lookup(list) => {
|
||||||
|
list.get(index) == Some(&LookupOption::None(MissOption::ManualInputMbid))
|
||||||
|
}
|
||||||
|
ListOption::Search(list) => {
|
||||||
|
list.get(index) == Some(&SearchOption::None(MissOption::ManualInputMbid))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ArtistMatches {
|
impl ArtistMatches {
|
||||||
fn len(&self) -> usize {
|
fn len(&self) -> usize {
|
||||||
self.list.len()
|
self.list.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_cannot_have_mbid(&mut self) {
|
fn push_cannot_have_mbid(&mut self) {
|
||||||
self.list.push(MatchOption::CannotHaveMbid)
|
self.list.push_cannot_have_mbid();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_manual_input_mbid(&mut self) {
|
fn push_manual_input_mbid(&mut self) {
|
||||||
self.list.push(MatchOption::ManualInputMbid)
|
self.list.push_manual_input_mbid();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_manual_input_mbid(&self, index: usize) -> bool {
|
fn is_manual_input_mbid(&self, index: usize) -> bool {
|
||||||
self.list.get(index) == Some(&MatchOption::ManualInputMbid)
|
self.list.is_manual_input_mbid(index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,15 +66,15 @@ impl AlbumMatches {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn push_cannot_have_mbid(&mut self) {
|
fn push_cannot_have_mbid(&mut self) {
|
||||||
self.list.push(MatchOption::CannotHaveMbid)
|
self.list.push_cannot_have_mbid();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_manual_input_mbid(&mut self) {
|
fn push_manual_input_mbid(&mut self) {
|
||||||
self.list.push(MatchOption::ManualInputMbid)
|
self.list.push_manual_input_mbid();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_manual_input_mbid(&self, index: usize) -> bool {
|
fn is_manual_input_mbid(&self, index: usize) -> bool {
|
||||||
self.list.get(index) == Some(&MatchOption::ManualInputMbid)
|
self.list.is_manual_input_mbid(index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,14 +86,14 @@ impl MatchStateInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_cannot_have_mbid(&mut self) {
|
pub fn push_cannot_have_mbid(&mut self) {
|
||||||
match self {
|
match self {
|
||||||
Self::Artist(a) => a.push_cannot_have_mbid(),
|
Self::Artist(a) => a.push_cannot_have_mbid(),
|
||||||
Self::Album(a) => a.push_cannot_have_mbid(),
|
Self::Album(a) => a.push_cannot_have_mbid(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_manual_input_mbid(&mut self) {
|
pub fn push_manual_input_mbid(&mut self) {
|
||||||
match self {
|
match self {
|
||||||
Self::Artist(a) => a.push_manual_input_mbid(),
|
Self::Artist(a) => a.push_manual_input_mbid(),
|
||||||
Self::Album(a) => a.push_manual_input_mbid(),
|
Self::Album(a) => a.push_manual_input_mbid(),
|
||||||
@ -99,7 +135,22 @@ impl AppMachine<MatchState> {
|
|||||||
AppMachine::new(inner, state)
|
AppMachine::new(inner, state)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn submit_input(&mut self, _input: Input) {}
|
pub fn submit_input(self, input: Input) -> App {
|
||||||
|
let mbid: Mbid = match input.value().try_into() {
|
||||||
|
Ok(mbid) => mbid,
|
||||||
|
Err(err) => return AppMachine::error_state(self.inner, err.to_string()).into(),
|
||||||
|
};
|
||||||
|
match self.state.current.as_ref().unwrap() {
|
||||||
|
MatchStateInfo::Artist(artist_matches) => {
|
||||||
|
let matching = &artist_matches.matching;
|
||||||
|
AppMachine::app_lookup_artist(self.inner, self.state.fetch, matching, mbid)
|
||||||
|
}
|
||||||
|
MatchStateInfo::Album(album_matches) => {
|
||||||
|
let matching = &album_matches.matching;
|
||||||
|
AppMachine::app_lookup_album(self.inner, self.state.fetch, matching, mbid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<AppMachine<MatchState>> for App {
|
impl From<AppMachine<MatchState>> for App {
|
||||||
@ -167,8 +218,9 @@ impl IAppInteractMatch for AppMachine<MatchState> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::sync::mpsc;
|
use std::{collections::VecDeque, sync::mpsc};
|
||||||
|
|
||||||
|
use mockall::predicate;
|
||||||
use musichoard::collection::{
|
use musichoard::collection::{
|
||||||
album::{AlbumDate, AlbumId, AlbumMeta, AlbumPrimaryType, AlbumSecondaryType},
|
album::{AlbumDate, AlbumId, AlbumMeta, AlbumPrimaryType, AlbumSecondaryType},
|
||||||
artist::{ArtistId, ArtistMeta},
|
artist::{ArtistId, ArtistMeta},
|
||||||
@ -176,10 +228,13 @@ mod tests {
|
|||||||
|
|
||||||
use crate::tui::{
|
use crate::tui::{
|
||||||
app::{
|
app::{
|
||||||
machine::tests::{inner, music_hoard},
|
machine::tests::{inner, inner_with_mb, input_event, music_hoard},
|
||||||
IApp, IAppAccess, IAppInput,
|
IApp, IAppAccess, IAppInput,
|
||||||
},
|
},
|
||||||
lib::interface::musicbrainz::api::Match,
|
lib::interface::musicbrainz::{
|
||||||
|
api::{Lookup, Match},
|
||||||
|
daemon::{MbParams, MockIMbJobSender},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -194,6 +249,15 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T> Lookup<T> {
|
||||||
|
pub fn new(item: T) -> Self {
|
||||||
|
Lookup {
|
||||||
|
item,
|
||||||
|
disambiguation: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn artist_match() -> MatchStateInfo {
|
fn artist_match() -> MatchStateInfo {
|
||||||
let artist = ArtistMeta::new(ArtistId::new("Artist"));
|
let artist = ArtistMeta::new(ArtistId::new("Artist"));
|
||||||
|
|
||||||
@ -205,7 +269,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(artist, list)
|
MatchStateInfo::artist_search(artist, list)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn artist_lookup() -> MatchStateInfo {
|
||||||
|
let artist = ArtistMeta::new(ArtistId::new("Artist"));
|
||||||
|
let lookup = Lookup::new(artist.clone());
|
||||||
|
MatchStateInfo::artist_lookup(artist, lookup)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn album_match() -> MatchStateInfo {
|
fn album_match() -> MatchStateInfo {
|
||||||
@ -225,7 +295,18 @@ mod tests {
|
|||||||
let album_match_2 = Match::new(100, album_2);
|
let album_match_2 = Match::new(100, album_2);
|
||||||
|
|
||||||
let list = vec![album_match_1.clone(), album_match_2.clone()];
|
let list = vec![album_match_1.clone(), album_match_2.clone()];
|
||||||
MatchStateInfo::album(album, list)
|
MatchStateInfo::album_search(album, list)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn album_lookup() -> MatchStateInfo {
|
||||||
|
let album = AlbumMeta::new(
|
||||||
|
AlbumId::new("Album"),
|
||||||
|
AlbumDate::new(Some(1990), Some(5), None),
|
||||||
|
Some(AlbumPrimaryType::Album),
|
||||||
|
vec![AlbumSecondaryType::Live, AlbumSecondaryType::Compilation],
|
||||||
|
);
|
||||||
|
let lookup = Lookup::new(album.clone());
|
||||||
|
MatchStateInfo::album_lookup(album, lookup)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fetch_state() -> FetchState {
|
fn fetch_state() -> FetchState {
|
||||||
@ -233,8 +314,8 @@ mod tests {
|
|||||||
FetchState::new(rx)
|
FetchState::new(rx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn match_state(matches_info: Option<MatchStateInfo>) -> MatchState {
|
fn match_state(match_state_info: Option<MatchStateInfo>) -> MatchState {
|
||||||
MatchState::new(matches_info, fetch_state())
|
MatchState::new(match_state_info, fetch_state())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -278,7 +359,7 @@ mod tests {
|
|||||||
assert_eq!(public_matches.state, &widget_state);
|
assert_eq!(public_matches.state, &widget_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn match_state_flow(mut matches_info: MatchStateInfo) {
|
fn match_state_flow(mut matches_info: MatchStateInfo, len: usize) {
|
||||||
// tx must exist for rx to return Empty rather than Disconnected.
|
// tx must exist for rx to return Empty rather than Disconnected.
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
let (tx, rx) = mpsc::channel();
|
let (tx, rx) = mpsc::channel();
|
||||||
@ -299,27 +380,30 @@ mod tests {
|
|||||||
assert_eq!(matches.state.current.as_ref(), Some(&matches_info));
|
assert_eq!(matches.state.current.as_ref(), Some(&matches_info));
|
||||||
assert_eq!(matches.state.state.list.selected(), Some(0));
|
assert_eq!(matches.state.state.list.selected(), Some(0));
|
||||||
|
|
||||||
let matches = matches.next_match().unwrap_match();
|
let mut matches = matches;
|
||||||
|
for ii in 1..len {
|
||||||
|
matches = matches.next_match().unwrap_match();
|
||||||
|
|
||||||
assert_eq!(matches.state.current.as_ref(), Some(&matches_info));
|
assert_eq!(matches.state.current.as_ref(), Some(&matches_info));
|
||||||
assert_eq!(matches.state.state.list.selected(), Some(1));
|
assert_eq!(matches.state.state.list.selected(), Some(ii));
|
||||||
|
}
|
||||||
|
|
||||||
// Next is CannotHaveMBID
|
// Next is CannotHaveMBID
|
||||||
let matches = matches.next_match().unwrap_match();
|
let matches = matches.next_match().unwrap_match();
|
||||||
|
|
||||||
assert_eq!(matches.state.current.as_ref(), Some(&matches_info));
|
assert_eq!(matches.state.current.as_ref(), Some(&matches_info));
|
||||||
assert_eq!(matches.state.state.list.selected(), Some(2));
|
assert_eq!(matches.state.state.list.selected(), Some(len));
|
||||||
|
|
||||||
// Next is ManualInputMbid
|
// Next is ManualInputMbid
|
||||||
let matches = matches.next_match().unwrap_match();
|
let matches = matches.next_match().unwrap_match();
|
||||||
|
|
||||||
assert_eq!(matches.state.current.as_ref(), Some(&matches_info));
|
assert_eq!(matches.state.current.as_ref(), Some(&matches_info));
|
||||||
assert_eq!(matches.state.state.list.selected(), Some(3));
|
assert_eq!(matches.state.state.list.selected(), Some(len + 1));
|
||||||
|
|
||||||
let matches = matches.next_match().unwrap_match();
|
let matches = matches.next_match().unwrap_match();
|
||||||
|
|
||||||
assert_eq!(matches.state.current.as_ref(), Some(&matches_info));
|
assert_eq!(matches.state.current.as_ref(), Some(&matches_info));
|
||||||
assert_eq!(matches.state.state.list.selected(), Some(3));
|
assert_eq!(matches.state.state.list.selected(), Some(len + 1));
|
||||||
|
|
||||||
// Go prev_match first as selecting on manual input does not go back to fetch.
|
// Go prev_match first as selecting on manual input does not go back to fetch.
|
||||||
let matches = matches.prev_match().unwrap_match();
|
let matches = matches.prev_match().unwrap_match();
|
||||||
@ -328,12 +412,22 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn artist_matches_flow() {
|
fn artist_matches_flow() {
|
||||||
match_state_flow(artist_match());
|
match_state_flow(artist_match(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn artist_lookup_flow() {
|
||||||
|
match_state_flow(artist_lookup(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn album_matches_flow() {
|
fn album_matches_flow() {
|
||||||
match_state_flow(album_match());
|
match_state_flow(album_match(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn album_lookup_flow() {
|
||||||
|
match_state_flow(album_lookup(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -364,7 +458,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn select_manual_input() {
|
fn select_manual_input_empty() {
|
||||||
let matches =
|
let matches =
|
||||||
AppMachine::match_state(inner(music_hoard(vec![])), match_state(Some(album_match())));
|
AppMachine::match_state(inner(music_hoard(vec![])), match_state(Some(album_match())));
|
||||||
|
|
||||||
@ -377,6 +471,75 @@ mod tests {
|
|||||||
let app = matches.select();
|
let app = matches.select();
|
||||||
|
|
||||||
let input = app.mode().unwrap_input();
|
let input = app.mode().unwrap_input();
|
||||||
input.confirm().unwrap_match();
|
input.confirm().unwrap_error();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mbid() -> Mbid {
|
||||||
|
"00000000-0000-0000-0000-000000000000".try_into().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn input_mbid(mut app: App) -> App {
|
||||||
|
let mbid = mbid().uuid().to_string();
|
||||||
|
for c in mbid.chars() {
|
||||||
|
let input = app.mode().unwrap_input();
|
||||||
|
app = input.input(input_event(c));
|
||||||
|
}
|
||||||
|
app
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn select_manual_input_artist() {
|
||||||
|
let mut mb_job_sender = MockIMbJobSender::new();
|
||||||
|
let artist = ArtistMeta::new(ArtistId::new("Artist"));
|
||||||
|
let requests = VecDeque::from([MbParams::lookup_artist(artist.clone(), mbid())]);
|
||||||
|
mb_job_sender
|
||||||
|
.expect_submit_foreground_job()
|
||||||
|
.with(predicate::always(), predicate::eq(requests))
|
||||||
|
.return_once(|_, _| Ok(()));
|
||||||
|
|
||||||
|
let matches_vec: Vec<Match<ArtistMeta>> = vec![];
|
||||||
|
let artist_match = MatchStateInfo::artist_search(artist.clone(), matches_vec);
|
||||||
|
let matches = AppMachine::match_state(
|
||||||
|
inner_with_mb(music_hoard(vec![]), mb_job_sender),
|
||||||
|
match_state(Some(artist_match)),
|
||||||
|
);
|
||||||
|
|
||||||
|
// There are no matches which means that the second option should be manual input.
|
||||||
|
let matches = matches.next_match().unwrap_match();
|
||||||
|
let matches = matches.next_match().unwrap_match();
|
||||||
|
|
||||||
|
let mut app = matches.select();
|
||||||
|
app = input_mbid(app);
|
||||||
|
|
||||||
|
let input = app.mode().unwrap_input();
|
||||||
|
input.confirm();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn select_manual_input_album() {
|
||||||
|
let mut mb_job_sender = MockIMbJobSender::new();
|
||||||
|
let album = AlbumMeta::new("Album", 1990, None, vec![]);
|
||||||
|
let requests = VecDeque::from([MbParams::lookup_release_group(album.clone(), mbid())]);
|
||||||
|
mb_job_sender
|
||||||
|
.expect_submit_foreground_job()
|
||||||
|
.with(predicate::always(), predicate::eq(requests))
|
||||||
|
.return_once(|_, _| Ok(()));
|
||||||
|
|
||||||
|
let matches_vec: Vec<Match<AlbumMeta>> = vec![];
|
||||||
|
let album_match = MatchStateInfo::album_search(album.clone(), matches_vec);
|
||||||
|
let matches = AppMachine::match_state(
|
||||||
|
inner_with_mb(music_hoard(vec![]), mb_job_sender),
|
||||||
|
match_state(Some(album_match)),
|
||||||
|
);
|
||||||
|
|
||||||
|
// There are no matches which means that the second option should be manual input.
|
||||||
|
let matches = matches.next_match().unwrap_match();
|
||||||
|
let matches = matches.next_match().unwrap_match();
|
||||||
|
|
||||||
|
let mut app = matches.select();
|
||||||
|
app = input_mbid(app);
|
||||||
|
|
||||||
|
let input = app.mode().unwrap_input();
|
||||||
|
input.confirm();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -222,7 +222,7 @@ mod tests {
|
|||||||
use musichoard::collection::Collection;
|
use musichoard::collection::Collection;
|
||||||
|
|
||||||
use crate::tui::{
|
use crate::tui::{
|
||||||
app::{AppState, IApp, IAppInput, IAppInteractBrowse},
|
app::{AppState, IApp, IAppInput, IAppInteractBrowse, InputEvent},
|
||||||
lib::{interface::musicbrainz::daemon::MockIMbJobSender, MockIMusicHoard},
|
lib::{interface::musicbrainz::daemon::MockIMbJobSender, MockIMusicHoard},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -355,6 +355,14 @@ mod tests {
|
|||||||
AppInner::new(music_hoard, mb_job_sender)
|
AppInner::new(music_hoard, mb_job_sender)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn input_event(c: char) -> InputEvent {
|
||||||
|
crossterm::event::KeyEvent::new(
|
||||||
|
crossterm::event::KeyCode::Char(c),
|
||||||
|
crossterm::event::KeyModifiers::empty(),
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn input_mode() {
|
fn input_mode() {
|
||||||
let app = App::new(music_hoard_init(vec![]), mb_job_sender());
|
let app = App::new(music_hoard_init(vec![]), mb_job_sender());
|
||||||
|
@ -32,6 +32,8 @@ macro_rules! IAppState {
|
|||||||
}
|
}
|
||||||
use IAppState;
|
use IAppState;
|
||||||
|
|
||||||
|
use super::lib::interface::musicbrainz::api::Lookup;
|
||||||
|
|
||||||
pub trait IApp {
|
pub trait IApp {
|
||||||
type BrowseState: IAppBase<APP = Self> + IAppInteractBrowse<APP = Self>;
|
type BrowseState: IAppBase<APP = Self> + IAppInteractBrowse<APP = Self>;
|
||||||
type InfoState: IAppBase<APP = Self> + IAppInteractInfo<APP = Self>;
|
type InfoState: IAppBase<APP = Self> + IAppInteractInfo<APP = Self>;
|
||||||
@ -175,28 +177,51 @@ pub struct AppPublicInner<'app> {
|
|||||||
pub type InputPublic<'app> = &'app tui_input::Input;
|
pub type InputPublic<'app> = &'app tui_input::Input;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum MatchOption<T> {
|
pub enum MissOption {
|
||||||
Match(Match<T>),
|
|
||||||
CannotHaveMbid,
|
CannotHaveMbid,
|
||||||
ManualInputMbid,
|
ManualInputMbid,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> From<Match<T>> for MatchOption<T> {
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum SearchOption<T> {
|
||||||
|
Match(Match<T>),
|
||||||
|
None(MissOption),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum LookupOption<T> {
|
||||||
|
Match(Lookup<T>),
|
||||||
|
None(MissOption),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum ListOption<T> {
|
||||||
|
Search(Vec<SearchOption<T>>),
|
||||||
|
Lookup(Vec<LookupOption<T>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<Match<T>> for SearchOption<T> {
|
||||||
fn from(value: Match<T>) -> Self {
|
fn from(value: Match<T>) -> Self {
|
||||||
MatchOption::Match(value)
|
SearchOption::Match(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<Lookup<T>> for LookupOption<T> {
|
||||||
|
fn from(value: Lookup<T>) -> Self {
|
||||||
|
LookupOption::Match(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct ArtistMatches {
|
pub struct ArtistMatches {
|
||||||
pub matching: ArtistMeta,
|
pub matching: ArtistMeta,
|
||||||
pub list: Vec<MatchOption<ArtistMeta>>,
|
pub list: ListOption<ArtistMeta>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct AlbumMatches {
|
pub struct AlbumMatches {
|
||||||
pub matching: AlbumMeta,
|
pub matching: AlbumMeta,
|
||||||
pub list: Vec<MatchOption<AlbumMeta>>,
|
pub list: ListOption<AlbumMeta>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
@ -206,13 +231,29 @@ pub enum MatchStateInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MatchStateInfo {
|
impl MatchStateInfo {
|
||||||
pub fn artist<M: Into<MatchOption<ArtistMeta>>>(matching: ArtistMeta, list: Vec<M>) -> Self {
|
pub fn artist_search<M: Into<SearchOption<ArtistMeta>>>(
|
||||||
let list: Vec<MatchOption<ArtistMeta>> = list.into_iter().map(Into::into).collect();
|
matching: ArtistMeta,
|
||||||
|
list: Vec<M>,
|
||||||
|
) -> Self {
|
||||||
|
let list = ListOption::Search(list.into_iter().map(Into::into).collect());
|
||||||
MatchStateInfo::Artist(ArtistMatches { matching, list })
|
MatchStateInfo::Artist(ArtistMatches { matching, list })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn album<M: Into<MatchOption<AlbumMeta>>>(matching: AlbumMeta, list: Vec<M>) -> Self {
|
pub fn album_search<M: Into<SearchOption<AlbumMeta>>>(
|
||||||
let list: Vec<MatchOption<AlbumMeta>> = list.into_iter().map(Into::into).collect();
|
matching: AlbumMeta,
|
||||||
|
list: Vec<M>,
|
||||||
|
) -> Self {
|
||||||
|
let list = ListOption::Search(list.into_iter().map(Into::into).collect());
|
||||||
|
MatchStateInfo::Album(AlbumMatches { matching, list })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn artist_lookup<M: Into<LookupOption<ArtistMeta>>>(matching: ArtistMeta, item: M) -> Self {
|
||||||
|
let list = ListOption::Lookup(vec![item.into()]);
|
||||||
|
MatchStateInfo::Artist(ArtistMatches { matching, list })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn album_lookup<M: Into<LookupOption<AlbumMeta>>>(matching: AlbumMeta, item: M) -> Self {
|
||||||
|
let list = ListOption::Lookup(vec![item.into()]);
|
||||||
MatchStateInfo::Album(AlbumMatches { matching, list })
|
MatchStateInfo::Album(AlbumMatches { matching, list })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
74
src/tui/lib/external/musicbrainz/api/mod.rs
vendored
74
src/tui/lib/external/musicbrainz/api/mod.rs
vendored
@ -5,11 +5,15 @@ use std::collections::HashMap;
|
|||||||
use musichoard::{
|
use musichoard::{
|
||||||
collection::{
|
collection::{
|
||||||
album::{AlbumDate, AlbumMeta, AlbumSeq},
|
album::{AlbumDate, AlbumMeta, AlbumSeq},
|
||||||
artist::ArtistMeta,
|
artist::{ArtistId, ArtistMeta},
|
||||||
musicbrainz::Mbid,
|
musicbrainz::Mbid,
|
||||||
},
|
},
|
||||||
external::musicbrainz::{
|
external::musicbrainz::{
|
||||||
api::{
|
api::{
|
||||||
|
lookup::{
|
||||||
|
LookupArtistRequest, LookupArtistResponse, LookupReleaseGroupRequest,
|
||||||
|
LookupReleaseGroupResponse,
|
||||||
|
},
|
||||||
search::{
|
search::{
|
||||||
SearchArtistRequest, SearchArtistResponseArtist, SearchReleaseGroupRequest,
|
SearchArtistRequest, SearchArtistResponseArtist, SearchReleaseGroupRequest,
|
||||||
SearchReleaseGroupResponseReleaseGroup,
|
SearchReleaseGroupResponseReleaseGroup,
|
||||||
@ -20,7 +24,7 @@ use musichoard::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::tui::lib::interface::musicbrainz::api::{Error, IMusicBrainz, Match};
|
use crate::tui::lib::interface::musicbrainz::api::{Error, IMusicBrainz, Lookup, Match};
|
||||||
|
|
||||||
// GRCOV_EXCL_START
|
// GRCOV_EXCL_START
|
||||||
pub struct MusicBrainz<Http> {
|
pub struct MusicBrainz<Http> {
|
||||||
@ -34,6 +38,22 @@ impl<Http> MusicBrainz<Http> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<Http: IMusicBrainzHttp> IMusicBrainz for MusicBrainz<Http> {
|
impl<Http: IMusicBrainzHttp> IMusicBrainz for MusicBrainz<Http> {
|
||||||
|
fn lookup_artist(&mut self, mbid: &Mbid) -> Result<Lookup<ArtistMeta>, Error> {
|
||||||
|
let request = LookupArtistRequest::new(mbid);
|
||||||
|
|
||||||
|
let mb_response = self.client.lookup_artist(request)?;
|
||||||
|
|
||||||
|
Ok(from_lookup_artist_response(mb_response))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lookup_release_group(&mut self, mbid: &Mbid) -> Result<Lookup<AlbumMeta>, Error> {
|
||||||
|
let request = LookupReleaseGroupRequest::new(mbid);
|
||||||
|
|
||||||
|
let mb_response = self.client.lookup_release_group(request)?;
|
||||||
|
|
||||||
|
Ok(from_lookup_release_group_response(mb_response))
|
||||||
|
}
|
||||||
|
|
||||||
fn search_artist(&mut self, artist: &ArtistMeta) -> Result<Vec<Match<ArtistMeta>>, Error> {
|
fn search_artist(&mut self, artist: &ArtistMeta) -> Result<Vec<Match<ArtistMeta>>, Error> {
|
||||||
let query = SearchArtistRequest::new().string(&artist.id.name);
|
let query = SearchArtistRequest::new().string(&artist.id.name);
|
||||||
|
|
||||||
@ -72,16 +92,48 @@ impl<Http: IMusicBrainzHttp> IMusicBrainz for MusicBrainz<Http> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn from_lookup_artist_response(entity: LookupArtistResponse) -> Lookup<ArtistMeta> {
|
||||||
|
let sort: Option<ArtistId> = Some(entity.meta.sort_name)
|
||||||
|
.filter(|s| s != &entity.meta.name)
|
||||||
|
.map(Into::into);
|
||||||
|
Lookup {
|
||||||
|
item: ArtistMeta {
|
||||||
|
id: entity.meta.name,
|
||||||
|
sort,
|
||||||
|
musicbrainz: Some(entity.meta.id.into()),
|
||||||
|
properties: HashMap::new(),
|
||||||
|
},
|
||||||
|
disambiguation: entity.meta.disambiguation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_lookup_release_group_response(entity: LookupReleaseGroupResponse) -> Lookup<AlbumMeta> {
|
||||||
|
Lookup {
|
||||||
|
item: AlbumMeta {
|
||||||
|
id: entity.meta.title,
|
||||||
|
date: entity.meta.first_release_date,
|
||||||
|
seq: AlbumSeq::default(),
|
||||||
|
musicbrainz: Some(entity.meta.id.into()),
|
||||||
|
primary_type: Some(entity.meta.primary_type),
|
||||||
|
secondary_types: entity.meta.secondary_types.unwrap_or_default(),
|
||||||
|
},
|
||||||
|
disambiguation: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn from_search_artist_response_artist(entity: SearchArtistResponseArtist) -> Match<ArtistMeta> {
|
fn from_search_artist_response_artist(entity: SearchArtistResponseArtist) -> Match<ArtistMeta> {
|
||||||
|
let sort: Option<ArtistId> = Some(entity.meta.sort_name)
|
||||||
|
.filter(|s| s != &entity.meta.name)
|
||||||
|
.map(Into::into);
|
||||||
Match {
|
Match {
|
||||||
score: entity.score,
|
score: entity.score,
|
||||||
item: ArtistMeta {
|
item: ArtistMeta {
|
||||||
id: entity.name,
|
id: entity.meta.name,
|
||||||
sort: entity.sort.map(Into::into),
|
sort,
|
||||||
musicbrainz: Some(entity.id.into()),
|
musicbrainz: Some(entity.meta.id.into()),
|
||||||
properties: HashMap::new(),
|
properties: HashMap::new(),
|
||||||
},
|
},
|
||||||
disambiguation: entity.disambiguation,
|
disambiguation: entity.meta.disambiguation,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,12 +143,12 @@ fn from_search_release_group_response_release_group(
|
|||||||
Match {
|
Match {
|
||||||
score: entity.score,
|
score: entity.score,
|
||||||
item: AlbumMeta {
|
item: AlbumMeta {
|
||||||
id: entity.title,
|
id: entity.meta.title,
|
||||||
date: entity.first_release_date,
|
date: entity.meta.first_release_date,
|
||||||
seq: AlbumSeq::default(),
|
seq: AlbumSeq::default(),
|
||||||
musicbrainz: Some(entity.id.into()),
|
musicbrainz: Some(entity.meta.id.into()),
|
||||||
primary_type: Some(entity.primary_type),
|
primary_type: Some(entity.meta.primary_type),
|
||||||
secondary_types: entity.secondary_types.unwrap_or_default(),
|
secondary_types: entity.meta.secondary_types.unwrap_or_default(),
|
||||||
},
|
},
|
||||||
disambiguation: None,
|
disambiguation: None,
|
||||||
}
|
}
|
||||||
|
178
src/tui/lib/external/musicbrainz/daemon/mod.rs
vendored
178
src/tui/lib/external/musicbrainz/daemon/mod.rs
vendored
@ -5,7 +5,7 @@ use crate::tui::{
|
|||||||
event::IFetchCompleteEventSender,
|
event::IFetchCompleteEventSender,
|
||||||
lib::interface::musicbrainz::{
|
lib::interface::musicbrainz::{
|
||||||
api::{Error as ApiError, IMusicBrainz},
|
api::{Error as ApiError, IMusicBrainz},
|
||||||
daemon::{Error, IMbJobSender, MbParams, ResultSender, SearchParams},
|
daemon::{Error, IMbJobSender, LookupParams, MbParams, ResultSender, SearchParams},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -35,7 +35,6 @@ impl Job {
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum JobPriority {
|
enum JobPriority {
|
||||||
#[cfg(test)]
|
|
||||||
Foreground,
|
Foreground,
|
||||||
Background,
|
Background,
|
||||||
}
|
}
|
||||||
@ -106,24 +105,24 @@ impl JobChannel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl IMbJobSender for JobSender {
|
impl IMbJobSender for JobSender {
|
||||||
fn submit_background_job(
|
fn submit_foreground_job(
|
||||||
&self,
|
&self,
|
||||||
result_sender: ResultSender,
|
result_sender: ResultSender,
|
||||||
requests: VecDeque<MbParams>,
|
requests: VecDeque<MbParams>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
self.send_background_job(result_sender, requests)
|
self.send_job(JobPriority::Foreground, result_sender, requests)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl JobSender {
|
fn submit_background_job(
|
||||||
fn send_background_job(
|
|
||||||
&self,
|
&self,
|
||||||
result_sender: ResultSender,
|
result_sender: ResultSender,
|
||||||
requests: VecDeque<MbParams>,
|
requests: VecDeque<MbParams>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
self.send_job(JobPriority::Background, result_sender, requests)
|
self.send_job(JobPriority::Background, result_sender, requests)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JobSender {
|
||||||
fn send_job(
|
fn send_job(
|
||||||
&self,
|
&self,
|
||||||
priority: JobPriority,
|
priority: JobPriority,
|
||||||
@ -239,20 +238,25 @@ impl JobInstance {
|
|||||||
event_sender: &mut dyn IFetchCompleteEventSender,
|
event_sender: &mut dyn IFetchCompleteEventSender,
|
||||||
api_params: MbParams,
|
api_params: MbParams,
|
||||||
) -> Result<(), JobInstanceError> {
|
) -> Result<(), JobInstanceError> {
|
||||||
match api_params {
|
let result = match api_params {
|
||||||
MbParams::Search(search) => match search {
|
MbParams::Lookup(lookup) => match lookup {
|
||||||
SearchParams::Artist(params) => {
|
LookupParams::Artist(params) => musicbrainz
|
||||||
let result = musicbrainz.search_artist(¶ms.artist);
|
.lookup_artist(¶ms.mbid)
|
||||||
let result = result.map(|list| MatchStateInfo::artist(params.artist, list));
|
.map(|rv| MatchStateInfo::artist_lookup(params.artist, rv)),
|
||||||
self.return_result(event_sender, result)
|
LookupParams::ReleaseGroup(params) => musicbrainz
|
||||||
}
|
.lookup_release_group(¶ms.mbid)
|
||||||
SearchParams::ReleaseGroup(params) => {
|
.map(|rv| MatchStateInfo::album_lookup(params.album, rv)),
|
||||||
let result = musicbrainz.search_release_group(¶ms.arid, ¶ms.album);
|
|
||||||
let result = result.map(|list| MatchStateInfo::album(params.album, list));
|
|
||||||
self.return_result(event_sender, result)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
MbParams::Search(search) => match search {
|
||||||
|
SearchParams::Artist(params) => musicbrainz
|
||||||
|
.search_artist(¶ms.artist)
|
||||||
|
.map(|rv| MatchStateInfo::artist_search(params.artist, rv)),
|
||||||
|
SearchParams::ReleaseGroup(params) => musicbrainz
|
||||||
|
.search_release_group(¶ms.arid, ¶ms.album)
|
||||||
|
.map(|rv| MatchStateInfo::album_search(params.album, rv)),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
self.return_result(event_sender, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn return_result(
|
fn return_result(
|
||||||
@ -298,22 +302,12 @@ impl JobQueue {
|
|||||||
.or_else(|| self.background_queue.pop_front())
|
.or_else(|| self.background_queue.pop_front())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
fn push_back(&mut self, job: Job) {
|
fn push_back(&mut self, job: Job) {
|
||||||
match job.priority {
|
match job.priority {
|
||||||
JobPriority::Foreground => self.foreground_queue.push_back(job.instance),
|
JobPriority::Foreground => self.foreground_queue.push_back(job.instance),
|
||||||
JobPriority::Background => self.background_queue.push_back(job.instance),
|
JobPriority::Background => self.background_queue.push_back(job.instance),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GRCOV_EXCL_START
|
|
||||||
#[cfg(not(test))]
|
|
||||||
fn push_back(&mut self, job: Job) {
|
|
||||||
match job.priority {
|
|
||||||
JobPriority::Background => self.background_queue.push_back(job.instance),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// GRCOV_EXCL_STOP
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -327,7 +321,7 @@ mod tests {
|
|||||||
|
|
||||||
use crate::tui::{
|
use crate::tui::{
|
||||||
event::{Event, EventError, MockIFetchCompleteEventSender},
|
event::{Event, EventError, MockIFetchCompleteEventSender},
|
||||||
lib::interface::musicbrainz::api::{Match, MockIMusicBrainz},
|
lib::interface::musicbrainz::api::{Lookup, Match, MockIMusicBrainz},
|
||||||
testmod::COLLECTION,
|
testmod::COLLECTION,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -377,12 +371,28 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn mbid() -> Mbid {
|
||||||
|
"00000000-0000-0000-0000-000000000000".try_into().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lookup_artist_requests() -> VecDeque<MbParams> {
|
||||||
|
let artist = COLLECTION[3].meta.clone();
|
||||||
|
let mbid = mbid();
|
||||||
|
VecDeque::from([MbParams::lookup_artist(artist, mbid)])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lookup_release_group_requests() -> VecDeque<MbParams> {
|
||||||
|
let album = COLLECTION[1].albums[0].meta.clone();
|
||||||
|
let mbid = mbid();
|
||||||
|
VecDeque::from([MbParams::lookup_release_group(album, mbid)])
|
||||||
|
}
|
||||||
|
|
||||||
fn search_artist_requests() -> VecDeque<MbParams> {
|
fn search_artist_requests() -> VecDeque<MbParams> {
|
||||||
let artist = COLLECTION[3].meta.clone();
|
let artist = COLLECTION[3].meta.clone();
|
||||||
VecDeque::from([MbParams::search_artist(artist)])
|
VecDeque::from([MbParams::search_artist(artist)])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn artist_expectations() -> (ArtistMeta, Vec<Match<ArtistMeta>>) {
|
fn search_artist_expectations() -> (ArtistMeta, Vec<Match<ArtistMeta>>) {
|
||||||
let artist = COLLECTION[3].meta.clone();
|
let artist = COLLECTION[3].meta.clone();
|
||||||
|
|
||||||
let artist_match_1 = Match::new(100, artist.clone());
|
let artist_match_1 = Match::new(100, artist.clone());
|
||||||
@ -410,7 +420,7 @@ mod tests {
|
|||||||
mbref.unwrap().mbid().clone()
|
mbref.unwrap().mbid().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn album_expectations_1() -> (AlbumMeta, Vec<Match<AlbumMeta>>) {
|
fn search_album_expectations_1() -> (AlbumMeta, Vec<Match<AlbumMeta>>) {
|
||||||
let album_1 = COLLECTION[1].albums[0].meta.clone();
|
let album_1 = COLLECTION[1].albums[0].meta.clone();
|
||||||
let album_4 = COLLECTION[1].albums[3].meta.clone();
|
let album_4 = COLLECTION[1].albums[3].meta.clone();
|
||||||
|
|
||||||
@ -421,7 +431,7 @@ mod tests {
|
|||||||
(album_1, matches_1)
|
(album_1, matches_1)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn album_expectations_4() -> (AlbumMeta, Vec<Match<AlbumMeta>>) {
|
fn search_album_expectations_4() -> (AlbumMeta, Vec<Match<AlbumMeta>>) {
|
||||||
let album_1 = COLLECTION[1].albums[0].meta.clone();
|
let album_1 = COLLECTION[1].albums[0].meta.clone();
|
||||||
let album_4 = COLLECTION[1].albums[3].meta.clone();
|
let album_4 = COLLECTION[1].albums[3].meta.clone();
|
||||||
|
|
||||||
@ -505,6 +515,90 @@ mod tests {
|
|||||||
assert_eq!(result, Err(JobError::JobQueueEmpty));
|
assert_eq!(result, Err(JobError::JobQueueEmpty));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn lookup_artist_expectation(
|
||||||
|
musicbrainz: &mut MockIMusicBrainz,
|
||||||
|
mbid: &Mbid,
|
||||||
|
lookup: &Lookup<ArtistMeta>,
|
||||||
|
) {
|
||||||
|
let result = Ok(lookup.clone());
|
||||||
|
musicbrainz
|
||||||
|
.expect_lookup_artist()
|
||||||
|
.with(predicate::eq(mbid.clone()))
|
||||||
|
.times(1)
|
||||||
|
.return_once(|_| result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn execute_lookup_artist() {
|
||||||
|
let mut musicbrainz = musicbrainz();
|
||||||
|
let mbid = mbid();
|
||||||
|
let artist = COLLECTION[3].meta.clone();
|
||||||
|
let lookup = Lookup::new(artist.clone());
|
||||||
|
lookup_artist_expectation(&mut musicbrainz, &mbid, &lookup);
|
||||||
|
|
||||||
|
let mut event_sender = event_sender();
|
||||||
|
fetch_complete_expectation(&mut event_sender, 1);
|
||||||
|
|
||||||
|
let (job_sender, job_receiver) = job_channel();
|
||||||
|
let mut daemon = daemon_with(musicbrainz, job_receiver, event_sender);
|
||||||
|
|
||||||
|
let requests = lookup_artist_requests();
|
||||||
|
let (result_sender, result_receiver) = mpsc::channel();
|
||||||
|
let result = job_sender.submit_foreground_job(result_sender, requests);
|
||||||
|
assert_eq!(result, Ok(()));
|
||||||
|
|
||||||
|
let result = daemon.enqueue_all_pending_jobs();
|
||||||
|
assert_eq!(result, Ok(()));
|
||||||
|
|
||||||
|
let result = daemon.execute_next_job();
|
||||||
|
assert_eq!(result, Ok(()));
|
||||||
|
|
||||||
|
let result = result_receiver.try_recv().unwrap();
|
||||||
|
assert_eq!(result, Ok(MatchStateInfo::artist_lookup(artist, lookup)));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lookup_release_group_expectation(
|
||||||
|
musicbrainz: &mut MockIMusicBrainz,
|
||||||
|
mbid: &Mbid,
|
||||||
|
lookup: &Lookup<AlbumMeta>,
|
||||||
|
) {
|
||||||
|
let result = Ok(lookup.clone());
|
||||||
|
musicbrainz
|
||||||
|
.expect_lookup_release_group()
|
||||||
|
.with(predicate::eq(mbid.clone()))
|
||||||
|
.times(1)
|
||||||
|
.return_once(|_| result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn execute_lookup_release_group() {
|
||||||
|
let mut musicbrainz = musicbrainz();
|
||||||
|
let mbid = mbid();
|
||||||
|
let album = COLLECTION[1].albums[0].meta.clone();
|
||||||
|
let lookup = Lookup::new(album.clone());
|
||||||
|
lookup_release_group_expectation(&mut musicbrainz, &mbid, &lookup);
|
||||||
|
|
||||||
|
let mut event_sender = event_sender();
|
||||||
|
fetch_complete_expectation(&mut event_sender, 1);
|
||||||
|
|
||||||
|
let (job_sender, job_receiver) = job_channel();
|
||||||
|
let mut daemon = daemon_with(musicbrainz, job_receiver, event_sender);
|
||||||
|
|
||||||
|
let requests = lookup_release_group_requests();
|
||||||
|
let (result_sender, result_receiver) = mpsc::channel();
|
||||||
|
let result = job_sender.submit_foreground_job(result_sender, requests);
|
||||||
|
assert_eq!(result, Ok(()));
|
||||||
|
|
||||||
|
let result = daemon.enqueue_all_pending_jobs();
|
||||||
|
assert_eq!(result, Ok(()));
|
||||||
|
|
||||||
|
let result = daemon.execute_next_job();
|
||||||
|
assert_eq!(result, Ok(()));
|
||||||
|
|
||||||
|
let result = result_receiver.try_recv().unwrap();
|
||||||
|
assert_eq!(result, Ok(MatchStateInfo::album_lookup(album, lookup)));
|
||||||
|
}
|
||||||
|
|
||||||
fn search_artist_expectation(
|
fn search_artist_expectation(
|
||||||
musicbrainz: &mut MockIMusicBrainz,
|
musicbrainz: &mut MockIMusicBrainz,
|
||||||
artist: &ArtistMeta,
|
artist: &ArtistMeta,
|
||||||
@ -521,7 +615,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn execute_search_artist() {
|
fn execute_search_artist() {
|
||||||
let mut musicbrainz = musicbrainz();
|
let mut musicbrainz = musicbrainz();
|
||||||
let (artist, matches) = artist_expectations();
|
let (artist, matches) = search_artist_expectations();
|
||||||
search_artist_expectation(&mut musicbrainz, &artist, &matches);
|
search_artist_expectation(&mut musicbrainz, &artist, &matches);
|
||||||
|
|
||||||
let mut event_sender = event_sender();
|
let mut event_sender = event_sender();
|
||||||
@ -542,7 +636,7 @@ 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(artist, matches)));
|
assert_eq!(result, Ok(MatchStateInfo::artist_search(artist, matches)));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn search_release_group_expectation(
|
fn search_release_group_expectation(
|
||||||
@ -565,8 +659,8 @@ mod tests {
|
|||||||
fn execute_search_release_groups() {
|
fn execute_search_release_groups() {
|
||||||
let mut musicbrainz = musicbrainz();
|
let mut musicbrainz = musicbrainz();
|
||||||
let arid = album_arid_expectation();
|
let arid = album_arid_expectation();
|
||||||
let (album_1, matches_1) = album_expectations_1();
|
let (album_1, matches_1) = search_album_expectations_1();
|
||||||
let (album_4, matches_4) = album_expectations_4();
|
let (album_4, matches_4) = search_album_expectations_4();
|
||||||
|
|
||||||
let mut seq = Sequence::new();
|
let mut seq = Sequence::new();
|
||||||
search_release_group_expectation(&mut musicbrainz, &mut seq, &arid, &album_1, &matches_1);
|
search_release_group_expectation(&mut musicbrainz, &mut seq, &arid, &album_1, &matches_1);
|
||||||
@ -593,17 +687,17 @@ 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::album(album_1, matches_1)));
|
assert_eq!(result, Ok(MatchStateInfo::album_search(album_1, matches_1)));
|
||||||
|
|
||||||
let result = result_receiver.try_recv().unwrap();
|
let result = result_receiver.try_recv().unwrap();
|
||||||
assert_eq!(result, Ok(MatchStateInfo::album(album_4, matches_4)));
|
assert_eq!(result, Ok(MatchStateInfo::album_search(album_4, matches_4)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn execute_search_release_groups_result_disconnect() {
|
fn execute_search_release_groups_result_disconnect() {
|
||||||
let mut musicbrainz = musicbrainz();
|
let mut musicbrainz = musicbrainz();
|
||||||
let arid = album_arid_expectation();
|
let arid = album_arid_expectation();
|
||||||
let (album_1, matches_1) = album_expectations_1();
|
let (album_1, matches_1) = search_album_expectations_1();
|
||||||
|
|
||||||
let mut seq = Sequence::new();
|
let mut seq = Sequence::new();
|
||||||
search_release_group_expectation(&mut musicbrainz, &mut seq, &arid, &album_1, &matches_1);
|
search_release_group_expectation(&mut musicbrainz, &mut seq, &arid, &album_1, &matches_1);
|
||||||
@ -633,7 +727,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn execute_search_artist_event_disconnect() {
|
fn execute_search_artist_event_disconnect() {
|
||||||
let mut musicbrainz = musicbrainz();
|
let mut musicbrainz = musicbrainz();
|
||||||
let (artist, matches) = artist_expectations();
|
let (artist, matches) = search_artist_expectations();
|
||||||
search_artist_expectation(&mut musicbrainz, &artist, &matches);
|
search_artist_expectation(&mut musicbrainz, &artist, &matches);
|
||||||
|
|
||||||
let mut event_sender = event_sender();
|
let mut event_sender = event_sender();
|
||||||
|
@ -8,6 +8,8 @@ use musichoard::collection::{album::AlbumMeta, artist::ArtistMeta, musicbrainz::
|
|||||||
/// Trait for interacting with the MusicBrainz API.
|
/// Trait for interacting with the MusicBrainz API.
|
||||||
#[cfg_attr(test, automock)]
|
#[cfg_attr(test, automock)]
|
||||||
pub trait IMusicBrainz {
|
pub trait IMusicBrainz {
|
||||||
|
fn lookup_artist(&mut self, mbid: &Mbid) -> Result<Lookup<ArtistMeta>, Error>;
|
||||||
|
fn lookup_release_group(&mut self, mbid: &Mbid) -> Result<Lookup<AlbumMeta>, Error>;
|
||||||
fn search_artist(&mut self, artist: &ArtistMeta) -> Result<Vec<Match<ArtistMeta>>, Error>;
|
fn search_artist(&mut self, artist: &ArtistMeta) -> Result<Vec<Match<ArtistMeta>>, Error>;
|
||||||
fn search_release_group(
|
fn search_release_group(
|
||||||
&mut self,
|
&mut self,
|
||||||
@ -23,4 +25,10 @@ pub struct Match<T> {
|
|||||||
pub disambiguation: Option<String>,
|
pub disambiguation: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct Lookup<T> {
|
||||||
|
pub item: T,
|
||||||
|
pub disambiguation: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
pub type Error = musichoard::external::musicbrainz::api::Error;
|
pub type Error = musichoard::external::musicbrainz::api::Error;
|
||||||
|
@ -27,6 +27,12 @@ pub type ResultSender = mpsc::Sender<MbApiResult>;
|
|||||||
|
|
||||||
#[cfg_attr(test, automock)]
|
#[cfg_attr(test, automock)]
|
||||||
pub trait IMbJobSender {
|
pub trait IMbJobSender {
|
||||||
|
fn submit_foreground_job(
|
||||||
|
&self,
|
||||||
|
result_sender: ResultSender,
|
||||||
|
requests: VecDeque<MbParams>,
|
||||||
|
) -> Result<(), Error>;
|
||||||
|
|
||||||
fn submit_background_job(
|
fn submit_background_job(
|
||||||
&self,
|
&self,
|
||||||
result_sender: ResultSender,
|
result_sender: ResultSender,
|
||||||
@ -36,9 +42,28 @@ pub trait IMbJobSender {
|
|||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum MbParams {
|
pub enum MbParams {
|
||||||
|
Lookup(LookupParams),
|
||||||
Search(SearchParams),
|
Search(SearchParams),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum LookupParams {
|
||||||
|
Artist(LookupArtistParams),
|
||||||
|
ReleaseGroup(LookupReleaseGroupParams),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct LookupArtistParams {
|
||||||
|
pub artist: ArtistMeta,
|
||||||
|
pub mbid: Mbid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct LookupReleaseGroupParams {
|
||||||
|
pub album: AlbumMeta,
|
||||||
|
pub mbid: Mbid,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum SearchParams {
|
pub enum SearchParams {
|
||||||
Artist(SearchArtistParams),
|
Artist(SearchArtistParams),
|
||||||
@ -57,6 +82,17 @@ pub struct SearchReleaseGroupParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MbParams {
|
impl MbParams {
|
||||||
|
pub fn lookup_artist(artist: ArtistMeta, mbid: Mbid) -> Self {
|
||||||
|
MbParams::Lookup(LookupParams::Artist(LookupArtistParams { artist, mbid }))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lookup_release_group(album: AlbumMeta, mbid: Mbid) -> Self {
|
||||||
|
MbParams::Lookup(LookupParams::ReleaseGroup(LookupReleaseGroupParams {
|
||||||
|
album,
|
||||||
|
mbid,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn search_artist(artist: ArtistMeta) -> Self {
|
pub fn search_artist(artist: ArtistMeta) -> Self {
|
||||||
MbParams::Search(SearchParams::Artist(SearchArtistParams { artist }))
|
MbParams::Search(SearchParams::Artist(SearchArtistParams { artist }))
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ use musichoard::collection::{
|
|||||||
track::{TrackFormat, TrackQuality},
|
track::{TrackFormat, TrackQuality},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::tui::app::{MatchOption, MatchStateInfo};
|
use crate::tui::app::{LookupOption, MatchStateInfo, MissOption, SearchOption};
|
||||||
|
|
||||||
pub struct UiDisplay;
|
pub struct UiDisplay;
|
||||||
|
|
||||||
@ -124,37 +124,69 @@ impl UiDisplay {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn display_artist_match(match_option: &MatchOption<ArtistMeta>) -> String {
|
pub fn display_search_option_artist(match_option: &SearchOption<ArtistMeta>) -> String {
|
||||||
match match_option {
|
match match_option {
|
||||||
MatchOption::Match(match_artist) => format!(
|
SearchOption::Match(match_artist) => format!(
|
||||||
"{}{} ({}%)",
|
"{} ({}%)",
|
||||||
&match_artist.item.id.name,
|
Self::display_option_artist(&match_artist.item, &match_artist.disambiguation),
|
||||||
&match_artist
|
|
||||||
.disambiguation
|
|
||||||
.as_ref()
|
|
||||||
.map(|d| format!(" ({d})"))
|
|
||||||
.unwrap_or_default(),
|
|
||||||
match_artist.score,
|
match_artist.score,
|
||||||
),
|
),
|
||||||
MatchOption::CannotHaveMbid => Self::display_cannot_have_mbid().to_string(),
|
SearchOption::None(miss) => Self::display_miss_option(miss).to_string(),
|
||||||
MatchOption::ManualInputMbid => Self::display_manual_input_mbid().to_string(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn display_album_match(match_option: &MatchOption<AlbumMeta>) -> String {
|
pub fn display_lookup_option_artist(lookup_option: &LookupOption<ArtistMeta>) -> String {
|
||||||
|
match lookup_option {
|
||||||
|
LookupOption::Match(match_artist) => {
|
||||||
|
Self::display_option_artist(&match_artist.item, &match_artist.disambiguation)
|
||||||
|
}
|
||||||
|
LookupOption::None(miss) => Self::display_miss_option(miss).to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_option_artist(artist: &ArtistMeta, disambiguation: &Option<String>) -> String {
|
||||||
|
format!(
|
||||||
|
"{}{}",
|
||||||
|
artist.id.name,
|
||||||
|
disambiguation
|
||||||
|
.as_ref()
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(|d| format!(" ({d})"))
|
||||||
|
.unwrap_or_default(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn display_search_option_album(match_option: &SearchOption<AlbumMeta>) -> String {
|
||||||
match match_option {
|
match match_option {
|
||||||
MatchOption::Match(match_album) => format!(
|
SearchOption::Match(match_album) => format!(
|
||||||
"{:010} | {} [{}] ({}%)",
|
"{} ({}%)",
|
||||||
UiDisplay::display_album_date(&match_album.item.date),
|
Self::display_option_album(&match_album.item),
|
||||||
&match_album.item.id.title,
|
|
||||||
UiDisplay::display_type(
|
|
||||||
&match_album.item.primary_type,
|
|
||||||
&match_album.item.secondary_types
|
|
||||||
),
|
|
||||||
match_album.score,
|
match_album.score,
|
||||||
),
|
),
|
||||||
MatchOption::CannotHaveMbid => Self::display_cannot_have_mbid().to_string(),
|
SearchOption::None(miss) => Self::display_miss_option(miss).to_string(),
|
||||||
MatchOption::ManualInputMbid => Self::display_manual_input_mbid().to_string(),
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn display_lookup_option_album(lookup_option: &LookupOption<AlbumMeta>) -> String {
|
||||||
|
match lookup_option {
|
||||||
|
LookupOption::Match(match_album) => Self::display_option_album(&match_album.item),
|
||||||
|
LookupOption::None(miss) => Self::display_miss_option(miss).to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_option_album(album: &AlbumMeta) -> String {
|
||||||
|
format!(
|
||||||
|
"{:010} | {} [{}]",
|
||||||
|
UiDisplay::display_album_date(&album.date),
|
||||||
|
album.id.title,
|
||||||
|
UiDisplay::display_type(&album.primary_type, &album.secondary_types),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_miss_option(miss_option: &MissOption) -> &'static str {
|
||||||
|
match miss_option {
|
||||||
|
MissOption::CannotHaveMbid => Self::display_cannot_have_mbid(),
|
||||||
|
MissOption::ManualInputMbid => Self::display_manual_input_mbid(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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::{ListOption, MatchStateInfo, WidgetState},
|
||||||
ui::display::UiDisplay,
|
ui::display::UiDisplay,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ pub struct MatchOverlay<'a, 'b> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'b> MatchOverlay<'a, 'b> {
|
impl<'a, 'b> MatchOverlay<'a, 'b> {
|
||||||
pub fn new(info: Option<&MatchStateInfo>, state: &'b mut WidgetState) -> Self {
|
pub fn new(info: Option<&'a MatchStateInfo>, state: &'b mut WidgetState) -> Self {
|
||||||
match info {
|
match info {
|
||||||
Some(info) => match info {
|
Some(info) => match info {
|
||||||
MatchStateInfo::Artist(m) => Self::artists(&m.matching, &m.list, state),
|
MatchStateInfo::Artist(m) => Self::artists(&m.matching, &m.list, state),
|
||||||
@ -33,18 +33,19 @@ impl<'a, 'b> MatchOverlay<'a, 'b> {
|
|||||||
|
|
||||||
fn artists(
|
fn artists(
|
||||||
matching: &ArtistMeta,
|
matching: &ArtistMeta,
|
||||||
matches: &[MatchOption<ArtistMeta>],
|
matches: &'a ListOption<ArtistMeta>,
|
||||||
state: &'b mut WidgetState,
|
state: &'b mut WidgetState,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let matching = UiDisplay::display_artist_matching(matching);
|
let matching = UiDisplay::display_artist_matching(matching);
|
||||||
|
|
||||||
let list = List::new(
|
let list = match matches {
|
||||||
matches
|
ListOption::Search(matches) => {
|
||||||
.iter()
|
Self::display_list(UiDisplay::display_search_option_artist, matches)
|
||||||
.map(UiDisplay::display_artist_match)
|
}
|
||||||
.map(ListItem::new)
|
ListOption::Lookup(matches) => {
|
||||||
.collect::<Vec<ListItem>>(),
|
Self::display_list(UiDisplay::display_lookup_option_artist, matches)
|
||||||
);
|
}
|
||||||
|
};
|
||||||
|
|
||||||
MatchOverlay {
|
MatchOverlay {
|
||||||
matching,
|
matching,
|
||||||
@ -55,18 +56,19 @@ impl<'a, 'b> MatchOverlay<'a, 'b> {
|
|||||||
|
|
||||||
fn albums(
|
fn albums(
|
||||||
matching: &AlbumMeta,
|
matching: &AlbumMeta,
|
||||||
matches: &[MatchOption<AlbumMeta>],
|
matches: &'a ListOption<AlbumMeta>,
|
||||||
state: &'b mut WidgetState,
|
state: &'b mut WidgetState,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let matching = UiDisplay::display_album_matching(matching);
|
let matching = UiDisplay::display_album_matching(matching);
|
||||||
|
|
||||||
let list = List::new(
|
let list = match matches {
|
||||||
matches
|
ListOption::Search(matches) => {
|
||||||
.iter()
|
Self::display_list(UiDisplay::display_search_option_album, matches)
|
||||||
.map(UiDisplay::display_album_match)
|
}
|
||||||
.map(ListItem::new)
|
ListOption::Lookup(matches) => {
|
||||||
.collect::<Vec<ListItem>>(),
|
Self::display_list(UiDisplay::display_lookup_option_album, matches)
|
||||||
);
|
}
|
||||||
|
};
|
||||||
|
|
||||||
MatchOverlay {
|
MatchOverlay {
|
||||||
matching,
|
matching,
|
||||||
@ -74,4 +76,17 @@ impl<'a, 'b> MatchOverlay<'a, 'b> {
|
|||||||
state,
|
state,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn display_list<F, T>(display: F, options: &[T]) -> List
|
||||||
|
where
|
||||||
|
F: FnMut(&T) -> String,
|
||||||
|
{
|
||||||
|
List::new(
|
||||||
|
options
|
||||||
|
.iter()
|
||||||
|
.map(display)
|
||||||
|
.map(ListItem::new)
|
||||||
|
.collect::<Vec<ListItem>>(),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -206,8 +206,8 @@ mod tests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::tui::{
|
use crate::tui::{
|
||||||
app::{AppPublic, AppPublicInner, Delta, MatchOption, MatchStatePublic},
|
app::{AppPublic, AppPublicInner, Delta, MatchStatePublic},
|
||||||
lib::interface::musicbrainz::api::Match,
|
lib::interface::musicbrainz::api::{Lookup, Match},
|
||||||
testmod::COLLECTION,
|
testmod::COLLECTION,
|
||||||
tests::terminal,
|
tests::terminal,
|
||||||
};
|
};
|
||||||
@ -250,20 +250,6 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn artist_matches(matching: ArtistMeta, list: Vec<Match<ArtistMeta>>) -> MatchStateInfo {
|
|
||||||
let mut list: Vec<MatchOption<ArtistMeta>> = list.into_iter().map(Into::into).collect();
|
|
||||||
list.push(MatchOption::CannotHaveMbid);
|
|
||||||
list.push(MatchOption::ManualInputMbid);
|
|
||||||
MatchStateInfo::artist(matching, list)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn album_matches(matching: AlbumMeta, list: Vec<Match<AlbumMeta>>) -> MatchStateInfo {
|
|
||||||
let mut list: Vec<MatchOption<AlbumMeta>> = list.into_iter().map(Into::into).collect();
|
|
||||||
list.push(MatchOption::CannotHaveMbid);
|
|
||||||
list.push(MatchOption::ManualInputMbid);
|
|
||||||
MatchStateInfo::album(matching, list)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_test_suite(collection: &Collection, selection: &mut Selection) {
|
fn draw_test_suite(collection: &Collection, selection: &mut Selection) {
|
||||||
let mut terminal = terminal();
|
let mut terminal = terminal();
|
||||||
|
|
||||||
@ -354,68 +340,83 @@ mod tests {
|
|||||||
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
|
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
fn artist_meta() -> ArtistMeta {
|
||||||
fn draw_artist_matches() {
|
ArtistMeta::new(ArtistId::new("an artist"))
|
||||||
let collection = &COLLECTION;
|
|
||||||
let mut selection = Selection::new(collection);
|
|
||||||
|
|
||||||
let mut terminal = terminal();
|
|
||||||
|
|
||||||
let artist = ArtistMeta::new(ArtistId::new("an artist"));
|
|
||||||
let artist_match = Match {
|
|
||||||
score: 80,
|
|
||||||
item: artist.clone(),
|
|
||||||
disambiguation: None,
|
|
||||||
};
|
|
||||||
let list = vec![artist_match.clone(), artist_match.clone()];
|
|
||||||
let artist_matches = artist_matches(artist, list);
|
|
||||||
|
|
||||||
let mut widget_state = WidgetState::default();
|
|
||||||
widget_state.list.select(Some(0));
|
|
||||||
|
|
||||||
let mut app = AppPublic {
|
|
||||||
inner: public_inner(collection, &mut selection),
|
|
||||||
state: AppState::Match(MatchStatePublic {
|
|
||||||
info: Some(&artist_matches),
|
|
||||||
state: &mut widget_state,
|
|
||||||
}),
|
|
||||||
input: None,
|
|
||||||
};
|
|
||||||
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
|
|
||||||
|
|
||||||
let input = tui_input::Input::default();
|
|
||||||
app.input = Some(&input);
|
|
||||||
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
fn artist_matches() -> MatchStateInfo {
|
||||||
fn draw_album_matches() {
|
let artist = artist_meta();
|
||||||
let collection = &COLLECTION;
|
let artist_match = Match::new(80, artist.clone());
|
||||||
let mut selection = Selection::new(collection);
|
let list = vec![artist_match.clone(), artist_match.clone()];
|
||||||
|
|
||||||
let mut terminal = terminal();
|
let mut info = MatchStateInfo::artist_search(artist, list);
|
||||||
|
info.push_cannot_have_mbid();
|
||||||
|
info.push_manual_input_mbid();
|
||||||
|
info
|
||||||
|
}
|
||||||
|
|
||||||
let album = AlbumMeta::new(
|
fn artist_lookup() -> MatchStateInfo {
|
||||||
|
let artist = artist_meta();
|
||||||
|
let artist_lookup = Lookup::new(artist.clone());
|
||||||
|
|
||||||
|
let mut info = MatchStateInfo::artist_lookup(artist, artist_lookup);
|
||||||
|
info.push_cannot_have_mbid();
|
||||||
|
info.push_manual_input_mbid();
|
||||||
|
info
|
||||||
|
}
|
||||||
|
|
||||||
|
fn album_meta() -> AlbumMeta {
|
||||||
|
AlbumMeta::new(
|
||||||
AlbumId::new("An Album"),
|
AlbumId::new("An Album"),
|
||||||
AlbumDate::new(Some(1990), Some(5), None),
|
AlbumDate::new(Some(1990), Some(5), None),
|
||||||
Some(AlbumPrimaryType::Album),
|
Some(AlbumPrimaryType::Album),
|
||||||
vec![AlbumSecondaryType::Live, AlbumSecondaryType::Compilation],
|
vec![AlbumSecondaryType::Live, AlbumSecondaryType::Compilation],
|
||||||
);
|
)
|
||||||
let album_match = Match {
|
}
|
||||||
score: 80,
|
|
||||||
item: album.clone(),
|
|
||||||
disambiguation: None,
|
|
||||||
};
|
|
||||||
let list = vec![album_match.clone(), album_match.clone()];
|
|
||||||
let album_matches = album_matches(album, list);
|
|
||||||
|
|
||||||
|
fn album_matches() -> MatchStateInfo {
|
||||||
|
let album = album_meta();
|
||||||
|
let album_match = Match::new(80, album.clone());
|
||||||
|
let list = vec![album_match.clone(), album_match.clone()];
|
||||||
|
|
||||||
|
let mut info = MatchStateInfo::album_search(album, list);
|
||||||
|
info.push_cannot_have_mbid();
|
||||||
|
info.push_manual_input_mbid();
|
||||||
|
info
|
||||||
|
}
|
||||||
|
|
||||||
|
fn album_lookup() -> MatchStateInfo {
|
||||||
|
let album = album_meta();
|
||||||
|
let album_lookup = Lookup::new(album.clone());
|
||||||
|
|
||||||
|
let mut info = MatchStateInfo::album_lookup(album, album_lookup);
|
||||||
|
info.push_cannot_have_mbid();
|
||||||
|
info.push_manual_input_mbid();
|
||||||
|
info
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn draw_matche_state_suite() {
|
||||||
|
let collection = &COLLECTION;
|
||||||
|
let mut selection = Selection::new(collection);
|
||||||
|
|
||||||
|
let mut terminal = terminal();
|
||||||
|
|
||||||
|
let match_state_infos = vec![
|
||||||
|
artist_matches(),
|
||||||
|
album_matches(),
|
||||||
|
artist_lookup(),
|
||||||
|
album_lookup(),
|
||||||
|
];
|
||||||
|
|
||||||
|
for info in match_state_infos.iter() {
|
||||||
let mut widget_state = WidgetState::default();
|
let mut widget_state = WidgetState::default();
|
||||||
widget_state.list.select(Some(0));
|
widget_state.list.select(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: Some(&album_matches),
|
info: Some(info),
|
||||||
state: &mut widget_state,
|
state: &mut widget_state,
|
||||||
}),
|
}),
|
||||||
input: None,
|
input: None,
|
||||||
@ -426,4 +427,5 @@ mod tests {
|
|||||||
app.input = Some(&input);
|
app.input = Some(&input);
|
||||||
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
|
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user