Sort albums by month if two releases of the same artist happen in the same year #155

Merged
30 changed files with 1786 additions and 797 deletions

View File

@ -3,7 +3,7 @@ use std::path::PathBuf;
use structopt::{clap::AppSettings, StructOpt}; use structopt::{clap::AppSettings, StructOpt};
use musichoard::{ use musichoard::{
collection::artist::ArtistId, collection::{album::AlbumId, artist::ArtistId},
database::json::{backend::JsonDatabaseFileBackend, JsonDatabase}, database::json::{backend::JsonDatabaseFileBackend, JsonDatabase},
MusicHoard, MusicHoardBuilder, NoLibrary, MusicHoard, MusicHoardBuilder, NoLibrary,
}; };
@ -22,56 +22,54 @@ struct Opt {
database_file_path: PathBuf, database_file_path: PathBuf,
#[structopt(subcommand)] #[structopt(subcommand)]
category: Category, command: Command,
} }
#[derive(StructOpt, Debug)] #[derive(StructOpt, Debug)]
enum Category { enum Command {
#[structopt(about = "Edit artist information")] #[structopt(about = "Modify artist information")]
Artist(ArtistCommand), Artist(ArtistOpt),
} }
impl Category { #[derive(StructOpt, Debug)]
fn handle(self, music_hoard: &mut MH) { struct ArtistOpt {
match self { // For some reason, not specyfing the artist name with the `long` name makes StructOpt failed
Category::Artist(artist_command) => artist_command.handle(music_hoard), // for inexplicable reason. For example, it won't recognise `Abadde` or `Abadden` as a name and
} // will insteady try to process it as a command.
} #[structopt(long, help = "The name of the artist")]
name: String,
#[structopt(subcommand)]
command: ArtistCommand,
} }
#[derive(StructOpt, Debug)] #[derive(StructOpt, Debug)]
enum ArtistCommand { enum ArtistCommand {
#[structopt(about = "Add a new artist to the collection")] #[structopt(about = "Add a new artist to the collection")]
Add(ArtistValue), Add,
#[structopt(about = "Remove an artist from the collection")] #[structopt(about = "Remove an artist from the collection")]
Remove(ArtistValue), Remove,
#[structopt(about = "Edit the artist's sort name")] #[structopt(about = "Edit the artist's sort name")]
Sort(SortCommand), Sort(SortCommand),
#[structopt(name = "musicbrainz", about = "Edit the MusicBrainz URL of an artist")] #[structopt(name = "musicbrainz", about = "Edit the MusicBrainz URL of an artist")]
MusicBrainz(MusicBrainzCommand), MusicBrainz(MusicBrainzCommand),
#[structopt(name = "property", about = "Edit a property of an artist")] #[structopt(about = "Edit a property of an artist")]
Property(PropertyCommand), Property(PropertyCommand),
#[structopt(about = "Modify the artist's album information")]
Album(AlbumOpt),
} }
#[derive(StructOpt, Debug)] #[derive(StructOpt, Debug)]
enum SortCommand { enum SortCommand {
#[structopt(about = "Set the provided name as the artist's sort name")] #[structopt(about = "Set the provided name as the artist's sort name")]
Set(ArtistSortValue), Set(SortValue),
#[structopt(about = "Clear the artist's sort name")] #[structopt(about = "Clear the artist's sort name")]
Clear(ArtistValue), Clear,
} }
#[derive(StructOpt, Debug)] #[derive(StructOpt, Debug)]
struct ArtistValue { struct SortValue {
#[structopt(help = "The name of the artist")] #[structopt(help = "The sort name")]
artist: String,
}
#[derive(StructOpt, Debug)]
struct ArtistSortValue {
#[structopt(help = "The name of the artist")]
artist: String,
#[structopt(help = "The sort name of the artist")]
sort: String, sort: String,
} }
@ -80,13 +78,11 @@ enum MusicBrainzCommand {
#[structopt(about = "Set the MusicBrainz URL overwriting any existing value")] #[structopt(about = "Set the MusicBrainz URL overwriting any existing value")]
Set(MusicBrainzValue), Set(MusicBrainzValue),
#[structopt(about = "Clear the MusicBrainz URL)")] #[structopt(about = "Clear the MusicBrainz URL)")]
Clear(ArtistValue), Clear,
} }
#[derive(StructOpt, Debug)] #[derive(StructOpt, Debug)]
struct MusicBrainzValue { struct MusicBrainzValue {
#[structopt(help = "The name of the artist")]
artist: String,
#[structopt(help = "The MusicBrainz URL")] #[structopt(help = "The MusicBrainz URL")]
url: String, url: String,
} }
@ -105,8 +101,6 @@ enum PropertyCommand {
#[derive(StructOpt, Debug)] #[derive(StructOpt, Debug)]
struct PropertyValue { struct PropertyValue {
#[structopt(help = "The name of the artist")]
artist: String,
#[structopt(help = "The name of the property")] #[structopt(help = "The name of the property")]
property: String, property: String,
#[structopt(help = "The list of values")] #[structopt(help = "The list of values")]
@ -115,101 +109,176 @@ struct PropertyValue {
#[derive(StructOpt, Debug)] #[derive(StructOpt, Debug)]
struct PropertyName { struct PropertyName {
#[structopt(help = "The name of the artist")]
artist: String,
#[structopt(help = "The name of the property")] #[structopt(help = "The name of the property")]
property: String, property: String,
} }
impl ArtistCommand { #[derive(StructOpt, Debug)]
struct AlbumOpt {
// Using `long` for consistency with `ArtistOpt`.
#[structopt(long, help = "The title of the album")]
title: String,
#[structopt(subcommand)]
command: AlbumCommand,
}
#[derive(StructOpt, Debug)]
enum AlbumCommand {
#[structopt(about = "Edit the album's sequence value")]
Seq(AlbumSeqCommand),
}
#[derive(StructOpt, Debug)]
enum AlbumSeqCommand {
#[structopt(about = "Set the sequence value overwriting any existing value")]
Set(AlbumSeqValue),
#[structopt(about = "Clear the sequence value")]
Clear,
}
#[derive(StructOpt, Debug)]
struct AlbumSeqValue {
#[structopt(help = "The new sequence value")]
value: u8,
}
impl Command {
fn handle(self, music_hoard: &mut MH) { fn handle(self, music_hoard: &mut MH) {
match self { match self {
ArtistCommand::Add(artist_value) => { Command::Artist(artist_opt) => artist_opt.handle(music_hoard),
}
}
}
impl ArtistOpt {
fn handle(self, music_hoard: &mut MH) {
self.command.handle(music_hoard, &self.name)
}
}
impl ArtistCommand {
fn handle(self, music_hoard: &mut MH, artist_name: &str) {
match self {
ArtistCommand::Add => {
music_hoard music_hoard
.add_artist(ArtistId::new(artist_value.artist)) .add_artist(ArtistId::new(artist_name))
.expect("failed to add artist"); .expect("failed to add artist");
} }
ArtistCommand::Remove(artist_value) => { ArtistCommand::Remove => {
music_hoard music_hoard
.remove_artist(ArtistId::new(artist_value.artist)) .remove_artist(ArtistId::new(artist_name))
.expect("failed to remove artist"); .expect("failed to remove artist");
} }
ArtistCommand::Sort(sort_command) => { ArtistCommand::Sort(sort_command) => {
sort_command.handle(music_hoard); sort_command.handle(music_hoard, artist_name);
} }
ArtistCommand::MusicBrainz(musicbrainz_command) => { ArtistCommand::MusicBrainz(musicbrainz_command) => {
musicbrainz_command.handle(music_hoard) musicbrainz_command.handle(music_hoard, artist_name)
} }
ArtistCommand::Property(property_command) => { ArtistCommand::Property(property_command) => {
property_command.handle(music_hoard); property_command.handle(music_hoard, artist_name);
}
ArtistCommand::Album(album_opt) => {
album_opt.handle(music_hoard, artist_name);
} }
} }
} }
} }
impl SortCommand { impl SortCommand {
fn handle(self, music_hoard: &mut MH) { fn handle(self, music_hoard: &mut MH, artist_name: &str) {
match self { match self {
SortCommand::Set(artist_sort_value) => music_hoard SortCommand::Set(artist_sort_value) => music_hoard
.set_artist_sort( .set_artist_sort(
ArtistId::new(artist_sort_value.artist), ArtistId::new(artist_name),
ArtistId::new(artist_sort_value.sort), ArtistId::new(artist_sort_value.sort),
) )
.expect("faild to set artist sort name"), .expect("faild to set artist sort name"),
SortCommand::Clear(artist_value) => music_hoard SortCommand::Clear => music_hoard
.clear_artist_sort(ArtistId::new(artist_value.artist)) .clear_artist_sort(ArtistId::new(artist_name))
.expect("failed to clear artist sort name"), .expect("failed to clear artist sort name"),
} }
} }
} }
impl MusicBrainzCommand { impl MusicBrainzCommand {
fn handle(self, music_hoard: &mut MH) { fn handle(self, music_hoard: &mut MH, artist_name: &str) {
match self { match self {
MusicBrainzCommand::Set(musicbrainz_value) => music_hoard MusicBrainzCommand::Set(musicbrainz_value) => music_hoard
.set_musicbrainz_url( .set_artist_musicbrainz(ArtistId::new(artist_name), musicbrainz_value.url)
ArtistId::new(musicbrainz_value.artist),
musicbrainz_value.url,
)
.expect("failed to set MusicBrainz URL"), .expect("failed to set MusicBrainz URL"),
MusicBrainzCommand::Clear(artist_value) => music_hoard MusicBrainzCommand::Clear => music_hoard
.clear_musicbrainz_url(ArtistId::new(artist_value.artist)) .clear_artist_musicbrainz(ArtistId::new(artist_name))
.expect("failed to clear MusicBrainz URL"), .expect("failed to clear MusicBrainz URL"),
} }
} }
} }
impl PropertyCommand { impl PropertyCommand {
fn handle(self, music_hoard: &mut MH) { fn handle(self, music_hoard: &mut MH, artist_name: &str) {
match self { match self {
PropertyCommand::Add(property_value) => music_hoard PropertyCommand::Add(property_value) => music_hoard
.add_to_property( .add_to_artist_property(
ArtistId::new(property_value.artist), ArtistId::new(artist_name),
property_value.property, property_value.property,
property_value.values, property_value.values,
) )
.expect("failed to add values to property"), .expect("failed to add values to property"),
PropertyCommand::Remove(property_value) => music_hoard PropertyCommand::Remove(property_value) => music_hoard
.remove_from_property( .remove_from_artist_property(
ArtistId::new(property_value.artist), ArtistId::new(artist_name),
property_value.property, property_value.property,
property_value.values, property_value.values,
) )
.expect("failed to remove values from property"), .expect("failed to remove values from property"),
PropertyCommand::Set(property_value) => music_hoard PropertyCommand::Set(property_value) => music_hoard
.set_property( .set_artist_property(
ArtistId::new(property_value.artist), ArtistId::new(artist_name),
property_value.property, property_value.property,
property_value.values, property_value.values,
) )
.expect("failed to set property"), .expect("failed to set property"),
PropertyCommand::Clear(property_name) => music_hoard PropertyCommand::Clear(property_name) => music_hoard
.clear_property(ArtistId::new(property_name.artist), property_name.property) .clear_artist_property(ArtistId::new(artist_name), property_name.property)
.expect("failed to clear property"), .expect("failed to clear property"),
} }
} }
} }
impl AlbumOpt {
fn handle(self, music_hoard: &mut MH, artist_name: &str) {
self.command.handle(music_hoard, artist_name, &self.title)
}
}
impl AlbumCommand {
fn handle(self, music_hoard: &mut MH, artist_name: &str, album_name: &str) {
match self {
AlbumCommand::Seq(seq_command) => {
seq_command.handle(music_hoard, artist_name, album_name);
}
}
}
}
impl AlbumSeqCommand {
fn handle(self, music_hoard: &mut MH, artist_name: &str, album_name: &str) {
match self {
AlbumSeqCommand::Set(seq_value) => music_hoard
.set_album_seq(
ArtistId::new(artist_name),
AlbumId::new(album_name),
seq_value.value,
)
.expect("failed to set sequence value"),
AlbumSeqCommand::Clear => music_hoard
.clear_album_seq(ArtistId::new(artist_name), AlbumId::new(album_name))
.expect("failed to clear sequence value"),
}
}
}
fn main() { fn main() {
let opt = Opt::from_args(); let opt = Opt::from_args();
@ -219,5 +288,5 @@ fn main() {
.set_database(db) .set_database(db)
.build() .build()
.expect("failed to initialise MusicHoard"); .expect("failed to initialise MusicHoard");
opt.category.handle(&mut music_hoard); opt.command.handle(&mut music_hoard);
} }

View File

@ -1,7 +1,10 @@
use std::mem; use std::{
fmt::{self, Display},
mem,
};
use crate::core::collection::{ use crate::core::collection::{
merge::{Merge, MergeSorted}, merge::{Merge, MergeSorted, WithId},
track::Track, track::Track,
}; };
@ -9,19 +12,106 @@ use crate::core::collection::{
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct Album { pub struct Album {
pub id: AlbumId, pub id: AlbumId,
pub date: AlbumDate,
pub seq: AlbumSeq,
pub tracks: Vec<Track>, pub tracks: Vec<Track>,
} }
impl WithId for Album {
type Id = AlbumId;
fn id(&self) -> &Self::Id {
&self.id
}
}
/// The album identifier. /// The album identifier.
#[derive(Clone, Debug, PartialEq, PartialOrd, Ord, Eq, Hash)] #[derive(Clone, Debug, PartialEq, PartialOrd, Ord, Eq, Hash)]
pub struct AlbumId { pub struct AlbumId {
pub year: u32,
pub title: String, pub title: String,
} }
// There are crates for handling dates, but we don't need much complexity beyond year-month-day.
/// The album's release date.
#[derive(Clone, Debug, Default, PartialEq, PartialOrd, Ord, Eq, Hash)]
pub struct AlbumDate {
pub year: u32,
pub month: AlbumMonth,
pub day: u8,
}
impl Display for AlbumDate {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if self.month.is_none() {
write!(f, "{}", self.year)
} else if self.day == 0 {
write!(f, "{}{:02}", self.year, self.month as u8)
} else {
write!(f, "{}{:02}{:02}", self.year, self.month as u8, self.day)
}
}
}
/// The album's sequence to determine order when two or more albums have the same release date.
#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd, Ord, Eq, Hash)]
pub struct AlbumSeq(pub u8);
#[repr(u8)]
#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd, Ord, Eq, Hash)]
pub enum AlbumMonth {
#[default]
None = 0,
January = 1,
February = 2,
March = 3,
April = 4,
May = 5,
June = 6,
July = 7,
August = 8,
September = 9,
October = 10,
November = 11,
December = 12,
}
impl From<u8> for AlbumMonth {
fn from(value: u8) -> Self {
match value {
1 => AlbumMonth::January,
2 => AlbumMonth::February,
3 => AlbumMonth::March,
4 => AlbumMonth::April,
5 => AlbumMonth::May,
6 => AlbumMonth::June,
7 => AlbumMonth::July,
8 => AlbumMonth::August,
9 => AlbumMonth::September,
10 => AlbumMonth::October,
11 => AlbumMonth::November,
12 => AlbumMonth::December,
_ => AlbumMonth::None,
}
}
}
impl AlbumMonth {
fn is_none(&self) -> bool {
matches!(self, AlbumMonth::None)
}
}
impl Album { impl Album {
pub fn get_sort_key(&self) -> &AlbumId { pub fn get_sort_key(&self) -> (&AlbumDate, &AlbumSeq, &AlbumId) {
&self.id (&self.date, &self.seq, &self.id)
}
pub fn set_seq(&mut self, seq: AlbumSeq) {
self.seq = seq;
}
pub fn clear_seq(&mut self) {
self.seq = AlbumSeq::default();
} }
} }
@ -33,24 +123,140 @@ impl PartialOrd for Album {
impl Ord for Album { impl Ord for Album {
fn cmp(&self, other: &Self) -> std::cmp::Ordering { fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.id.cmp(&other.id) self.get_sort_key().cmp(&other.get_sort_key())
} }
} }
impl Merge for Album { impl Merge for Album {
fn merge_in_place(&mut self, other: Self) { fn merge_in_place(&mut self, other: Self) {
assert_eq!(self.id, other.id); assert_eq!(self.id, other.id);
self.seq = std::cmp::max(self.seq, other.seq);
let tracks = mem::take(&mut self.tracks); let tracks = mem::take(&mut self.tracks);
self.tracks = MergeSorted::new(tracks.into_iter(), other.tracks.into_iter()).collect(); self.tracks = MergeSorted::new(tracks.into_iter(), other.tracks.into_iter()).collect();
} }
} }
impl<S: Into<String>> From<S> for AlbumId {
fn from(value: S) -> Self {
AlbumId::new(value)
}
}
impl AsRef<AlbumId> for AlbumId {
fn as_ref(&self) -> &AlbumId {
self
}
}
impl AlbumId {
pub fn new<S: Into<String>>(name: S) -> AlbumId {
AlbumId { title: name.into() }
}
}
impl Display for AlbumId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.title)
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::core::testmod::FULL_COLLECTION; use crate::core::testmod::FULL_COLLECTION;
use super::*; use super::*;
impl AlbumDate {
fn new<M: Into<AlbumMonth>>(year: u32, month: M, day: u8) -> Self {
AlbumDate {
year,
month: month.into(),
day,
}
}
}
#[test]
fn album_month() {
assert_eq!(<u8 as Into<AlbumMonth>>::into(0), AlbumMonth::None);
assert_eq!(<u8 as Into<AlbumMonth>>::into(1), AlbumMonth::January);
assert_eq!(<u8 as Into<AlbumMonth>>::into(2), AlbumMonth::February);
assert_eq!(<u8 as Into<AlbumMonth>>::into(3), AlbumMonth::March);
assert_eq!(<u8 as Into<AlbumMonth>>::into(4), AlbumMonth::April);
assert_eq!(<u8 as Into<AlbumMonth>>::into(5), AlbumMonth::May);
assert_eq!(<u8 as Into<AlbumMonth>>::into(6), AlbumMonth::June);
assert_eq!(<u8 as Into<AlbumMonth>>::into(7), AlbumMonth::July);
assert_eq!(<u8 as Into<AlbumMonth>>::into(8), AlbumMonth::August);
assert_eq!(<u8 as Into<AlbumMonth>>::into(9), AlbumMonth::September);
assert_eq!(<u8 as Into<AlbumMonth>>::into(10), AlbumMonth::October);
assert_eq!(<u8 as Into<AlbumMonth>>::into(11), AlbumMonth::November);
assert_eq!(<u8 as Into<AlbumMonth>>::into(12), AlbumMonth::December);
assert_eq!(<u8 as Into<AlbumMonth>>::into(13), AlbumMonth::None);
assert_eq!(<u8 as Into<AlbumMonth>>::into(255), AlbumMonth::None);
}
#[test]
fn album_display() {
assert_eq!(AlbumDate::default().to_string(), "0");
assert_eq!(AlbumDate::new(1990, 0, 0).to_string(), "1990");
assert_eq!(AlbumDate::new(1990, 5, 0).to_string(), "199005");
assert_eq!(AlbumDate::new(1990, 5, 6).to_string(), "19900506");
}
#[test]
fn same_date_seq_cmp() {
let date = AlbumDate::new(2024, 3, 2);
let album_id_1 = AlbumId {
title: String::from("album z"),
};
let album_1 = Album {
id: album_id_1,
date: date.clone(),
seq: AlbumSeq(1),
tracks: vec![],
};
let album_id_2 = AlbumId {
title: String::from("album a"),
};
let album_2 = Album {
id: album_id_2,
date: date.clone(),
seq: AlbumSeq(2),
tracks: vec![],
};
assert_ne!(album_1, album_2);
assert!(album_1 < album_2);
}
#[test]
fn set_clear_seq() {
let mut album = Album {
id: "an album".into(),
date: AlbumDate::default(),
seq: AlbumSeq::default(),
tracks: vec![],
};
assert_eq!(album.seq, AlbumSeq(0));
// Setting a seq on an album.
album.set_seq(AlbumSeq(6));
assert_eq!(album.seq, AlbumSeq(6));
album.set_seq(AlbumSeq(6));
assert_eq!(album.seq, AlbumSeq(6));
album.set_seq(AlbumSeq(8));
assert_eq!(album.seq, AlbumSeq(8));
// Clearing seq.
album.clear_seq();
assert_eq!(album.seq, AlbumSeq(0));
}
#[test] #[test]
fn merge_album_no_overlap() { fn merge_album_no_overlap() {
let left = FULL_COLLECTION[0].albums[0].to_owned(); let left = FULL_COLLECTION[0].albums[0].to_owned();
@ -64,9 +270,9 @@ mod tests {
let merged = left.clone().merge(right.clone()); let merged = left.clone().merge(right.clone());
assert_eq!(expected, merged); assert_eq!(expected, merged);
// Non-overlapping merge should be commutative. // Non-overlapping merge should be commutative in the tracks.
let merged = right.clone().merge(left.clone()); let merged = right.clone().merge(left.clone());
assert_eq!(expected, merged); assert_eq!(expected.tracks, merged.tracks);
} }
#[test] #[test]

View File

@ -2,6 +2,7 @@ use std::{
collections::HashMap, collections::HashMap,
fmt::{self, Debug, Display}, fmt::{self, Debug, Display},
mem, mem,
str::FromStr,
}; };
use url::Url; use url::Url;
@ -9,7 +10,7 @@ use uuid::Uuid;
use crate::core::collection::{ use crate::core::collection::{
album::Album, album::Album,
merge::{Merge, MergeSorted}, merge::{Merge, MergeCollections, WithId},
Error, Error,
}; };
@ -23,6 +24,14 @@ pub struct Artist {
pub albums: Vec<Album>, pub albums: Vec<Album>,
} }
impl WithId for Artist {
type Id = ArtistId;
fn id(&self) -> &Self::Id {
&self.id
}
}
/// The artist identifier. /// The artist identifier.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ArtistId { pub struct ArtistId {
@ -41,8 +50,8 @@ impl Artist {
} }
} }
pub fn get_sort_key(&self) -> &ArtistId { pub fn get_sort_key(&self) -> (&ArtistId,) {
self.sort.as_ref().unwrap_or(&self.id) (self.sort.as_ref().unwrap_or(&self.id),)
} }
pub fn set_sort_key<SORT: Into<ArtistId>>(&mut self, sort: SORT) { pub fn set_sort_key<SORT: Into<ArtistId>>(&mut self, sort: SORT) {
@ -114,18 +123,26 @@ impl PartialOrd for Artist {
impl Ord for Artist { impl Ord for Artist {
fn cmp(&self, other: &Self) -> std::cmp::Ordering { fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.get_sort_key().cmp(other.get_sort_key()) self.get_sort_key().cmp(&other.get_sort_key())
} }
} }
impl Merge for Artist { impl Merge for Artist {
fn merge_in_place(&mut self, other: Self) { fn merge_in_place(&mut self, other: Self) {
assert_eq!(self.id, other.id); assert_eq!(self.id, other.id);
self.sort = self.sort.take().or(other.sort); self.sort = self.sort.take().or(other.sort);
self.musicbrainz = self.musicbrainz.take().or(other.musicbrainz); self.musicbrainz = self.musicbrainz.take().or(other.musicbrainz);
self.properties.merge_in_place(other.properties); self.properties.merge_in_place(other.properties);
let albums = mem::take(&mut self.albums); let albums = mem::take(&mut self.albums);
self.albums = MergeSorted::new(albums.into_iter(), other.albums.into_iter()).collect(); self.albums = MergeCollections::merge_iter(albums, other.albums);
}
}
impl<S: Into<String>> From<S> for ArtistId {
fn from(value: S) -> Self {
ArtistId::new(value)
} }
} }
@ -159,9 +176,13 @@ pub struct MusicBrainz(Url);
impl MusicBrainz { impl MusicBrainz {
/// Validate and wrap a MusicBrainz URL. /// Validate and wrap a MusicBrainz URL.
pub fn new<S: AsRef<str>>(url: S) -> Result<Self, Error> { pub fn new_from_str<S: AsRef<str>>(url: S) -> Result<Self, Error> {
let url = Url::parse(url.as_ref())?; let url = Url::parse(url.as_ref())?;
Self::new_from_url(url)
}
/// Validate and wrap a MusicBrainz URL.
pub fn new_from_url(url: Url) -> Result<Self, Error> {
if !url if !url
.domain() .domain()
.map(|u| u.ends_with("musicbrainz.org")) .map(|u| u.ends_with("musicbrainz.org"))
@ -189,11 +210,36 @@ impl AsRef<str> for MusicBrainz {
} }
} }
impl TryFrom<&str> for MusicBrainz { impl FromStr for MusicBrainz {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
MusicBrainz::new_from_str(s)
}
}
// A blanket TryFrom would be better, but https://stackoverflow.com/a/64407892
macro_rules! impl_try_from_for_musicbrainz {
($from:ty) => {
impl TryFrom<$from> for MusicBrainz {
type Error = Error;
fn try_from(value: $from) -> Result<Self, Self::Error> {
MusicBrainz::new_from_str(value)
}
}
};
}
impl_try_from_for_musicbrainz!(&str);
impl_try_from_for_musicbrainz!(&String);
impl_try_from_for_musicbrainz!(String);
impl TryFrom<Url> for MusicBrainz {
type Error = Error; type Error = Error;
fn try_from(value: &str) -> Result<Self, Self::Error> { fn try_from(value: Url) -> Result<Self, Self::Error> {
MusicBrainz::new(value) MusicBrainz::new_from_url(value)
} }
} }
@ -220,34 +266,35 @@ mod tests {
#[test] #[test]
fn musicbrainz() { fn musicbrainz() {
let uuid = "d368baa8-21ca-4759-9731-0b2753071ad8"; let uuid = "d368baa8-21ca-4759-9731-0b2753071ad8";
let url = format!("https://musicbrainz.org/artist/{uuid}"); let url_str = format!("https://musicbrainz.org/artist/{uuid}");
let mb = MusicBrainz::new(&url).unwrap(); let url: Url = url_str.as_str().try_into().unwrap();
assert_eq!(url, mb.as_ref()); let mb: MusicBrainz = url.try_into().unwrap();
assert_eq!(url_str, 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();
let expected_error: Error = url::ParseError::RelativeUrlWithoutBase.into(); let expected_error: Error = url::ParseError::RelativeUrlWithoutBase.into();
let actual_error = MusicBrainz::new(url).unwrap_err(); let actual_error = MusicBrainz::from_str(&url).unwrap_err();
assert_eq!(actual_error, expected_error); assert_eq!(actual_error, expected_error);
assert_eq!(actual_error.to_string(), expected_error.to_string()); assert_eq!(actual_error.to_string(), expected_error.to_string());
let url = "https://musicbrainz.org/artist/i-am-not-a-uuid".to_string(); let url = "https://musicbrainz.org/artist/i-am-not-a-uuid".to_string();
let expected_error: Error = Uuid::try_parse("i-am-not-a-uuid").unwrap_err().into(); let expected_error: Error = Uuid::try_parse("i-am-not-a-uuid").unwrap_err().into();
let actual_error = MusicBrainz::new(url).unwrap_err(); let actual_error = MusicBrainz::from_str(&url).unwrap_err();
assert_eq!(actual_error, expected_error); assert_eq!(actual_error, expected_error);
assert_eq!(actual_error.to_string(), expected_error.to_string()); assert_eq!(actual_error.to_string(), expected_error.to_string());
let url = "https://musicbrainz.org/artist".to_string(); let url = "https://musicbrainz.org/artist".to_string();
let expected_error = Error::UrlError(format!("invalid MusicBrainz URL: {url}")); let expected_error = Error::UrlError(format!("invalid MusicBrainz URL: {url}"));
let actual_error = MusicBrainz::new(&url).unwrap_err(); let actual_error = MusicBrainz::from_str(&url).unwrap_err();
assert_eq!(actual_error, expected_error); assert_eq!(actual_error, expected_error);
assert_eq!(actual_error.to_string(), expected_error.to_string()); assert_eq!(actual_error.to_string(), expected_error.to_string());
} }
#[test] #[test]
fn urls() { fn urls() {
assert!(MusicBrainz::new(MUSICBRAINZ).is_ok()); assert!(MusicBrainz::from_str(MUSICBRAINZ).is_ok());
assert!(MusicBrainz::new(MUSICBUTLER).is_err()); assert!(MusicBrainz::from_str(MUSICBUTLER).is_err());
} }
#[test] #[test]
@ -256,11 +303,11 @@ mod tests {
let sort_id_1 = ArtistId::new("sort id 1"); let sort_id_1 = ArtistId::new("sort id 1");
let sort_id_2 = ArtistId::new("sort id 2"); let sort_id_2 = ArtistId::new("sort id 2");
let mut artist = Artist::new(artist_id.clone()); let mut artist = Artist::new(&artist_id.name);
assert_eq!(artist.id, artist_id); assert_eq!(artist.id, artist_id);
assert_eq!(artist.sort, None); assert_eq!(artist.sort, None);
assert_eq!(artist.get_sort_key(), &artist_id); assert_eq!(artist.get_sort_key(), (&artist_id,));
assert!(artist < Artist::new(sort_id_1.clone())); assert!(artist < Artist::new(sort_id_1.clone()));
assert!(artist < Artist::new(sort_id_2.clone())); assert!(artist < Artist::new(sort_id_2.clone()));
@ -268,7 +315,7 @@ mod tests {
assert_eq!(artist.id, artist_id); assert_eq!(artist.id, artist_id);
assert_eq!(artist.sort.as_ref(), Some(&sort_id_1)); assert_eq!(artist.sort.as_ref(), Some(&sort_id_1));
assert_eq!(artist.get_sort_key(), &sort_id_1); assert_eq!(artist.get_sort_key(), (&sort_id_1,));
assert!(artist > Artist::new(artist_id.clone())); assert!(artist > Artist::new(artist_id.clone()));
assert!(artist < Artist::new(sort_id_2.clone())); assert!(artist < Artist::new(sort_id_2.clone()));
@ -276,7 +323,7 @@ mod tests {
assert_eq!(artist.id, artist_id); assert_eq!(artist.id, artist_id);
assert_eq!(artist.sort.as_ref(), Some(&sort_id_2)); assert_eq!(artist.sort.as_ref(), Some(&sort_id_2));
assert_eq!(artist.get_sort_key(), &sort_id_2); assert_eq!(artist.get_sort_key(), (&sort_id_2,));
assert!(artist > Artist::new(artist_id.clone())); assert!(artist > Artist::new(artist_id.clone()));
assert!(artist > Artist::new(sort_id_1.clone())); assert!(artist > Artist::new(sort_id_1.clone()));
@ -284,7 +331,7 @@ mod tests {
assert_eq!(artist.id, artist_id); assert_eq!(artist.id, artist_id);
assert_eq!(artist.sort, None); assert_eq!(artist.sort, None);
assert_eq!(artist.get_sort_key(), &artist_id); assert_eq!(artist.get_sort_key(), (&artist_id,));
assert!(artist < Artist::new(sort_id_1.clone())); assert!(artist < Artist::new(sort_id_1.clone()));
assert!(artist < Artist::new(sort_id_2.clone())); assert!(artist < Artist::new(sort_id_2.clone()));
} }
@ -307,14 +354,14 @@ mod tests {
// Setting a URL on an artist. // Setting a URL on an artist.
artist.set_musicbrainz_url(MUSICBRAINZ.try_into().unwrap()); artist.set_musicbrainz_url(MUSICBRAINZ.try_into().unwrap());
_ = expected.insert(MusicBrainz::new(MUSICBRAINZ).unwrap()); _ = expected.insert(MUSICBRAINZ.try_into().unwrap());
assert_eq!(artist.musicbrainz, expected); assert_eq!(artist.musicbrainz, expected);
artist.set_musicbrainz_url(MUSICBRAINZ.try_into().unwrap()); artist.set_musicbrainz_url(MUSICBRAINZ.try_into().unwrap());
assert_eq!(artist.musicbrainz, expected); assert_eq!(artist.musicbrainz, expected);
artist.set_musicbrainz_url(MUSICBRAINZ_2.try_into().unwrap()); artist.set_musicbrainz_url(MUSICBRAINZ_2.try_into().unwrap());
_ = expected.insert(MusicBrainz::new(MUSICBRAINZ_2).unwrap()); _ = expected.insert(MUSICBRAINZ_2.try_into().unwrap());
assert_eq!(artist.musicbrainz, expected); assert_eq!(artist.musicbrainz, expected);
// Clearing URLs. // Clearing URLs.

View File

@ -1,4 +1,4 @@
use std::{cmp::Ordering, collections::HashMap, hash::Hash, iter::Peekable}; use std::{cmp::Ordering, collections::HashMap, hash::Hash, iter::Peekable, marker::PhantomData};
/// A trait for merging two objects. The merge is asymmetric with the left argument considered to be /// A trait for merging two objects. The merge is asymmetric with the left argument considered to be
/// the primary whose properties are to be kept in case of collisions. /// the primary whose properties are to be kept in case of collisions.
@ -79,3 +79,45 @@ where
} }
} }
} }
pub trait WithId {
type Id;
fn id(&self) -> &Self::Id;
}
pub struct MergeCollections<ID, T, IT> {
_id: PhantomData<ID>,
_t: PhantomData<T>,
_it: PhantomData<IT>,
}
impl<ID, T, IT> MergeCollections<ID, T, IT>
where
ID: Eq + Hash + Clone,
T: WithId<Id = ID> + Merge + Ord,
IT: IntoIterator<Item = T>,
{
pub fn merge_iter(primary: IT, secondary: IT) -> Vec<T> {
let primary = primary
.into_iter()
.map(|item| (item.id().clone(), item))
.collect();
Self::merge(primary, secondary)
}
pub fn merge(mut primary: HashMap<ID, T>, secondary: IT) -> Vec<T> {
for secondary_item in secondary {
if let Some(ref mut primary_item) = primary.get_mut(secondary_item.id()) {
primary_item.merge_in_place(secondary_item);
} else {
primary.insert(secondary_item.id().clone(), secondary_item);
}
}
let mut collection: Vec<T> = primary.into_values().collect();
collection.sort_unstable();
collection
}
}

View File

@ -5,7 +5,7 @@ pub mod artist;
pub mod track; pub mod track;
mod merge; mod merge;
pub use merge::Merge; pub use merge::MergeCollections;
use std::fmt::{self, Display}; use std::fmt::{self, Display};

View File

@ -4,33 +4,37 @@ use crate::core::collection::merge::Merge;
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct Track { pub struct Track {
pub id: TrackId, pub id: TrackId,
pub number: TrackNum,
pub artist: Vec<String>, pub artist: Vec<String>,
pub quality: Quality, pub quality: TrackQuality,
} }
/// The track identifier. /// The track identifier.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TrackId { pub struct TrackId {
pub number: u32,
pub title: String, pub title: String,
} }
/// The track number.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TrackNum(pub u32);
/// The track quality. Combines format and bitrate information. /// The track quality. Combines format and bitrate information.
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Quality { pub struct TrackQuality {
pub format: Format, pub format: TrackFormat,
pub bitrate: u32, pub bitrate: u32,
} }
impl Track { impl Track {
pub fn get_sort_key(&self) -> &TrackId { pub fn get_sort_key(&self) -> (&TrackNum, &TrackId) {
&self.id (&self.number, &self.id)
} }
} }
/// The track file format. /// The track file format.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Format { pub enum TrackFormat {
Flac, Flac,
Mp3, Mp3,
} }
@ -43,7 +47,7 @@ impl PartialOrd for Track {
impl Ord for Track { impl Ord for Track {
fn cmp(&self, other: &Self) -> std::cmp::Ordering { fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.id.cmp(&other.id) self.get_sort_key().cmp(&other.get_sort_key())
} }
} }
@ -61,20 +65,21 @@ mod tests {
fn merge_track() { fn merge_track() {
let left = Track { let left = Track {
id: TrackId { id: TrackId {
number: 4,
title: String::from("a title"), title: String::from("a title"),
}, },
number: TrackNum(4),
artist: vec![String::from("left artist")], artist: vec![String::from("left artist")],
quality: Quality { quality: TrackQuality {
format: Format::Flac, format: TrackFormat::Flac,
bitrate: 1411, bitrate: 1411,
}, },
}; };
let right = Track { let right = Track {
id: left.id.clone(), id: left.id.clone(),
number: left.number,
artist: vec![String::from("right artist")], artist: vec![String::from("right artist")],
quality: Quality { quality: TrackQuality {
format: Format::Mp3, format: TrackFormat::Mp3,
bitrate: 320, bitrate: 320,
}, },
}; };

View File

@ -72,7 +72,7 @@ mod tests {
use mockall::predicate; use mockall::predicate;
use crate::core::{ use crate::core::{
collection::{artist::Artist, Collection}, collection::{album::AlbumDate, artist::Artist, Collection},
testmod::FULL_COLLECTION, testmod::FULL_COLLECTION,
}; };
@ -82,7 +82,10 @@ mod tests {
fn expected() -> Collection { fn expected() -> Collection {
let mut expected = FULL_COLLECTION.to_owned(); let mut expected = FULL_COLLECTION.to_owned();
for artist in expected.iter_mut() { for artist in expected.iter_mut() {
artist.albums.clear(); for album in artist.albums.iter_mut() {
album.date = AlbumDate::default();
album.tracks.clear();
}
} }
expected expected
} }

View File

@ -1,5 +1,5 @@
pub static DATABASE_JSON: &str = "{\ pub static DATABASE_JSON: &str = "{\
\"V20240210\":\ \"V20240302\":\
[\ [\
{\ {\
\"name\":\"Album_Artist A\",\ \"name\":\"Album_Artist A\",\
@ -8,7 +8,11 @@ pub static DATABASE_JSON: &str = "{\
\"properties\":{\ \"properties\":{\
\"MusicButler\":[\"https://www.musicbutler.io/artist-page/000000000\"],\ \"MusicButler\":[\"https://www.musicbutler.io/artist-page/000000000\"],\
\"Qobuz\":[\"https://www.qobuz.com/nl-nl/interpreter/artist-a/download-streaming-albums\"]\ \"Qobuz\":[\"https://www.qobuz.com/nl-nl/interpreter/artist-a/download-streaming-albums\"]\
}\ },\
\"albums\":[\
{\"title\":\"album_title a.a\",\"seq\":1},\
{\"title\":\"album_title a.b\",\"seq\":1}\
]\
},\ },\
{\ {\
\"name\":\"Album_Artist B\",\ \"name\":\"Album_Artist B\",\
@ -21,19 +25,33 @@ pub static DATABASE_JSON: &str = "{\
\"https://www.musicbutler.io/artist-page/111111112\"\ \"https://www.musicbutler.io/artist-page/111111112\"\
],\ ],\
\"Qobuz\":[\"https://www.qobuz.com/nl-nl/interpreter/artist-b/download-streaming-albums\"]\ \"Qobuz\":[\"https://www.qobuz.com/nl-nl/interpreter/artist-b/download-streaming-albums\"]\
}\ },\
\"albums\":[\
{\"title\":\"album_title b.a\",\"seq\":1},\
{\"title\":\"album_title b.b\",\"seq\":3},\
{\"title\":\"album_title b.c\",\"seq\":2},\
{\"title\":\"album_title b.d\",\"seq\":4}\
]\
},\ },\
{\ {\
\"name\":\"The Album_Artist C\",\ \"name\":\"The Album_Artist C\",\
\"sort\":\"Album_Artist C, The\",\ \"sort\":\"Album_Artist C, The\",\
\"musicbrainz\":\"https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111\",\ \"musicbrainz\":\"https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111\",\
\"properties\":{}\ \"properties\":{},\
\"albums\":[\
{\"title\":\"album_title c.a\",\"seq\":0},\
{\"title\":\"album_title c.b\",\"seq\":0}\
]\
},\ },\
{\ {\
\"name\":\"Album_Artist D\",\ \"name\":\"Album_Artist D\",\
\"sort\":null,\ \"sort\":null,\
\"musicbrainz\":null,\ \"musicbrainz\":null,\
\"properties\":{}\ \"properties\":{},\
\"albums\":[\
{\"title\":\"album_title d.a\",\"seq\":0},\
{\"title\":\"album_title d.b\",\"seq\":0}\
]\
}\ }\
]\ ]\
}"; }";

View File

@ -2,15 +2,19 @@ use std::collections::HashMap;
use serde::Deserialize; use serde::Deserialize;
use crate::{ use crate::core::{
collection::artist::{ArtistId, MusicBrainz}, collection::{
core::{ album::{Album, AlbumDate, AlbumId, AlbumSeq},
collection::{artist::Artist, Collection}, artist::{Artist, ArtistId},
database::{serde::Database, LoadError}, Collection,
}, },
database::LoadError,
}; };
pub type DeserializeDatabase = Database<DeserializeArtist>; #[derive(Debug, Deserialize)]
pub enum DeserializeDatabase {
V20240302(Vec<DeserializeArtist>),
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct DeserializeArtist { pub struct DeserializeArtist {
@ -18,6 +22,13 @@ pub struct DeserializeArtist {
sort: Option<String>, sort: Option<String>,
musicbrainz: Option<String>, musicbrainz: Option<String>,
properties: HashMap<String, Vec<String>>, properties: HashMap<String, Vec<String>>,
albums: Vec<DeserializeAlbum>,
}
#[derive(Debug, Deserialize)]
pub struct DeserializeAlbum {
title: String,
seq: u8,
} }
impl TryFrom<DeserializeDatabase> for Collection { impl TryFrom<DeserializeDatabase> for Collection {
@ -25,7 +36,7 @@ impl TryFrom<DeserializeDatabase> for Collection {
fn try_from(database: DeserializeDatabase) -> Result<Self, Self::Error> { fn try_from(database: DeserializeDatabase) -> Result<Self, Self::Error> {
match database { match database {
Database::V20240210(collection) => collection DeserializeDatabase::V20240302(collection) => collection
.into_iter() .into_iter()
.map(|artist| artist.try_into()) .map(|artist| artist.try_into())
.collect(), .collect(),
@ -40,9 +51,20 @@ impl TryFrom<DeserializeArtist> for Artist {
Ok(Artist { Ok(Artist {
id: ArtistId::new(artist.name), id: ArtistId::new(artist.name),
sort: artist.sort.map(ArtistId::new), sort: artist.sort.map(ArtistId::new),
musicbrainz: artist.musicbrainz.map(MusicBrainz::new).transpose()?, musicbrainz: artist.musicbrainz.map(TryInto::try_into).transpose()?,
properties: artist.properties, properties: artist.properties,
albums: vec![], albums: artist.albums.into_iter().map(Into::into).collect(),
}) })
} }
} }
impl From<DeserializeAlbum> for Album {
fn from(album: DeserializeAlbum) -> Self {
Album {
id: AlbumId { title: album.title },
date: AlbumDate::default(),
seq: AlbumSeq(album.seq),
tracks: vec![],
}
}
}

View File

@ -2,10 +2,3 @@
pub mod deserialize; pub mod deserialize;
pub mod serialize; pub mod serialize;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub enum Database<ARTIST> {
V20240210(Vec<ARTIST>),
}

View File

@ -2,12 +2,12 @@ use std::collections::BTreeMap;
use serde::Serialize; use serde::Serialize;
use crate::core::{ use crate::core::collection::{album::Album, artist::Artist, Collection};
collection::{artist::Artist, Collection},
database::serde::Database,
};
pub type SerializeDatabase<'a> = Database<SerializeArtist<'a>>; #[derive(Debug, Serialize)]
pub enum SerializeDatabase<'a> {
V20240302(Vec<SerializeArtist<'a>>),
}
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
pub struct SerializeArtist<'a> { pub struct SerializeArtist<'a> {
@ -15,11 +15,18 @@ pub struct SerializeArtist<'a> {
sort: Option<&'a str>, sort: Option<&'a str>,
musicbrainz: Option<&'a str>, musicbrainz: Option<&'a str>,
properties: BTreeMap<&'a str, &'a Vec<String>>, properties: BTreeMap<&'a str, &'a Vec<String>>,
albums: Vec<SerializeAlbum<'a>>,
}
#[derive(Debug, Serialize)]
pub struct SerializeAlbum<'a> {
title: &'a str,
seq: u8,
} }
impl<'a> From<&'a Collection> for SerializeDatabase<'a> { impl<'a> From<&'a Collection> for SerializeDatabase<'a> {
fn from(collection: &'a Collection) -> Self { fn from(collection: &'a Collection) -> Self {
Database::V20240210(collection.iter().map(|artist| artist.into()).collect()) SerializeDatabase::V20240302(collection.iter().map(Into::into).collect())
} }
} }
@ -34,6 +41,16 @@ impl<'a> From<&'a Artist> for SerializeArtist<'a> {
.iter() .iter()
.map(|(k, v)| (k.as_ref(), v)) .map(|(k, v)| (k.as_ref(), v))
.collect(), .collect(),
albums: artist.albums.iter().map(Into::into).collect(),
}
}
}
impl<'a> From<&'a Album> for SerializeAlbum<'a> {
fn from(album: &'a Album) -> Self {
SerializeAlbum {
title: &album.id.title,
seq: album.seq.0,
} }
} }
} }

View File

@ -7,7 +7,7 @@ pub mod executor;
use mockall::automock; use mockall::automock;
use crate::core::{ use crate::core::{
collection::track::Format, collection::track::TrackFormat,
library::{Error, Field, ILibrary, Item, Query}, library::{Error, Field, ILibrary, Item, Query},
}; };
@ -27,6 +27,10 @@ const LIST_FORMAT_ARG: &str = concat!(
list_format_separator!(), list_format_separator!(),
"$year", "$year",
list_format_separator!(), list_format_separator!(),
"$month",
list_format_separator!(),
"$day",
list_format_separator!(),
"$album", "$album",
list_format_separator!(), list_format_separator!(),
"$track", "$track",
@ -42,6 +46,21 @@ const LIST_FORMAT_ARG: &str = concat!(
const TRACK_FORMAT_FLAC: &str = "FLAC"; const TRACK_FORMAT_FLAC: &str = "FLAC";
const TRACK_FORMAT_MP3: &str = "MP3"; const TRACK_FORMAT_MP3: &str = "MP3";
fn format_to_str(format: &TrackFormat) -> &'static str {
match format {
TrackFormat::Flac => TRACK_FORMAT_FLAC,
TrackFormat::Mp3 => TRACK_FORMAT_MP3,
}
}
fn str_to_format(format: &str) -> Option<TrackFormat> {
match format {
TRACK_FORMAT_FLAC => Some(TrackFormat::Flac),
TRACK_FORMAT_MP3 => Some(TrackFormat::Mp3),
_ => None,
}
}
trait ToBeetsArg { trait ToBeetsArg {
fn to_arg(&self, include: bool) -> String; fn to_arg(&self, include: bool) -> String;
} }
@ -57,10 +76,13 @@ impl ToBeetsArg for Field {
Field::AlbumArtist(ref s) => format!("{negate}albumartist:{s}"), Field::AlbumArtist(ref s) => format!("{negate}albumartist:{s}"),
Field::AlbumArtistSort(ref s) => format!("{negate}albumartist_sort:{s}"), Field::AlbumArtistSort(ref s) => format!("{negate}albumartist_sort:{s}"),
Field::AlbumYear(ref u) => format!("{negate}year:{u}"), Field::AlbumYear(ref u) => format!("{negate}year:{u}"),
Field::AlbumMonth(ref e) => format!("{negate}month:{}", *e as u8),
Field::AlbumDay(ref u) => format!("{negate}day:{u}"),
Field::AlbumTitle(ref s) => format!("{negate}album:{s}"), Field::AlbumTitle(ref s) => format!("{negate}album:{s}"),
Field::TrackNumber(ref u) => format!("{negate}track:{u}"), Field::TrackNumber(ref u) => format!("{negate}track:{u}"),
Field::TrackTitle(ref s) => format!("{negate}title:{s}"), Field::TrackTitle(ref s) => format!("{negate}title:{s}"),
Field::TrackArtist(ref v) => format!("{negate}artist:{}", v.join("; ")), Field::TrackArtist(ref v) => format!("{negate}artist:{}", v.join("; ")),
Field::TrackFormat(ref f) => format!("{negate}format:{}", format_to_str(f)),
Field::All(ref s) => format!("{negate}{s}"), Field::All(ref s) => format!("{negate}{s}"),
} }
} }
@ -127,36 +149,38 @@ 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() != 9 { if split.len() != 11 {
return Err(Error::Invalid(line.to_string())); return Err(Error::Invalid(line.to_string()));
} }
let album_artist = split[0].to_string(); let album_artist = split[0].to_string();
let album_artist_sort = if !split[1].is_empty() { let album_artist_sort = match !split[1].is_empty() {
Some(split[1].to_string()) true => Some(split[1].to_string()),
} else { false => None,
None
}; };
let album_year = split[2].parse::<u32>()?; let album_year = split[2].parse::<u32>()?;
let album_title = split[3].to_string(); let album_month = split[3].parse::<u8>()?.into();
let track_number = split[4].parse::<u32>()?; let album_day = split[4].parse::<u8>()?;
let track_title = split[5].to_string(); let album_title = split[5].to_string();
let track_artist = split[6] let track_number = split[6].parse::<u32>()?;
let track_title = split[7].to_string();
let track_artist = split[8]
.to_string() .to_string()
.split("; ") .split("; ")
.map(|s| s.to_owned()) .map(|s| s.to_owned())
.collect(); .collect();
let track_format = match split[7].to_string().as_str() { let track_format = match str_to_format(split[9].to_string().as_str()) {
TRACK_FORMAT_FLAC => Format::Flac, Some(format) => format,
TRACK_FORMAT_MP3 => Format::Mp3, None => return Err(Error::Invalid(line.to_string())),
_ => return Err(Error::Invalid(line.to_string())),
}; };
let track_bitrate = split[8].trim_end_matches("kbps").parse::<u32>()?; let track_bitrate = split[10].trim_end_matches("kbps").parse::<u32>()?;
items.push(Item { items.push(Item {
album_artist, album_artist,
album_artist_sort, album_artist_sort,
album_year, album_year,
album_month,
album_day,
album_title, album_title,
track_number, track_number,
track_title, track_title,
@ -177,7 +201,7 @@ mod testmod;
mod tests { mod tests {
use mockall::predicate; use mockall::predicate;
use crate::core::library::testmod::LIBRARY_ITEMS; use crate::{collection::album::AlbumMonth, core::library::testmod::LIBRARY_ITEMS};
use super::*; use super::*;
use testmod::LIBRARY_BEETS; use testmod::LIBRARY_BEETS;
@ -191,6 +215,7 @@ mod tests {
String::from("some.artist.1"), String::from("some.artist.1"),
String::from("some.artist.2"), String::from("some.artist.2"),
])) ]))
.exclude(Field::TrackFormat(TrackFormat::Mp3))
.exclude(Field::All(String::from("some.all"))) .exclude(Field::All(String::from("some.all")))
.to_args(); .to_args();
query.sort(); query.sort();
@ -199,6 +224,7 @@ mod tests {
query, query,
vec![ vec![
String::from("^album:some.album"), String::from("^album:some.album"),
String::from("^format:MP3"),
String::from("^some.all"), String::from("^some.all"),
String::from("artist:some.artist.1; some.artist.2"), String::from("artist:some.artist.1; some.artist.2"),
String::from("track:5"), String::from("track:5"),
@ -209,7 +235,10 @@ mod tests {
.exclude(Field::AlbumArtist(String::from("some.albumartist"))) .exclude(Field::AlbumArtist(String::from("some.albumartist")))
.exclude(Field::AlbumArtistSort(String::from("some.albumartist"))) .exclude(Field::AlbumArtistSort(String::from("some.albumartist")))
.include(Field::AlbumYear(3030)) .include(Field::AlbumYear(3030))
.include(Field::AlbumMonth(AlbumMonth::April))
.include(Field::AlbumDay(6))
.include(Field::TrackTitle(String::from("some.track"))) .include(Field::TrackTitle(String::from("some.track")))
.include(Field::TrackFormat(TrackFormat::Flac))
.exclude(Field::TrackArtist(vec![ .exclude(Field::TrackArtist(vec![
String::from("some.artist.1"), String::from("some.artist.1"),
String::from("some.artist.2"), String::from("some.artist.2"),
@ -223,6 +252,9 @@ mod tests {
String::from("^albumartist:some.albumartist"), String::from("^albumartist:some.albumartist"),
String::from("^albumartist_sort:some.albumartist"), String::from("^albumartist_sort:some.albumartist"),
String::from("^artist:some.artist.1; some.artist.2"), String::from("^artist:some.artist.1; some.artist.2"),
String::from("day:6"),
String::from("format:FLAC"),
String::from("month:4"),
String::from("title:some.track"), String::from("title:some.track"),
String::from("year:3030"), String::from("year:3030"),
] ]
@ -335,8 +367,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[7].clear(); invalid_string[9].clear();
invalid_string[7].push_str("invalid format"); invalid_string[9].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

@ -2,27 +2,115 @@ use once_cell::sync::Lazy;
pub static LIBRARY_BEETS: Lazy<Vec<String>> = Lazy::new(|| -> Vec<String> { pub static LIBRARY_BEETS: Lazy<Vec<String>> = Lazy::new(|| -> Vec<String> {
vec![ vec![
String::from("Album_Artist A -*^- -*^- 1998 -*^- album_title a.a -*^- 1 -*^- track a.a.1 -*^- artist a.a.1 -*^- FLAC -*^- 992"), String::from(
String::from("Album_Artist A -*^- -*^- 1998 -*^- album_title a.a -*^- 2 -*^- track a.a.2 -*^- artist a.a.2.1; artist a.a.2.2 -*^- MP3 -*^- 320"), "Album_Artist A -*^- -*^- \
String::from("Album_Artist A -*^- -*^- 1998 -*^- album_title a.a -*^- 3 -*^- track a.a.3 -*^- artist a.a.3 -*^- FLAC -*^- 1061"), 1998 -*^- 00 -*^- 00 -*^- album_title a.a -*^- \
String::from("Album_Artist A -*^- -*^- 1998 -*^- album_title a.a -*^- 4 -*^- track a.a.4 -*^- artist a.a.4 -*^- FLAC -*^- 1042"), 1 -*^- track a.a.1 -*^- artist a.a.1 -*^- FLAC -*^- 992",
String::from("Album_Artist A -*^- -*^- 2015 -*^- album_title a.b -*^- 1 -*^- track a.b.1 -*^- artist a.b.1 -*^- FLAC -*^- 1004"), ),
String::from("Album_Artist A -*^- -*^- 2015 -*^- album_title a.b -*^- 2 -*^- track a.b.2 -*^- artist a.b.2 -*^- FLAC -*^- 1077"), String::from(
String::from("Album_Artist B -*^- -*^- 2003 -*^- album_title b.a -*^- 1 -*^- track b.a.1 -*^- artist b.a.1 -*^- MP3 -*^- 190"), "Album_Artist A -*^- -*^- \
String::from("Album_Artist B -*^- -*^- 2003 -*^- album_title b.a -*^- 2 -*^- track b.a.2 -*^- artist b.a.2.1; artist b.a.2.2 -*^- MP3 -*^- 120"), 1998 -*^- 00 -*^- 00 -*^- album_title a.a -*^- \
String::from("Album_Artist B -*^- -*^- 2008 -*^- album_title b.b -*^- 1 -*^- track b.b.1 -*^- artist b.b.1 -*^- FLAC -*^- 1077"), 2 -*^- track a.a.2 -*^- artist a.a.2.1; artist a.a.2.2 -*^- MP3 -*^- 320",
String::from("Album_Artist B -*^- -*^- 2008 -*^- album_title b.b -*^- 2 -*^- track b.b.2 -*^- artist b.b.2.1; artist b.b.2.2 -*^- MP3 -*^- 320"), ),
String::from("Album_Artist B -*^- -*^- 2009 -*^- album_title b.c -*^- 1 -*^- track b.c.1 -*^- artist b.c.1 -*^- MP3 -*^- 190"), String::from(
String::from("Album_Artist B -*^- -*^- 2009 -*^- album_title b.c -*^- 2 -*^- track b.c.2 -*^- artist b.c.2.1; artist b.c.2.2 -*^- MP3 -*^- 120"), "Album_Artist A -*^- -*^- \
String::from("Album_Artist B -*^- -*^- 2015 -*^- album_title b.d -*^- 1 -*^- track b.d.1 -*^- artist b.d.1 -*^- MP3 -*^- 190"), 1998 -*^- 00 -*^- 00 -*^- album_title a.a -*^- \
String::from("Album_Artist B -*^- -*^- 2015 -*^- album_title b.d -*^- 2 -*^- track b.d.2 -*^- artist b.d.2.1; artist b.d.2.2 -*^- MP3 -*^- 120"), 3 -*^- track a.a.3 -*^- artist a.a.3 -*^- FLAC -*^- 1061",
String::from("The Album_Artist C -*^- Album_Artist C, The -*^- 1985 -*^- album_title c.a -*^- 1 -*^- track c.a.1 -*^- artist c.a.1 -*^- MP3 -*^- 320"), ),
String::from("The Album_Artist C -*^- Album_Artist C, The -*^- 1985 -*^- album_title c.a -*^- 2 -*^- track c.a.2 -*^- artist c.a.2.1; artist c.a.2.2 -*^- MP3 -*^- 120"), String::from(
String::from("The Album_Artist C -*^- Album_Artist C, The -*^- 2018 -*^- album_title c.b -*^- 1 -*^- track c.b.1 -*^- artist c.b.1 -*^- FLAC -*^- 1041"), "Album_Artist A -*^- -*^- \
String::from("The Album_Artist C -*^- Album_Artist C, The -*^- 2018 -*^- album_title c.b -*^- 2 -*^- track c.b.2 -*^- artist c.b.2.1; artist c.b.2.2 -*^- FLAC -*^- 756"), 1998 -*^- 00 -*^- 00 -*^- album_title a.a -*^- \
String::from("Album_Artist D -*^- -*^- 1995 -*^- album_title d.a -*^- 1 -*^- track d.a.1 -*^- artist d.a.1 -*^- MP3 -*^- 120"), 4 -*^- track a.a.4 -*^- artist a.a.4 -*^- FLAC -*^- 1042",
String::from("Album_Artist D -*^- -*^- 1995 -*^- album_title d.a -*^- 2 -*^- track d.a.2 -*^- artist d.a.2.1; artist d.a.2.2 -*^- MP3 -*^- 120"), ),
String::from("Album_Artist D -*^- -*^- 2028 -*^- album_title d.b -*^- 1 -*^- track d.b.1 -*^- artist d.b.1 -*^- FLAC -*^- 841"), String::from(
String::from("Album_Artist D -*^- -*^- 2028 -*^- album_title d.b -*^- 2 -*^- track d.b.2 -*^- artist d.b.2.1; artist d.b.2.2 -*^- FLAC -*^- 756") "Album_Artist A -*^- -*^- \
2015 -*^- 04 -*^- 00 -*^- album_title a.b -*^- \
1 -*^- track a.b.1 -*^- artist a.b.1 -*^- FLAC -*^- 1004",
),
String::from(
"Album_Artist A -*^- -*^- \
2015 -*^- 04 -*^- 00 -*^- album_title a.b -*^- \
2 -*^- track a.b.2 -*^- artist a.b.2 -*^- FLAC -*^- 1077",
),
String::from(
"Album_Artist B -*^- -*^- \
2003 -*^- 06 -*^- 06 -*^- album_title b.a -*^- \
1 -*^- track b.a.1 -*^- artist b.a.1 -*^- MP3 -*^- 190",
),
String::from(
"Album_Artist B -*^- -*^- \
2003 -*^- 06 -*^- 06 -*^- album_title b.a -*^- \
2 -*^- track b.a.2 -*^- artist b.a.2.1; artist b.a.2.2 -*^- MP3 -*^- 120",
),
String::from(
"Album_Artist B -*^- -*^- \
2008 -*^- 00 -*^- 00 -*^- album_title b.b -*^- \
1 -*^- track b.b.1 -*^- artist b.b.1 -*^- FLAC -*^- 1077",
),
String::from(
"Album_Artist B -*^- -*^- \
2008 -*^- 00 -*^- 00 -*^- album_title b.b -*^- \
2 -*^- track b.b.2 -*^- artist b.b.2.1; artist b.b.2.2 -*^- MP3 -*^- 320",
),
String::from(
"Album_Artist B -*^- -*^- \
2009 -*^- 00 -*^- 00 -*^- album_title b.c -*^- \
1 -*^- track b.c.1 -*^- artist b.c.1 -*^- MP3 -*^- 190",
),
String::from(
"Album_Artist B -*^- -*^- \
2009 -*^- 00 -*^- 00 -*^- album_title b.c -*^- \
2 -*^- track b.c.2 -*^- artist b.c.2.1; artist b.c.2.2 -*^- MP3 -*^- 120",
),
String::from(
"Album_Artist B -*^- -*^- \
2015 -*^- 00 -*^- 00 -*^- album_title b.d -*^- \
1 -*^- track b.d.1 -*^- artist b.d.1 -*^- MP3 -*^- 190",
),
String::from(
"Album_Artist B -*^- -*^- \
2015 -*^- 00 -*^- 00 -*^- album_title b.d -*^- \
2 -*^- track b.d.2 -*^- artist b.d.2.1; artist b.d.2.2 -*^- MP3 -*^- 120",
),
String::from(
"The Album_Artist C -*^- Album_Artist C, The -*^- \
1985 -*^- 00 -*^- 00 -*^- album_title c.a -*^- \
1 -*^- track c.a.1 -*^- artist c.a.1 -*^- MP3 -*^- 320",
),
String::from(
"The Album_Artist C -*^- Album_Artist C, The -*^- \
1985 -*^- 00 -*^- 00 -*^- album_title c.a -*^- \
2 -*^- track c.a.2 -*^- artist c.a.2.1; artist c.a.2.2 -*^- MP3 -*^- 120",
),
String::from(
"The Album_Artist C -*^- Album_Artist C, The -*^- \
2018 -*^- 00 -*^- 00 -*^- album_title c.b -*^- \
1 -*^- track c.b.1 -*^- artist c.b.1 -*^- FLAC -*^- 1041",
),
String::from(
"The Album_Artist C -*^- Album_Artist C, The -*^- \
2018 -*^- 00 -*^- 00 -*^- album_title c.b -*^- \
2 -*^- track c.b.2 -*^- artist c.b.2.1; artist c.b.2.2 -*^- FLAC -*^- 756",
),
String::from(
"Album_Artist D -*^- -*^- \
1995 -*^- 00 -*^- 00 -*^- album_title d.a -*^- \
1 -*^- track d.a.1 -*^- artist d.a.1 -*^- MP3 -*^- 120",
),
String::from(
"Album_Artist D -*^- -*^- \
1995 -*^- 00 -*^- 00 -*^- album_title d.a -*^- \
2 -*^- track d.a.2 -*^- artist d.a.2.1; artist d.a.2.2 -*^- MP3 -*^- 120",
),
String::from(
"Album_Artist D -*^- -*^- \
2028 -*^- 00 -*^- 00 -*^- album_title d.b -*^- \
1 -*^- track d.b.1 -*^- artist d.b.1 -*^- FLAC -*^- 841",
),
String::from(
"Album_Artist D -*^- -*^- \
2028 -*^- 00 -*^- 00 -*^- album_title d.b -*^- \
2 -*^- track d.b.2 -*^- artist d.b.2.1; artist d.b.2.2 -*^- FLAC -*^- 756",
),
] ]
}); });

View File

@ -8,7 +8,7 @@ use std::{collections::HashSet, fmt, num::ParseIntError, str::Utf8Error};
#[cfg(test)] #[cfg(test)]
use mockall::automock; use mockall::automock;
use crate::core::collection::track::Format; use crate::core::collection::{album::AlbumMonth, track::TrackFormat};
/// Trait for interacting with the music library. /// Trait for interacting with the music library.
#[cfg_attr(test, automock)] #[cfg_attr(test, automock)]
@ -32,11 +32,13 @@ pub struct Item {
pub album_artist: String, pub album_artist: String,
pub album_artist_sort: Option<String>, pub album_artist_sort: Option<String>,
pub album_year: u32, pub album_year: u32,
pub album_month: AlbumMonth,
pub album_day: u8,
pub album_title: String, pub album_title: String,
pub track_number: u32, pub track_number: u32,
pub track_title: String, pub track_title: String,
pub track_artist: Vec<String>, pub track_artist: Vec<String>,
pub track_format: Format, pub track_format: TrackFormat,
pub track_bitrate: u32, pub track_bitrate: u32,
} }
@ -46,10 +48,13 @@ pub enum Field {
AlbumArtist(String), AlbumArtist(String),
AlbumArtistSort(String), AlbumArtistSort(String),
AlbumYear(u32), AlbumYear(u32),
AlbumMonth(AlbumMonth),
AlbumDay(u8),
AlbumTitle(String), AlbumTitle(String),
TrackNumber(u32), TrackNumber(u32),
TrackTitle(String), TrackTitle(String),
TrackArtist(Vec<String>), TrackArtist(Vec<String>),
TrackFormat(TrackFormat),
All(String), All(String),
} }

View File

@ -1,6 +1,9 @@
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use crate::core::{collection::track::Format, library::Item}; use crate::core::{
collection::{album::AlbumMonth, track::TrackFormat},
library::Item,
};
pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> { pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
vec![ vec![
@ -8,17 +11,21 @@ pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
album_artist: String::from("Album_Artist A"), album_artist: String::from("Album_Artist A"),
album_artist_sort: None, album_artist_sort: None,
album_year: 1998, album_year: 1998,
album_month: AlbumMonth::None,
album_day: 0,
album_title: String::from("album_title a.a"), album_title: String::from("album_title a.a"),
track_number: 1, track_number: 1,
track_title: String::from("track a.a.1"), track_title: String::from("track a.a.1"),
track_artist: vec![String::from("artist a.a.1")], track_artist: vec![String::from("artist a.a.1")],
track_format: Format::Flac, track_format: TrackFormat::Flac,
track_bitrate: 992, track_bitrate: 992,
}, },
Item { Item {
album_artist: String::from("Album_Artist A"), album_artist: String::from("Album_Artist A"),
album_artist_sort: None, album_artist_sort: None,
album_year: 1998, album_year: 1998,
album_month: AlbumMonth::None,
album_day: 0,
album_title: String::from("album_title a.a"), album_title: String::from("album_title a.a"),
track_number: 2, track_number: 2,
track_title: String::from("track a.a.2"), track_title: String::from("track a.a.2"),
@ -26,68 +33,80 @@ pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
String::from("artist a.a.2.1"), String::from("artist a.a.2.1"),
String::from("artist a.a.2.2"), String::from("artist a.a.2.2"),
], ],
track_format: Format::Mp3, track_format: TrackFormat::Mp3,
track_bitrate: 320, track_bitrate: 320,
}, },
Item { Item {
album_artist: String::from("Album_Artist A"), album_artist: String::from("Album_Artist A"),
album_artist_sort: None, album_artist_sort: None,
album_year: 1998, album_year: 1998,
album_month: AlbumMonth::None,
album_day: 0,
album_title: String::from("album_title a.a"), album_title: String::from("album_title a.a"),
track_number: 3, track_number: 3,
track_title: String::from("track a.a.3"), track_title: String::from("track a.a.3"),
track_artist: vec![String::from("artist a.a.3")], track_artist: vec![String::from("artist a.a.3")],
track_format: Format::Flac, track_format: TrackFormat::Flac,
track_bitrate: 1061, track_bitrate: 1061,
}, },
Item { Item {
album_artist: String::from("Album_Artist A"), album_artist: String::from("Album_Artist A"),
album_artist_sort: None, album_artist_sort: None,
album_year: 1998, album_year: 1998,
album_month: AlbumMonth::None,
album_day: 0,
album_title: String::from("album_title a.a"), album_title: String::from("album_title a.a"),
track_number: 4, track_number: 4,
track_title: String::from("track a.a.4"), track_title: String::from("track a.a.4"),
track_artist: vec![String::from("artist a.a.4")], track_artist: vec![String::from("artist a.a.4")],
track_format: Format::Flac, track_format: TrackFormat::Flac,
track_bitrate: 1042, track_bitrate: 1042,
}, },
Item { Item {
album_artist: String::from("Album_Artist A"), album_artist: String::from("Album_Artist A"),
album_artist_sort: None, album_artist_sort: None,
album_year: 2015, album_year: 2015,
album_month: AlbumMonth::April,
album_day: 0,
album_title: String::from("album_title a.b"), album_title: String::from("album_title a.b"),
track_number: 1, track_number: 1,
track_title: String::from("track a.b.1"), track_title: String::from("track a.b.1"),
track_artist: vec![String::from("artist a.b.1")], track_artist: vec![String::from("artist a.b.1")],
track_format: Format::Flac, track_format: TrackFormat::Flac,
track_bitrate: 1004, track_bitrate: 1004,
}, },
Item { Item {
album_artist: String::from("Album_Artist A"), album_artist: String::from("Album_Artist A"),
album_artist_sort: None, album_artist_sort: None,
album_year: 2015, album_year: 2015,
album_month: AlbumMonth::April,
album_day: 0,
album_title: String::from("album_title a.b"), album_title: String::from("album_title a.b"),
track_number: 2, track_number: 2,
track_title: String::from("track a.b.2"), track_title: String::from("track a.b.2"),
track_artist: vec![String::from("artist a.b.2")], track_artist: vec![String::from("artist a.b.2")],
track_format: Format::Flac, track_format: TrackFormat::Flac,
track_bitrate: 1077, track_bitrate: 1077,
}, },
Item { Item {
album_artist: String::from("Album_Artist B"), album_artist: String::from("Album_Artist B"),
album_artist_sort: None, album_artist_sort: None,
album_year: 2003, album_year: 2003,
album_month: AlbumMonth::June,
album_day: 6,
album_title: String::from("album_title b.a"), album_title: String::from("album_title b.a"),
track_number: 1, track_number: 1,
track_title: String::from("track b.a.1"), track_title: String::from("track b.a.1"),
track_artist: vec![String::from("artist b.a.1")], track_artist: vec![String::from("artist b.a.1")],
track_format: Format::Mp3, track_format: TrackFormat::Mp3,
track_bitrate: 190, track_bitrate: 190,
}, },
Item { Item {
album_artist: String::from("Album_Artist B"), album_artist: String::from("Album_Artist B"),
album_artist_sort: None, album_artist_sort: None,
album_year: 2003, album_year: 2003,
album_month: AlbumMonth::June,
album_day: 6,
album_title: String::from("album_title b.a"), album_title: String::from("album_title b.a"),
track_number: 2, track_number: 2,
track_title: String::from("track b.a.2"), track_title: String::from("track b.a.2"),
@ -95,24 +114,28 @@ pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
String::from("artist b.a.2.1"), String::from("artist b.a.2.1"),
String::from("artist b.a.2.2"), String::from("artist b.a.2.2"),
], ],
track_format: Format::Mp3, track_format: TrackFormat::Mp3,
track_bitrate: 120, track_bitrate: 120,
}, },
Item { Item {
album_artist: String::from("Album_Artist B"), album_artist: String::from("Album_Artist B"),
album_artist_sort: None, album_artist_sort: None,
album_year: 2008, album_year: 2008,
album_month: AlbumMonth::None,
album_day: 0,
album_title: String::from("album_title b.b"), album_title: String::from("album_title b.b"),
track_number: 1, track_number: 1,
track_title: String::from("track b.b.1"), track_title: String::from("track b.b.1"),
track_artist: vec![String::from("artist b.b.1")], track_artist: vec![String::from("artist b.b.1")],
track_format: Format::Flac, track_format: TrackFormat::Flac,
track_bitrate: 1077, track_bitrate: 1077,
}, },
Item { Item {
album_artist: String::from("Album_Artist B"), album_artist: String::from("Album_Artist B"),
album_artist_sort: None, album_artist_sort: None,
album_year: 2008, album_year: 2008,
album_month: AlbumMonth::None,
album_day: 0,
album_title: String::from("album_title b.b"), album_title: String::from("album_title b.b"),
track_number: 2, track_number: 2,
track_title: String::from("track b.b.2"), track_title: String::from("track b.b.2"),
@ -120,24 +143,28 @@ pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
String::from("artist b.b.2.1"), String::from("artist b.b.2.1"),
String::from("artist b.b.2.2"), String::from("artist b.b.2.2"),
], ],
track_format: Format::Mp3, track_format: TrackFormat::Mp3,
track_bitrate: 320, track_bitrate: 320,
}, },
Item { Item {
album_artist: String::from("Album_Artist B"), album_artist: String::from("Album_Artist B"),
album_artist_sort: None, album_artist_sort: None,
album_year: 2009, album_year: 2009,
album_month: AlbumMonth::None,
album_day: 0,
album_title: String::from("album_title b.c"), album_title: String::from("album_title b.c"),
track_number: 1, track_number: 1,
track_title: String::from("track b.c.1"), track_title: String::from("track b.c.1"),
track_artist: vec![String::from("artist b.c.1")], track_artist: vec![String::from("artist b.c.1")],
track_format: Format::Mp3, track_format: TrackFormat::Mp3,
track_bitrate: 190, track_bitrate: 190,
}, },
Item { Item {
album_artist: String::from("Album_Artist B"), album_artist: String::from("Album_Artist B"),
album_artist_sort: None, album_artist_sort: None,
album_year: 2009, album_year: 2009,
album_month: AlbumMonth::None,
album_day: 0,
album_title: String::from("album_title b.c"), album_title: String::from("album_title b.c"),
track_number: 2, track_number: 2,
track_title: String::from("track b.c.2"), track_title: String::from("track b.c.2"),
@ -145,24 +172,28 @@ pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
String::from("artist b.c.2.1"), String::from("artist b.c.2.1"),
String::from("artist b.c.2.2"), String::from("artist b.c.2.2"),
], ],
track_format: Format::Mp3, track_format: TrackFormat::Mp3,
track_bitrate: 120, track_bitrate: 120,
}, },
Item { Item {
album_artist: String::from("Album_Artist B"), album_artist: String::from("Album_Artist B"),
album_artist_sort: None, album_artist_sort: None,
album_year: 2015, album_year: 2015,
album_month: AlbumMonth::None,
album_day: 0,
album_title: String::from("album_title b.d"), album_title: String::from("album_title b.d"),
track_number: 1, track_number: 1,
track_title: String::from("track b.d.1"), track_title: String::from("track b.d.1"),
track_artist: vec![String::from("artist b.d.1")], track_artist: vec![String::from("artist b.d.1")],
track_format: Format::Mp3, track_format: TrackFormat::Mp3,
track_bitrate: 190, track_bitrate: 190,
}, },
Item { Item {
album_artist: String::from("Album_Artist B"), album_artist: String::from("Album_Artist B"),
album_artist_sort: None, album_artist_sort: None,
album_year: 2015, album_year: 2015,
album_month: AlbumMonth::None,
album_day: 0,
album_title: String::from("album_title b.d"), album_title: String::from("album_title b.d"),
track_number: 2, track_number: 2,
track_title: String::from("track b.d.2"), track_title: String::from("track b.d.2"),
@ -170,24 +201,28 @@ pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
String::from("artist b.d.2.1"), String::from("artist b.d.2.1"),
String::from("artist b.d.2.2"), String::from("artist b.d.2.2"),
], ],
track_format: Format::Mp3, track_format: TrackFormat::Mp3,
track_bitrate: 120, track_bitrate: 120,
}, },
Item { Item {
album_artist: String::from("The Album_Artist C"), album_artist: String::from("The Album_Artist C"),
album_artist_sort: Some(String::from("Album_Artist C, The")), album_artist_sort: Some(String::from("Album_Artist C, The")),
album_year: 1985, album_year: 1985,
album_month: AlbumMonth::None,
album_day: 0,
album_title: String::from("album_title c.a"), album_title: String::from("album_title c.a"),
track_number: 1, track_number: 1,
track_title: String::from("track c.a.1"), track_title: String::from("track c.a.1"),
track_artist: vec![String::from("artist c.a.1")], track_artist: vec![String::from("artist c.a.1")],
track_format: Format::Mp3, track_format: TrackFormat::Mp3,
track_bitrate: 320, track_bitrate: 320,
}, },
Item { Item {
album_artist: String::from("The Album_Artist C"), album_artist: String::from("The Album_Artist C"),
album_artist_sort: Some(String::from("Album_Artist C, The")), album_artist_sort: Some(String::from("Album_Artist C, The")),
album_year: 1985, album_year: 1985,
album_month: AlbumMonth::None,
album_day: 0,
album_title: String::from("album_title c.a"), album_title: String::from("album_title c.a"),
track_number: 2, track_number: 2,
track_title: String::from("track c.a.2"), track_title: String::from("track c.a.2"),
@ -195,24 +230,28 @@ pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
String::from("artist c.a.2.1"), String::from("artist c.a.2.1"),
String::from("artist c.a.2.2"), String::from("artist c.a.2.2"),
], ],
track_format: Format::Mp3, track_format: TrackFormat::Mp3,
track_bitrate: 120, track_bitrate: 120,
}, },
Item { Item {
album_artist: String::from("The Album_Artist C"), album_artist: String::from("The Album_Artist C"),
album_artist_sort: Some(String::from("Album_Artist C, The")), album_artist_sort: Some(String::from("Album_Artist C, The")),
album_year: 2018, album_year: 2018,
album_month: AlbumMonth::None,
album_day: 0,
album_title: String::from("album_title c.b"), album_title: String::from("album_title c.b"),
track_number: 1, track_number: 1,
track_title: String::from("track c.b.1"), track_title: String::from("track c.b.1"),
track_artist: vec![String::from("artist c.b.1")], track_artist: vec![String::from("artist c.b.1")],
track_format: Format::Flac, track_format: TrackFormat::Flac,
track_bitrate: 1041, track_bitrate: 1041,
}, },
Item { Item {
album_artist: String::from("The Album_Artist C"), album_artist: String::from("The Album_Artist C"),
album_artist_sort: Some(String::from("Album_Artist C, The")), album_artist_sort: Some(String::from("Album_Artist C, The")),
album_year: 2018, album_year: 2018,
album_month: AlbumMonth::None,
album_day: 0,
album_title: String::from("album_title c.b"), album_title: String::from("album_title c.b"),
track_number: 2, track_number: 2,
track_title: String::from("track c.b.2"), track_title: String::from("track c.b.2"),
@ -220,24 +259,28 @@ pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
String::from("artist c.b.2.1"), String::from("artist c.b.2.1"),
String::from("artist c.b.2.2"), String::from("artist c.b.2.2"),
], ],
track_format: Format::Flac, track_format: TrackFormat::Flac,
track_bitrate: 756, track_bitrate: 756,
}, },
Item { Item {
album_artist: String::from("Album_Artist D"), album_artist: String::from("Album_Artist D"),
album_artist_sort: None, album_artist_sort: None,
album_year: 1995, album_year: 1995,
album_month: AlbumMonth::None,
album_day: 0,
album_title: String::from("album_title d.a"), album_title: String::from("album_title d.a"),
track_number: 1, track_number: 1,
track_title: String::from("track d.a.1"), track_title: String::from("track d.a.1"),
track_artist: vec![String::from("artist d.a.1")], track_artist: vec![String::from("artist d.a.1")],
track_format: Format::Mp3, track_format: TrackFormat::Mp3,
track_bitrate: 120, track_bitrate: 120,
}, },
Item { Item {
album_artist: String::from("Album_Artist D"), album_artist: String::from("Album_Artist D"),
album_artist_sort: None, album_artist_sort: None,
album_year: 1995, album_year: 1995,
album_month: AlbumMonth::None,
album_day: 0,
album_title: String::from("album_title d.a"), album_title: String::from("album_title d.a"),
track_number: 2, track_number: 2,
track_title: String::from("track d.a.2"), track_title: String::from("track d.a.2"),
@ -245,24 +288,28 @@ pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
String::from("artist d.a.2.1"), String::from("artist d.a.2.1"),
String::from("artist d.a.2.2"), String::from("artist d.a.2.2"),
], ],
track_format: Format::Mp3, track_format: TrackFormat::Mp3,
track_bitrate: 120, track_bitrate: 120,
}, },
Item { Item {
album_artist: String::from("Album_Artist D"), album_artist: String::from("Album_Artist D"),
album_artist_sort: None, album_artist_sort: None,
album_year: 2028, album_year: 2028,
album_month: AlbumMonth::None,
album_day: 0,
album_title: String::from("album_title d.b"), album_title: String::from("album_title d.b"),
track_number: 1, track_number: 1,
track_title: String::from("track d.b.1"), track_title: String::from("track d.b.1"),
track_artist: vec![String::from("artist d.b.1")], track_artist: vec![String::from("artist d.b.1")],
track_format: Format::Flac, track_format: TrackFormat::Flac,
track_bitrate: 841, track_bitrate: 841,
}, },
Item { Item {
album_artist: String::from("Album_Artist D"), album_artist: String::from("Album_Artist D"),
album_artist_sort: None, album_artist_sort: None,
album_year: 2028, album_year: 2028,
album_month: AlbumMonth::None,
album_day: 0,
album_title: String::from("album_title d.b"), album_title: String::from("album_title d.b"),
track_number: 2, track_number: 2,
track_title: String::from("track d.b.2"), track_title: String::from("track d.b.2"),
@ -270,7 +317,7 @@ pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
String::from("artist d.b.2.1"), String::from("artist d.b.2.1"),
String::from("artist d.b.2.2"), String::from("artist d.b.2.2"),
], ],
track_format: Format::Flac, track_format: TrackFormat::Flac,
track_bitrate: 756, track_bitrate: 756,
}, },
] ]

