Make fetch also fetch artist MBID if it is missing #201
5
Cargo.lock
generated
5
Cargo.lock
generated
@ -645,6 +645,7 @@ dependencies = [
|
||||
"mockall",
|
||||
"once_cell",
|
||||
"openssh",
|
||||
"paste",
|
||||
"ratatui",
|
||||
"reqwest",
|
||||
"serde",
|
||||
@ -812,9 +813,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.14"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
|
@ -10,6 +10,7 @@ aho-corasick = { version = "1.1.2", optional = true }
|
||||
crossterm = { version = "0.27.0", optional = true}
|
||||
once_cell = { version = "1.19.0", optional = true}
|
||||
openssh = { version = "0.10.3", features = ["native-mux"], default-features = false, optional = true}
|
||||
paste = { version = "1.0.15", optional = true }
|
||||
ratatui = { version = "0.26.0", optional = true}
|
||||
reqwest = { version = "0.11.25", features = ["blocking", "json"], optional = true }
|
||||
serde = { version = "1.0.196", features = ["derive"], optional = true }
|
||||
@ -33,7 +34,7 @@ bin = ["structopt"]
|
||||
database-json = ["serde", "serde_json"]
|
||||
library-beets = []
|
||||
library-beets-ssh = ["openssh", "tokio"]
|
||||
musicbrainz = ["reqwest", "serde", "serde_json"]
|
||||
musicbrainz = ["paste", "reqwest", "serde", "serde_json"]
|
||||
tui = ["aho-corasick", "crossterm", "once_cell", "ratatui"]
|
||||
|
||||
[[bin]]
|
||||
@ -50,8 +51,8 @@ path = "examples/musicbrainz_api/lookup_artist.rs"
|
||||
required-features = ["bin", "musicbrainz"]
|
||||
|
||||
[[example]]
|
||||
name = "musicbrainz-api---search-release-group"
|
||||
path = "examples/musicbrainz_api/search_release_group.rs"
|
||||
name = "musicbrainz-api---search"
|
||||
path = "examples/musicbrainz_api/search.rs"
|
||||
required-features = ["bin", "musicbrainz"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
|
150
examples/musicbrainz_api/search.rs
Normal file
150
examples/musicbrainz_api/search.rs
Normal file
@ -0,0 +1,150 @@
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use std::{num::ParseIntError, str::FromStr};
|
||||
|
||||
use musichoard::{
|
||||
collection::{album::AlbumDate, musicbrainz::Mbid},
|
||||
external::musicbrainz::{
|
||||
api::{
|
||||
search::{SearchArtistRequest, SearchReleaseGroupRequest},
|
||||
MusicBrainzClient,
|
||||
},
|
||||
http::MusicBrainzHttp,
|
||||
},
|
||||
};
|
||||
use structopt::StructOpt;
|
||||
use uuid::Uuid;
|
||||
|
||||
const USER_AGENT: &str = concat!(
|
||||
"MusicHoard---examples---musicbrainz-api---search/",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
" ( musichoard@thenineworlds.net )"
|
||||
);
|
||||
|
||||
#[derive(StructOpt)]
|
||||
struct Opt {
|
||||
#[structopt(subcommand)]
|
||||
entity: OptEntity,
|
||||
}
|
||||
|
||||
#[derive(StructOpt)]
|
||||
enum OptEntity {
|
||||
#[structopt(about = "Search artist")]
|
||||
Artist(OptArtist),
|
||||
#[structopt(about = "Search release group")]
|
||||
ReleaseGroup(OptReleaseGroup),
|
||||
}
|
||||
|
||||
#[derive(StructOpt)]
|
||||
struct OptArtist {
|
||||
#[structopt(help = "Artist search string")]
|
||||
string: String,
|
||||
}
|
||||
|
||||
#[derive(StructOpt)]
|
||||
enum OptReleaseGroup {
|
||||
#[structopt(about = "Search by artist MBID, title(, and date)")]
|
||||
Title(OptReleaseGroupTitle),
|
||||
#[structopt(about = "Search by release group MBID")]
|
||||
Rgid(OptReleaseGroupRgid),
|
||||
}
|
||||
|
||||
#[derive(StructOpt)]
|
||||
struct OptReleaseGroupTitle {
|
||||
#[structopt(help = "Release group's artist MBID")]
|
||||
arid: Uuid,
|
||||
|
||||
#[structopt(help = "Release group title")]
|
||||
title: String,
|
||||
|
||||
#[structopt(help = "Release group release date")]
|
||||
date: Option<Date>,
|
||||
}
|
||||
|
||||
#[derive(StructOpt)]
|
||||
struct OptReleaseGroupRgid {
|
||||
#[structopt(help = "Release group MBID")]
|
||||
rgid: Uuid,
|
||||
}
|
||||
|
||||
struct Date(AlbumDate);
|
||||
|
||||
impl FromStr for Date {
|
||||
type Err = ParseIntError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut elems = s.split('-');
|
||||
|
||||
let elem = elems.next();
|
||||
let year = elem.map(|s| s.parse()).transpose()?;
|
||||
|
||||
let elem = elems.next();
|
||||
let month = elem.map(|s| s.parse()).transpose()?;
|
||||
|
||||
let elem = elems.next();
|
||||
let day = elem.map(|s| s.parse()).transpose()?;
|
||||
|
||||
Ok(Date(AlbumDate::new(year, month, day)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Date> for AlbumDate {
|
||||
fn from(value: Date) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let opt = Opt::from_args();
|
||||
|
||||
println!("USER_AGENT: {USER_AGENT}");
|
||||
|
||||
let http = MusicBrainzHttp::new(USER_AGENT).expect("failed to create API client");
|
||||
let mut client = MusicBrainzClient::new(http);
|
||||
|
||||
match opt.entity {
|
||||
OptEntity::Artist(opt_artist) => {
|
||||
let query = SearchArtistRequest::new().string(&opt_artist.string);
|
||||
|
||||
println!("Query: {query}");
|
||||
|
||||
let matches = client
|
||||
.search_artist(query)
|
||||
.expect("failed to make API call");
|
||||
|
||||
println!("{matches:#?}");
|
||||
}
|
||||
OptEntity::ReleaseGroup(opt_release_group) => {
|
||||
let arid: Mbid;
|
||||
let date: AlbumDate;
|
||||
let title: String;
|
||||
let rgid: Mbid;
|
||||
|
||||
let query = match opt_release_group {
|
||||
OptReleaseGroup::Title(opt_title) => {
|
||||
arid = opt_title.arid.into();
|
||||
date = opt_title.date.map(Into::into).unwrap_or_default();
|
||||
title = opt_title.title;
|
||||
SearchReleaseGroupRequest::new()
|
||||
.arid(&arid)
|
||||
.and()
|
||||
.release_group(&title)
|
||||
.and()
|
||||
.first_release_date(&date)
|
||||
}
|
||||
OptReleaseGroup::Rgid(opt_rgid) => {
|
||||
rgid = opt_rgid.rgid.into();
|
||||
SearchReleaseGroupRequest::new().rgid(&rgid)
|
||||
}
|
||||
};
|
||||
|
||||
println!("Query: {query}");
|
||||
|
||||
let matches = client
|
||||
.search_release_group(query)
|
||||
.expect("failed to make API call");
|
||||
|
||||
println!("{matches:#?}");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,114 +0,0 @@
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use std::{num::ParseIntError, str::FromStr};
|
||||
|
||||
use musichoard::{
|
||||
collection::{album::AlbumDate, musicbrainz::Mbid},
|
||||
external::musicbrainz::{
|
||||
api::{search::SearchReleaseGroupRequest, MusicBrainzClient},
|
||||
http::MusicBrainzHttp,
|
||||
},
|
||||
};
|
||||
use structopt::StructOpt;
|
||||
use uuid::Uuid;
|
||||
|
||||
const USER_AGENT: &str = concat!(
|
||||
"MusicHoard---examples---musicbrainz-api---search-release-group/",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
" ( musichoard@thenineworlds.net )"
|
||||
);
|
||||
|
||||
#[derive(StructOpt)]
|
||||
struct Opt {
|
||||
#[structopt(subcommand)]
|
||||
command: OptCommand,
|
||||
}
|
||||
|
||||
#[derive(StructOpt)]
|
||||
enum OptCommand {
|
||||
#[structopt(about = "Search by artist MBID, title(, and date)")]
|
||||
Title(OptTitle),
|
||||
#[structopt(about = "Search by release group MBID")]
|
||||
Rgid(OptRgid),
|
||||
}
|
||||
|
||||
#[derive(StructOpt)]
|
||||
struct OptTitle {
|
||||
#[structopt(help = "Release group's artist MBID")]
|
||||
arid: Uuid,
|
||||
|
||||
#[structopt(help = "Release group title")]
|
||||
title: String,
|
||||
|
||||
#[structopt(help = "Release group release date")]
|
||||
date: Option<Date>,
|
||||
}
|
||||
|
||||
#[derive(StructOpt)]
|
||||
struct OptRgid {
|
||||
#[structopt(help = "Release group MBID")]
|
||||
rgid: Uuid,
|
||||
}
|
||||
|
||||
struct Date(AlbumDate);
|
||||
|
||||
impl FromStr for Date {
|
||||
type Err = ParseIntError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut elems = s.split('-');
|
||||
|
||||
let elem = elems.next();
|
||||
let year = elem.map(|s| s.parse()).transpose()?;
|
||||
|
||||
let elem = elems.next();
|
||||
let month = elem.map(|s| s.parse()).transpose()?;
|
||||
|
||||
let elem = elems.next();
|
||||
let day = elem.map(|s| s.parse()).transpose()?;
|
||||
|
||||
Ok(Date(AlbumDate::new(year, month, day)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Date> for AlbumDate {
|
||||
fn from(value: Date) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let opt = Opt::from_args();
|
||||
|
||||
println!("USER_AGENT: {USER_AGENT}");
|
||||
|
||||
let http = MusicBrainzHttp::new(USER_AGENT).expect("failed to create API client");
|
||||
let mut client = MusicBrainzClient::new(http);
|
||||
|
||||
let mut request = SearchReleaseGroupRequest::default();
|
||||
let arid: Mbid;
|
||||
let date: AlbumDate;
|
||||
let title: String;
|
||||
let rgid: Mbid;
|
||||
match opt.command {
|
||||
OptCommand::Title(opt_title) => {
|
||||
arid = opt_title.arid.into();
|
||||
date = opt_title.date.map(Into::into).unwrap_or_default();
|
||||
title = opt_title.title;
|
||||
request
|
||||
.arid(&arid)
|
||||
.first_release_date(&date)
|
||||
.release_group(&title);
|
||||
}
|
||||
OptCommand::Rgid(opt_rgid) => {
|
||||
rgid = opt_rgid.rgid.into();
|
||||
request.rgid(&rgid);
|
||||
}
|
||||
};
|
||||
|
||||
let matches = client
|
||||
.search_release_group(request)
|
||||
.expect("failed to make API call");
|
||||
|
||||
println!("{matches:#?}");
|
||||
}
|
33
src/external/musicbrainz/api/mod.rs
vendored
33
src/external/musicbrainz/api/mod.rs
vendored
@ -56,17 +56,21 @@ impl<Http> MusicBrainzClient<Http> {
|
||||
pub fn new(http: Http) -> Self {
|
||||
MusicBrainzClient { http }
|
||||
}
|
||||
}
|
||||
|
||||
fn format_album_date(date: &AlbumDate) -> Option<String> {
|
||||
pub struct ApiDisplay;
|
||||
|
||||
impl ApiDisplay {
|
||||
fn format_album_date(date: &AlbumDate) -> String {
|
||||
match date.year {
|
||||
Some(year) => match date.month {
|
||||
Some(month) => match date.day {
|
||||
Some(day) => Some(format!("{year}-{month:02}-{day:02}")),
|
||||
None => Some(format!("{year}-{month:02}")),
|
||||
Some(day) => format!("{year}-{month:02}-{day:02}"),
|
||||
None => format!("{year}-{month:02}"),
|
||||
},
|
||||
None => Some(format!("{year}")),
|
||||
None => format!("{year}"),
|
||||
},
|
||||
None => None,
|
||||
None => String::from("*"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -241,22 +245,15 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn format_album_date() {
|
||||
struct Null;
|
||||
assert_eq!(
|
||||
MusicBrainzClient::<Null>::format_album_date(&AlbumDate::new(None, None, None)),
|
||||
None
|
||||
ApiDisplay::format_album_date(&AlbumDate::new(None, None, None)),
|
||||
"*"
|
||||
);
|
||||
assert_eq!(ApiDisplay::format_album_date(&(1986).into()), "1986");
|
||||
assert_eq!(ApiDisplay::format_album_date(&(1986, 4).into()), "1986-04");
|
||||
assert_eq!(
|
||||
MusicBrainzClient::<Null>::format_album_date(&(1986).into()),
|
||||
Some(String::from("1986"))
|
||||
);
|
||||
assert_eq!(
|
||||
MusicBrainzClient::<Null>::format_album_date(&(1986, 4).into()),
|
||||
Some(String::from("1986-04"))
|
||||
);
|
||||
assert_eq!(
|
||||
MusicBrainzClient::<Null>::format_album_date(&(1986, 4, 21).into()),
|
||||
Some(String::from("1986-04-21"))
|
||||
ApiDisplay::format_album_date(&(1986, 4, 21).into()),
|
||||
"1986-04-21"
|
||||
);
|
||||
}
|
||||
|
||||
|
269
src/external/musicbrainz/api/search.rs
vendored
269
src/external/musicbrainz/api/search.rs
vendored
@ -1,269 +0,0 @@
|
||||
//! Module for interacting with the [MusicBrainz API](https://musicbrainz.org/doc/MusicBrainz_API).
|
||||
|
||||
use serde::Deserialize;
|
||||
use url::form_urlencoded;
|
||||
|
||||
use crate::{
|
||||
collection::{album::AlbumDate, musicbrainz::Mbid},
|
||||
core::collection::album::{AlbumPrimaryType, AlbumSecondaryType},
|
||||
external::musicbrainz::{
|
||||
api::{
|
||||
Error, MusicBrainzClient, SerdeAlbumDate, SerdeAlbumPrimaryType,
|
||||
SerdeAlbumSecondaryType, SerdeMbid, MB_BASE_URL,
|
||||
},
|
||||
IMusicBrainzHttp,
|
||||
},
|
||||
};
|
||||
|
||||
impl<Http: IMusicBrainzHttp> MusicBrainzClient<Http> {
|
||||
pub fn search_release_group(
|
||||
&mut self,
|
||||
request: SearchReleaseGroupRequest,
|
||||
) -> Result<SearchReleaseGroupResponse, Error> {
|
||||
let mut query: Vec<String> = vec![];
|
||||
|
||||
if let Some(arid) = request.arid {
|
||||
query.push(format!("arid:{}", arid.uuid().as_hyphenated()));
|
||||
}
|
||||
|
||||
if let Some(date) = request.first_release_date {
|
||||
if let Some(date_string) = Self::format_album_date(date) {
|
||||
query.push(format!("firstreleasedate:{date_string}"))
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(release_group) = request.release_group {
|
||||
query.push(format!("releasegroup:\"{release_group}\""));
|
||||
}
|
||||
|
||||
if let Some(rgid) = request.rgid {
|
||||
query.push(format!("rgid:{}", rgid.uuid().as_hyphenated()));
|
||||
}
|
||||
|
||||
let query: String =
|
||||
form_urlencoded::byte_serialize(query.join(" AND ").as_bytes()).collect();
|
||||
let url = format!("{MB_BASE_URL}/release-group?query={query}");
|
||||
|
||||
let response: DeserializeSearchReleaseGroupResponse = self.http.get(&url)?;
|
||||
Ok(response.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SearchReleaseGroupRequest<'a> {
|
||||
arid: Option<&'a Mbid>,
|
||||
first_release_date: Option<&'a AlbumDate>,
|
||||
release_group: Option<&'a str>,
|
||||
rgid: Option<&'a Mbid>,
|
||||
}
|
||||
|
||||
impl<'a> SearchReleaseGroupRequest<'a> {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn arid(&mut self, arid: &'a Mbid) -> &mut Self {
|
||||
self.arid = Some(arid);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn first_release_date(&mut self, first_release_date: &'a AlbumDate) -> &mut Self {
|
||||
self.first_release_date = Some(first_release_date);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn release_group(&mut self, release_group: &'a str) -> &mut Self {
|
||||
self.release_group = Some(release_group);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn rgid(&mut self, rgid: &'a Mbid) -> &mut Self {
|
||||
self.rgid = Some(rgid);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct SearchReleaseGroupResponse {
|
||||
pub release_groups: Vec<SearchReleaseGroupResponseReleaseGroup>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
#[serde(rename_all(deserialize = "kebab-case"))]
|
||||
struct DeserializeSearchReleaseGroupResponse {
|
||||
release_groups: Vec<DeserializeSearchReleaseGroupResponseReleaseGroup>,
|
||||
}
|
||||
|
||||
impl From<DeserializeSearchReleaseGroupResponse> for SearchReleaseGroupResponse {
|
||||
fn from(value: DeserializeSearchReleaseGroupResponse) -> Self {
|
||||
SearchReleaseGroupResponse {
|
||||
release_groups: value.release_groups.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SearchReleaseGroupResponseReleaseGroup {
|
||||
pub score: u8,
|
||||
pub id: Mbid,
|
||||
pub title: String,
|
||||
pub first_release_date: AlbumDate,
|
||||
pub primary_type: AlbumPrimaryType,
|
||||
pub secondary_types: Option<Vec<AlbumSecondaryType>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
#[serde(rename_all(deserialize = "kebab-case"))]
|
||||
struct DeserializeSearchReleaseGroupResponseReleaseGroup {
|
||||
score: u8,
|
||||
id: SerdeMbid,
|
||||
title: String,
|
||||
first_release_date: SerdeAlbumDate,
|
||||
primary_type: SerdeAlbumPrimaryType,
|
||||
secondary_types: Option<Vec<SerdeAlbumSecondaryType>>,
|
||||
}
|
||||
|
||||
impl From<DeserializeSearchReleaseGroupResponseReleaseGroup>
|
||||
for SearchReleaseGroupResponseReleaseGroup
|
||||
{
|
||||
fn from(value: DeserializeSearchReleaseGroupResponseReleaseGroup) -> Self {
|
||||
SearchReleaseGroupResponseReleaseGroup {
|
||||
score: value.score,
|
||||
id: value.id.into(),
|
||||
title: value.title,
|
||||
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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use mockall::{predicate, Sequence};
|
||||
|
||||
use crate::{collection::album::AlbumId, external::musicbrainz::MockIMusicBrainzHttp};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn search_release_group() {
|
||||
let mut http = MockIMusicBrainzHttp::new();
|
||||
let url_title = format!(
|
||||
"https://musicbrainz.org/ws/2\
|
||||
/release-group\
|
||||
?query=arid%3A{arid}+AND+firstreleasedate%3A{date}+AND+releasegroup%3A%22{title}%22",
|
||||
arid = "00000000-0000-0000-0000-000000000000",
|
||||
date = "1986-04",
|
||||
title = "an+album",
|
||||
);
|
||||
let url_rgid = format!(
|
||||
"https://musicbrainz.org/ws/2\
|
||||
/release-group\
|
||||
?query=rgid%3A{rgid}",
|
||||
rgid = "11111111-1111-1111-1111-111111111111",
|
||||
);
|
||||
|
||||
let de_release_group = DeserializeSearchReleaseGroupResponseReleaseGroup {
|
||||
score: 67,
|
||||
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::Live)]),
|
||||
};
|
||||
let de_response = DeserializeSearchReleaseGroupResponse {
|
||||
release_groups: vec![de_release_group.clone()],
|
||||
};
|
||||
|
||||
let release_group = SearchReleaseGroupResponseReleaseGroup {
|
||||
score: 67,
|
||||
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
|
||||
.map(|v| v.into_iter().map(|st| st.0).collect()),
|
||||
};
|
||||
let response = SearchReleaseGroupResponse {
|
||||
release_groups: vec![release_group.clone()],
|
||||
};
|
||||
|
||||
let mut seq = Sequence::new();
|
||||
|
||||
let title_response = de_response.clone();
|
||||
http.expect_get()
|
||||
.times(1)
|
||||
.with(predicate::eq(url_title))
|
||||
.return_once(|_| Ok(title_response))
|
||||
.in_sequence(&mut seq);
|
||||
|
||||
let rgid_response = de_response;
|
||||
http.expect_get()
|
||||
.times(1)
|
||||
.with(predicate::eq(url_rgid))
|
||||
.return_once(|_| Ok(rgid_response))
|
||||
.in_sequence(&mut seq);
|
||||
|
||||
let mut client = MusicBrainzClient::new(http);
|
||||
|
||||
let arid: Mbid = "00000000-0000-0000-0000-000000000000".try_into().unwrap();
|
||||
let title: AlbumId = AlbumId::new("an album");
|
||||
let date = (1986, 4).into();
|
||||
|
||||
let mut request = SearchReleaseGroupRequest::new();
|
||||
request
|
||||
.arid(&arid)
|
||||
.release_group(&title.title)
|
||||
.first_release_date(&date);
|
||||
|
||||
let matches = client.search_release_group(request).unwrap();
|
||||
assert_eq!(matches, response);
|
||||
|
||||
let rgid: Mbid = "11111111-1111-1111-1111-111111111111".try_into().unwrap();
|
||||
|
||||
let mut request = SearchReleaseGroupRequest::new();
|
||||
request.rgid(&rgid);
|
||||
|
||||
let matches = client.search_release_group(request).unwrap();
|
||||
assert_eq!(matches, response);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_release_group_empty_date() {
|
||||
let mut http = MockIMusicBrainzHttp::new();
|
||||
let url = format!(
|
||||
"https://musicbrainz.org/ws/2\
|
||||
/release-group\
|
||||
?query=arid%3A{arid}+AND+releasegroup%3A%22{title}%22",
|
||||
arid = "00000000-0000-0000-0000-000000000000",
|
||||
title = "an+album",
|
||||
);
|
||||
|
||||
let de_response = DeserializeSearchReleaseGroupResponse {
|
||||
release_groups: vec![],
|
||||
};
|
||||
|
||||
http.expect_get()
|
||||
.times(1)
|
||||
.with(predicate::eq(url))
|
||||
.return_once(|_| Ok(de_response));
|
||||
|
||||
let mut client = MusicBrainzClient::new(http);
|
||||
|
||||
let arid: Mbid = "00000000-0000-0000-0000-000000000000".try_into().unwrap();
|
||||
let title: AlbumId = AlbumId::new("an album");
|
||||
let date = AlbumDate::default();
|
||||
|
||||
let mut request = SearchReleaseGroupRequest::new();
|
||||
request
|
||||
.arid(&arid)
|
||||
.release_group(&title.title)
|
||||
.first_release_date(&date);
|
||||
|
||||
let _ = client.search_release_group(request).unwrap();
|
||||
}
|
||||
}
|
145
src/external/musicbrainz/api/search/artist.rs
vendored
Normal file
145
src/external/musicbrainz/api/search/artist.rs
vendored
Normal file
@ -0,0 +1,145 @@
|
||||
use std::fmt;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
collection::{artist::ArtistId, musicbrainz::Mbid},
|
||||
external::musicbrainz::api::SerdeMbid,
|
||||
};
|
||||
|
||||
use super::query::{impl_term, EmptyQuery, EmptyQueryJoin, Query, QueryJoin};
|
||||
|
||||
pub enum SearchArtist<'a> {
|
||||
String(&'a str),
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for SearchArtist<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::String(s) => write!(f, "\"{s}\""),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type SearchArtistRequest<'a> = Query<SearchArtist<'a>>;
|
||||
|
||||
impl_term!(string, SearchArtist<'a>, String, &'a str);
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct SearchArtistResponse {
|
||||
pub artists: Vec<SearchArtistResponseArtist>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
#[serde(rename_all(deserialize = "kebab-case"))]
|
||||
pub struct DeserializeSearchArtistResponse {
|
||||
artists: Vec<DeserializeSearchArtistResponseArtist>,
|
||||
}
|
||||
|
||||
impl From<DeserializeSearchArtistResponse> for SearchArtistResponse {
|
||||
fn from(value: DeserializeSearchArtistResponse) -> Self {
|
||||
SearchArtistResponse {
|
||||
artists: value.artists.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SearchArtistResponseArtist {
|
||||
pub score: u8,
|
||||
pub id: Mbid,
|
||||
pub name: ArtistId,
|
||||
pub sort: Option<ArtistId>,
|
||||
pub disambiguation: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
#[serde(rename_all(deserialize = "kebab-case"))]
|
||||
struct DeserializeSearchArtistResponseArtist {
|
||||
score: u8,
|
||||
id: SerdeMbid,
|
||||
name: String,
|
||||
sort_name: String,
|
||||
disambiguation: Option<String>,
|
||||
}
|
||||
|
||||
impl From<DeserializeSearchArtistResponseArtist> for SearchArtistResponseArtist {
|
||||
fn from(value: DeserializeSearchArtistResponseArtist) -> Self {
|
||||
let sort: Option<ArtistId> = Some(value.sort_name)
|
||||
.filter(|s| s != &value.name)
|
||||
.map(Into::into);
|
||||
SearchArtistResponseArtist {
|
||||
score: value.score,
|
||||
id: value.id.into(),
|
||||
name: value.name.into(),
|
||||
sort,
|
||||
disambiguation: value.disambiguation,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use mockall::predicate;
|
||||
|
||||
use crate::external::musicbrainz::{api::MusicBrainzClient, MockIMusicBrainzHttp};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn de_response() -> DeserializeSearchArtistResponse {
|
||||
let de_artist = DeserializeSearchArtistResponseArtist {
|
||||
score: 67,
|
||||
id: SerdeMbid("11111111-1111-1111-1111-111111111111".try_into().unwrap()),
|
||||
name: String::from("an artist"),
|
||||
sort_name: String::from("artist, an"),
|
||||
disambiguation: None,
|
||||
};
|
||||
DeserializeSearchArtistResponse {
|
||||
artists: vec![de_artist.clone()],
|
||||
}
|
||||
}
|
||||
|
||||
fn response(de_response: DeserializeSearchArtistResponse) -> SearchArtistResponse {
|
||||
SearchArtistResponse {
|
||||
artists: de_response
|
||||
.artists
|
||||
.into_iter()
|
||||
.map(|a| SearchArtistResponseArtist {
|
||||
score: 67,
|
||||
id: a.id.0,
|
||||
name: a.name.clone().into(),
|
||||
sort: Some(a.sort_name).filter(|sn| sn != &a.name).map(Into::into),
|
||||
disambiguation: a.disambiguation,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_string() {
|
||||
let mut http = MockIMusicBrainzHttp::new();
|
||||
let url = format!(
|
||||
"https://musicbrainz.org/ws/2\
|
||||
/artist\
|
||||
?query=%22{no_field}%22",
|
||||
no_field = "an+artist",
|
||||
);
|
||||
|
||||
let de_response = de_response();
|
||||
let response = response(de_response.clone());
|
||||
|
||||
http.expect_get()
|
||||
.times(1)
|
||||
.with(predicate::eq(url))
|
||||
.return_once(|_| Ok(de_response));
|
||||
|
||||
let mut client = MusicBrainzClient::new(http);
|
||||
|
||||
let name = "an artist";
|
||||
|
||||
let query = SearchArtistRequest::new().string(name);
|
||||
|
||||
let matches = client.search_artist(query).unwrap();
|
||||
assert_eq!(matches, response);
|
||||
}
|
||||
}
|
46
src/external/musicbrainz/api/search/mod.rs
vendored
Normal file
46
src/external/musicbrainz/api/search/mod.rs
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
//! Module for interacting with the [MusicBrainz API](https://musicbrainz.org/doc/MusicBrainz_API).
|
||||
mod artist;
|
||||
mod query;
|
||||
mod release_group;
|
||||
|
||||
pub use artist::{SearchArtistRequest, SearchArtistResponse, SearchArtistResponseArtist};
|
||||
pub use release_group::{
|
||||
SearchReleaseGroupRequest, SearchReleaseGroupResponse, SearchReleaseGroupResponseReleaseGroup,
|
||||
};
|
||||
|
||||
use paste::paste;
|
||||
use url::form_urlencoded;
|
||||
|
||||
use crate::external::musicbrainz::{
|
||||
api::{
|
||||
search::{
|
||||
artist::DeserializeSearchArtistResponse,
|
||||
release_group::DeserializeSearchReleaseGroupResponse,
|
||||
},
|
||||
Error, MusicBrainzClient, MB_BASE_URL,
|
||||
},
|
||||
IMusicBrainzHttp,
|
||||
};
|
||||
|
||||
macro_rules! impl_search_entity {
|
||||
($name:ident, $entity:literal) => {
|
||||
paste! {
|
||||
pub fn [<search_ $name:snake>](
|
||||
&mut self,
|
||||
query: [<Search $name Request>]
|
||||
) -> Result<[<Search $name Response>], Error> {
|
||||
let query: String =
|
||||
form_urlencoded::byte_serialize(format!("{query}").as_bytes()).collect();
|
||||
let url = format!("{MB_BASE_URL}/{entity}?query={query}", entity = $entity);
|
||||
|
||||
let response: [<DeserializeSearch $name Response>] = self.http.get(&url)?;
|
||||
Ok(response.into())
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl<Http: IMusicBrainzHttp> MusicBrainzClient<Http> {
|
||||
impl_search_entity!(Artist, "artist");
|
||||
impl_search_entity!(ReleaseGroup, "release-group");
|
||||
}
|
312
src/external/musicbrainz/api/search/query.rs
vendored
Normal file
312
src/external/musicbrainz/api/search/query.rs
vendored
Normal file
@ -0,0 +1,312 @@
|
||||
use std::{fmt, marker::PhantomData};
|
||||
|
||||
pub enum Logical {
|
||||
Unary(Unary),
|
||||
Binary(Boolean),
|
||||
}
|
||||
|
||||
impl fmt::Display for Logical {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Logical::Unary(u) => write!(f, "{u}"),
|
||||
Logical::Binary(b) => write!(f, "{b}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Unary {
|
||||
Require,
|
||||
Prohibit,
|
||||
}
|
||||
|
||||
impl fmt::Display for Unary {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Unary::Require => write!(f, "+"),
|
||||
Unary::Prohibit => write!(f, "-"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Boolean {
|
||||
And,
|
||||
Or,
|
||||
Not,
|
||||
}
|
||||
|
||||
impl fmt::Display for Boolean {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Boolean::And => write!(f, "AND "),
|
||||
Boolean::Or => write!(f, "OR "),
|
||||
Boolean::Not => write!(f, "NOT "),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Expression<Entity> {
|
||||
Term(Entity),
|
||||
Expr(Query<Entity>),
|
||||
}
|
||||
|
||||
impl<Entity> From<Entity> for Expression<Entity> {
|
||||
fn from(value: Entity) -> Self {
|
||||
Expression::Term(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Entity> From<Query<Entity>> for Expression<Entity> {
|
||||
fn from(value: Query<Entity>) -> Self {
|
||||
Expression::Expr(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Entity: fmt::Display> fmt::Display for Expression<Entity> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Expression::Term(t) => write!(f, "{t}"),
|
||||
Expression::Expr(q) => write!(f, "({q})"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EmptyQuery<Entity> {
|
||||
_marker: PhantomData<Entity>,
|
||||
}
|
||||
|
||||
impl<Entity> Default for EmptyQuery<Entity> {
|
||||
fn default() -> Self {
|
||||
EmptyQuery {
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Entity> EmptyQuery<Entity> {
|
||||
pub fn expression<Expr: Into<Expression<Entity>>>(self, expr: Expr) -> Query<Entity> {
|
||||
Query {
|
||||
left: (None, Box::new(expr.into())),
|
||||
right: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn require(self) -> EmptyQueryJoin<Entity> {
|
||||
EmptyQueryJoin {
|
||||
unary: Unary::Require,
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prohibit(self) -> EmptyQueryJoin<Entity> {
|
||||
EmptyQueryJoin {
|
||||
unary: Unary::Prohibit,
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EmptyQueryJoin<Entity> {
|
||||
unary: Unary,
|
||||
_marker: PhantomData<Entity>,
|
||||
}
|
||||
|
||||
impl<Entity> EmptyQueryJoin<Entity> {
|
||||
pub fn expression<Expr: Into<Expression<Entity>>>(self, expr: Expr) -> Query<Entity> {
|
||||
Query {
|
||||
left: (Some(self.unary), Box::new(expr.into())),
|
||||
right: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Query<Entity> {
|
||||
left: (Option<Unary>, Box<Expression<Entity>>),
|
||||
right: Vec<(Logical, Box<Expression<Entity>>)>,
|
||||
}
|
||||
|
||||
impl<Entity: fmt::Display> fmt::Display for Query<Entity> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if let Some(u) = &self.left.0 {
|
||||
write!(f, "{u}")?;
|
||||
}
|
||||
|
||||
write!(f, "{}", self.left.1)?;
|
||||
|
||||
for (logical, expr) in self.right.iter() {
|
||||
write!(f, " {logical}{expr}")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<Entity> Query<Entity> {
|
||||
#[allow(clippy::new_ret_no_self)]
|
||||
pub fn new() -> EmptyQuery<Entity> {
|
||||
EmptyQuery::default()
|
||||
}
|
||||
|
||||
pub fn require(self) -> QueryJoin<Entity> {
|
||||
QueryJoin {
|
||||
logical: Logical::Unary(Unary::Require),
|
||||
query: self,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prohibit(self) -> QueryJoin<Entity> {
|
||||
QueryJoin {
|
||||
logical: Logical::Unary(Unary::Prohibit),
|
||||
query: self,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn and(self) -> QueryJoin<Entity> {
|
||||
QueryJoin {
|
||||
logical: Logical::Binary(Boolean::And),
|
||||
query: self,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn or(self) -> QueryJoin<Entity> {
|
||||
QueryJoin {
|
||||
logical: Logical::Binary(Boolean::Or),
|
||||
query: self,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn not(self) -> QueryJoin<Entity> {
|
||||
QueryJoin {
|
||||
logical: Logical::Binary(Boolean::Not),
|
||||
query: self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct QueryJoin<Entity> {
|
||||
logical: Logical,
|
||||
query: Query<Entity>,
|
||||
}
|
||||
|
||||
impl<Entity> QueryJoin<Entity> {
|
||||
pub fn expression<Expr: Into<Expression<Entity>>>(mut self, expr: Expr) -> Query<Entity> {
|
||||
self.query.right.push((self.logical, Box::new(expr.into())));
|
||||
self.query
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_term {
|
||||
($name:ident, $enum:ty, $variant:ident, $type:ty) => {
|
||||
impl<'a> EmptyQuery<$enum> {
|
||||
pub fn $name(self, $name: $type) -> Query<$enum> {
|
||||
self.expression(<$enum>::$variant($name))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> EmptyQueryJoin<$enum> {
|
||||
pub fn $name(self, $name: $type) -> Query<$enum> {
|
||||
self.expression(<$enum>::$variant($name))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> QueryJoin<$enum> {
|
||||
pub fn $name(self, $name: $type) -> Query<$enum> {
|
||||
self.expression(<$enum>::$variant($name))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use impl_term;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fmt;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub enum TestEntity<'a> {
|
||||
String(&'a str),
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for TestEntity<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::String(s) => write!(f, "\"{s}\""),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type TestEntityRequest<'a> = Query<TestEntity<'a>>;
|
||||
|
||||
impl_term!(string, TestEntity<'a>, String, &'a str);
|
||||
|
||||
#[test]
|
||||
fn lucene_logical() {
|
||||
let query = TestEntityRequest::new()
|
||||
.string("jakarta apache")
|
||||
.or()
|
||||
.string("jakarta");
|
||||
assert_eq!(format!("{query}"), "\"jakarta apache\" OR \"jakarta\"");
|
||||
|
||||
let query = TestEntityRequest::new()
|
||||
.string("jakarta apache")
|
||||
.and()
|
||||
.string("jakarta");
|
||||
assert_eq!(format!("{query}"), "\"jakarta apache\" AND \"jakarta\"");
|
||||
|
||||
let query = TestEntityRequest::new()
|
||||
.require()
|
||||
.string("jakarta")
|
||||
.or()
|
||||
.string("lucene");
|
||||
assert_eq!(format!("{query}"), "+\"jakarta\" OR \"lucene\"");
|
||||
|
||||
let query = TestEntityRequest::new()
|
||||
.string("lucene")
|
||||
.require()
|
||||
.string("jakarta");
|
||||
assert_eq!(format!("{query}"), "\"lucene\" +\"jakarta\"");
|
||||
|
||||
let query = TestEntityRequest::new()
|
||||
.string("jakarta apache")
|
||||
.not()
|
||||
.string("Apache Lucene");
|
||||
assert_eq!(
|
||||
format!("{query}"),
|
||||
"\"jakarta apache\" NOT \"Apache Lucene\""
|
||||
);
|
||||
|
||||
let query = TestEntityRequest::new()
|
||||
.prohibit()
|
||||
.string("Apache Lucene")
|
||||
.or()
|
||||
.string("jakarta apache");
|
||||
assert_eq!(
|
||||
format!("{query}"),
|
||||
"-\"Apache Lucene\" OR \"jakarta apache\""
|
||||
);
|
||||
|
||||
let query = TestEntityRequest::new()
|
||||
.string("jakarta apache")
|
||||
.prohibit()
|
||||
.string("Apache Lucene");
|
||||
assert_eq!(format!("{query}"), "\"jakarta apache\" -\"Apache Lucene\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lucene_grouping() {
|
||||
let query = TestEntityRequest::new()
|
||||
.expression(
|
||||
TestEntityRequest::new()
|
||||
.string("jakarta")
|
||||
.or()
|
||||
.string("apache"),
|
||||
)
|
||||
.and()
|
||||
.string("website");
|
||||
assert_eq!(
|
||||
format!("{query}"),
|
||||
"(\"jakarta\" OR \"apache\") AND \"website\""
|
||||
);
|
||||
}
|
||||
}
|
244
src/external/musicbrainz/api/search/release_group.rs
vendored
Normal file
244
src/external/musicbrainz/api/search/release_group.rs
vendored
Normal file
@ -0,0 +1,244 @@
|
||||
use std::fmt;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
collection::{
|
||||
album::{AlbumDate, AlbumId, AlbumPrimaryType, AlbumSecondaryType},
|
||||
musicbrainz::Mbid,
|
||||
},
|
||||
external::musicbrainz::api::{
|
||||
ApiDisplay, SerdeAlbumDate, SerdeAlbumPrimaryType, SerdeAlbumSecondaryType, SerdeMbid,
|
||||
},
|
||||
};
|
||||
|
||||
use super::query::{impl_term, EmptyQuery, EmptyQueryJoin, Query, QueryJoin};
|
||||
|
||||
pub enum SearchReleaseGroup<'a> {
|
||||
String(&'a str),
|
||||
Arid(&'a Mbid),
|
||||
FirstReleaseDate(&'a AlbumDate),
|
||||
ReleaseGroup(&'a str),
|
||||
Rgid(&'a Mbid),
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for SearchReleaseGroup<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::String(s) => write!(f, "\"{s}\""),
|
||||
Self::Arid(arid) => write!(f, "arid:{}", arid.uuid().as_hyphenated()),
|
||||
Self::FirstReleaseDate(date) => write!(
|
||||
f,
|
||||
"firstreleasedate:{}",
|
||||
ApiDisplay::format_album_date(date)
|
||||
),
|
||||
Self::ReleaseGroup(release_group) => write!(f, "releasegroup:\"{release_group}\""),
|
||||
Self::Rgid(rgid) => write!(f, "rgid:{}", rgid.uuid().as_hyphenated()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type SearchReleaseGroupRequest<'a> = Query<SearchReleaseGroup<'a>>;
|
||||
|
||||
impl_term!(string, SearchReleaseGroup<'a>, String, &'a str);
|
||||
impl_term!(arid, SearchReleaseGroup<'a>, Arid, &'a Mbid);
|
||||
impl_term!(
|
||||
first_release_date,
|
||||
SearchReleaseGroup<'a>,
|
||||
FirstReleaseDate,
|
||||
&'a AlbumDate
|
||||
);
|
||||
impl_term!(release_group, SearchReleaseGroup<'a>, ReleaseGroup, &'a str);
|
||||
impl_term!(rgid, SearchReleaseGroup<'a>, Rgid, &'a Mbid);
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct SearchReleaseGroupResponse {
|
||||
pub release_groups: Vec<SearchReleaseGroupResponseReleaseGroup>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
#[serde(rename_all(deserialize = "kebab-case"))]
|
||||
pub struct DeserializeSearchReleaseGroupResponse {
|
||||
release_groups: Vec<DeserializeSearchReleaseGroupResponseReleaseGroup>,
|
||||
}
|
||||
|
||||
impl From<DeserializeSearchReleaseGroupResponse> for SearchReleaseGroupResponse {
|
||||
fn from(value: DeserializeSearchReleaseGroupResponse) -> Self {
|
||||
SearchReleaseGroupResponse {
|
||||
release_groups: value.release_groups.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SearchReleaseGroupResponseReleaseGroup {
|
||||
pub score: u8,
|
||||
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 DeserializeSearchReleaseGroupResponseReleaseGroup {
|
||||
score: u8,
|
||||
id: SerdeMbid,
|
||||
title: String,
|
||||
first_release_date: SerdeAlbumDate,
|
||||
primary_type: SerdeAlbumPrimaryType,
|
||||
secondary_types: Option<Vec<SerdeAlbumSecondaryType>>,
|
||||
}
|
||||
|
||||
impl From<DeserializeSearchReleaseGroupResponseReleaseGroup>
|
||||
for SearchReleaseGroupResponseReleaseGroup
|
||||
{
|
||||
fn from(value: DeserializeSearchReleaseGroupResponseReleaseGroup) -> Self {
|
||||
SearchReleaseGroupResponseReleaseGroup {
|
||||
score: value.score,
|
||||
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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use mockall::predicate;
|
||||
|
||||
use crate::external::musicbrainz::{api::MusicBrainzClient, MockIMusicBrainzHttp};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn de_response() -> DeserializeSearchReleaseGroupResponse {
|
||||
let de_release_group = DeserializeSearchReleaseGroupResponseReleaseGroup {
|
||||
score: 67,
|
||||
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::Live)]),
|
||||
};
|
||||
DeserializeSearchReleaseGroupResponse {
|
||||
release_groups: vec![de_release_group.clone()],
|
||||
}
|
||||
}
|
||||
|
||||
fn response(de_response: DeserializeSearchReleaseGroupResponse) -> SearchReleaseGroupResponse {
|
||||
SearchReleaseGroupResponse {
|
||||
release_groups: de_response
|
||||
.release_groups
|
||||
.into_iter()
|
||||
.map(|rg| SearchReleaseGroupResponseReleaseGroup {
|
||||
score: 67,
|
||||
id: rg.id.0,
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_string() {
|
||||
let mut http = MockIMusicBrainzHttp::new();
|
||||
let url = format!(
|
||||
"https://musicbrainz.org/ws/2\
|
||||
/release-group\
|
||||
?query=%22{title}%22",
|
||||
title = "an+album",
|
||||
);
|
||||
|
||||
let de_response = de_response();
|
||||
let response = response(de_response.clone());
|
||||
|
||||
http.expect_get()
|
||||
.times(1)
|
||||
.with(predicate::eq(url))
|
||||
.return_once(|_| Ok(de_response));
|
||||
|
||||
let mut client = MusicBrainzClient::new(http);
|
||||
|
||||
let title = "an album";
|
||||
|
||||
let query = SearchReleaseGroupRequest::new().string(title);
|
||||
|
||||
let matches = client.search_release_group(query).unwrap();
|
||||
assert_eq!(matches, response);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_arid_album_date_release_group() {
|
||||
let mut http = MockIMusicBrainzHttp::new();
|
||||
let url = format!(
|
||||
"https://musicbrainz.org/ws/2\
|
||||
/release-group\
|
||||
?query=arid%3A{arid}+AND+releasegroup%3A%22{title}%22+AND+firstreleasedate%3A{date}",
|
||||
arid = "00000000-0000-0000-0000-000000000000",
|
||||
date = "1986-04",
|
||||
title = "an+album",
|
||||
);
|
||||
|
||||
let de_response = de_response();
|
||||
let response = response(de_response.clone());
|
||||
|
||||
http.expect_get()
|
||||
.times(1)
|
||||
.with(predicate::eq(url))
|
||||
.return_once(|_| Ok(de_response));
|
||||
|
||||
let mut client = MusicBrainzClient::new(http);
|
||||
|
||||
let arid: Mbid = "00000000-0000-0000-0000-000000000000".try_into().unwrap();
|
||||
let title = "an album";
|
||||
let date = (1986, 4).into();
|
||||
|
||||
let query = SearchReleaseGroupRequest::new()
|
||||
.arid(&arid)
|
||||
.and()
|
||||
.release_group(title)
|
||||
.and()
|
||||
.first_release_date(&date);
|
||||
|
||||
let matches = client.search_release_group(query).unwrap();
|
||||
assert_eq!(matches, response);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_rgid() {
|
||||
let mut http = MockIMusicBrainzHttp::new();
|
||||
let url = format!(
|
||||
"https://musicbrainz.org/ws/2\
|
||||
/release-group\
|
||||
?query=rgid%3A{rgid}",
|
||||
rgid = "11111111-1111-1111-1111-111111111111",
|
||||
);
|
||||
|
||||
let de_response = de_response();
|
||||
let response = response(de_response.clone());
|
||||
|
||||
http.expect_get()
|
||||
.times(1)
|
||||
.with(predicate::eq(url))
|
||||
.return_once(|_| Ok(de_response));
|
||||
|
||||
let mut client = MusicBrainzClient::new(http);
|
||||
|
||||
let rgid: Mbid = "11111111-1111-1111-1111-111111111111".try_into().unwrap();
|
||||
|
||||
let query = SearchReleaseGroupRequest::new().rgid(&rgid);
|
||||
|
||||
let matches = client.search_release_group(query).unwrap();
|
||||
assert_eq!(matches, response);
|
||||
}
|
||||
}
|
@ -91,14 +91,12 @@ impl IAppInteractBrowse for AppMachine<AppBrowse> {
|
||||
}
|
||||
};
|
||||
|
||||
let arid = match artist.musicbrainz {
|
||||
Some(ref mbid) => mbid.mbid(),
|
||||
None => {
|
||||
return AppMachine::error(self.inner, "cannot fetch: missing artist MBID").into()
|
||||
}
|
||||
};
|
||||
let mut matches = vec![];
|
||||
|
||||
match artist.musicbrainz {
|
||||
Some(ref mbid) => {
|
||||
let arid = mbid.mbid();
|
||||
|
||||
let mut artist_album_matches = vec![];
|
||||
let mut album_iter = artist.albums.iter().peekable();
|
||||
while let Some(album) = album_iter.next() {
|
||||
if album.musicbrainz.is_some() {
|
||||
@ -106,10 +104,7 @@ impl IAppInteractBrowse for AppMachine<AppBrowse> {
|
||||
}
|
||||
|
||||
match self.inner.musicbrainz.search_release_group(arid, album) {
|
||||
Ok(matches) => artist_album_matches.push(AppMatchesInfo {
|
||||
matching: album.clone(),
|
||||
matches,
|
||||
}),
|
||||
Ok(list) => matches.push(AppMatchesInfo::album(album.clone(), list)),
|
||||
Err(err) => return AppMachine::error(self.inner, err.to_string()).into(),
|
||||
}
|
||||
|
||||
@ -117,8 +112,14 @@ impl IAppInteractBrowse for AppMachine<AppBrowse> {
|
||||
thread::sleep(time::Duration::from_secs(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
None => match self.inner.musicbrainz.search_artist(artist) {
|
||||
Ok(list) => matches.push(AppMatchesInfo::artist(artist.clone(), list)),
|
||||
Err(err) => return AppMachine::error(self.inner, err.to_string()).into(),
|
||||
},
|
||||
};
|
||||
|
||||
AppMachine::matches(self.inner, artist_album_matches).into()
|
||||
AppMachine::matches(self.inner, matches).into()
|
||||
}
|
||||
|
||||
fn no_op(self) -> Self::APP {
|
||||
@ -129,7 +130,7 @@ impl IAppInteractBrowse for AppMachine<AppBrowse> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use mockall::{predicate, Sequence};
|
||||
use musichoard::collection::{album::Album, musicbrainz::Mbid};
|
||||
use musichoard::collection::{album::Album, artist::Artist, musicbrainz::Mbid};
|
||||
|
||||
use crate::tui::{
|
||||
app::{
|
||||
@ -257,8 +258,19 @@ mod tests {
|
||||
|
||||
let public_matches = public.state.unwrap_matches();
|
||||
|
||||
assert_eq!(public_matches.matching, Some(&album_1));
|
||||
assert_eq!(public_matches.matches, Some(matches_1.as_slice()));
|
||||
assert_eq!(
|
||||
public_matches
|
||||
.matches
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.album_ref()
|
||||
.matching,
|
||||
&album_1
|
||||
);
|
||||
assert_eq!(
|
||||
public_matches.matches.as_ref().unwrap().album_ref().list,
|
||||
matches_1.as_slice()
|
||||
);
|
||||
|
||||
let mut app = app.unwrap_matches().select();
|
||||
|
||||
@ -267,8 +279,19 @@ mod tests {
|
||||
|
||||
let public_matches = public.state.unwrap_matches();
|
||||
|
||||
assert_eq!(public_matches.matching, Some(&album_4));
|
||||
assert_eq!(public_matches.matches, Some(matches_4.as_slice()));
|
||||
assert_eq!(
|
||||
public_matches
|
||||
.matches
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.album_ref()
|
||||
.matching,
|
||||
&album_4
|
||||
);
|
||||
assert_eq!(
|
||||
public_matches.matches.as_ref().unwrap().album_ref().list,
|
||||
matches_4.as_slice()
|
||||
);
|
||||
|
||||
let app = app.unwrap_matches().select();
|
||||
app.unwrap_browse();
|
||||
@ -282,19 +305,78 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fetch_musicbrainz_no_mbid() {
|
||||
let browse = AppMachine::browse(inner(music_hoard(COLLECTION.to_owned())));
|
||||
fn fetch_musicbrainz_no_artist_mbid() {
|
||||
let mut mb_api = Box::new(MockIMusicBrainz::new());
|
||||
|
||||
let artist = COLLECTION[3].clone();
|
||||
|
||||
let artist_match_1 = Match::new(100, artist.clone());
|
||||
let artist_match_2 = Match::new(50, artist.clone());
|
||||
let matches = vec![artist_match_1.clone(), artist_match_2.clone()];
|
||||
|
||||
let result: Result<Vec<Match<Artist>>, musicbrainz::Error> = Ok(matches.clone());
|
||||
|
||||
mb_api
|
||||
.expect_search_artist()
|
||||
.with(predicate::eq(artist.clone()))
|
||||
.times(1)
|
||||
.return_once(|_| result);
|
||||
|
||||
let browse = AppMachine::browse(inner_with_mb(music_hoard(COLLECTION.to_owned()), mb_api));
|
||||
|
||||
// Use the fourth artist for this test as they have no MBID.
|
||||
let browse = browse.increment_selection(Delta::Line).unwrap_browse();
|
||||
let browse = browse.increment_selection(Delta::Line).unwrap_browse();
|
||||
let browse = browse.increment_selection(Delta::Line).unwrap_browse();
|
||||
let mut app = browse.fetch_musicbrainz();
|
||||
|
||||
let public = app.get();
|
||||
assert!(matches!(public.state, AppState::Matches(_)));
|
||||
|
||||
let public_matches = public.state.unwrap_matches();
|
||||
|
||||
assert_eq!(
|
||||
public_matches
|
||||
.matches
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.artist_ref()
|
||||
.matching,
|
||||
&artist
|
||||
);
|
||||
assert_eq!(
|
||||
public_matches.matches.as_ref().unwrap().artist_ref().list,
|
||||
matches.as_slice()
|
||||
);
|
||||
|
||||
let app = app.unwrap_matches().select();
|
||||
app.unwrap_browse();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fetch_musicbrainz_artist_api_error() {
|
||||
let mut mb_api = Box::new(MockIMusicBrainz::new());
|
||||
|
||||
let error = Err(musicbrainz::Error::RateLimit);
|
||||
|
||||
mb_api
|
||||
.expect_search_artist()
|
||||
.times(1)
|
||||
.return_once(|_| error);
|
||||
|
||||
let browse = AppMachine::browse(inner_with_mb(music_hoard(COLLECTION.to_owned()), mb_api));
|
||||
|
||||
// Use the fourth artist for this test as they have no MBID.
|
||||
let browse = browse.increment_selection(Delta::Line).unwrap_browse();
|
||||
let browse = browse.increment_selection(Delta::Line).unwrap_browse();
|
||||
let browse = browse.increment_selection(Delta::Line).unwrap_browse();
|
||||
|
||||
let app = browse.fetch_musicbrainz();
|
||||
app.unwrap_error();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fetch_musicbrainz_api_error() {
|
||||
fn fetch_musicbrainz_album_api_error() {
|
||||
let mut mb_api = Box::new(MockIMusicBrainz::new());
|
||||
|
||||
let error = Err(musicbrainz::Error::RateLimit);
|
||||
|
@ -1,19 +1,103 @@
|
||||
use std::cmp;
|
||||
|
||||
use musichoard::collection::album::Album;
|
||||
use musichoard::collection::{album::Album, artist::Artist};
|
||||
|
||||
use crate::tui::{
|
||||
app::{
|
||||
machine::{App, AppInner, AppMachine},
|
||||
AppPublic, AppPublicMatches, AppState, IAppInteractMatches, WidgetState,
|
||||
AppPublic, AppPublicAlbumMatches, AppPublicArtistMatches, AppPublicMatches,
|
||||
AppPublicMatchesInfo, AppState, IAppInteractMatches, WidgetState,
|
||||
},
|
||||
lib::interface::musicbrainz::Match,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct AppMatchesInfo {
|
||||
pub struct AppArtistMatchesInfo {
|
||||
pub matching: Artist,
|
||||
pub list: Vec<Match<Artist>>,
|
||||
}
|
||||
|
||||
impl AppArtistMatchesInfo {
|
||||
fn is_empty(&self) -> bool {
|
||||
self.list.is_empty()
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.list.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'app> From<&'app AppArtistMatchesInfo> for AppPublicArtistMatches<'app> {
|
||||
fn from(value: &'app AppArtistMatchesInfo) -> Self {
|
||||
AppPublicArtistMatches {
|
||||
matching: &value.matching,
|
||||
list: &value.list,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct AppAlbumMatchesInfo {
|
||||
pub matching: Album,
|
||||
pub matches: Vec<Match<Album>>,
|
||||
pub list: Vec<Match<Album>>,
|
||||
}
|
||||
|
||||
impl AppAlbumMatchesInfo {
|
||||
fn is_empty(&self) -> bool {
|
||||
self.list.is_empty()
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.list.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'app> From<&'app AppAlbumMatchesInfo> for AppPublicAlbumMatches<'app> {
|
||||
fn from(value: &'app AppAlbumMatchesInfo) -> Self {
|
||||
AppPublicAlbumMatches {
|
||||
matching: &value.matching,
|
||||
list: &value.list,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum AppMatchesInfo {
|
||||
Artist(AppArtistMatchesInfo),
|
||||
Album(AppAlbumMatchesInfo),
|
||||
}
|
||||
|
||||
impl AppMatchesInfo {
|
||||
fn is_empty(&self) -> bool {
|
||||
match self {
|
||||
Self::Artist(a) => a.is_empty(),
|
||||
Self::Album(a) => a.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
match self {
|
||||
Self::Artist(a) => a.len(),
|
||||
Self::Album(a) => a.len(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn artist(matching: Artist, list: Vec<Match<Artist>>) -> Self {
|
||||
AppMatchesInfo::Artist(AppArtistMatchesInfo { matching, list })
|
||||
}
|
||||
|
||||
pub fn album(matching: Album, list: Vec<Match<Album>>) -> Self {
|
||||
AppMatchesInfo::Album(AppAlbumMatchesInfo { matching, list })
|
||||
}
|
||||
}
|
||||
|
||||
impl<'app> From<&'app AppMatchesInfo> for AppPublicMatchesInfo<'app> {
|
||||
fn from(value: &'app AppMatchesInfo) -> Self {
|
||||
match value {
|
||||
AppMatchesInfo::Artist(a) => AppPublicMatchesInfo::Artist(a.into()),
|
||||
AppMatchesInfo::Album(a) => AppPublicMatchesInfo::Album(a.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AppMatches {
|
||||
@ -28,7 +112,7 @@ impl AppMachine<AppMatches> {
|
||||
let mut state = WidgetState::default();
|
||||
if let Some(matches_info) = matches_info_vec.first() {
|
||||
index = Some(0);
|
||||
if !matches_info.matches.is_empty() {
|
||||
if !matches_info.is_empty() {
|
||||
state.list.select(Some(0));
|
||||
}
|
||||
}
|
||||
@ -52,18 +136,14 @@ impl From<AppMachine<AppMatches>> for App {
|
||||
|
||||
impl<'a> From<&'a mut AppMachine<AppMatches>> for AppPublic<'a> {
|
||||
fn from(machine: &'a mut AppMachine<AppMatches>) -> Self {
|
||||
let (matching, matches) = match machine.state.index {
|
||||
Some(index) => (
|
||||
Some(&machine.state.matches_info_vec[index].matching),
|
||||
Some(machine.state.matches_info_vec[index].matches.as_slice()),
|
||||
),
|
||||
None => (None, None),
|
||||
};
|
||||
let matches = machine
|
||||
.state
|
||||
.index
|
||||
.map(|index| (&machine.state.matches_info_vec[index]).into());
|
||||
|
||||
AppPublic {
|
||||
inner: (&mut machine.inner).into(),
|
||||
state: AppState::Matches(AppPublicMatches {
|
||||
matching,
|
||||
matches,
|
||||
state: &mut machine.state.state,
|
||||
}),
|
||||
@ -89,7 +169,6 @@ impl IAppInteractMatches for AppMachine<AppMatches> {
|
||||
let to = cmp::min(
|
||||
result,
|
||||
self.state.matches_info_vec[self.state.index.unwrap()]
|
||||
.matches
|
||||
.len()
|
||||
.saturating_sub(1),
|
||||
);
|
||||
@ -104,7 +183,7 @@ impl IAppInteractMatches for AppMachine<AppMatches> {
|
||||
self.state.state = WidgetState::default();
|
||||
if let Some(index) = self.state.index {
|
||||
if let Some(matches_info) = self.state.matches_info_vec.get(index) {
|
||||
if !matches_info.matches.is_empty() {
|
||||
if !matches_info.is_empty() {
|
||||
self.state.state.list.select(Some(0));
|
||||
}
|
||||
return self.into();
|
||||
@ -125,7 +204,10 @@ impl IAppInteractMatches for AppMachine<AppMatches> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use musichoard::collection::album::{AlbumDate, AlbumId, AlbumPrimaryType, AlbumSecondaryType};
|
||||
use musichoard::collection::{
|
||||
album::{AlbumDate, AlbumId, AlbumPrimaryType, AlbumSecondaryType},
|
||||
artist::ArtistId,
|
||||
};
|
||||
|
||||
use crate::tui::app::{
|
||||
machine::tests::{inner, music_hoard},
|
||||
@ -134,7 +216,40 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
fn matches_info_vec() -> Vec<AppMatchesInfo> {
|
||||
impl AppMatchesInfo {
|
||||
fn album_ref(&self) -> &AppAlbumMatchesInfo {
|
||||
match self {
|
||||
Self::Album(a) => a,
|
||||
Self::Artist(_) => panic!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn artist_matches_info_vec() -> Vec<AppMatchesInfo> {
|
||||
let artist_1 = Artist::new(ArtistId::new("Artist 1"));
|
||||
|
||||
let artist_1_1 = artist_1.clone();
|
||||
let artist_match_1_1 = Match::new(100, artist_1_1);
|
||||
|
||||
let artist_1_2 = artist_1.clone();
|
||||
let mut artist_match_1_2 = Match::new(100, artist_1_2);
|
||||
artist_match_1_2.set_disambiguation("some disambiguation");
|
||||
|
||||
let list = vec![artist_match_1_1.clone(), artist_match_1_2.clone()];
|
||||
let matches_info_1 = AppMatchesInfo::artist(artist_1.clone(), list);
|
||||
|
||||
let artist_2 = Artist::new(ArtistId::new("Artist 2"));
|
||||
|
||||
let artist_2_1 = artist_1.clone();
|
||||
let album_match_2_1 = Match::new(100, artist_2_1);
|
||||
|
||||
let list = vec![album_match_2_1.clone()];
|
||||
let matches_info_2 = AppMatchesInfo::artist(artist_2.clone(), list);
|
||||
|
||||
vec![matches_info_1, matches_info_2]
|
||||
}
|
||||
|
||||
fn album_matches_info_vec() -> Vec<AppMatchesInfo> {
|
||||
let album_1 = Album::new(
|
||||
AlbumId::new("Album 1"),
|
||||
AlbumDate::new(Some(1990), Some(5), None),
|
||||
@ -143,23 +258,15 @@ mod tests {
|
||||
);
|
||||
|
||||
let album_1_1 = album_1.clone();
|
||||
let album_match_1_1 = Match {
|
||||
score: 100,
|
||||
item: album_1_1,
|
||||
};
|
||||
let album_match_1_1 = Match::new(100, album_1_1);
|
||||
|
||||
let mut album_1_2 = album_1.clone();
|
||||
album_1_2.id.title.push_str(" extra title part");
|
||||
album_1_2.secondary_types.pop();
|
||||
let album_match_1_2 = Match {
|
||||
score: 100,
|
||||
item: album_1_2,
|
||||
};
|
||||
let album_match_1_2 = Match::new(100, album_1_2);
|
||||
|
||||
let matches_info_1 = AppMatchesInfo {
|
||||
matching: album_1.clone(),
|
||||
matches: vec![album_match_1_1.clone(), album_match_1_2.clone()],
|
||||
};
|
||||
let list = vec![album_match_1_1.clone(), album_match_1_2.clone()];
|
||||
let matches_info_1 = AppMatchesInfo::album(album_1.clone(), list);
|
||||
|
||||
let album_2 = Album::new(
|
||||
AlbumId::new("Album 2"),
|
||||
@ -169,15 +276,10 @@ mod tests {
|
||||
);
|
||||
|
||||
let album_2_1 = album_1.clone();
|
||||
let album_match_2_1 = Match {
|
||||
score: 100,
|
||||
item: album_2_1,
|
||||
};
|
||||
let album_match_2_1 = Match::new(100, album_2_1);
|
||||
|
||||
let matches_info_2 = AppMatchesInfo {
|
||||
matching: album_2.clone(),
|
||||
matches: vec![album_match_2_1.clone()],
|
||||
};
|
||||
let list = vec![album_match_2_1.clone()];
|
||||
let matches_info_2 = AppMatchesInfo::album(album_2.clone(), list);
|
||||
|
||||
vec![matches_info_1, matches_info_2]
|
||||
}
|
||||
@ -196,14 +298,13 @@ mod tests {
|
||||
let public = app.get();
|
||||
let public_matches = public.state.unwrap_matches();
|
||||
|
||||
assert_eq!(public_matches.matching, None);
|
||||
assert_eq!(public_matches.matches, None);
|
||||
assert_eq!(public_matches.state, &widget_state);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_nonempty() {
|
||||
let matches_info_vec = matches_info_vec();
|
||||
let matches_info_vec = album_matches_info_vec();
|
||||
let matches = AppMachine::matches(inner(music_hoard(vec![])), matches_info_vec.clone());
|
||||
|
||||
let mut widget_state = WidgetState::default();
|
||||
@ -217,17 +318,23 @@ mod tests {
|
||||
let public = app.get();
|
||||
let public_matches = public.state.unwrap_matches();
|
||||
|
||||
assert_eq!(public_matches.matching, Some(&matches_info_vec[0].matching));
|
||||
assert_eq!(
|
||||
public_matches.matches,
|
||||
Some(matches_info_vec[0].matches.as_slice())
|
||||
public_matches
|
||||
.matches
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.album_ref()
|
||||
.matching,
|
||||
&matches_info_vec[0].album_ref().matching
|
||||
);
|
||||
assert_eq!(
|
||||
public_matches.matches.as_ref().unwrap().album_ref().list,
|
||||
matches_info_vec[0].album_ref().list.as_slice()
|
||||
);
|
||||
assert_eq!(public_matches.state, &widget_state);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_flow() {
|
||||
let matches_info_vec = matches_info_vec();
|
||||
fn matches_flow(matches_info_vec: Vec<AppMatchesInfo>) {
|
||||
let matches = AppMachine::matches(inner(music_hoard(vec![])), matches_info_vec.clone());
|
||||
|
||||
let mut widget_state = WidgetState::default();
|
||||
@ -261,9 +368,19 @@ mod tests {
|
||||
matches.select().unwrap_browse();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artist_matches_flow() {
|
||||
matches_flow(artist_matches_info_vec());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn album_matches_flow() {
|
||||
matches_flow(album_matches_info_vec());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_abort() {
|
||||
let matches_info_vec = matches_info_vec();
|
||||
let matches_info_vec = album_matches_info_vec();
|
||||
let matches = AppMachine::matches(inner(music_hoard(vec![])), matches_info_vec.clone());
|
||||
|
||||
let mut widget_state = WidgetState::default();
|
||||
|
@ -4,7 +4,7 @@ mod selection;
|
||||
pub use machine::App;
|
||||
pub use selection::{Category, Delta, Selection, WidgetState};
|
||||
|
||||
use musichoard::collection::{album::Album, Collection};
|
||||
use musichoard::collection::{album::Album, artist::Artist, Collection};
|
||||
|
||||
use crate::tui::lib::interface::musicbrainz::Match;
|
||||
|
||||
@ -129,9 +129,26 @@ pub struct AppPublicInner<'app> {
|
||||
pub selection: &'app mut Selection,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct AppPublicArtistMatches<'app> {
|
||||
pub matching: &'app Artist,
|
||||
pub list: &'app [Match<Artist>],
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct AppPublicAlbumMatches<'app> {
|
||||
pub matching: &'app Album,
|
||||
pub list: &'app [Match<Album>],
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum AppPublicMatchesInfo<'app> {
|
||||
Artist(AppPublicArtistMatches<'app>),
|
||||
Album(AppPublicAlbumMatches<'app>),
|
||||
}
|
||||
|
||||
pub struct AppPublicMatches<'app> {
|
||||
pub matching: Option<&'app Album>,
|
||||
pub matches: Option<&'app [Match<Album>]>,
|
||||
pub matches: Option<AppPublicMatchesInfo<'app>>,
|
||||
pub state: &'app mut WidgetState,
|
||||
}
|
||||
|
||||
@ -148,6 +165,22 @@ impl<BS, IS, RS, SS, MS, ES, CS> AppState<BS, IS, RS, SS, MS, ES, CS> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
impl<'app> AppPublicMatchesInfo<'app> {
|
||||
pub fn artist_ref(&self) -> &AppPublicArtistMatches<'app> {
|
||||
match self {
|
||||
Self::Artist(m) => m,
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn album_ref(&self) -> &AppPublicAlbumMatches<'app> {
|
||||
match self {
|
||||
Self::Album(m) => m,
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_is_state() {
|
||||
let state = AppPublicState::Search("get rekt");
|
||||
|
40
src/tui/lib/external/musicbrainz/mod.rs
vendored
40
src/tui/lib/external/musicbrainz/mod.rs
vendored
@ -3,11 +3,15 @@
|
||||
use musichoard::{
|
||||
collection::{
|
||||
album::{Album, AlbumDate},
|
||||
artist::Artist,
|
||||
musicbrainz::Mbid,
|
||||
},
|
||||
external::musicbrainz::{
|
||||
api::{
|
||||
search::{SearchReleaseGroupRequest, SearchReleaseGroupResponseReleaseGroup},
|
||||
search::{
|
||||
SearchArtistRequest, SearchArtistResponseArtist, SearchReleaseGroupRequest,
|
||||
SearchReleaseGroupResponseReleaseGroup,
|
||||
},
|
||||
MusicBrainzClient,
|
||||
},
|
||||
IMusicBrainzHttp,
|
||||
@ -28,6 +32,18 @@ impl<Http> MusicBrainz<Http> {
|
||||
}
|
||||
|
||||
impl<Http: IMusicBrainzHttp> IMusicBrainz for MusicBrainz<Http> {
|
||||
fn search_artist(&mut self, artist: &Artist) -> Result<Vec<Match<Artist>>, Error> {
|
||||
let query = SearchArtistRequest::new().string(&artist.id.name);
|
||||
|
||||
let mb_response = self.client.search_artist(query)?;
|
||||
|
||||
Ok(mb_response
|
||||
.artists
|
||||
.into_iter()
|
||||
.map(from_search_artist_response_artist)
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn search_release_group(
|
||||
&mut self,
|
||||
arid: &Mbid,
|
||||
@ -37,13 +53,14 @@ impl<Http: IMusicBrainzHttp> IMusicBrainz for MusicBrainz<Http> {
|
||||
// with just the year should be enough anyway.
|
||||
let date = AlbumDate::new(album.date.year, None, None);
|
||||
|
||||
let mut request = SearchReleaseGroupRequest::default();
|
||||
request
|
||||
let query = SearchReleaseGroupRequest::new()
|
||||
.arid(arid)
|
||||
.and()
|
||||
.first_release_date(&date)
|
||||
.and()
|
||||
.release_group(&album.id.title);
|
||||
|
||||
let mb_response = self.client.search_release_group(request)?;
|
||||
let mb_response = self.client.search_release_group(query)?;
|
||||
|
||||
Ok(mb_response
|
||||
.release_groups
|
||||
@ -53,6 +70,21 @@ impl<Http: IMusicBrainzHttp> IMusicBrainz for MusicBrainz<Http> {
|
||||
}
|
||||
}
|
||||
|
||||
fn from_search_artist_response_artist(entity: SearchArtistResponseArtist) -> Match<Artist> {
|
||||
let mut artist = Artist::new(entity.name);
|
||||
if let Some(sort) = entity.sort {
|
||||
artist.set_sort_key(sort);
|
||||
}
|
||||
artist.set_musicbrainz_ref(entity.id.into());
|
||||
|
||||
let mut artist_match = Match::new(entity.score, artist);
|
||||
if let Some(disambiguation) = entity.disambiguation {
|
||||
artist_match.set_disambiguation(disambiguation);
|
||||
}
|
||||
|
||||
artist_match
|
||||
}
|
||||
|
||||
fn from_search_release_group_response_release_group(
|
||||
entity: SearchReleaseGroupResponseReleaseGroup,
|
||||
) -> Match<Album> {
|
||||
|
@ -3,11 +3,12 @@
|
||||
#[cfg(test)]
|
||||
use mockall::automock;
|
||||
|
||||
use musichoard::collection::{album::Album, musicbrainz::Mbid};
|
||||
use musichoard::collection::{album::Album, artist::Artist, musicbrainz::Mbid};
|
||||
|
||||
/// Trait for interacting with the MusicBrainz API.
|
||||
#[cfg_attr(test, automock)]
|
||||
pub trait IMusicBrainz {
|
||||
fn search_artist(&mut self, name: &Artist) -> Result<Vec<Match<Artist>>, Error>;
|
||||
fn search_release_group(
|
||||
&mut self,
|
||||
arid: &Mbid,
|
||||
@ -19,11 +20,20 @@ pub trait IMusicBrainz {
|
||||
pub struct Match<T> {
|
||||
pub score: u8,
|
||||
pub item: T,
|
||||
pub disambiguation: Option<String>,
|
||||
}
|
||||
|
||||
impl<T> Match<T> {
|
||||
pub fn new(score: u8, item: T) -> Self {
|
||||
Match { score, item }
|
||||
Match {
|
||||
score,
|
||||
item,
|
||||
disambiguation: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_disambiguation<S: Into<String>>(&mut self, disambiguation: S) {
|
||||
self.disambiguation = Some(disambiguation.into())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,10 @@
|
||||
use musichoard::collection::{
|
||||
album::{Album, AlbumDate, AlbumPrimaryType, AlbumSecondaryType, AlbumSeq, AlbumStatus},
|
||||
artist::Artist,
|
||||
track::{TrackFormat, TrackQuality},
|
||||
};
|
||||
|
||||
use crate::tui::lib::interface::musicbrainz::Match;
|
||||
use crate::tui::{app::AppPublicMatchesInfo, lib::interface::musicbrainz::Match};
|
||||
|
||||
pub struct UiDisplay;
|
||||
|
||||
@ -97,18 +98,46 @@ impl UiDisplay {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn display_matching_info(matching: Option<&Album>) -> String {
|
||||
match matching {
|
||||
Some(matching) => format!(
|
||||
"Matching: {} | {}",
|
||||
UiDisplay::display_album_date(&matching.date),
|
||||
&matching.id.title
|
||||
),
|
||||
None => String::from("Matching: nothing"),
|
||||
pub fn display_artist_matching(artist: &Artist) -> String {
|
||||
format!("Matching artist: {}", &artist.id.name)
|
||||
}
|
||||
|
||||
pub fn display_album_matching(album: &Album) -> String {
|
||||
format!(
|
||||
"Matching album: {} | {}",
|
||||
UiDisplay::display_album_date(&album.date),
|
||||
&album.id.title
|
||||
)
|
||||
}
|
||||
|
||||
pub fn display_nothing_matching() -> &'static str {
|
||||
"Matching nothing"
|
||||
}
|
||||
|
||||
pub fn display_matching_info(matches: Option<&AppPublicMatchesInfo>) -> String {
|
||||
match matches.as_ref() {
|
||||
Some(kind) => match kind {
|
||||
AppPublicMatchesInfo::Artist(m) => UiDisplay::display_artist_matching(m.matching),
|
||||
AppPublicMatchesInfo::Album(m) => UiDisplay::display_album_matching(m.matching),
|
||||
},
|
||||
None => UiDisplay::display_nothing_matching().to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn display_match_string(match_album: &Match<Album>) -> String {
|
||||
pub fn display_artist_match(match_artist: &Match<Artist>) -> String {
|
||||
format!(
|
||||
"{}{} ({}%)",
|
||||
&match_artist.item.id.name,
|
||||
&match_artist
|
||||
.disambiguation
|
||||
.as_ref()
|
||||
.map(|d| format!(" ({d})"))
|
||||
.unwrap_or_default(),
|
||||
match_artist.score,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn display_album_match(match_album: &Match<Album>) -> String {
|
||||
format!(
|
||||
"{:010} | {} [{}] ({}%)",
|
||||
UiDisplay::display_album_date(&match_album.item.date),
|
||||
|
@ -1,23 +1,70 @@
|
||||
use musichoard::collection::album::Album;
|
||||
use musichoard::collection::{album::Album, artist::Artist};
|
||||
use ratatui::widgets::{List, ListItem};
|
||||
|
||||
use crate::tui::{app::WidgetState, lib::interface::musicbrainz::Match, ui::display::UiDisplay};
|
||||
use crate::tui::{
|
||||
app::{AppPublicMatchesInfo, WidgetState},
|
||||
lib::interface::musicbrainz::Match,
|
||||
ui::display::UiDisplay,
|
||||
};
|
||||
|
||||
pub struct AlbumMatchesState<'a, 'b> {
|
||||
pub struct MatchesState<'a, 'b> {
|
||||
pub matching: String,
|
||||
pub list: List<'a>,
|
||||
pub state: &'b mut WidgetState,
|
||||
}
|
||||
|
||||
impl<'a, 'b> AlbumMatchesState<'a, 'b> {
|
||||
pub fn new(matches: &[Match<Album>], state: &'b mut WidgetState) -> Self {
|
||||
impl<'a, 'b> MatchesState<'a, 'b> {
|
||||
pub fn new(matches: Option<AppPublicMatchesInfo>, state: &'b mut WidgetState) -> Self {
|
||||
match matches {
|
||||
Some(info) => match info {
|
||||
AppPublicMatchesInfo::Artist(m) => Self::artists(m.matching, m.list, state),
|
||||
AppPublicMatchesInfo::Album(m) => Self::albums(m.matching, m.list, state),
|
||||
},
|
||||
None => Self::empty(state),
|
||||
}
|
||||
}
|
||||
|
||||
fn empty(state: &'b mut WidgetState) -> Self {
|
||||
MatchesState {
|
||||
matching: UiDisplay::display_nothing_matching().to_string(),
|
||||
list: List::default(),
|
||||
state,
|
||||
}
|
||||
}
|
||||
|
||||
fn artists(matching: &Artist, matches: &[Match<Artist>], state: &'b mut WidgetState) -> Self {
|
||||
let matching = UiDisplay::display_artist_matching(matching);
|
||||
|
||||
let list = List::new(
|
||||
matches
|
||||
.iter()
|
||||
.map(UiDisplay::display_match_string)
|
||||
.map(UiDisplay::display_artist_match)
|
||||
.map(ListItem::new)
|
||||
.collect::<Vec<ListItem>>(),
|
||||
);
|
||||
|
||||
AlbumMatchesState { list, state }
|
||||
MatchesState {
|
||||
matching,
|
||||
list,
|
||||
state,
|
||||
}
|
||||
}
|
||||
|
||||
fn albums(matching: &Album, matches: &[Match<Album>], state: &'b mut WidgetState) -> Self {
|
||||
let matching = UiDisplay::display_album_matching(matching);
|
||||
|
||||
let list = List::new(
|
||||
matches
|
||||
.iter()
|
||||
.map(UiDisplay::display_album_match)
|
||||
.map(ListItem::new)
|
||||
.collect::<Vec<ListItem>>(),
|
||||
);
|
||||
|
||||
MatchesState {
|
||||
matching,
|
||||
list,
|
||||
state,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ impl Minibuffer<'_> {
|
||||
},
|
||||
AppState::Matches(public) => Minibuffer {
|
||||
paragraphs: vec![
|
||||
Paragraph::new(UiDisplay::display_matching_info(public.matching)),
|
||||
Paragraph::new(UiDisplay::display_matching_info(public.matches.as_ref())),
|
||||
Paragraph::new("q: abort"),
|
||||
],
|
||||
columns: 2,
|
||||
|
@ -14,8 +14,10 @@ use ratatui::{layout::Rect, widgets::Paragraph, Frame};
|
||||
use musichoard::collection::{album::Album, Collection};
|
||||
|
||||
use crate::tui::{
|
||||
app::{AppPublicState, AppState, Category, IAppAccess, Selection, WidgetState},
|
||||
lib::interface::musicbrainz::Match,
|
||||
app::{
|
||||
AppPublicMatchesInfo, AppPublicState, AppState, Category, IAppAccess, Selection,
|
||||
WidgetState,
|
||||
},
|
||||
ui::{
|
||||
browse::{
|
||||
AlbumArea, AlbumState, ArtistArea, ArtistState, FrameArea, TrackArea, TrackState,
|
||||
@ -23,7 +25,7 @@ use crate::tui::{
|
||||
display::UiDisplay,
|
||||
error::ErrorOverlay,
|
||||
info::{AlbumOverlay, ArtistOverlay},
|
||||
matches::AlbumMatchesState,
|
||||
matches::MatchesState,
|
||||
minibuffer::Minibuffer,
|
||||
overlay::{OverlayBuilder, OverlaySize},
|
||||
reload::ReloadOverlay,
|
||||
@ -133,15 +135,13 @@ impl Ui {
|
||||
}
|
||||
|
||||
fn render_matches_overlay(
|
||||
matching: Option<&Album>,
|
||||
matches: Option<&[Match<Album>]>,
|
||||
matches: Option<AppPublicMatchesInfo>,
|
||||
state: &mut WidgetState,
|
||||
frame: &mut Frame,
|
||||
) {
|
||||
let area = OverlayBuilder::default().build(frame.size());
|
||||
let matching_string = UiDisplay::display_matching_info(matching);
|
||||
let st = AlbumMatchesState::new(matches.unwrap_or_default(), state);
|
||||
UiWidget::render_overlay_list_widget(&matching_string, st.list, st.state, true, area, frame)
|
||||
let st = MatchesState::new(matches, state);
|
||||
UiWidget::render_overlay_list_widget(&st.matching, st.list, st.state, true, area, frame)
|
||||
}
|
||||
|
||||
fn render_error_overlay<S: AsRef<str>>(title: S, msg: S, frame: &mut Frame) {
|
||||
@ -167,7 +167,7 @@ impl IUi for Ui {
|
||||
match state {
|
||||
AppState::Info(_) => Self::render_info_overlay(collection, selection, frame),
|
||||
AppState::Matches(public) => {
|
||||
Self::render_matches_overlay(public.matching, public.matches, public.state, frame)
|
||||
Self::render_matches_overlay(public.matches, public.state, frame)
|
||||
}
|
||||
AppState::Reload(_) => Self::render_reload_overlay(frame),
|
||||
AppState::Error(msg) => Self::render_error_overlay("Error", msg, frame),
|
||||
@ -185,15 +185,46 @@ mod tests {
|
||||
};
|
||||
|
||||
use crate::tui::{
|
||||
app::{AppPublic, AppPublicInner, AppPublicMatches, Delta},
|
||||
app::{
|
||||
AppPublic, AppPublicAlbumMatches, AppPublicArtistMatches, AppPublicInner,
|
||||
AppPublicMatches, Delta,
|
||||
},
|
||||
lib::interface::musicbrainz::Match,
|
||||
testmod::COLLECTION,
|
||||
tests::terminal,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
impl<'app> AppPublicArtistMatches<'app> {
|
||||
fn get(&self) -> AppPublicArtistMatches<'app> {
|
||||
AppPublicArtistMatches {
|
||||
matching: self.matching,
|
||||
list: self.list,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'app> AppPublicAlbumMatches<'app> {
|
||||
fn get(&self) -> AppPublicAlbumMatches<'app> {
|
||||
AppPublicAlbumMatches {
|
||||
matching: self.matching,
|
||||
list: self.list,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'app> AppPublicMatchesInfo<'app> {
|
||||
fn get(&self) -> AppPublicMatchesInfo<'app> {
|
||||
match self {
|
||||
Self::Artist(a) => Self::Artist(a.get()),
|
||||
Self::Album(a) => Self::Album(a.get()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Automock does not support returning types with generic lifetimes.
|
||||
impl IAppAccess for AppPublic<'_> {
|
||||
impl<'app> IAppAccess for AppPublic<'app> {
|
||||
fn get(&mut self) -> AppPublic {
|
||||
AppPublic {
|
||||
inner: AppPublicInner {
|
||||
@ -206,8 +237,7 @@ mod tests {
|
||||
AppState::Reload(()) => AppState::Reload(()),
|
||||
AppState::Search(s) => AppState::Search(s),
|
||||
AppState::Matches(ref mut m) => AppState::Matches(AppPublicMatches {
|
||||
matching: m.matching,
|
||||
matches: m.matches,
|
||||
matches: m.matches.as_mut().map(|k| k.get()),
|
||||
state: m.state,
|
||||
}),
|
||||
AppState::Error(s) => AppState::Error(s),
|
||||
@ -217,26 +247,35 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn public_inner<'app>(
|
||||
collection: &'app Collection,
|
||||
selection: &'app mut Selection,
|
||||
) -> AppPublicInner<'app> {
|
||||
AppPublicInner {
|
||||
collection,
|
||||
selection,
|
||||
}
|
||||
}
|
||||
|
||||
fn artist_matches<'app>(
|
||||
matching: &'app Artist,
|
||||
list: &'app [Match<Artist>],
|
||||
) -> AppPublicMatchesInfo<'app> {
|
||||
AppPublicMatchesInfo::Artist(AppPublicArtistMatches { matching, list })
|
||||
}
|
||||
|
||||
fn album_matches<'app>(
|
||||
matching: &'app Album,
|
||||
list: &'app [Match<Album>],
|
||||
) -> AppPublicMatchesInfo<'app> {
|
||||
AppPublicMatchesInfo::Album(AppPublicAlbumMatches { matching, list })
|
||||
}
|
||||
|
||||
fn draw_test_suite(collection: &Collection, selection: &mut Selection) {
|
||||
let mut terminal = terminal();
|
||||
|
||||
let album = Album::new(
|
||||
AlbumId::new("An Album"),
|
||||
AlbumDate::new(Some(1990), Some(5), None),
|
||||
Some(AlbumPrimaryType::Album),
|
||||
vec![AlbumSecondaryType::Live, AlbumSecondaryType::Compilation],
|
||||
);
|
||||
|
||||
let album_match = Match {
|
||||
score: 80,
|
||||
item: album.clone(),
|
||||
};
|
||||
|
||||
let mut app = AppPublic {
|
||||
inner: AppPublicInner {
|
||||
collection,
|
||||
selection,
|
||||
},
|
||||
inner: public_inner(collection, selection),
|
||||
state: AppState::Browse(()),
|
||||
};
|
||||
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
|
||||
@ -250,26 +289,6 @@ mod tests {
|
||||
app.state = AppState::Search("");
|
||||
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
|
||||
|
||||
let album_matches = [album_match.clone(), album_match.clone()];
|
||||
let mut widget_state = WidgetState::default();
|
||||
widget_state.list.select(Some(0));
|
||||
|
||||
app.state = AppState::Matches(AppPublicMatches {
|
||||
matching: Some(&album),
|
||||
matches: Some(&album_matches),
|
||||
state: &mut widget_state,
|
||||
});
|
||||
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
|
||||
|
||||
let mut widget_state = WidgetState::default();
|
||||
|
||||
app.state = AppState::Matches(AppPublicMatches {
|
||||
matching: None,
|
||||
matches: None,
|
||||
state: &mut widget_state,
|
||||
});
|
||||
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
|
||||
|
||||
app.state = AppState::Error("get rekt scrub");
|
||||
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
|
||||
|
||||
@ -317,4 +336,86 @@ mod tests {
|
||||
|
||||
draw_test_suite(artists, &mut selection);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draw_empty_matches() {
|
||||
let collection = &COLLECTION;
|
||||
let mut selection = Selection::new(collection);
|
||||
|
||||
let mut terminal = terminal();
|
||||
|
||||
let mut widget_state = WidgetState::default();
|
||||
|
||||
let mut app = AppPublic {
|
||||
inner: public_inner(collection, &mut selection),
|
||||
state: AppState::Matches(AppPublicMatches {
|
||||
matches: None,
|
||||
state: &mut widget_state,
|
||||
}),
|
||||
};
|
||||
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draw_artist_matches() {
|
||||
let collection = &COLLECTION;
|
||||
let mut selection = Selection::new(collection);
|
||||
|
||||
let mut terminal = terminal();
|
||||
|
||||
let artist = Artist::new(ArtistId::new("an artist"));
|
||||
|
||||
let artist_match = Match {
|
||||
score: 80,
|
||||
item: artist.clone(),
|
||||
disambiguation: None,
|
||||
};
|
||||
|
||||
let list = [artist_match.clone(), artist_match.clone()];
|
||||
let mut widget_state = WidgetState::default();
|
||||
widget_state.list.select(Some(0));
|
||||
|
||||
let mut app = AppPublic {
|
||||
inner: public_inner(collection, &mut selection),
|
||||
state: AppState::Matches(AppPublicMatches {
|
||||
matches: Some(artist_matches(&artist, &list)),
|
||||
state: &mut widget_state,
|
||||
}),
|
||||
};
|
||||
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draw_album_matches() {
|
||||
let collection = &COLLECTION;
|
||||
let mut selection = Selection::new(collection);
|
||||
|
||||
let mut terminal = terminal();
|
||||
|
||||
let album = Album::new(
|
||||
AlbumId::new("An Album"),
|
||||
AlbumDate::new(Some(1990), Some(5), None),
|
||||
Some(AlbumPrimaryType::Album),
|
||||
vec![AlbumSecondaryType::Live, AlbumSecondaryType::Compilation],
|
||||
);
|
||||
|
||||
let album_match = Match {
|
||||
score: 80,
|
||||
item: album.clone(),
|
||||
disambiguation: None,
|
||||
};
|
||||
|
||||
let list = [album_match.clone(), album_match.clone()];
|
||||
let mut widget_state = WidgetState::default();
|
||||
widget_state.list.select(Some(0));
|
||||
|
||||
let mut app = AppPublic {
|
||||
inner: public_inner(collection, &mut selection),
|
||||
state: AppState::Matches(AppPublicMatches {
|
||||
matches: Some(album_matches(&album, &list)),
|
||||
state: &mut widget_state,
|
||||
}),
|
||||
};
|
||||
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user