Sort by <field>_sort from tags if it is available #107

Merged
wojtek merged 5 commits from 73---sort-by-<field>_sort-from-tags-if-it-is-available into main 2024-01-13 15:42:05 +01:00
8 changed files with 251 additions and 240 deletions
Showing only changes of commit 25a13601fd - Show all commits

View File

@ -65,26 +65,28 @@ mod tests {
use super::*; use super::*;
use crate::{tests::COLLECTION, Artist, ArtistId, Collection, Format, IUrl}; use crate::{tests::COLLECTION, Artist, ArtistId, Collection, Format};
fn opt_to_url<U: IUrl>(opt: &Option<U>) -> String { fn opt_to_str<S: AsRef<str>>(opt: &Option<S>) -> String {
match opt { match opt {
Some(mb) => format!("\"{}\"", mb.url()), Some(val) => format!("\"{}\"", val.as_ref()),
None => String::from("null"), None => String::from("null"),
} }
} }
fn vec_to_urls<U: IUrl>(vec: &[U]) -> String { fn vec_to_str<S: AsRef<str>>(vec: &[S]) -> String {
let mut urls: Vec<String> = vec![]; let mut urls: Vec<String> = vec![];
for item in vec.iter() { for item in vec.iter() {
urls.push(format!("\"{}\"", item.url())); urls.push(format!("\"{}\"", item.as_ref()));
} }
format!("[{}]", urls.join(",")) format!("[{}]", urls.join(","))
} }
fn artist_to_json(artist: &Artist) -> String { fn artist_id_to_str(id: &ArtistId) -> String {
let album_artist = &artist.id.name; format!("{{\"name\":\"{}\"}}", id.name)
}
fn artist_to_json(artist: &Artist) -> String {
let mut albums: Vec<String> = vec![]; let mut albums: Vec<String> = vec![];
for album in artist.albums.iter() { for album in artist.albums.iter() {
let album_year = album.id.year; let album_year = album.id.year;
@ -127,10 +129,10 @@ mod tests {
} }
let albums = albums.join(","); let albums = albums.join(",");
let musicbrainz = opt_to_url(&artist.properties.musicbrainz); let musicbrainz = opt_to_str(&artist.properties.musicbrainz);
let musicbutler = vec_to_urls(&artist.properties.musicbutler); let musicbutler = vec_to_str(&artist.properties.musicbutler);
let bandcamp = vec_to_urls(&artist.properties.bandcamp); let bandcamp = vec_to_str(&artist.properties.bandcamp);
let qobuz = opt_to_url(&artist.properties.qobuz); let qobuz = opt_to_str(&artist.properties.qobuz);
let properties = format!( let properties = format!(
"{{\ "{{\
@ -141,9 +143,17 @@ mod tests {
}}" }}"
); );
let album_artist = artist_id_to_str(&artist.id);
let album_artist_sort = artist
.sort
.as_ref()
.map(artist_id_to_str)
.unwrap_or_else(|| "null".to_string());
format!( format!(
"{{\ "{{\
\"id\":{{\"name\":\"{album_artist}\"}},\ \"id\":{album_artist},\
\"sort\":{album_artist_sort},\
\"properties\":{properties},\ \"properties\":{properties},\
\"albums\":[{albums}]\ \"albums\":[{albums}]\
}}" }}"

View File

@ -18,11 +18,6 @@ use serde::{Deserialize, Serialize};
use url::Url; use url::Url;
use uuid::Uuid; use uuid::Uuid;
/// An object with the [`IUrl`] trait contains a valid URL.
pub trait IUrl {
fn url(&self) -> &str;
}
/// An object with the [`IMbid`] trait contains a [MusicBrainz /// An object with the [`IMbid`] trait contains a [MusicBrainz
/// Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) (MBID). /// Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) (MBID).
pub trait IMbid { pub trait IMbid {
@ -83,6 +78,12 @@ impl MusicBrainz {
} }
} }
impl AsRef<str> for MusicBrainz {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl TryFrom<&str> for MusicBrainz { impl TryFrom<&str> for MusicBrainz {
type Error = Error; type Error = Error;
@ -97,12 +98,6 @@ impl Display for MusicBrainz {
} }
} }
impl IUrl for MusicBrainz {
fn url(&self) -> &str {
self.0.as_str()
}
}
impl IMbid for MusicBrainz { impl IMbid for MusicBrainz {
fn mbid(&self) -> &str { fn mbid(&self) -> &str {
// The URL is assumed to have been validated. // The URL is assumed to have been validated.
@ -142,6 +137,12 @@ impl MusicButler {
} }
} }
impl AsRef<str> for MusicButler {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl TryFrom<&str> for MusicButler { impl TryFrom<&str> for MusicButler {
type Error = Error; type Error = Error;
@ -150,12 +151,6 @@ impl TryFrom<&str> for MusicButler {
} }
} }
impl IUrl for MusicButler {
fn url(&self) -> &str {
self.0.as_str()
}
}
/// Bandcamp reference. /// Bandcamp reference.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct Bandcamp(Url); pub struct Bandcamp(Url);
@ -188,6 +183,12 @@ impl Bandcamp {
} }
} }
impl AsRef<str> for Bandcamp {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl TryFrom<&str> for Bandcamp { impl TryFrom<&str> for Bandcamp {
type Error = Error; type Error = Error;
@ -196,12 +197,6 @@ impl TryFrom<&str> for Bandcamp {
} }
} }
impl IUrl for Bandcamp {
fn url(&self) -> &str {
self.0.as_str()
}
}
/// Qobuz reference. /// Qobuz reference.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct Qobuz(Url); pub struct Qobuz(Url);
@ -230,6 +225,12 @@ impl Qobuz {
} }
} }
impl AsRef<str> for Qobuz {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl TryFrom<&str> for Qobuz { impl TryFrom<&str> for Qobuz {
type Error = Error; type Error = Error;
@ -244,12 +245,6 @@ impl Display for Qobuz {
} }
} }
impl IUrl for Qobuz {
fn url(&self) -> &str {
self.0.as_str()
}
}
/// The track file format. /// The track file format.
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)] #[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)]
pub enum Format { pub enum Format {
@ -582,6 +577,7 @@ impl Ord for Artist {
impl Merge for Artist { impl Merge for Artist {
fn merge(mut self, other: Self) -> Self { fn merge(mut self, other: Self) -> Self {
assert_eq!(self.id, other.id); assert_eq!(self.id, other.id);
self.sort = Self::merge_opts(self.sort, other.sort);
self.properties = self.properties.merge(other.properties); self.properties = self.properties.merge(other.properties);
self.albums = MergeSorted::new(self.albums.into_iter(), other.albums.into_iter()).collect(); self.albums = MergeSorted::new(self.albums.into_iter(), other.albums.into_iter()).collect();
self self
@ -1126,7 +1122,7 @@ mod tests {
let uuid = "d368baa8-21ca-4759-9731-0b2753071ad8"; let uuid = "d368baa8-21ca-4759-9731-0b2753071ad8";
let url = format!("https://musicbrainz.org/artist/{uuid}"); let url = format!("https://musicbrainz.org/artist/{uuid}");
let mb = MusicBrainz::new(&url).unwrap(); let mb = MusicBrainz::new(&url).unwrap();
assert_eq!(url, mb.url()); assert_eq!(url, mb.as_ref());
assert_eq!(uuid, mb.mbid()); assert_eq!(uuid, mb.mbid());
let url = "not a url at all".to_string(); let url = "not a url at all".to_string();

View File

@ -126,7 +126,7 @@ impl<BLE: IBeetsLibraryExecutor> ILibraryPrivate for BeetsLibrary<BLE> {
} }
let split: Vec<&str> = line.split(LIST_FORMAT_SEPARATOR).collect(); let split: Vec<&str> = line.split(LIST_FORMAT_SEPARATOR).collect();
if split.len() != 8 { if split.len() != 9 {
return Err(Error::Invalid(line.to_string())); return Err(Error::Invalid(line.to_string()));
} }
@ -363,8 +363,8 @@ mod tests {
.split(LIST_FORMAT_SEPARATOR) .split(LIST_FORMAT_SEPARATOR)
.map(|s| s.to_owned()) .map(|s| s.to_owned())
.collect::<Vec<String>>(); .collect::<Vec<String>>();
invalid_string[6].clear(); invalid_string[7].clear();
invalid_string[6].push_str("invalid format"); invalid_string[7].push_str("invalid format");
let invalid_string = invalid_string.join(LIST_FORMAT_SEPARATOR); let invalid_string = invalid_string.join(LIST_FORMAT_SEPARATOR);
output[2] = invalid_string.clone(); output[2] = invalid_string.clone();
let result = Ok(output); let result = Ok(output);

View File

@ -1,6 +1,6 @@
use std::fmt; use std::fmt;
use musichoard::{Album, Artist, Collection, Format, IUrl, Track}; use musichoard::{Album, Artist, Collection, Format, Track};
use ratatui::{ use ratatui::{
backend::Backend, backend::Backend,
layout::{Alignment, Rect}, layout::{Alignment, Rect},
@ -409,20 +409,23 @@ struct ArtistOverlay<'a> {
} }
impl<'a> ArtistOverlay<'a> { impl<'a> ArtistOverlay<'a> {
fn opt_opt_to_str<U: IUrl>(opt: Option<Option<&U>>) -> &str { fn opt_opt_to_str<S: AsRef<str>>(opt: Option<Option<&S>>) -> &str {
opt.flatten().map(|item| item.url()).unwrap_or("") opt.flatten().map(|item| item.as_ref()).unwrap_or("")
} }
fn opt_vec_to_string<U: IUrl>(opt_vec: Option<&Vec<U>>, indent: &str) -> String { fn opt_vec_to_string<S: AsRef<str>>(opt_vec: Option<&Vec<S>>, indent: &str) -> String {
opt_vec opt_vec
.map(|vec| { .map(|vec| {
if vec.len() < 2 { if vec.len() < 2 {
vec.first().map(|item| item.url()).unwrap_or("").to_string() vec.first()
.map(|item| item.as_ref())
.unwrap_or("")
.to_string()
} else { } else {
let indent = format!("\n{indent}"); let indent = format!("\n{indent}");
let list = vec let list = vec
.iter() .iter()
.map(|item| item.url()) .map(|item| item.as_ref())
.collect::<Vec<&str>>() .collect::<Vec<&str>>()
.join(&indent); .join(&indent);
format!("{indent}{list}") format!("{indent}{list}")

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -108,7 +108,7 @@ fn test_album_artist_query() {
.list(Query::new().include(Field::AlbumArtist(String::from("Аркона")))) .list(Query::new().include(Field::AlbumArtist(String::from("Аркона"))))
.unwrap(); .unwrap();
let expected: Vec<Item> = artists_to_items(&COLLECTION[4..5]); let expected: Vec<Item> = artists_to_items(&COLLECTION[0..1]);
assert_eq!(output, expected); assert_eq!(output, expected);
} }
@ -121,7 +121,7 @@ fn test_album_title_query() {
.list(Query::new().include(Field::AlbumTitle(String::from("Slovo")))) .list(Query::new().include(Field::AlbumTitle(String::from("Slovo"))))
.unwrap(); .unwrap();
let expected: Vec<Item> = artists_to_items(&COLLECTION[4..5]); let expected: Vec<Item> = artists_to_items(&COLLECTION[0..1]);
assert_eq!(output, expected); assert_eq!(output, expected);
} }
@ -133,7 +133,7 @@ fn test_exclude_query() {
let output = beets let output = beets
.list(Query::new().exclude(Field::AlbumArtist(String::from("Аркона")))) .list(Query::new().exclude(Field::AlbumArtist(String::from("Аркона"))))
.unwrap(); .unwrap();
let expected: Vec<Item> = artists_to_items(&COLLECTION[..4]); let expected: Vec<Item> = artists_to_items(&COLLECTION[1..]);
let output: HashSet<_> = output.iter().collect(); let output: HashSet<_> = output.iter().collect();
let expected: HashSet<_> = expected.iter().collect(); let expected: HashSet<_> = expected.iter().collect();

View File

@ -6,6 +6,190 @@ use once_cell::sync::Lazy;
pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection { pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
vec![ vec![
Artist {
id: ArtistId {
name: String::from("Аркона"),
},
sort: Some(ArtistId{
name: String::from("Arkona")
}),
properties: ArtistProperties {
musicbrainz: Some(MusicBrainz::new(
"https://musicbrainz.org/artist/baad262d-55ef-427a-83c7-f7530964f212",
).unwrap()),
musicbutler: vec![
MusicButler::new("https://www.musicbutler.io/artist-page/283448581").unwrap(),
],
bandcamp: vec![
Bandcamp::new("https://arkonamoscow.bandcamp.com/").unwrap(),
],
qobuz: Some(Qobuz::new(
"https://www.qobuz.com/nl-nl/interpreter/arkona/download-streaming-albums",
).unwrap()),
},
albums: vec![Album {
id: AlbumId {
year: 2011,
title: String::from("Slovo"),
},
tracks: vec![
Track {
id: TrackId {
number: 1,
title: String::from("Az"),
},
artist: vec![String::from("Аркона")],
quality: Quality {
format: Format::Flac,
bitrate: 992,
},
},
Track {
id: TrackId {
number: 2,
title: String::from("Arkaim"),
},
artist: vec![String::from("Аркона")],
quality: Quality {
format: Format::Flac,
bitrate: 1061,
},
},
Track {
id: TrackId {
number: 3,
title: String::from("Bolno mne"),
},
artist: vec![String::from("Аркона")],
quality: Quality {
format: Format::Flac,
bitrate: 1004,
},
},
Track {
id: TrackId {
number: 4,
title: String::from("Leshiy"),
},
artist: vec![String::from("Аркона")],
quality: Quality {
format: Format::Flac,
bitrate: 1077,
},
},
Track {
id: TrackId {
number: 5,
title: String::from("Zakliatie"),
},
artist: vec![String::from("Аркона")],
quality: Quality {
format: Format::Flac,
bitrate: 1041,
},
},
Track {
id: TrackId {
number: 6,
title: String::from("Predok"),
},
artist: vec![String::from("Аркона")],
quality: Quality {
format: Format::Flac,
bitrate: 756,
},
},
Track {
id: TrackId {
number: 7,
title: String::from("Nikogda"),
},
artist: vec![String::from("Аркона")],
quality: Quality {
format: Format::Flac,
bitrate: 1059,
},
},
Track {
id: TrackId {
number: 8,
title: String::from("Tam za tumanami"),
},
artist: vec![String::from("Аркона")],
quality: Quality {
format: Format::Flac,
bitrate: 1023,
},
},
Track {
id: TrackId {
number: 9,
title: String::from("Potomok"),
},
artist: vec![String::from("Аркона")],
quality: Quality {
format: Format::Flac,
bitrate: 838,
},
},
Track {
id: TrackId {
number: 10,
title: String::from("Slovo"),
},
artist: vec![String::from("Аркона")],
quality: Quality {
format: Format::Flac,
bitrate: 1028,
},
},
Track {
id: TrackId {
number: 11,
title: String::from("Odna"),
},
artist: vec![String::from("Аркона")],
quality: Quality {
format: Format::Flac,
bitrate: 991,
},
},
Track {
id: TrackId {
number: 12,
title: String::from("Vo moiom sadochke…"),
},
artist: vec![String::from("Аркона")],
quality: Quality {
format: Format::Flac,
bitrate: 919,
},
},
Track {
id: TrackId {
number: 13,
title: String::from("Stenka na stenku"),
},
artist: vec![String::from("Аркона")],
quality: Quality {
format: Format::Flac,
bitrate: 1039,
},
},
Track {
id: TrackId {
number: 14,
title: String::from("Zimushka"),
},
artist: vec![String::from("Аркона")],
quality: Quality {
format: Format::Flac,
bitrate: 974,
},
},
],
}],
},
Artist { Artist {
id: ArtistId { id: ArtistId {
name: String::from("Eluveitie"), name: String::from("Eluveitie"),
@ -391,7 +575,9 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
id: ArtistId { id: ArtistId {
name: String::from("Heavens Basement"), name: String::from("Heavens Basement"),
}, },
sort: None, sort: Some(ArtistId {
name: String::from("Heavens Basement"),
}),
properties: ArtistProperties { properties: ArtistProperties {
musicbrainz: Some(MusicBrainz::new( musicbrainz: Some(MusicBrainz::new(
"https://musicbrainz.org/artist/c2c4d56a-d599-4a18-bd2f-ae644e2198cc", "https://musicbrainz.org/artist/c2c4d56a-d599-4a18-bd2f-ae644e2198cc",
@ -863,189 +1049,5 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
}, },
], ],
}, },
Artist {
id: ArtistId {
name: String::from("Аркона"),
},
sort: Some(ArtistId{
name: String::from("Arkona")
}),
properties: ArtistProperties {
musicbrainz: Some(MusicBrainz::new(
"https://musicbrainz.org/artist/baad262d-55ef-427a-83c7-f7530964f212",
).unwrap()),
musicbutler: vec![
MusicButler::new("https://www.musicbutler.io/artist-page/283448581").unwrap(),
],
bandcamp: vec![
Bandcamp::new("https://arkonamoscow.bandcamp.com/").unwrap(),
],
qobuz: Some(Qobuz::new(
"https://www.qobuz.com/nl-nl/interpreter/arkona/download-streaming-albums",
).unwrap()),
},
albums: vec![Album {
id: AlbumId {
year: 2011,
title: String::from("Slovo"),
},
tracks: vec![
Track {
id: TrackId {
number: 1,
title: String::from("Az"),
},
artist: vec![String::from("Аркона")],
quality: Quality {
format: Format::Flac,
bitrate: 992,
},
},
Track {
id: TrackId {
number: 2,
title: String::from("Arkaim"),
},
artist: vec![String::from("Аркона")],
quality: Quality {
format: Format::Flac,
bitrate: 1061,
},
},
Track {
id: TrackId {
number: 3,
title: String::from("Bolno mne"),
},
artist: vec![String::from("Аркона")],
quality: Quality {
format: Format::Flac,
bitrate: 1004,
},
},
Track {
id: TrackId {
number: 4,
title: String::from("Leshiy"),
},
artist: vec![String::from("Аркона")],
quality: Quality {
format: Format::Flac,
bitrate: 1077,
},
},
Track {
id: TrackId {
number: 5,
title: String::from("Zakliatie"),
},
artist: vec![String::from("Аркона")],
quality: Quality {
format: Format::Flac,
bitrate: 1041,
},
},
Track {
id: TrackId {
number: 6,
title: String::from("Predok"),
},
artist: vec![String::from("Аркона")],
quality: Quality {
format: Format::Flac,
bitrate: 756,
},
},
Track {
id: TrackId {
number: 7,
title: String::from("Nikogda"),
},
artist: vec![String::from("Аркона")],
quality: Quality {
format: Format::Flac,
bitrate: 1059,
},
},
Track {
id: TrackId {
number: 8,
title: String::from("Tam za tumanami"),
},
artist: vec![String::from("Аркона")],
quality: Quality {
format: Format::Flac,
bitrate: 1023,
},
},
Track {
id: TrackId {
number: 9,
title: String::from("Potomok"),
},
artist: vec![String::from("Аркона")],
quality: Quality {
format: Format::Flac,
bitrate: 838,
},
},
Track {
id: TrackId {
number: 10,
title: String::from("Slovo"),
},
artist: vec![String::from("Аркона")],
quality: Quality {
format: Format::Flac,
bitrate: 1028,
},
},
Track {
id: TrackId {
number: 11,
title: String::from("Odna"),
},
artist: vec![String::from("Аркона")],
quality: Quality {
format: Format::Flac,
bitrate: 991,
},
},
Track {
id: TrackId {
number: 12,
title: String::from("Vo moiom sadochke…"),
},
artist: vec![String::from("Аркона")],
quality: Quality {
format: Format::Flac,
bitrate: 919,
},
},
Track {
id: TrackId {
number: 13,
title: String::from("Stenka na stenku"),
},
artist: vec![String::from("Аркона")],
quality: Quality {
format: Format::Flac,
bitrate: 1039,
},
},
Track {
id: TrackId {
number: 14,
title: String::from("Zimushka"),
},
artist: vec![String::from("Аркона")],
quality: Quality {
format: Format::Flac,
bitrate: 974,
},
},
],
}],
},
] ]
}); });