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",
|
"mockall",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"openssh",
|
"openssh",
|
||||||
|
"paste",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
@ -812,9 +813,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "paste"
|
name = "paste"
|
||||||
version = "1.0.14"
|
version = "1.0.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
|
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
|
@ -10,6 +10,7 @@ aho-corasick = { version = "1.1.2", optional = true }
|
|||||||
crossterm = { version = "0.27.0", optional = true}
|
crossterm = { version = "0.27.0", optional = true}
|
||||||
once_cell = { version = "1.19.0", optional = true}
|
once_cell = { version = "1.19.0", optional = true}
|
||||||
openssh = { version = "0.10.3", features = ["native-mux"], default-features = false, 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}
|
ratatui = { version = "0.26.0", optional = true}
|
||||||
reqwest = { version = "0.11.25", features = ["blocking", "json"], optional = true }
|
reqwest = { version = "0.11.25", features = ["blocking", "json"], optional = true }
|
||||||
serde = { version = "1.0.196", features = ["derive"], optional = true }
|
serde = { version = "1.0.196", features = ["derive"], optional = true }
|
||||||
@ -33,7 +34,7 @@ bin = ["structopt"]
|
|||||||
database-json = ["serde", "serde_json"]
|
database-json = ["serde", "serde_json"]
|
||||||
library-beets = []
|
library-beets = []
|
||||||
library-beets-ssh = ["openssh", "tokio"]
|
library-beets-ssh = ["openssh", "tokio"]
|
||||||
musicbrainz = ["reqwest", "serde", "serde_json"]
|
musicbrainz = ["paste", "reqwest", "serde", "serde_json"]
|
||||||
tui = ["aho-corasick", "crossterm", "once_cell", "ratatui"]
|
tui = ["aho-corasick", "crossterm", "once_cell", "ratatui"]
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
@ -50,8 +51,8 @@ path = "examples/musicbrainz_api/lookup_artist.rs"
|
|||||||
required-features = ["bin", "musicbrainz"]
|
required-features = ["bin", "musicbrainz"]
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "musicbrainz-api---search-release-group"
|
name = "musicbrainz-api---search"
|
||||||
path = "examples/musicbrainz_api/search_release_group.rs"
|
path = "examples/musicbrainz_api/search.rs"
|
||||||
required-features = ["bin", "musicbrainz"]
|
required-features = ["bin", "musicbrainz"]
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[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 {
|
pub fn new(http: Http) -> Self {
|
||||||
MusicBrainzClient { http }
|
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 {
|
match date.year {
|
||||||
Some(year) => match date.month {
|
Some(year) => match date.month {
|
||||||
Some(month) => match date.day {
|
Some(month) => match date.day {
|
||||||
Some(day) => Some(format!("{year}-{month:02}-{day:02}")),
|
Some(day) => format!("{year}-{month:02}-{day:02}"),
|
||||||
None => Some(format!("{year}-{month: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]
|
#[test]
|
||||||
fn format_album_date() {
|
fn format_album_date() {
|
||||||
struct Null;
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
MusicBrainzClient::<Null>::format_album_date(&AlbumDate::new(None, None, None)),
|
ApiDisplay::format_album_date(&AlbumDate::new(None, 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!(
|
assert_eq!(
|
||||||
MusicBrainzClient::<Null>::format_album_date(&(1986).into()),
|
ApiDisplay::format_album_date(&(1986, 4, 21).into()),
|
||||||
Some(String::from("1986"))
|
"1986-04-21"
|
||||||
);
|
|
||||||
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"))
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,34 +91,35 @@ impl IAppInteractBrowse for AppMachine<AppBrowse> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let arid = match artist.musicbrainz {
|
let mut matches = vec![];
|
||||||
Some(ref mbid) => mbid.mbid(),
|
|
||||||
None => {
|
match artist.musicbrainz {
|
||||||
return AppMachine::error(self.inner, "cannot fetch: missing artist MBID").into()
|
Some(ref mbid) => {
|
||||||
|
let arid = mbid.mbid();
|
||||||
|
|
||||||
|
let mut album_iter = artist.albums.iter().peekable();
|
||||||
|
while let Some(album) = album_iter.next() {
|
||||||
|
if album.musicbrainz.is_some() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.inner.musicbrainz.search_release_group(arid, album) {
|
||||||
|
Ok(list) => matches.push(AppMatchesInfo::album(album.clone(), list)),
|
||||||
|
Err(err) => return AppMachine::error(self.inner, err.to_string()).into(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if album_iter.peek().is_some() {
|
||||||
|
thread::sleep(time::Duration::from_secs(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
None => match self.inner.musicbrainz.search_artist(artist) {
|
||||||
|
Ok(list) => matches.push(AppMatchesInfo::artist(artist.clone(), list)),
|
||||||
|
Err(err) => return AppMachine::error(self.inner, err.to_string()).into(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut artist_album_matches = vec![];
|
AppMachine::matches(self.inner, matches).into()
|
||||||
let mut album_iter = artist.albums.iter().peekable();
|
|
||||||
while let Some(album) = album_iter.next() {
|
|
||||||
if album.musicbrainz.is_some() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
match self.inner.musicbrainz.search_release_group(arid, album) {
|
|
||||||
Ok(matches) => artist_album_matches.push(AppMatchesInfo {
|
|
||||||
matching: album.clone(),
|
|
||||||
matches,
|
|
||||||
}),
|
|
||||||
Err(err) => return AppMachine::error(self.inner, err.to_string()).into(),
|
|
||||||
}
|
|
||||||
|
|
||||||
if album_iter.peek().is_some() {
|
|
||||||
thread::sleep(time::Duration::from_secs(1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AppMachine::matches(self.inner, artist_album_matches).into()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn no_op(self) -> Self::APP {
|
fn no_op(self) -> Self::APP {
|
||||||
@ -129,7 +130,7 @@ impl IAppInteractBrowse for AppMachine<AppBrowse> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use mockall::{predicate, Sequence};
|
use mockall::{predicate, Sequence};
|
||||||
use musichoard::collection::{album::Album, musicbrainz::Mbid};
|
use musichoard::collection::{album::Album, artist::Artist, musicbrainz::Mbid};
|
||||||
|
|
||||||
use crate::tui::{
|
use crate::tui::{
|
||||||
app::{
|
app::{
|
||||||
@ -257,8 +258,19 @@ mod tests {
|
|||||||
|
|
||||||
let public_matches = public.state.unwrap_matches();
|
let public_matches = public.state.unwrap_matches();
|
||||||
|
|
||||||
assert_eq!(public_matches.matching, Some(&album_1));
|
assert_eq!(
|
||||||
assert_eq!(public_matches.matches, Some(matches_1.as_slice()));
|
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();
|
let mut app = app.unwrap_matches().select();
|
||||||
|
|
||||||
@ -267,8 +279,19 @@ mod tests {
|
|||||||
|
|
||||||
let public_matches = public.state.unwrap_matches();
|
let public_matches = public.state.unwrap_matches();
|
||||||
|
|
||||||
assert_eq!(public_matches.matching, Some(&album_4));
|
assert_eq!(
|
||||||
assert_eq!(public_matches.matches, Some(matches_4.as_slice()));
|
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();
|
let app = app.unwrap_matches().select();
|
||||||
app.unwrap_browse();
|
app.unwrap_browse();
|
||||||
@ -282,19 +305,78 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn fetch_musicbrainz_no_mbid() {
|
fn fetch_musicbrainz_no_artist_mbid() {
|
||||||
let browse = AppMachine::browse(inner(music_hoard(COLLECTION.to_owned())));
|
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.
|
// 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 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();
|
let app = browse.fetch_musicbrainz();
|
||||||
app.unwrap_error();
|
app.unwrap_error();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn fetch_musicbrainz_api_error() {
|
fn fetch_musicbrainz_album_api_error() {
|
||||||
let mut mb_api = Box::new(MockIMusicBrainz::new());
|
let mut mb_api = Box::new(MockIMusicBrainz::new());
|
||||||
|
|
||||||
let error = Err(musicbrainz::Error::RateLimit);
|
let error = Err(musicbrainz::Error::RateLimit);
|
||||||
|
@ -1,19 +1,103 @@
|
|||||||
use std::cmp;
|
use std::cmp;
|
||||||
|
|
||||||
use musichoard::collection::album::Album;
|
use musichoard::collection::{album::Album, artist::Artist};
|
||||||
|
|
||||||
use crate::tui::{
|
use crate::tui::{
|
||||||
app::{
|
app::{
|
||||||
machine::{App, AppInner, AppMachine},
|
machine::{App, AppInner, AppMachine},
|
||||||
AppPublic, AppPublicMatches, AppState, IAppInteractMatches, WidgetState,
|
AppPublic, AppPublicAlbumMatches, AppPublicArtistMatches, AppPublicMatches,
|
||||||
|
AppPublicMatchesInfo, AppState, IAppInteractMatches, WidgetState,
|
||||||
},
|
},
|
||||||
lib::interface::musicbrainz::Match,
|
lib::interface::musicbrainz::Match,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[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 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 {
|
pub struct AppMatches {
|
||||||
@ -28,7 +112,7 @@ impl AppMachine<AppMatches> {
|
|||||||
let mut state = WidgetState::default();
|
let mut state = WidgetState::default();
|
||||||
if let Some(matches_info) = matches_info_vec.first() {
|
if let Some(matches_info) = matches_info_vec.first() {
|
||||||
index = Some(0);
|
index = Some(0);
|
||||||
if !matches_info.matches.is_empty() {
|
if !matches_info.is_empty() {
|
||||||
state.list.select(Some(0));
|
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> {
|
impl<'a> From<&'a mut AppMachine<AppMatches>> for AppPublic<'a> {
|
||||||
fn from(machine: &'a mut AppMachine<AppMatches>) -> Self {
|
fn from(machine: &'a mut AppMachine<AppMatches>) -> Self {
|
||||||
let (matching, matches) = match machine.state.index {
|
let matches = machine
|
||||||
Some(index) => (
|
.state
|
||||||
Some(&machine.state.matches_info_vec[index].matching),
|
.index
|
||||||
Some(machine.state.matches_info_vec[index].matches.as_slice()),
|
.map(|index| (&machine.state.matches_info_vec[index]).into());
|
||||||
),
|
|
||||||
None => (None, None),
|
|
||||||
};
|
|
||||||
|
|
||||||
AppPublic {
|
AppPublic {
|
||||||
inner: (&mut machine.inner).into(),
|
inner: (&mut machine.inner).into(),
|
||||||
state: AppState::Matches(AppPublicMatches {
|
state: AppState::Matches(AppPublicMatches {
|
||||||
matching,
|
|
||||||
matches,
|
matches,
|
||||||
state: &mut machine.state.state,
|
state: &mut machine.state.state,
|
||||||
}),
|
}),
|
||||||
@ -89,7 +169,6 @@ impl IAppInteractMatches for AppMachine<AppMatches> {
|
|||||||
let to = cmp::min(
|
let to = cmp::min(
|
||||||
result,
|
result,
|
||||||
self.state.matches_info_vec[self.state.index.unwrap()]
|
self.state.matches_info_vec[self.state.index.unwrap()]
|
||||||
.matches
|
|
||||||
.len()
|
.len()
|
||||||
.saturating_sub(1),
|
.saturating_sub(1),
|
||||||
);
|
);
|
||||||
@ -104,7 +183,7 @@ impl IAppInteractMatches for AppMachine<AppMatches> {
|
|||||||
self.state.state = WidgetState::default();
|
self.state.state = WidgetState::default();
|
||||||
if let Some(index) = self.state.index {
|
if let Some(index) = self.state.index {
|
||||||
if let Some(matches_info) = self.state.matches_info_vec.get(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));
|
self.state.state.list.select(Some(0));
|
||||||
}
|
}
|
||||||
return self.into();
|
return self.into();
|
||||||
@ -125,7 +204,10 @@ impl IAppInteractMatches for AppMachine<AppMatches> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use musichoard::collection::album::{AlbumDate, AlbumId, AlbumPrimaryType, AlbumSecondaryType};
|
use musichoard::collection::{
|
||||||
|
album::{AlbumDate, AlbumId, AlbumPrimaryType, AlbumSecondaryType},
|
||||||
|
artist::ArtistId,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::tui::app::{
|
use crate::tui::app::{
|
||||||
machine::tests::{inner, music_hoard},
|
machine::tests::{inner, music_hoard},
|
||||||
@ -134,7 +216,40 @@ mod tests {
|
|||||||
|
|
||||||
use super::*;
|
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(
|
let album_1 = Album::new(
|
||||||
AlbumId::new("Album 1"),
|
AlbumId::new("Album 1"),
|
||||||
AlbumDate::new(Some(1990), Some(5), None),
|
AlbumDate::new(Some(1990), Some(5), None),
|
||||||
@ -143,23 +258,15 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let album_1_1 = album_1.clone();
|
let album_1_1 = album_1.clone();
|
||||||
let album_match_1_1 = Match {
|
let album_match_1_1 = Match::new(100, album_1_1);
|
||||||
score: 100,
|
|
||||||
item: album_1_1,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut album_1_2 = album_1.clone();
|
let mut album_1_2 = album_1.clone();
|
||||||
album_1_2.id.title.push_str(" extra title part");
|
album_1_2.id.title.push_str(" extra title part");
|
||||||
album_1_2.secondary_types.pop();
|
album_1_2.secondary_types.pop();
|
||||||
let album_match_1_2 = Match {
|
let album_match_1_2 = Match::new(100, album_1_2);
|
||||||
score: 100,
|
|
||||||
item: album_1_2,
|
|
||||||
};
|
|
||||||
|
|
||||||
let matches_info_1 = AppMatchesInfo {
|
let list = vec![album_match_1_1.clone(), album_match_1_2.clone()];
|
||||||
matching: album_1.clone(),
|
let matches_info_1 = AppMatchesInfo::album(album_1.clone(), list);
|
||||||
matches: vec![album_match_1_1.clone(), album_match_1_2.clone()],
|
|
||||||
};
|
|
||||||
|
|
||||||
let album_2 = Album::new(
|
let album_2 = Album::new(
|
||||||
AlbumId::new("Album 2"),
|
AlbumId::new("Album 2"),
|
||||||
@ -169,15 +276,10 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let album_2_1 = album_1.clone();
|
let album_2_1 = album_1.clone();
|
||||||
let album_match_2_1 = Match {
|
let album_match_2_1 = Match::new(100, album_2_1);
|
||||||
score: 100,
|
|
||||||
item: album_2_1,
|
|
||||||
};
|
|
||||||
|
|
||||||
let matches_info_2 = AppMatchesInfo {
|
let list = vec![album_match_2_1.clone()];
|
||||||
matching: album_2.clone(),
|
let matches_info_2 = AppMatchesInfo::album(album_2.clone(), list);
|
||||||
matches: vec![album_match_2_1.clone()],
|
|
||||||
};
|
|
||||||
|
|
||||||
vec![matches_info_1, matches_info_2]
|
vec![matches_info_1, matches_info_2]
|
||||||
}
|
}
|
||||||
@ -196,14 +298,13 @@ mod tests {
|
|||||||
let public = app.get();
|
let public = app.get();
|
||||||
let public_matches = public.state.unwrap_matches();
|
let public_matches = public.state.unwrap_matches();
|
||||||
|
|
||||||
assert_eq!(public_matches.matching, None);
|
|
||||||
assert_eq!(public_matches.matches, None);
|
assert_eq!(public_matches.matches, None);
|
||||||
assert_eq!(public_matches.state, &widget_state);
|
assert_eq!(public_matches.state, &widget_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn create_nonempty() {
|
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 matches = AppMachine::matches(inner(music_hoard(vec![])), matches_info_vec.clone());
|
||||||
|
|
||||||
let mut widget_state = WidgetState::default();
|
let mut widget_state = WidgetState::default();
|
||||||
@ -217,17 +318,23 @@ mod tests {
|
|||||||
let public = app.get();
|
let public = app.get();
|
||||||
let public_matches = public.state.unwrap_matches();
|
let public_matches = public.state.unwrap_matches();
|
||||||
|
|
||||||
assert_eq!(public_matches.matching, Some(&matches_info_vec[0].matching));
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
public_matches.matches,
|
public_matches
|
||||||
Some(matches_info_vec[0].matches.as_slice())
|
.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);
|
assert_eq!(public_matches.state, &widget_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
fn matches_flow(matches_info_vec: Vec<AppMatchesInfo>) {
|
||||||
fn matches_flow() {
|
|
||||||
let matches_info_vec = matches_info_vec();
|
|
||||||
let matches = AppMachine::matches(inner(music_hoard(vec![])), matches_info_vec.clone());
|
let matches = AppMachine::matches(inner(music_hoard(vec![])), matches_info_vec.clone());
|
||||||
|
|
||||||
let mut widget_state = WidgetState::default();
|
let mut widget_state = WidgetState::default();
|
||||||
@ -261,9 +368,19 @@ mod tests {
|
|||||||
matches.select().unwrap_browse();
|
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]
|
#[test]
|
||||||
fn matches_abort() {
|
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 matches = AppMachine::matches(inner(music_hoard(vec![])), matches_info_vec.clone());
|
||||||
|
|
||||||
let mut widget_state = WidgetState::default();
|
let mut widget_state = WidgetState::default();
|
||||||
|
@ -4,7 +4,7 @@ mod selection;
|
|||||||
pub use machine::App;
|
pub use machine::App;
|
||||||
pub use selection::{Category, Delta, Selection, WidgetState};
|
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;
|
use crate::tui::lib::interface::musicbrainz::Match;
|
||||||
|
|
||||||
@ -129,9 +129,26 @@ pub struct AppPublicInner<'app> {
|
|||||||
pub selection: &'app mut Selection,
|
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 struct AppPublicMatches<'app> {
|
||||||
pub matching: Option<&'app Album>,
|
pub matches: Option<AppPublicMatchesInfo<'app>>,
|
||||||
pub matches: Option<&'app [Match<Album>]>,
|
|
||||||
pub state: &'app mut WidgetState,
|
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 {
|
mod tests {
|
||||||
use super::*;
|
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]
|
#[test]
|
||||||
fn app_is_state() {
|
fn app_is_state() {
|
||||||
let state = AppPublicState::Search("get rekt");
|
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::{
|
use musichoard::{
|
||||||
collection::{
|
collection::{
|
||||||
album::{Album, AlbumDate},
|
album::{Album, AlbumDate},
|
||||||
|
artist::Artist,
|
||||||
musicbrainz::Mbid,
|
musicbrainz::Mbid,
|
||||||
},
|
},
|
||||||
external::musicbrainz::{
|
external::musicbrainz::{
|
||||||
api::{
|
api::{
|
||||||
search::{SearchReleaseGroupRequest, SearchReleaseGroupResponseReleaseGroup},
|
search::{
|
||||||
|
SearchArtistRequest, SearchArtistResponseArtist, SearchReleaseGroupRequest,
|
||||||
|
SearchReleaseGroupResponseReleaseGroup,
|
||||||
|
},
|
||||||
MusicBrainzClient,
|
MusicBrainzClient,
|
||||||
},
|
},
|
||||||
IMusicBrainzHttp,
|
IMusicBrainzHttp,
|
||||||
@ -28,6 +32,18 @@ impl<Http> MusicBrainz<Http> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<Http: IMusicBrainzHttp> IMusicBrainz for 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(
|
fn search_release_group(
|
||||||
&mut self,
|
&mut self,
|
||||||
arid: &Mbid,
|
arid: &Mbid,
|
||||||
@ -37,13 +53,14 @@ impl<Http: IMusicBrainzHttp> IMusicBrainz for MusicBrainz<Http> {
|
|||||||
// with just the year should be enough anyway.
|
// with just the year should be enough anyway.
|
||||||
let date = AlbumDate::new(album.date.year, None, None);
|
let date = AlbumDate::new(album.date.year, None, None);
|
||||||
|
|
||||||
let mut request = SearchReleaseGroupRequest::default();
|
let query = SearchReleaseGroupRequest::new()
|
||||||
request
|
|
||||||
.arid(arid)
|
.arid(arid)
|
||||||
|
.and()
|
||||||
.first_release_date(&date)
|
.first_release_date(&date)
|
||||||
|
.and()
|
||||||
.release_group(&album.id.title);
|
.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
|
Ok(mb_response
|
||||||
.release_groups
|
.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(
|
fn from_search_release_group_response_release_group(
|
||||||
entity: SearchReleaseGroupResponseReleaseGroup,
|
entity: SearchReleaseGroupResponseReleaseGroup,
|
||||||
) -> Match<Album> {
|
) -> Match<Album> {
|
||||||
|
@ -3,11 +3,12 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use mockall::automock;
|
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.
|
/// Trait for interacting with the MusicBrainz API.
|
||||||
#[cfg_attr(test, automock)]
|
#[cfg_attr(test, automock)]
|
||||||
pub trait IMusicBrainz {
|
pub trait IMusicBrainz {
|
||||||
|
fn search_artist(&mut self, name: &Artist) -> Result<Vec<Match<Artist>>, Error>;
|
||||||
fn search_release_group(
|
fn search_release_group(
|
||||||
&mut self,
|
&mut self,
|
||||||
arid: &Mbid,
|
arid: &Mbid,
|
||||||
@ -19,11 +20,20 @@ pub trait IMusicBrainz {
|
|||||||
pub struct Match<T> {
|
pub struct Match<T> {
|
||||||
pub score: u8,
|
pub score: u8,
|
||||||
pub item: T,
|
pub item: T,
|
||||||
|
pub disambiguation: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Match<T> {
|
impl<T> Match<T> {
|
||||||
pub fn new(score: u8, item: T) -> Self {
|
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::{
|
use musichoard::collection::{
|
||||||
album::{Album, AlbumDate, AlbumPrimaryType, AlbumSecondaryType, AlbumSeq, AlbumStatus},
|
album::{Album, AlbumDate, AlbumPrimaryType, AlbumSecondaryType, AlbumSeq, AlbumStatus},
|
||||||
|
artist::Artist,
|
||||||
track::{TrackFormat, TrackQuality},
|
track::{TrackFormat, TrackQuality},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::tui::lib::interface::musicbrainz::Match;
|
use crate::tui::{app::AppPublicMatchesInfo, lib::interface::musicbrainz::Match};
|
||||||
|
|
||||||
pub struct UiDisplay;
|
pub struct UiDisplay;
|
||||||
|
|
||||||
@ -97,18 +98,46 @@ impl UiDisplay {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn display_matching_info(matching: Option<&Album>) -> String {
|
pub fn display_artist_matching(artist: &Artist) -> String {
|
||||||
match matching {
|
format!("Matching artist: {}", &artist.id.name)
|
||||||
Some(matching) => format!(
|
}
|
||||||
"Matching: {} | {}",
|
|
||||||
UiDisplay::display_album_date(&matching.date),
|
pub fn display_album_matching(album: &Album) -> String {
|
||||||
&matching.id.title
|
format!(
|
||||||
),
|
"Matching album: {} | {}",
|
||||||
None => String::from("Matching: nothing"),
|
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!(
|
format!(
|
||||||
"{:010} | {} [{}] ({}%)",
|
"{:010} | {} [{}] ({}%)",
|
||||||
UiDisplay::display_album_date(&match_album.item.date),
|
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 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 list: List<'a>,
|
||||||
pub state: &'b mut WidgetState,
|
pub state: &'b mut WidgetState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'b> AlbumMatchesState<'a, 'b> {
|
impl<'a, 'b> MatchesState<'a, 'b> {
|
||||||
pub fn new(matches: &[Match<Album>], state: &'b mut WidgetState) -> Self {
|
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(
|
let list = List::new(
|
||||||
matches
|
matches
|
||||||
.iter()
|
.iter()
|
||||||
.map(UiDisplay::display_match_string)
|
.map(UiDisplay::display_artist_match)
|
||||||
.map(ListItem::new)
|
.map(ListItem::new)
|
||||||
.collect::<Vec<ListItem>>(),
|
.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 {
|
AppState::Matches(public) => Minibuffer {
|
||||||
paragraphs: vec![
|
paragraphs: vec![
|
||||||
Paragraph::new(UiDisplay::display_matching_info(public.matching)),
|
Paragraph::new(UiDisplay::display_matching_info(public.matches.as_ref())),
|
||||||
Paragraph::new("q: abort"),
|
Paragraph::new("q: abort"),
|
||||||
],
|
],
|
||||||
columns: 2,
|
columns: 2,
|
||||||
|
@ -14,8 +14,10 @@ use ratatui::{layout::Rect, widgets::Paragraph, Frame};
|
|||||||
use musichoard::collection::{album::Album, Collection};
|
use musichoard::collection::{album::Album, Collection};
|
||||||
|
|
||||||
use crate::tui::{
|
use crate::tui::{
|
||||||
app::{AppPublicState, AppState, Category, IAppAccess, Selection, WidgetState},
|
app::{
|
||||||
lib::interface::musicbrainz::Match,
|
AppPublicMatchesInfo, AppPublicState, AppState, Category, IAppAccess, Selection,
|
||||||
|
WidgetState,
|
||||||
|
},
|
||||||
ui::{
|
ui::{
|
||||||
browse::{
|
browse::{
|
||||||
AlbumArea, AlbumState, ArtistArea, ArtistState, FrameArea, TrackArea, TrackState,
|
AlbumArea, AlbumState, ArtistArea, ArtistState, FrameArea, TrackArea, TrackState,
|
||||||
@ -23,7 +25,7 @@ use crate::tui::{
|
|||||||
display::UiDisplay,
|
display::UiDisplay,
|
||||||
error::ErrorOverlay,
|
error::ErrorOverlay,
|
||||||
info::{AlbumOverlay, ArtistOverlay},
|
info::{AlbumOverlay, ArtistOverlay},
|
||||||
matches::AlbumMatchesState,
|
matches::MatchesState,
|
||||||
minibuffer::Minibuffer,
|
minibuffer::Minibuffer,
|
||||||
overlay::{OverlayBuilder, OverlaySize},
|
overlay::{OverlayBuilder, OverlaySize},
|
||||||
reload::ReloadOverlay,
|
reload::ReloadOverlay,
|
||||||
@ -133,15 +135,13 @@ impl Ui {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_matches_overlay(
|
fn render_matches_overlay(
|
||||||
matching: Option<&Album>,
|
matches: Option<AppPublicMatchesInfo>,
|
||||||
matches: Option<&[Match<Album>]>,
|
|
||||||
state: &mut WidgetState,
|
state: &mut WidgetState,
|
||||||
frame: &mut Frame,
|
frame: &mut Frame,
|
||||||
) {
|
) {
|
||||||
let area = OverlayBuilder::default().build(frame.size());
|
let area = OverlayBuilder::default().build(frame.size());
|
||||||
let matching_string = UiDisplay::display_matching_info(matching);
|
let st = MatchesState::new(matches, state);
|
||||||
let st = AlbumMatchesState::new(matches.unwrap_or_default(), state);
|
UiWidget::render_overlay_list_widget(&st.matching, st.list, st.state, true, area, frame)
|
||||||
UiWidget::render_overlay_list_widget(&matching_string, st.list, st.state, true, area, frame)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_error_overlay<S: AsRef<str>>(title: S, msg: S, frame: &mut 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 {
|
match state {
|
||||||
AppState::Info(_) => Self::render_info_overlay(collection, selection, frame),
|
AppState::Info(_) => Self::render_info_overlay(collection, selection, frame),
|
||||||
AppState::Matches(public) => {
|
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::Reload(_) => Self::render_reload_overlay(frame),
|
||||||
AppState::Error(msg) => Self::render_error_overlay("Error", msg, frame),
|
AppState::Error(msg) => Self::render_error_overlay("Error", msg, frame),
|
||||||
@ -185,15 +185,46 @@ mod tests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::tui::{
|
use crate::tui::{
|
||||||
app::{AppPublic, AppPublicInner, AppPublicMatches, Delta},
|
app::{
|
||||||
|
AppPublic, AppPublicAlbumMatches, AppPublicArtistMatches, AppPublicInner,
|
||||||
|
AppPublicMatches, Delta,
|
||||||
|
},
|
||||||
|
lib::interface::musicbrainz::Match,
|
||||||
testmod::COLLECTION,
|
testmod::COLLECTION,
|
||||||
tests::terminal,
|
tests::terminal,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::*;
|
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.
|
// Automock does not support returning types with generic lifetimes.
|
||||||
impl IAppAccess for AppPublic<'_> {
|
impl<'app> IAppAccess for AppPublic<'app> {
|
||||||
fn get(&mut self) -> AppPublic {
|
fn get(&mut self) -> AppPublic {
|
||||||
AppPublic {
|
AppPublic {
|
||||||
inner: AppPublicInner {
|
inner: AppPublicInner {
|
||||||
@ -206,8 +237,7 @@ mod tests {
|
|||||||
AppState::Reload(()) => AppState::Reload(()),
|
AppState::Reload(()) => AppState::Reload(()),
|
||||||
AppState::Search(s) => AppState::Search(s),
|
AppState::Search(s) => AppState::Search(s),
|
||||||
AppState::Matches(ref mut m) => AppState::Matches(AppPublicMatches {
|
AppState::Matches(ref mut m) => AppState::Matches(AppPublicMatches {
|
||||||
matching: m.matching,
|
matches: m.matches.as_mut().map(|k| k.get()),
|
||||||
matches: m.matches,
|
|
||||||
state: m.state,
|
state: m.state,
|
||||||
}),
|
}),
|
||||||
AppState::Error(s) => AppState::Error(s),
|
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) {
|
fn draw_test_suite(collection: &Collection, selection: &mut Selection) {
|
||||||
let mut terminal = terminal();
|
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 {
|
let mut app = AppPublic {
|
||||||
inner: AppPublicInner {
|
inner: public_inner(collection, selection),
|
||||||
collection,
|
|
||||||
selection,
|
|
||||||
},
|
|
||||||
state: AppState::Browse(()),
|
state: AppState::Browse(()),
|
||||||
};
|
};
|
||||||
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
|
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
|
||||||
@ -250,26 +289,6 @@ mod tests {
|
|||||||
app.state = AppState::Search("");
|
app.state = AppState::Search("");
|
||||||
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
|
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");
|
app.state = AppState::Error("get rekt scrub");
|
||||||
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
|
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
|
||||||
|
|
||||||
@ -317,4 +336,86 @@ mod tests {
|
|||||||
|
|
||||||
draw_test_suite(artists, &mut selection);
|
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