View File

@ -2,10 +2,10 @@ use std::collections::HashMap;
use crate::core::{ use crate::core::{
collection::{ collection::{
album::{Album, AlbumId}, album::{Album, AlbumDate, AlbumId, AlbumSeq},
artist::{Artist, ArtistId}, artist::{Artist, ArtistId, MusicBrainz},
track::{Quality, Track, TrackId}, track::{Track, TrackId, TrackNum, TrackQuality},
Collection, Merge, Collection, MergeCollections,
}, },
database::IDatabase, database::IDatabase,
library::{ILibrary, Item, Query}, library::{ILibrary, Item, Query},
@ -73,19 +73,7 @@ impl<LIB, DB> MusicHoard<LIB, DB> {
} }
fn merge_collections(&self) -> Collection { fn merge_collections(&self) -> Collection {
let mut primary = self.library_cache.clone(); MergeCollections::merge(self.library_cache.clone(), self.database_cache.clone())
for secondary_artist in self.database_cache.iter().cloned() {
if let Some(ref mut primary_artist) = primary.get_mut(&secondary_artist.id) {
primary_artist.merge_in_place(secondary_artist);
} else {
primary.insert(secondary_artist.id.clone(), secondary_artist);
}
}
let mut collection: Collection = primary.into_values().collect();
Self::sort_artists(&mut collection);
collection
} }
fn items_to_artists(items: Vec<Item>) -> Result<HashMap<ArtistId, Artist>, Error> { fn items_to_artists(items: Vec<Item>) -> Result<HashMap<ArtistId, Artist>, Error> {
@ -99,17 +87,22 @@ impl<LIB, DB> MusicHoard<LIB, DB> {
let artist_sort = item.album_artist_sort.map(|s| ArtistId { name: s }); let artist_sort = item.album_artist_sort.map(|s| ArtistId { name: s });
let album_id = AlbumId { let album_id = AlbumId {
year: item.album_year,
title: item.album_title, title: item.album_title,
}; };
let album_date = AlbumDate {
year: item.album_year,
month: item.album_month,
day: item.album_day,
};
let track = Track { let track = Track {
id: TrackId { id: TrackId {
number: item.track_number,
title: item.track_title, title: item.track_title,
}, },
number: TrackNum(item.track_number),
artist: item.track_artist, artist: item.track_artist,
quality: Quality { quality: TrackQuality {
format: item.track_format, format: item.track_format,
bitrate: item.track_bitrate, bitrate: item.track_bitrate,
}, },
@ -149,6 +142,8 @@ impl<LIB, DB> MusicHoard<LIB, DB> {
Some(album) => album.tracks.push(track), Some(album) => album.tracks.push(track),
None => artist.albums.push(Album { None => artist.albums.push(Album {
id: album_id, id: album_id,
date: album_date,
seq: AlbumSeq(0),
tracks: vec![track], tracks: vec![track],
}), }),
} }
@ -176,6 +171,22 @@ impl<LIB, DB> MusicHoard<LIB, DB> {
Error::CollectionError(format!("artist '{}' is not in the collection", artist_id)) Error::CollectionError(format!("artist '{}' is not in the collection", artist_id))
}) })
} }
fn get_album_mut<'a>(artist: &'a mut Artist, album_id: &AlbumId) -> Option<&'a mut Album> {
artist.albums.iter_mut().find(|a| &a.id == album_id)
}
fn get_album_mut_or_err<'a>(
artist: &'a mut Artist,
album_id: &AlbumId,
) -> Result<&'a mut Album, Error> {
Self::get_album_mut(artist, album_id).ok_or_else(|| {
Error::CollectionError(format!(
"album '{}' does not belong to the artist",
album_id
))
})
}
} }
impl<LIB: ILibrary> MusicHoard<LIB, NoDatabase> { impl<LIB: ILibrary> MusicHoard<LIB, NoDatabase> {
@ -248,39 +259,61 @@ impl<LIB, DB: IDatabase> MusicHoard<LIB, DB> {
Ok(()) Ok(())
} }
fn update_collection<F>(&mut self, func: F) -> Result<(), Error> fn update_collection<FnColl>(&mut self, fn_coll: FnColl) -> Result<(), Error>
where where
F: FnOnce(&mut Collection), FnColl: FnOnce(&mut Collection),
{ {
func(&mut self.pre_commit); fn_coll(&mut self.pre_commit);
self.commit() self.commit()
} }
fn update_artist_and<ID: AsRef<ArtistId>, F1, F2>( fn update_artist_and<FnArtist, FnColl>(
&mut self, &mut self,
artist_id: ID, artist_id: &ArtistId,
f1: F1, fn_artist: FnArtist,
f2: F2, fn_coll: FnColl,
) -> Result<(), Error> ) -> Result<(), Error>
where where
F1: FnOnce(&mut Artist), FnArtist: FnOnce(&mut Artist),
F2: FnOnce(&mut Collection), FnColl: FnOnce(&mut Collection),
{ {
f1(Self::get_artist_mut_or_err( let artist = Self::get_artist_mut_or_err(&mut self.pre_commit, artist_id)?;
&mut self.pre_commit, fn_artist(artist);
artist_id.as_ref(), self.update_collection(fn_coll)
)?);
self.update_collection(f2)
} }
fn update_artist<ID: AsRef<ArtistId>, F>(&mut self, artist_id: ID, func: F) -> Result<(), Error> fn update_artist<FnArtist>(
&mut self,
artist_id: &ArtistId,
fn_artist: FnArtist,
) -> Result<(), Error>
where where
F: FnOnce(&mut Artist), FnArtist: FnOnce(&mut Artist),
{ {
self.update_artist_and(artist_id, func, |_| {}) self.update_artist_and(artist_id, fn_artist, |_| {})
} }
pub fn add_artist<ID: Into<ArtistId>>(&mut self, artist_id: ID) -> Result<(), Error> { fn update_album_and<FnAlbum, FnArtist, FnColl>(
&mut self,
artist_id: &ArtistId,
album_id: &AlbumId,
fn_album: FnAlbum,
fn_artist: FnArtist,
fn_coll: FnColl,
) -> Result<(), Error>
where
FnAlbum: FnOnce(&mut Album),
FnArtist: FnOnce(&mut Artist),
FnColl: FnOnce(&mut Collection),
{
let artist = Self::get_artist_mut_or_err(&mut self.pre_commit, artist_id)?;
let album = Self::get_album_mut_or_err(artist, album_id)?;
fn_album(album);
fn_artist(artist);
self.update_collection(fn_coll)
}
pub fn add_artist<IntoId: Into<ArtistId>>(&mut self, artist_id: IntoId) -> Result<(), Error> {
let artist_id: ArtistId = artist_id.into(); let artist_id: ArtistId = artist_id.into();
self.update_collection(|collection| { self.update_collection(|collection| {
@ -291,7 +324,7 @@ impl<LIB, DB: IDatabase> MusicHoard<LIB, DB> {
}) })
} }
pub fn remove_artist<ID: AsRef<ArtistId>>(&mut self, artist_id: ID) -> Result<(), Error> { pub fn remove_artist<Id: AsRef<ArtistId>>(&mut self, artist_id: Id) -> Result<(), Error> {
self.update_collection(|collection| { self.update_collection(|collection| {
let index_opt = collection.iter().position(|a| &a.id == artist_id.as_ref()); let index_opt = collection.iter().position(|a| &a.id == artist_id.as_ref());
if let Some(index) = index_opt { if let Some(index) = index_opt {
@ -300,77 +333,113 @@ impl<LIB, DB: IDatabase> MusicHoard<LIB, DB> {
}) })
} }
pub fn set_artist_sort<ID: AsRef<ArtistId>, SORT: Into<ArtistId>>( pub fn set_artist_sort<Id: AsRef<ArtistId>, IntoId: Into<ArtistId>>(
&mut self, &mut self,
artist_id: ID, artist_id: Id,
artist_sort: SORT, artist_sort: IntoId,
) -> Result<(), Error> { ) -> Result<(), Error> {
self.update_artist_and( self.update_artist_and(
artist_id, artist_id.as_ref(),
|artist| artist.set_sort_key(artist_sort), |artist| artist.set_sort_key(artist_sort),
|collection| Self::sort_artists(collection), |collection| Self::sort_artists(collection),
) )
} }
pub fn clear_artist_sort<ID: AsRef<ArtistId>>(&mut self, artist_id: ID) -> Result<(), Error> { pub fn clear_artist_sort<Id: AsRef<ArtistId>>(&mut self, artist_id: Id) -> Result<(), Error> {
self.update_artist_and( self.update_artist_and(
artist_id, artist_id.as_ref(),
|artist| artist.clear_sort_key(), |artist| artist.clear_sort_key(),
|collection| Self::sort_artists(collection), |collection| Self::sort_artists(collection),
) )
} }
pub fn set_musicbrainz_url<ID: AsRef<ArtistId>, S: AsRef<str>>( pub fn set_artist_musicbrainz<Id: AsRef<ArtistId>, Mb: TryInto<MusicBrainz, Error = E>, E>(
&mut self, &mut self,
artist_id: ID, artist_id: Id,
url: S, url: Mb,
) -> Result<(), Error> { ) -> Result<(), Error>
let url = url.as_ref().try_into()?; where
self.update_artist(artist_id, |artist| artist.set_musicbrainz_url(url)) Error: From<E>,
{
let mb = url.try_into()?;
self.update_artist(artist_id.as_ref(), |artist| artist.set_musicbrainz_url(mb))
} }
pub fn clear_musicbrainz_url<ID: AsRef<ArtistId>>( pub fn clear_artist_musicbrainz<Id: AsRef<ArtistId>>(
&mut self, &mut self,
artist_id: ID, artist_id: Id,
) -> Result<(), Error> { ) -> Result<(), Error> {
self.update_artist(artist_id, |artist| artist.clear_musicbrainz_url()) self.update_artist(artist_id.as_ref(), |artist| artist.clear_musicbrainz_url())
} }
pub fn add_to_property<ID: AsRef<ArtistId>, S: AsRef<str> + Into<String>>( pub fn add_to_artist_property<Id: AsRef<ArtistId>, S: AsRef<str> + Into<String>>(
&mut self, &mut self,
artist_id: ID, artist_id: Id,
property: S, property: S,
values: Vec<S>, values: Vec<S>,
) -> Result<(), Error> { ) -> Result<(), Error> {
self.update_artist(artist_id, |artist| artist.add_to_property(property, values)) self.update_artist(artist_id.as_ref(), |artist| {
artist.add_to_property(property, values)
})
} }
pub fn remove_from_property<ID: AsRef<ArtistId>, S: AsRef<str>>( pub fn remove_from_artist_property<Id: AsRef<ArtistId>, S: AsRef<str>>(
&mut self, &mut self,
artist_id: ID, artist_id: Id,
property: S, property: S,
values: Vec<S>, values: Vec<S>,
) -> Result<(), Error> { ) -> Result<(), Error> {
self.update_artist(artist_id, |artist| { self.update_artist(artist_id.as_ref(), |artist| {
artist.remove_from_property(property, values) artist.remove_from_property(property, values)
}) })
} }
pub fn set_property<ID: AsRef<ArtistId>, S: AsRef<str> + Into<String>>( pub fn set_artist_property<Id: AsRef<ArtistId>, S: AsRef<str> + Into<String>>(
&mut self, &mut self,
artist_id: ID, artist_id: Id,
property: S, property: S,
values: Vec<S>, values: Vec<S>,
) -> Result<(), Error> { ) -> Result<(), Error> {
self.update_artist(artist_id, |artist| artist.set_property(property, values)) self.update_artist(artist_id.as_ref(), |artist| {
artist.set_property(property, values)
})
} }
pub fn clear_property<ID: AsRef<ArtistId>, S: AsRef<str>>( pub fn clear_artist_property<Id: AsRef<ArtistId>, S: AsRef<str>>(
&mut self, &mut self,
artist_id: ID, artist_id: Id,
property: S, property: S,
) -> Result<(), Error> { ) -> Result<(), Error> {
self.update_artist(artist_id, |artist| artist.clear_property(property)) self.update_artist(artist_id.as_ref(), |artist| artist.clear_property(property))
}
pub fn set_album_seq<ArtistIdRef: AsRef<ArtistId>, AlbumIdRef: AsRef<AlbumId>>(
&mut self,
artist_id: ArtistIdRef,
album_id: AlbumIdRef,
seq: u8,
) -> Result<(), Error> {
self.update_album_and(
artist_id.as_ref(),
album_id.as_ref(),
|album| album.set_seq(AlbumSeq(seq)),
|artist| artist.albums.sort_unstable(),
|_| {},
)
}
pub fn clear_album_seq<ArtistIdRef: AsRef<ArtistId>, AlbumIdRef: AsRef<AlbumId>>(
&mut self,
artist_id: ArtistIdRef,
album_id: AlbumIdRef,
) -> Result<(), Error> {
self.update_album_and(
artist_id.as_ref(),
album_id.as_ref(),
|album| album.clear_seq(),
|artist| artist.albums.sort_unstable(),
|_| {},
)
} }
} }
@ -523,7 +592,7 @@ mod tests {
assert!(music_hoard.add_artist(artist_id.clone()).is_ok()); assert!(music_hoard.add_artist(artist_id.clone()).is_ok());
let actual_err = music_hoard let actual_err = music_hoard
.set_musicbrainz_url(&artist_id, MUSICBUTLER) .set_artist_musicbrainz(&artist_id, MUSICBUTLER)
.unwrap_err(); .unwrap_err();
let expected_err = Error::CollectionError(format!( let expected_err = Error::CollectionError(format!(
"an error occurred when processing a URL: invalid MusicBrainz URL: {MUSICBUTLER}" "an error occurred when processing a URL: invalid MusicBrainz URL: {MUSICBUTLER}"
@ -549,23 +618,23 @@ mod tests {
// Setting a URL on an artist not in the collection is an error. // Setting a URL on an artist not in the collection is an error.
assert!(music_hoard assert!(music_hoard
.set_musicbrainz_url(&artist_id_2, MUSICBRAINZ) .set_artist_musicbrainz(&artist_id_2, MUSICBRAINZ)
.is_err()); .is_err());
assert_eq!(music_hoard.collection[0].musicbrainz, expected); assert_eq!(music_hoard.collection[0].musicbrainz, expected);
// Setting a URL on an artist. // Setting a URL on an artist.
assert!(music_hoard assert!(music_hoard
.set_musicbrainz_url(&artist_id, MUSICBRAINZ) .set_artist_musicbrainz(&artist_id, MUSICBRAINZ)
.is_ok()); .is_ok());
_ = expected.insert(MusicBrainz::new(MUSICBRAINZ).unwrap()); _ = expected.insert(MUSICBRAINZ.try_into().unwrap());
assert_eq!(music_hoard.collection[0].musicbrainz, expected); assert_eq!(music_hoard.collection[0].musicbrainz, expected);
// Clearing URLs on an artist that does not exist is an error. // Clearing URLs on an artist that does not exist is an error.
assert!(music_hoard.clear_musicbrainz_url(&artist_id_2).is_err()); assert!(music_hoard.clear_artist_musicbrainz(&artist_id_2).is_err());
assert_eq!(music_hoard.collection[0].musicbrainz, expected); assert_eq!(music_hoard.collection[0].musicbrainz, expected);
// Clearing URLs. // Clearing URLs.
assert!(music_hoard.clear_musicbrainz_url(&artist_id).is_ok()); assert!(music_hoard.clear_artist_musicbrainz(&artist_id).is_ok());
_ = expected.take(); _ = expected.take();
assert_eq!(music_hoard.collection[0].musicbrainz, expected); assert_eq!(music_hoard.collection[0].musicbrainz, expected);
} }
@ -587,13 +656,13 @@ mod tests {
// Adding URLs to an artist not in the collection is an error. // Adding URLs to an artist not in the collection is an error.
assert!(music_hoard assert!(music_hoard
.add_to_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER]) .add_to_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
.is_err()); .is_err());
assert!(music_hoard.collection[0].properties.is_empty()); assert!(music_hoard.collection[0].properties.is_empty());
// Adding mutliple URLs without clashes. // Adding mutliple URLs without clashes.
assert!(music_hoard assert!(music_hoard
.add_to_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2]) .add_to_artist_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2])
.is_ok()); .is_ok());
expected.push(MUSICBUTLER.to_owned()); expected.push(MUSICBUTLER.to_owned());
expected.push(MUSICBUTLER_2.to_owned()); expected.push(MUSICBUTLER_2.to_owned());
@ -604,7 +673,7 @@ mod tests {
// Removing URLs from an artist not in the collection is an error. // Removing URLs from an artist not in the collection is an error.
assert!(music_hoard assert!(music_hoard
.remove_from_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER]) .remove_from_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
.is_err()); .is_err());
assert_eq!( assert_eq!(
music_hoard.collection[0].properties.get("MusicButler"), music_hoard.collection[0].properties.get("MusicButler"),
@ -613,7 +682,11 @@ mod tests {
// Removing multiple URLs without clashes. // Removing multiple URLs without clashes.
assert!(music_hoard assert!(music_hoard
.remove_from_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2]) .remove_from_artist_property(
&artist_id,
"MusicButler",
vec![MUSICBUTLER, MUSICBUTLER_2]
)
.is_ok()); .is_ok());
expected.clear(); expected.clear();
assert!(music_hoard.collection[0].properties.is_empty()); assert!(music_hoard.collection[0].properties.is_empty());
@ -636,13 +709,13 @@ mod tests {
// Seting URL on an artist not in the collection is an error. // Seting URL on an artist not in the collection is an error.
assert!(music_hoard assert!(music_hoard
.set_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER]) .set_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
.is_err()); .is_err());
assert!(music_hoard.collection[0].properties.is_empty()); assert!(music_hoard.collection[0].properties.is_empty());
// Set URLs. // Set URLs.
assert!(music_hoard assert!(music_hoard
.set_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2]) .set_artist_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2])
.is_ok()); .is_ok());
expected.clear(); expected.clear();
expected.push(MUSICBUTLER.to_owned()); expected.push(MUSICBUTLER.to_owned());
@ -654,17 +727,62 @@ mod tests {
// Clearing URLs on an artist that does not exist is an error. // Clearing URLs on an artist that does not exist is an error.
assert!(music_hoard assert!(music_hoard
.clear_property(&artist_id_2, "MusicButler") .clear_artist_property(&artist_id_2, "MusicButler")
.is_err()); .is_err());
// Clear URLs. // Clear URLs.
assert!(music_hoard assert!(music_hoard
.clear_property(&artist_id, "MusicButler") .clear_artist_property(&artist_id, "MusicButler")
.is_ok()); .is_ok());
expected.clear(); expected.clear();
assert!(music_hoard.collection[0].properties.is_empty()); assert!(music_hoard.collection[0].properties.is_empty());
} }
#[test]
fn set_clear_album_seq() {
let mut database = MockIDatabase::new();
let artist_id = ArtistId::new("an artist");
let album_id = AlbumId::new("an album");
let album_id_2 = AlbumId::new("another album");
let mut database_result = vec![Artist::new(artist_id.clone())];
database_result[0].albums.push(Album {
id: album_id.clone(),
date: AlbumDate::default(),
seq: AlbumSeq::default(),
tracks: vec![],
});
database
.expect_load()
.times(1)
.return_once(|| Ok(database_result));
database.expect_save().times(2).returning(|_| Ok(()));
let mut music_hoard = MusicHoard::database(database).unwrap();
assert_eq!(music_hoard.collection[0].albums[0].seq, AlbumSeq(0));
// Seting seq on an album not belonging to the artist is an error.
assert!(music_hoard
.set_album_seq(&artist_id, &album_id_2, 6)
.is_err());
assert_eq!(music_hoard.collection[0].albums[0].seq, AlbumSeq(0));
// Set seq.
assert!(music_hoard.set_album_seq(&artist_id, &album_id, 6).is_ok());
assert_eq!(music_hoard.collection[0].albums[0].seq, AlbumSeq(6));
// Clearing seq on an album that does not exist is an error.
assert!(music_hoard
.clear_album_seq(&artist_id, &album_id_2)
.is_err());
// Clear seq.
assert!(music_hoard.clear_album_seq(&artist_id, &album_id).is_ok());
assert_eq!(music_hoard.collection[0].albums[0].seq, AlbumSeq(0));
}
#[test] #[test]
fn merge_collection_no_overlap() { fn merge_collection_no_overlap() {
let half: usize = FULL_COLLECTION.len() / 2; let half: usize = FULL_COLLECTION.len() / 2;
@ -887,7 +1005,7 @@ mod tests {
} }
#[test] #[test]
fn rescan_library_album_title_year_clash() { fn rescan_library_album_id_clash() {
let mut library = MockILibrary::new(); let mut library = MockILibrary::new();
let mut expected = LIBRARY_COLLECTION.to_owned(); let mut expected = LIBRARY_COLLECTION.to_owned();
@ -895,10 +1013,10 @@ mod tests {
let clashed_album_id = &expected[1].albums[0].id; let clashed_album_id = &expected[1].albums[0].id;
let mut items = LIBRARY_ITEMS.to_owned(); let mut items = LIBRARY_ITEMS.to_owned();
for item in items.iter_mut().filter(|it| { for item in items
(it.album_year == removed_album_id.year) && (it.album_title == removed_album_id.title) .iter_mut()
}) { .filter(|it| it.album_title == removed_album_id.title)
item.album_year = clashed_album_id.year; {
item.album_title = clashed_album_id.title.clone(); item.album_title = clashed_album_id.title.clone();
} }
@ -916,6 +1034,7 @@ mod tests {
let mut music_hoard = MusicHoard::library(library); let mut music_hoard = MusicHoard::library(library);
music_hoard.rescan_library().unwrap(); music_hoard.rescan_library().unwrap();
assert_eq!(music_hoard.get_collection()[0], expected[0]);
assert_eq!(music_hoard.get_collection(), &expected); assert_eq!(music_hoard.get_collection(), &expected);
} }

View File

@ -1,10 +1,10 @@
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::collections::HashMap; use std::{collections::HashMap, str::FromStr};
use crate::core::collection::{ use crate::core::collection::{
album::{Album, AlbumId}, album::{Album, AlbumDate, AlbumId, AlbumMonth, AlbumSeq},
artist::{Artist, ArtistId, MusicBrainz}, artist::{Artist, ArtistId, MusicBrainz},
track::{Format, Quality, Track, TrackId}, track::{Track, TrackFormat, TrackId, TrackNum, TrackQuality},
}; };
use crate::tests::*; use crate::tests::*;

View File

@ -11,54 +11,59 @@ macro_rules! library_collection {
albums: vec![ albums: vec![
Album { Album {
id: AlbumId { id: AlbumId {
year: 1998,
title: "album_title a.a".to_string(), title: "album_title a.a".to_string(),
}, },
date: AlbumDate {
year: 1998,
month: AlbumMonth::None,
day: 0,
},
seq: AlbumSeq(0),
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {
number: 1,
title: "track a.a.1".to_string(), title: "track a.a.1".to_string(),
}, },
number: TrackNum(1),
artist: vec!["artist a.a.1".to_string()], artist: vec!["artist a.a.1".to_string()],
quality: Quality { quality: TrackQuality {
format: Format::Flac, format: TrackFormat::Flac,
bitrate: 992, bitrate: 992,
}, },
}, },
Track { Track {
id: TrackId { id: TrackId {
number: 2,
title: "track a.a.2".to_string(), title: "track a.a.2".to_string(),
}, },
number: TrackNum(2),
artist: vec![ artist: vec![
"artist a.a.2.1".to_string(), "artist a.a.2.1".to_string(),
"artist a.a.2.2".to_string(), "artist a.a.2.2".to_string(),
], ],
quality: Quality { quality: TrackQuality {
format: Format::Mp3, format: TrackFormat::Mp3,
bitrate: 320, bitrate: 320,
}, },
}, },
Track { Track {
id: TrackId { id: TrackId {
number: 3,
title: "track a.a.3".to_string(), title: "track a.a.3".to_string(),
}, },
number: TrackNum(3),
artist: vec!["artist a.a.3".to_string()], artist: vec!["artist a.a.3".to_string()],
quality: Quality { quality: TrackQuality {
format: Format::Flac, format: TrackFormat::Flac,
bitrate: 1061, bitrate: 1061,
}, },
}, },
Track { Track {
id: TrackId { id: TrackId {
number: 4,
title: "track a.a.4".to_string(), title: "track a.a.4".to_string(),
}, },
number: TrackNum(4),
artist: vec!["artist a.a.4".to_string()], artist: vec!["artist a.a.4".to_string()],
quality: Quality { quality: TrackQuality {
format: Format::Flac, format: TrackFormat::Flac,
bitrate: 1042, bitrate: 1042,
}, },
}, },
@ -66,29 +71,34 @@ macro_rules! library_collection {
}, },
Album { Album {
id: AlbumId { id: AlbumId {
year: 2015,
title: "album_title a.b".to_string(), title: "album_title a.b".to_string(),
}, },
date: AlbumDate {
year: 2015,
month: AlbumMonth::April,
day: 0,
},
seq: AlbumSeq(0),
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {
number: 1,
title: "track a.b.1".to_string(), title: "track a.b.1".to_string(),
}, },
number: TrackNum(1),
artist: vec!["artist a.b.1".to_string()], artist: vec!["artist a.b.1".to_string()],
quality: Quality { quality: TrackQuality {
format: Format::Flac, format: TrackFormat::Flac,
bitrate: 1004, bitrate: 1004,
}, },
}, },
Track { Track {
id: TrackId { id: TrackId {
number: 2,
title: "track a.b.2".to_string(), title: "track a.b.2".to_string(),
}, },
number: TrackNum(2),
artist: vec!["artist a.b.2".to_string()], artist: vec!["artist a.b.2".to_string()],
quality: Quality { quality: TrackQuality {
format: Format::Flac, format: TrackFormat::Flac,
bitrate: 1077, bitrate: 1077,
}, },
}, },
@ -106,32 +116,37 @@ macro_rules! library_collection {
albums: vec![ albums: vec![
Album { Album {
id: AlbumId { id: AlbumId {
year: 2003,
title: "album_title b.a".to_string(), title: "album_title b.a".to_string(),
}, },
date: AlbumDate {
year: 2003,
month: AlbumMonth::June,
day: 6,
},
seq: AlbumSeq(0),
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {
number: 1,
title: "track b.a.1".to_string(), title: "track b.a.1".to_string(),
}, },
number: TrackNum(1),
artist: vec!["artist b.a.1".to_string()], artist: vec!["artist b.a.1".to_string()],
quality: Quality { quality: TrackQuality {
format: Format::Mp3, format: TrackFormat::Mp3,
bitrate: 190, bitrate: 190,
}, },
}, },
Track { Track {
id: TrackId { id: TrackId {
number: 2,
title: "track b.a.2".to_string(), title: "track b.a.2".to_string(),
}, },
number: TrackNum(2),
artist: vec![ artist: vec![
"artist b.a.2.1".to_string(), "artist b.a.2.1".to_string(),
"artist b.a.2.2".to_string(), "artist b.a.2.2".to_string(),
], ],
quality: Quality { quality: TrackQuality {
format: Format::Mp3, format: TrackFormat::Mp3,
bitrate: 120, bitrate: 120,
}, },
}, },
@ -139,32 +154,37 @@ macro_rules! library_collection {
}, },
Album { Album {
id: AlbumId { id: AlbumId {
year: 2008,
title: "album_title b.b".to_string(), title: "album_title b.b".to_string(),
}, },
date: AlbumDate {
year: 2008,
month: AlbumMonth::None,
day: 0,
},
seq: AlbumSeq(0),
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {
number: 1,
title: "track b.b.1".to_string(), title: "track b.b.1".to_string(),
}, },
number: TrackNum(1),
artist: vec!["artist b.b.1".to_string()], artist: vec!["artist b.b.1".to_string()],
quality: Quality { quality: TrackQuality {
format: Format::Flac, format: TrackFormat::Flac,
bitrate: 1077, bitrate: 1077,
}, },
}, },
Track { Track {
id: TrackId { id: TrackId {
number: 2,
title: "track b.b.2".to_string(), title: "track b.b.2".to_string(),
}, },
number: TrackNum(2),
artist: vec![ artist: vec![
"artist b.b.2.1".to_string(), "artist b.b.2.1".to_string(),
"artist b.b.2.2".to_string(), "artist b.b.2.2".to_string(),
], ],
quality: Quality { quality: TrackQuality {
format: Format::Mp3, format: TrackFormat::Mp3,
bitrate: 320, bitrate: 320,
}, },
}, },
@ -172,32 +192,37 @@ macro_rules! library_collection {
}, },
Album { Album {
id: AlbumId { id: AlbumId {
year: 2009,
title: "album_title b.c".to_string(), title: "album_title b.c".to_string(),
}, },
date: AlbumDate {
year: 2009,
month: AlbumMonth::None,
day: 0,
},
seq: AlbumSeq(0),
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {
number: 1,
title: "track b.c.1".to_string(), title: "track b.c.1".to_string(),
}, },
number: TrackNum(1),
artist: vec!["artist b.c.1".to_string()], artist: vec!["artist b.c.1".to_string()],
quality: Quality { quality: TrackQuality {
format: Format::Mp3, format: TrackFormat::Mp3,
bitrate: 190, bitrate: 190,
}, },
}, },
Track { Track {
id: TrackId { id: TrackId {
number: 2,
title: "track b.c.2".to_string(), title: "track b.c.2".to_string(),
}, },
number: TrackNum(2),
artist: vec![ artist: vec![
"artist b.c.2.1".to_string(), "artist b.c.2.1".to_string(),
"artist b.c.2.2".to_string(), "artist b.c.2.2".to_string(),
], ],
quality: Quality { quality: TrackQuality {
format: Format::Mp3, format: TrackFormat::Mp3,
bitrate: 120, bitrate: 120,
}, },
}, },
@ -205,32 +230,37 @@ macro_rules! library_collection {
}, },
Album { Album {
id: AlbumId { id: AlbumId {
year: 2015,
title: "album_title b.d".to_string(), title: "album_title b.d".to_string(),
}, },
date: AlbumDate {
year: 2015,
month: AlbumMonth::None,
day: 0,
},
seq: AlbumSeq(0),
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {
number: 1,
title: "track b.d.1".to_string(), title: "track b.d.1".to_string(),
}, },
number: TrackNum(1),
artist: vec!["artist b.d.1".to_string()], artist: vec!["artist b.d.1".to_string()],
quality: Quality { quality: TrackQuality {
format: Format::Mp3, format: TrackFormat::Mp3,
bitrate: 190, bitrate: 190,
}, },
}, },
Track { Track {
id: TrackId { id: TrackId {
number: 2,
title: "track b.d.2".to_string(), title: "track b.d.2".to_string(),
}, },
number: TrackNum(2),
artist: vec![ artist: vec![
"artist b.d.2.1".to_string(), "artist b.d.2.1".to_string(),
"artist b.d.2.2".to_string(), "artist b.d.2.2".to_string(),
], ],
quality: Quality { quality: TrackQuality {
format: Format::Mp3, format: TrackFormat::Mp3,
bitrate: 120, bitrate: 120,
}, },
}, },
@ -250,32 +280,37 @@ macro_rules! library_collection {
albums: vec![ albums: vec![
Album { Album {
id: AlbumId { id: AlbumId {
year: 1985,
title: "album_title c.a".to_string(), title: "album_title c.a".to_string(),
}, },
date: AlbumDate {
year: 1985,
month: AlbumMonth::None,
day: 0,
},
seq: AlbumSeq(0),
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {
number: 1,
title: "track c.a.1".to_string(), title: "track c.a.1".to_string(),
}, },
number: TrackNum(1),
artist: vec!["artist c.a.1".to_string()], artist: vec!["artist c.a.1".to_string()],
quality: Quality { quality: TrackQuality {
format: Format::Mp3, format: TrackFormat::Mp3,
bitrate: 320, bitrate: 320,
}, },
}, },
Track { Track {
id: TrackId { id: TrackId {
number: 2,
title: "track c.a.2".to_string(), title: "track c.a.2".to_string(),
}, },
number: TrackNum(2),
artist: vec![ artist: vec![
"artist c.a.2.1".to_string(), "artist c.a.2.1".to_string(),
"artist c.a.2.2".to_string(), "artist c.a.2.2".to_string(),
], ],
quality: Quality { quality: TrackQuality {
format: Format::Mp3, format: TrackFormat::Mp3,
bitrate: 120, bitrate: 120,
}, },
}, },
@ -283,32 +318,37 @@ macro_rules! library_collection {
}, },
Album { Album {
id: AlbumId { id: AlbumId {
year: 2018,
title: "album_title c.b".to_string(), title: "album_title c.b".to_string(),
}, },
date: AlbumDate {
year: 2018,
month: AlbumMonth::None,
day: 0,
},
seq: AlbumSeq(0),
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {
number: 1,
title: "track c.b.1".to_string(), title: "track c.b.1".to_string(),
}, },
number: TrackNum(1),
artist: vec!["artist c.b.1".to_string()], artist: vec!["artist c.b.1".to_string()],
quality: Quality { quality: TrackQuality {
format: Format::Flac, format: TrackFormat::Flac,
bitrate: 1041, bitrate: 1041,
}, },
}, },
Track { Track {
id: TrackId { id: TrackId {
number: 2,
title: "track c.b.2".to_string(), title: "track c.b.2".to_string(),
}, },
number: TrackNum(2),
artist: vec![ artist: vec![
"artist c.b.2.1".to_string(), "artist c.b.2.1".to_string(),
"artist c.b.2.2".to_string(), "artist c.b.2.2".to_string(),
], ],
quality: Quality { quality: TrackQuality {
format: Format::Flac, format: TrackFormat::Flac,
bitrate: 756, bitrate: 756,
}, },
}, },
@ -326,32 +366,37 @@ macro_rules! library_collection {
albums: vec![ albums: vec![
Album { Album {
id: AlbumId { id: AlbumId {
year: 1995,
title: "album_title d.a".to_string(), title: "album_title d.a".to_string(),
}, },
date: AlbumDate {
year: 1995,
month: AlbumMonth::None,
day: 0,
},
seq: AlbumSeq(0),
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {
number: 1,
title: "track d.a.1".to_string(), title: "track d.a.1".to_string(),
}, },
number: TrackNum(1),
artist: vec!["artist d.a.1".to_string()], artist: vec!["artist d.a.1".to_string()],
quality: Quality { quality: TrackQuality {
format: Format::Mp3, format: TrackFormat::Mp3,
bitrate: 120, bitrate: 120,
}, },
}, },
Track { Track {
id: TrackId { id: TrackId {
number: 2,
title: "track d.a.2".to_string(), title: "track d.a.2".to_string(),
}, },
number: TrackNum(2),
artist: vec![ artist: vec![
"artist d.a.2.1".to_string(), "artist d.a.2.1".to_string(),
"artist d.a.2.2".to_string(), "artist d.a.2.2".to_string(),
], ],
quality: Quality { quality: TrackQuality {
format: Format::Mp3, format: TrackFormat::Mp3,
bitrate: 120, bitrate: 120,
}, },
}, },
@ -359,32 +404,37 @@ macro_rules! library_collection {
}, },
Album { Album {
id: AlbumId { id: AlbumId {
year: 2028,
title: "album_title d.b".to_string(), title: "album_title d.b".to_string(),
}, },
date: AlbumDate {
year: 2028,
month: AlbumMonth::None,
day: 0,
},
seq: AlbumSeq(0),
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {
number: 1,
title: "track d.b.1".to_string(), title: "track d.b.1".to_string(),
}, },
number: TrackNum(1),
artist: vec!["artist d.b.1".to_string()], artist: vec!["artist d.b.1".to_string()],
quality: Quality { quality: TrackQuality {
format: Format::Flac, format: TrackFormat::Flac,
bitrate: 841, bitrate: 841,
}, },
}, },
Track { Track {
id: TrackId { id: TrackId {
number: 2,
title: "track d.b.2".to_string(), title: "track d.b.2".to_string(),
}, },
number: TrackNum(2),
artist: vec![ artist: vec![
"artist d.b.2.1".to_string(), "artist d.b.2.1".to_string(),
"artist d.b.2.2".to_string(), "artist d.b.2.2".to_string(),
], ],
quality: Quality { quality: TrackQuality {
format: Format::Flac, format: TrackFormat::Flac,
bitrate: 756, bitrate: 756,
}, },
}, },
@ -404,12 +454,9 @@ macro_rules! full_collection {
let artist_a = iter.next().unwrap(); let artist_a = iter.next().unwrap();
assert_eq!(artist_a.id.name, "Album_Artist A"); assert_eq!(artist_a.id.name, "Album_Artist A");
artist_a.musicbrainz = Some( artist_a.musicbrainz = Some(MusicBrainz::from_str(
MusicBrainz::new( "https://musicbrainz.org/artist/00000000-0000-0000-0000-000000000000",
"https://musicbrainz.org/artist/00000000-0000-0000-0000-000000000000", ).unwrap());
)
.unwrap(),
);
artist_a.properties = HashMap::from([ artist_a.properties = HashMap::from([
(String::from("MusicButler"), vec![ (String::from("MusicButler"), vec![
@ -422,14 +469,15 @@ macro_rules! full_collection {
]), ]),
]); ]);
artist_a.albums[0].seq = AlbumSeq(1);
artist_a.albums[1].seq = AlbumSeq(1);
let artist_b = iter.next().unwrap(); let artist_b = iter.next().unwrap();
assert_eq!(artist_b.id.name, "Album_Artist B"); assert_eq!(artist_b.id.name, "Album_Artist B");
artist_b.musicbrainz = Some( artist_b.musicbrainz = Some(MusicBrainz::from_str(
MusicBrainz::new( "https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111",
"https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111", ).unwrap());
).unwrap(),
);
artist_b.properties = HashMap::from([ artist_b.properties = HashMap::from([
(String::from("MusicButler"), vec![ (String::from("MusicButler"), vec![
@ -444,14 +492,17 @@ macro_rules! full_collection {
]), ]),
]); ]);
artist_b.albums[0].seq = AlbumSeq(1);
artist_b.albums[1].seq = AlbumSeq(3);
artist_b.albums[2].seq = AlbumSeq(2);
artist_b.albums[3].seq = AlbumSeq(4);
let artist_c = iter.next().unwrap(); let artist_c = iter.next().unwrap();
assert_eq!(artist_c.id.name, "The Album_Artist C"); assert_eq!(artist_c.id.name, "The Album_Artist C");
artist_c.musicbrainz = Some( artist_c.musicbrainz = Some(MusicBrainz::from_str(
MusicBrainz::new( "https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111",
"https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111", ).unwrap());
).unwrap(),
);
// Nothing for artist_d // Nothing for artist_d

View File

@ -1,7 +1,7 @@
use crate::tui::{ use crate::tui::{
app::{ app::{
machine::{App, AppInner, AppMachine}, machine::{App, AppInner, AppMachine},
selection::IdSelection, selection::KeySelection,
AppPublic, AppState, IAppInteractReload, AppPublic, AppState, IAppInteractReload,
}, },
lib::IMusicHoard, lib::IMusicHoard,
@ -36,7 +36,7 @@ impl<MH: IMusicHoard> IAppInteractReload for AppMachine<MH, AppReload> {
type APP = App<MH>; type APP = App<MH>;
fn reload_library(mut self) -> Self::APP { fn reload_library(mut self) -> Self::APP {
let previous = IdSelection::get( let previous = KeySelection::get(
self.inner.music_hoard.get_collection(), self.inner.music_hoard.get_collection(),
&self.inner.selection, &self.inner.selection,
); );
@ -45,7 +45,7 @@ impl<MH: IMusicHoard> IAppInteractReload for AppMachine<MH, AppReload> {
} }
fn reload_database(mut self) -> Self::APP { fn reload_database(mut self) -> Self::APP {
let previous = IdSelection::get( let previous = KeySelection::get(
self.inner.music_hoard.get_collection(), self.inner.music_hoard.get_collection(),
&self.inner.selection, &self.inner.selection,
); );
@ -63,11 +63,11 @@ impl<MH: IMusicHoard> IAppInteractReload for AppMachine<MH, AppReload> {
} }
trait IAppInteractReloadPrivate<MH: IMusicHoard> { trait IAppInteractReloadPrivate<MH: IMusicHoard> {
fn refresh(self, previous: IdSelection, result: Result<(), musichoard::Error>) -> App<MH>; fn refresh(self, previous: KeySelection, result: Result<(), musichoard::Error>) -> App<MH>;
} }
impl<MH: IMusicHoard> IAppInteractReloadPrivate<MH> for AppMachine<MH, AppReload> { impl<MH: IMusicHoard> IAppInteractReloadPrivate<MH> for AppMachine<MH, AppReload> {
fn refresh(mut self, previous: IdSelection, result: Result<(), musichoard::Error>) -> App<MH> { fn refresh(mut self, previous: KeySelection, result: Result<(), musichoard::Error>) -> App<MH> {
match result { match result {
Ok(()) => { Ok(()) => {
self.inner self.inner

View File

@ -1,12 +1,12 @@
use std::cmp; use std::cmp;
use musichoard::collection::{ use musichoard::collection::{
album::{Album, AlbumId}, album::{Album, AlbumDate, AlbumId, AlbumSeq},
track::Track, track::Track,
}; };
use crate::tui::app::selection::{ use crate::tui::app::selection::{
track::{IdSelectTrack, TrackSelection}, track::{KeySelectTrack, TrackSelection},
Delta, SelectionState, WidgetState, Delta, SelectionState, WidgetState,
}; };
@ -26,9 +26,9 @@ impl AlbumSelection {
selection selection
} }
pub fn reinitialise(&mut self, albums: &[Album], album: Option<IdSelectAlbum>) { pub fn reinitialise(&mut self, albums: &[Album], album: Option<KeySelectAlbum>) {
if let Some(album) = album { if let Some(album) = album {
let result = albums.binary_search_by(|a| a.get_sort_key().cmp(&album.album_id)); let result = albums.binary_search_by(|a| a.get_sort_key().cmp(&album.get_sort_key()));
match result { match result {
Ok(index) => self.reinitialise_with_index(albums, index, album.track), Ok(index) => self.reinitialise_with_index(albums, index, album.track),
Err(index) => self.reinitialise_with_index(albums, index, None), Err(index) => self.reinitialise_with_index(albums, index, None),
@ -42,7 +42,7 @@ impl AlbumSelection {
&mut self, &mut self,
albums: &[Album], albums: &[Album],
index: usize, index: usize,
active_track: Option<IdSelectTrack>, active_track: Option<KeySelectTrack>,
) { ) {
if albums.is_empty() { if albums.is_empty() {
self.state.list.select(None); self.state.list.select(None);
@ -160,21 +160,26 @@ impl AlbumSelection {
} }
} }
pub struct IdSelectAlbum { pub struct KeySelectAlbum {
album_id: AlbumId, key: (AlbumDate, AlbumSeq, AlbumId),
track: Option<IdSelectTrack>, track: Option<KeySelectTrack>,
} }
impl IdSelectAlbum { impl KeySelectAlbum {
pub fn get(albums: &[Album], selection: &AlbumSelection) -> Option<Self> { pub fn get(albums: &[Album], selection: &AlbumSelection) -> Option<Self> {
selection.state.list.selected().map(|index| { selection.state.list.selected().map(|index| {
let album = &albums[index]; let album = &albums[index];
IdSelectAlbum { let key = album.get_sort_key();
album_id: album.get_sort_key().clone(), KeySelectAlbum {
track: IdSelectTrack::get(&album.tracks, &selection.track), key: (key.0.to_owned(), key.1.to_owned(), key.2.to_owned()),
track: KeySelectTrack::get(&album.tracks, &selection.track),
} }
}) })
} }
pub fn get_sort_key(&self) -> (&AlbumDate, &AlbumSeq, &AlbumId) {
(&self.key.0, &self.key.1, &self.key.2)
}
} }
#[cfg(test)] #[cfg(test)]
@ -330,20 +335,20 @@ mod tests {
// Re-initialise. // Re-initialise.
let expected = sel.clone(); let expected = sel.clone();
let active_album = IdSelectAlbum::get(albums, &sel); let active_album = KeySelectAlbum::get(albums, &sel);
sel.reinitialise(albums, active_album); sel.reinitialise(albums, active_album);
assert_eq!(sel, expected); assert_eq!(sel, expected);
// Re-initialise out-of-bounds. // Re-initialise out-of-bounds.
let mut expected = sel.clone(); let mut expected = sel.clone();
expected.decrement(albums, Delta::Line); expected.decrement(albums, Delta::Line);
let active_album = IdSelectAlbum::get(albums, &sel); let active_album = KeySelectAlbum::get(albums, &sel);
sel.reinitialise(&albums[..(albums.len() - 1)], active_album); sel.reinitialise(&albums[..(albums.len() - 1)], active_album);
assert_eq!(sel, expected); assert_eq!(sel, expected);
// Re-initialise empty. // Re-initialise empty.
let expected = AlbumSelection::initialise(&[]); let expected = AlbumSelection::initialise(&[]);
let active_album = IdSelectAlbum::get(albums, &sel); let active_album = KeySelectAlbum::get(albums, &sel);
sel.reinitialise(&[], active_album); sel.reinitialise(&[], active_album);
assert_eq!(sel, expected); assert_eq!(sel, expected);
} }

View File

@ -7,7 +7,7 @@ use musichoard::collection::{
}; };
use crate::tui::app::selection::{ use crate::tui::app::selection::{
album::{AlbumSelection, IdSelectAlbum}, album::{AlbumSelection, KeySelectAlbum},
Delta, SelectionState, WidgetState, Delta, SelectionState, WidgetState,
}; };
@ -27,9 +27,9 @@ impl ArtistSelection {
selection selection
} }
pub fn reinitialise(&mut self, artists: &[Artist], active: Option<IdSelectArtist>) { pub fn reinitialise(&mut self, artists: &[Artist], active: Option<KeySelectArtist>) {
if let Some(active) = active { if let Some(active) = active {
let result = artists.binary_search_by(|a| a.get_sort_key().cmp(&active.artist_id)); let result = artists.binary_search_by(|a| a.get_sort_key().cmp(&active.get_sort_key()));
match result { match result {
Ok(index) => self.reinitialise_with_index(artists, index, active.album), Ok(index) => self.reinitialise_with_index(artists, index, active.album),
Err(index) => self.reinitialise_with_index(artists, index, None), Err(index) => self.reinitialise_with_index(artists, index, None),
@ -43,7 +43,7 @@ impl ArtistSelection {
&mut self, &mut self,
artists: &[Artist], artists: &[Artist],
index: usize, index: usize,
active_album: Option<IdSelectAlbum>, active_album: Option<KeySelectAlbum>,
) { ) {
if artists.is_empty() { if artists.is_empty() {
self.state.list.select(None); self.state.list.select(None);
@ -193,21 +193,26 @@ impl ArtistSelection {
} }
} }
pub struct IdSelectArtist { pub struct KeySelectArtist {
artist_id: ArtistId, key: (ArtistId,),
album: Option<IdSelectAlbum>, album: Option<KeySelectAlbum>,
} }
impl IdSelectArtist { impl KeySelectArtist {
pub fn get(artists: &[Artist], selection: &ArtistSelection) -> Option<Self> { pub fn get(artists: &[Artist], selection: &ArtistSelection) -> Option<Self> {
selection.state.list.selected().map(|index| { selection.state.list.selected().map(|index| {
let artist = &artists[index]; let artist = &artists[index];
IdSelectArtist { let key = artist.get_sort_key();
artist_id: artist.get_sort_key().clone(), KeySelectArtist {
album: IdSelectAlbum::get(&artist.albums, &selection.album), key: (key.0.to_owned(),),
album: KeySelectAlbum::get(&artist.albums, &selection.album),
} }
}) })
} }
pub fn get_sort_key(&self) -> (&ArtistId,) {
(&self.key.0,)
}
} }
#[cfg(test)] #[cfg(test)]
@ -385,20 +390,20 @@ mod tests {
// Re-initialise. // Re-initialise.
let expected = sel.clone(); let expected = sel.clone();
let active_artist = IdSelectArtist::get(artists, &sel); let active_artist = KeySelectArtist::get(artists, &sel);
sel.reinitialise(artists, active_artist); sel.reinitialise(artists, active_artist);
assert_eq!(sel, expected); assert_eq!(sel, expected);
// Re-initialise out-of-bounds. // Re-initialise out-of-bounds.
let mut expected = sel.clone(); let mut expected = sel.clone();
expected.decrement(artists, Delta::Line); expected.decrement(artists, Delta::Line);
let active_artist = IdSelectArtist::get(artists, &sel); let active_artist = KeySelectArtist::get(artists, &sel);
sel.reinitialise(&artists[..(artists.len() - 1)], active_artist); sel.reinitialise(&artists[..(artists.len() - 1)], active_artist);
assert_eq!(sel, expected); assert_eq!(sel, expected);
// Re-initialise empty. // Re-initialise empty.
let expected = ArtistSelection::initialise(&[]); let expected = ArtistSelection::initialise(&[]);
let active_artist = IdSelectArtist::get(artists, &sel); let active_artist = KeySelectArtist::get(artists, &sel);
sel.reinitialise(&[], active_artist); sel.reinitialise(&[], active_artist);
assert_eq!(sel, expected); assert_eq!(sel, expected);
} }

View File

@ -5,7 +5,7 @@ mod track;
use musichoard::collection::{album::Album, artist::Artist, track::Track, Collection}; use musichoard::collection::{album::Album, artist::Artist, track::Track, Collection};
use ratatui::widgets::ListState; use ratatui::widgets::ListState;
use artist::{ArtistSelection, IdSelectArtist}; use artist::{ArtistSelection, KeySelectArtist};
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Category { pub enum Category {
@ -64,7 +64,7 @@ impl Selection {
self.artist.album.track.state.list = selected.track; self.artist.album.track.state.list = selected.track;
} }
pub fn select_by_id(&mut self, artists: &[Artist], selected: IdSelection) { pub fn select_by_id(&mut self, artists: &[Artist], selected: KeySelection) {
self.artist.reinitialise(artists, selected.artist); self.artist.reinitialise(artists, selected.artist);
} }
@ -229,14 +229,14 @@ impl ListSelection {
} }
} }
pub struct IdSelection { pub struct KeySelection {
artist: Option<IdSelectArtist>, artist: Option<KeySelectArtist>,
} }
impl IdSelection { impl KeySelection {
pub fn get(collection: &Collection, selection: &Selection) -> Self { pub fn get(collection: &Collection, selection: &Selection) -> Self {
IdSelection { KeySelection {
artist: IdSelectArtist::get(collection, &selection.artist), artist: KeySelectArtist::get(collection, &selection.artist),
} }
} }
} }

View File

@ -1,6 +1,6 @@
use std::cmp; use std::cmp;
use musichoard::collection::track::{Track, TrackId}; use musichoard::collection::track::{Track, TrackId, TrackNum};
use crate::tui::app::selection::{Delta, SelectionState, WidgetState}; use crate::tui::app::selection::{Delta, SelectionState, WidgetState};
@ -18,9 +18,9 @@ impl TrackSelection {
selection selection
} }
pub fn reinitialise(&mut self, tracks: &[Track], track: Option<IdSelectTrack>) { pub fn reinitialise(&mut self, tracks: &[Track], track: Option<KeySelectTrack>) {
if let Some(track) = track { if let Some(track) = track {
let result = tracks.binary_search_by(|t| t.get_sort_key().cmp(&track.track_id)); let result = tracks.binary_search_by(|t| t.get_sort_key().cmp(&track.get_sort_key()));
match result { match result {
Ok(index) | Err(index) => self.reinitialise_with_index(tracks, index), Ok(index) | Err(index) => self.reinitialise_with_index(tracks, index),
} }
@ -100,19 +100,24 @@ impl TrackSelection {
} }
} }
pub struct IdSelectTrack { pub struct KeySelectTrack {
track_id: TrackId, key: (TrackNum, TrackId),
} }
impl IdSelectTrack { impl KeySelectTrack {
pub fn get(tracks: &[Track], selection: &TrackSelection) -> Option<Self> { pub fn get(tracks: &[Track], selection: &TrackSelection) -> Option<Self> {
selection.state.list.selected().map(|index| { selection.state.list.selected().map(|index| {
let track = &tracks[index]; let track = &tracks[index];
IdSelectTrack { let key = track.get_sort_key();
track_id: track.get_sort_key().clone(), KeySelectTrack {
key: (key.0.to_owned(), key.1.to_owned()),
} }
}) })
} }
pub fn get_sort_key(&self) -> (&TrackNum, &TrackId) {
(&self.key.0, &self.key.1)
}
} }
#[cfg(test)] #[cfg(test)]
@ -210,20 +215,20 @@ mod tests {
// Re-initialise. // Re-initialise.
let expected = sel.clone(); let expected = sel.clone();
let active_track = IdSelectTrack::get(tracks, &sel); let active_track = KeySelectTrack::get(tracks, &sel);
sel.reinitialise(tracks, active_track); sel.reinitialise(tracks, active_track);
assert_eq!(sel, expected); assert_eq!(sel, expected);
// Re-initialise out-of-bounds. // Re-initialise out-of-bounds.
let mut expected = sel.clone(); let mut expected = sel.clone();
expected.decrement(tracks, Delta::Line); expected.decrement(tracks, Delta::Line);
let active_track = IdSelectTrack::get(tracks, &sel); let active_track = KeySelectTrack::get(tracks, &sel);
sel.reinitialise(&tracks[..(tracks.len() - 1)], active_track); sel.reinitialise(&tracks[..(tracks.len() - 1)], active_track);
assert_eq!(sel, expected); assert_eq!(sel, expected);
// Re-initialise empty. // Re-initialise empty.
let expected = TrackSelection::initialise(&[]); let expected = TrackSelection::initialise(&[]);
let active_track = IdSelectTrack::get(tracks, &sel); let active_track = KeySelectTrack::get(tracks, &sel);
sel.reinitialise(&[], active_track); sel.reinitialise(&[], active_track);
assert_eq!(sel, expected); assert_eq!(sel, expected);
} }

View File

@ -11,12 +11,12 @@ pub use handler::EventHandler;
pub use listener::EventListener; pub use listener::EventListener;
pub use ui::Ui; pub use ui::Ui;
use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; use crossterm::{
use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}; event::{DisableMouseCapture, EnableMouseCapture},
use ratatui::backend::Backend; terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
use ratatui::Terminal; };
use std::io; use ratatui::{backend::Backend, Terminal};
use std::marker::PhantomData; use std::{io, marker::PhantomData};
use crate::tui::{ use crate::tui::{
app::{IAppAccess, IAppInteract}, app::{IAppAccess, IAppInteract},
@ -26,7 +26,7 @@ use crate::tui::{
ui::IUi, ui::IUi,
}; };
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, Eq, PartialEq)]
pub enum Error { pub enum Error {
Io(String), Io(String),
Event(String), Event(String),
@ -112,8 +112,8 @@ impl<B: Backend, UI: IUi, APP: IAppInteract + IAppAccess> Tui<B, UI, APP> {
match listener_handle.join() { match listener_handle.join() {
Ok(err) => return Err(err.into()), Ok(err) => return Err(err.into()),
// Calling std::panic::resume_unwind(err) as recommended by the Rust docs // Calling std::panic::resume_unwind(err) as recommended by the Rust docs
// will not produce an error message. The panic error message is printed at // will not produce an error message. This may be due to the panic simply
// the location of the panic which at the time is hidden by the TUI. // causing the process to abort in which case there is nothing to unwind.
Err(_) => return Err(Error::ListenerPanic), Err(_) => return Err(Error::ListenerPanic),
} }
} }
@ -251,10 +251,9 @@ mod tests {
let result = Tui::main(terminal, app, ui, handler, listener); let result = Tui::main(terminal, app, ui, handler, listener);
assert!(result.is_err()); assert!(result.is_err());
assert_eq!(
result.unwrap_err(), let error = EventError::Recv;
Error::Event(EventError::Recv.to_string()) assert_eq!(result.unwrap_err(), Error::Event(error.to_string()));
);
} }
#[test] #[test]

View File

@ -1,10 +1,11 @@
use std::{collections::HashMap, str::FromStr};
use musichoard::collection::{ use musichoard::collection::{
album::{Album, AlbumId}, album::{Album, AlbumDate, AlbumId, AlbumMonth, AlbumSeq},
artist::{Artist, ArtistId, MusicBrainz}, artist::{Artist, ArtistId, MusicBrainz},
track::{Format, Quality, Track, TrackId}, track::{Track, TrackFormat, TrackId, TrackNum, TrackQuality},
}; };
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::collections::HashMap;
use crate::tests::*; use crate::tests::*;

View File

@ -3,7 +3,7 @@ use std::collections::HashMap;
use musichoard::collection::{ use musichoard::collection::{
album::Album, album::Album,
artist::Artist, artist::Artist,
track::{Format, Track}, track::{Track, TrackFormat},
Collection, Collection,
}; };
use ratatui::{ use ratatui::{
@ -287,9 +287,13 @@ impl<'a, 'b> AlbumState<'a, 'b> {
let album = state.list.selected().map(|i| &albums[i]); let album = state.list.selected().map(|i| &albums[i]);
let info = Paragraph::new(format!( let info = Paragraph::new(format!(
"Title: {}\n\ "Title: {}\n\
Year: {}", Date: {}{}",
album.map(|a| a.id.title.as_str()).unwrap_or(""), album.map(|a| a.id.title.as_str()).unwrap_or(""),
album.map(|a| a.id.year.to_string()).unwrap_or_default(), album.map(|a| a.date.to_string()).unwrap_or_default(),
album
.filter(|a| a.seq.0 > 0)
.map(|a| format!(" ({})", a.seq.0))
.unwrap_or_default()
)); ));
AlbumState { AlbumState {
@ -323,13 +327,13 @@ impl<'a, 'b> TrackState<'a, 'b> {
Title: {}\n\ Title: {}\n\
Artist: {}\n\ Artist: {}\n\
Quality: {}", Quality: {}",
track.map(|t| t.id.number.to_string()).unwrap_or_default(), track.map(|t| t.number.0.to_string()).unwrap_or_default(),
track.map(|t| t.id.title.as_str()).unwrap_or(""), track.map(|t| t.id.title.as_str()).unwrap_or(""),
track.map(|t| t.artist.join("; ")).unwrap_or_default(), track.map(|t| t.artist.join("; ")).unwrap_or_default(),
track track
.map(|t| match t.quality.format { .map(|t| match t.quality.format {
Format::Flac => "FLAC".to_string(), TrackFormat::Flac => "FLAC".to_string(),
Format::Mp3 => format!("MP3 {}kbps", t.quality.bitrate), TrackFormat::Mp3 => format!("MP3 {}kbps", t.quality.bitrate),
}) })
.unwrap_or_default(), .unwrap_or_default(),
)); ));

View File

@ -4,7 +4,7 @@ use once_cell::sync::Lazy;
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
use musichoard::{ use musichoard::{
collection::{artist::Artist, Collection}, collection::{album::AlbumDate, artist::Artist, Collection},
database::{ database::{
json::{backend::JsonDatabaseFileBackend, JsonDatabase}, json::{backend::JsonDatabaseFileBackend, JsonDatabase},
IDatabase, IDatabase,
@ -19,7 +19,10 @@ pub static DATABASE_TEST_FILE: Lazy<PathBuf> =
fn expected() -> Collection { fn expected() -> Collection {
let mut expected = COLLECTION.to_owned(); let mut expected = COLLECTION.to_owned();
for artist in expected.iter_mut() { for artist in expected.iter_mut() {
artist.albums.clear(); for album in artist.albums.iter_mut() {
album.date = AlbumDate::default();
album.tracks.clear();
}
} }
expected expected
} }

View File

@ -1 +1 @@
{"V20240210":[{"name":"Аркона","sort":"Arkona","musicbrainz":"https://musicbrainz.org/artist/baad262d-55ef-427a-83c7-f7530964f212","properties":{"Bandcamp":["https://arkonamoscow.bandcamp.com/"],"MusicButler":["https://www.musicbutler.io/artist-page/283448581"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/arkona/download-streaming-albums"]}},{"name":"Eluveitie","sort":null,"musicbrainz":"https://musicbrainz.org/artist/8000598a-5edb-401c-8e6d-36b167feaf38","properties":{"MusicButler":["https://www.musicbutler.io/artist-page/269358403"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/eluveitie/download-streaming-albums"]}},{"name":"Frontside","sort":null,"musicbrainz":"https://musicbrainz.org/artist/3a901353-fccd-4afd-ad01-9c03f451b490","properties":{"MusicButler":["https://www.musicbutler.io/artist-page/826588800"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/frontside/download-streaming-albums"]}},{"name":"Heavens Basement","sort":"Heavens Basement","musicbrainz":"https://musicbrainz.org/artist/c2c4d56a-d599-4a18-bd2f-ae644e2198cc","properties":{"MusicButler":["https://www.musicbutler.io/artist-page/291158685"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/heaven-s-basement/download-streaming-albums"]}},{"name":"Metallica","sort":null,"musicbrainz":"https://musicbrainz.org/artist/65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab","properties":{"MusicButler":["https://www.musicbutler.io/artist-page/3996865"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/metallica/download-streaming-albums"]}}]} {"V20240302":[{"name":"Аркона","sort":"Arkona","musicbrainz":"https://musicbrainz.org/artist/baad262d-55ef-427a-83c7-f7530964f212","properties":{"Bandcamp":["https://arkonamoscow.bandcamp.com/"],"MusicButler":["https://www.musicbutler.io/artist-page/283448581"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/arkona/download-streaming-albums"]},"albums":[{"title":"Slovo","seq":0}]},{"name":"Eluveitie","sort":null,"musicbrainz":"https://musicbrainz.org/artist/8000598a-5edb-401c-8e6d-36b167feaf38","properties":{"MusicButler":["https://www.musicbutler.io/artist-page/269358403"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/eluveitie/download-streaming-albums"]},"albums":[{"title":"Vên [rerecorded]","seq":0},{"title":"Slania","seq":0}]},{"name":"Frontside","sort":null,"musicbrainz":"https://musicbrainz.org/artist/3a901353-fccd-4afd-ad01-9c03f451b490","properties":{"MusicButler":["https://www.musicbutler.io/artist-page/826588800"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/frontside/download-streaming-albums"]},"albums":[{"title":"…nasze jest królestwo, potęga i chwała na wieki…","seq":0}]},{"name":"Heavens Basement","sort":"Heavens Basement","musicbrainz":"https://musicbrainz.org/artist/c2c4d56a-d599-4a18-bd2f-ae644e2198cc","properties":{"MusicButler":["https://www.musicbutler.io/artist-page/291158685"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/heaven-s-basement/download-streaming-albums"]},"albums":[{"title":"Paper Plague","seq":0},{"title":"Unbreakable","seq":0}]},{"name":"Metallica","sort":null,"musicbrainz":"https://musicbrainz.org/artist/65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab","properties":{"MusicButler":["https://www.musicbutler.io/artist-page/3996865"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/metallica/download-streaming-albums"]},"albums":[{"title":"Ride the Lightning","seq":0},{"title":"S&M","seq":0}]}]}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff