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 musichoard::{
collection::artist::ArtistId,
collection::{album::AlbumId, artist::ArtistId},
database::json::{backend::JsonDatabaseFileBackend, JsonDatabase},
MusicHoard, MusicHoardBuilder, NoLibrary,
};
@ -22,56 +22,54 @@ struct Opt {
database_file_path: PathBuf,
#[structopt(subcommand)]
category: Category,
command: Command,
}
#[derive(StructOpt, Debug)]
enum Category {
#[structopt(about = "Edit artist information")]
Artist(ArtistCommand),
enum Command {
#[structopt(about = "Modify artist information")]
Artist(ArtistOpt),
}
impl Category {
fn handle(self, music_hoard: &mut MH) {
match self {
Category::Artist(artist_command) => artist_command.handle(music_hoard),
}
}
#[derive(StructOpt, Debug)]
struct ArtistOpt {
// For some reason, not specyfing the artist name with the `long` name makes StructOpt failed
// 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)]
enum ArtistCommand {
#[structopt(about = "Add a new artist to the collection")]
Add(ArtistValue),
Add,
#[structopt(about = "Remove an artist from the collection")]
Remove(ArtistValue),
Remove,
#[structopt(about = "Edit the artist's sort name")]
Sort(SortCommand),
#[structopt(name = "musicbrainz", about = "Edit the MusicBrainz URL of an artist")]
MusicBrainz(MusicBrainzCommand),
#[structopt(name = "property", about = "Edit a property of an artist")]
#[structopt(about = "Edit a property of an artist")]
Property(PropertyCommand),
#[structopt(about = "Modify the artist's album information")]
Album(AlbumOpt),
}
#[derive(StructOpt, Debug)]
enum SortCommand {
#[structopt(about = "Set the provided name as the artist's sort name")]
Set(ArtistSortValue),
Set(SortValue),
#[structopt(about = "Clear the artist's sort name")]
Clear(ArtistValue),
Clear,
}
#[derive(StructOpt, Debug)]
struct ArtistValue {
#[structopt(help = "The name of the artist")]
artist: String,
}
#[derive(StructOpt, Debug)]
struct ArtistSortValue {
#[structopt(help = "The name of the artist")]
artist: String,
#[structopt(help = "The sort name of the artist")]
struct SortValue {
#[structopt(help = "The sort name")]
sort: String,
}
@ -80,13 +78,11 @@ enum MusicBrainzCommand {
#[structopt(about = "Set the MusicBrainz URL overwriting any existing value")]
Set(MusicBrainzValue),
#[structopt(about = "Clear the MusicBrainz URL)")]
Clear(ArtistValue),
Clear,
}
#[derive(StructOpt, Debug)]
struct MusicBrainzValue {
#[structopt(help = "The name of the artist")]
artist: String,
#[structopt(help = "The MusicBrainz URL")]
url: String,
}
@ -105,8 +101,6 @@ enum PropertyCommand {
#[derive(StructOpt, Debug)]
struct PropertyValue {
#[structopt(help = "The name of the artist")]
artist: String,
#[structopt(help = "The name of the property")]
property: String,
#[structopt(help = "The list of values")]
@ -115,101 +109,176 @@ struct PropertyValue {
#[derive(StructOpt, Debug)]
struct PropertyName {
#[structopt(help = "The name of the artist")]
artist: String,
#[structopt(help = "The name of the property")]
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) {
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
.add_artist(ArtistId::new(artist_value.artist))
.add_artist(ArtistId::new(artist_name))
.expect("failed to add artist");
}
ArtistCommand::Remove(artist_value) => {
ArtistCommand::Remove => {
music_hoard
.remove_artist(ArtistId::new(artist_value.artist))
.remove_artist(ArtistId::new(artist_name))
.expect("failed to remove artist");
}
ArtistCommand::Sort(sort_command) => {
sort_command.handle(music_hoard);
sort_command.handle(music_hoard, artist_name);
}
ArtistCommand::MusicBrainz(musicbrainz_command) => {
musicbrainz_command.handle(music_hoard)
musicbrainz_command.handle(music_hoard, artist_name)
}
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 {
fn handle(self, music_hoard: &mut MH) {
fn handle(self, music_hoard: &mut MH, artist_name: &str) {
match self {
SortCommand::Set(artist_sort_value) => music_hoard
.set_artist_sort(
ArtistId::new(artist_sort_value.artist),
ArtistId::new(artist_name),
ArtistId::new(artist_sort_value.sort),
)
.expect("faild to set artist sort name"),
SortCommand::Clear(artist_value) => music_hoard
.clear_artist_sort(ArtistId::new(artist_value.artist))
SortCommand::Clear => music_hoard
.clear_artist_sort(ArtistId::new(artist_name))
.expect("failed to clear artist sort name"),
}
}
}
impl MusicBrainzCommand {
fn handle(self, music_hoard: &mut MH) {
fn handle(self, music_hoard: &mut MH, artist_name: &str) {
match self {
MusicBrainzCommand::Set(musicbrainz_value) => music_hoard
.set_musicbrainz_url(
ArtistId::new(musicbrainz_value.artist),
musicbrainz_value.url,
)
.set_artist_musicbrainz(ArtistId::new(artist_name), musicbrainz_value.url)
.expect("failed to set MusicBrainz URL"),
MusicBrainzCommand::Clear(artist_value) => music_hoard
.clear_musicbrainz_url(ArtistId::new(artist_value.artist))
MusicBrainzCommand::Clear => music_hoard
.clear_artist_musicbrainz(ArtistId::new(artist_name))
.expect("failed to clear MusicBrainz URL"),
}
}
}
impl PropertyCommand {
fn handle(self, music_hoard: &mut MH) {
fn handle(self, music_hoard: &mut MH, artist_name: &str) {
match self {
PropertyCommand::Add(property_value) => music_hoard
.add_to_property(
ArtistId::new(property_value.artist),
.add_to_artist_property(
ArtistId::new(artist_name),
property_value.property,
property_value.values,
)
.expect("failed to add values to property"),
PropertyCommand::Remove(property_value) => music_hoard
.remove_from_property(
ArtistId::new(property_value.artist),
.remove_from_artist_property(
ArtistId::new(artist_name),
property_value.property,
property_value.values,
)
.expect("failed to remove values from property"),
PropertyCommand::Set(property_value) => music_hoard
.set_property(
ArtistId::new(property_value.artist),
.set_artist_property(
ArtistId::new(artist_name),
property_value.property,
property_value.values,
)
.expect("failed to set property"),
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"),
}
}
}
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() {
let opt = Opt::from_args();
@ -219,5 +288,5 @@ fn main() {
.set_database(db)
.build()
.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::{
merge::{Merge, MergeSorted},
merge::{Merge, MergeSorted, WithId},
track::Track,
};
@ -9,19 +12,106 @@ use crate::core::collection::{
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Album {
pub id: AlbumId,
pub date: AlbumDate,
pub seq: AlbumSeq,
pub tracks: Vec<Track>,
}
impl WithId for Album {
type Id = AlbumId;
fn id(&self) -> &Self::Id {
&self.id
}
}
/// The album identifier.
#[derive(Clone, Debug, PartialEq, PartialOrd, Ord, Eq, Hash)]
pub struct AlbumId {
pub year: u32,
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 {
pub fn get_sort_key(&self) -> &AlbumId {
&self.id
pub fn get_sort_key(&self) -> (&AlbumDate, &AlbumSeq, &AlbumId) {
(&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 {
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 {
fn merge_in_place(&mut self, other: Self) {
assert_eq!(self.id, other.id);
self.seq = std::cmp::max(self.seq, other.seq);
let tracks = mem::take(&mut self.tracks);
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)]
mod tests {
use crate::core::testmod::FULL_COLLECTION;
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]
fn merge_album_no_overlap() {
let left = FULL_COLLECTION[0].albums[0].to_owned();
@ -64,9 +270,9 @@ mod tests {
let merged = left.clone().merge(right.clone());
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());
assert_eq!(expected, merged);
assert_eq!(expected.tracks, merged.tracks);
}
#[test]

View File

@ -2,6 +2,7 @@ use std::{
collections::HashMap,
fmt::{self, Debug, Display},
mem,
str::FromStr,
};
use url::Url;
@ -9,7 +10,7 @@ use uuid::Uuid;
use crate::core::collection::{
album::Album,
merge::{Merge, MergeSorted},
merge::{Merge, MergeCollections, WithId},
Error,
};
@ -23,6 +24,14 @@ pub struct Artist {
pub albums: Vec<Album>,
}
impl WithId for Artist {
type Id = ArtistId;
fn id(&self) -> &Self::Id {
&self.id
}
}
/// The artist identifier.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ArtistId {
@ -41,8 +50,8 @@ impl Artist {
}
}
pub fn get_sort_key(&self) -> &ArtistId {
self.sort.as_ref().unwrap_or(&self.id)
pub fn get_sort_key(&self) -> (&ArtistId,) {
(self.sort.as_ref().unwrap_or(&self.id),)
}
pub fn set_sort_key<SORT: Into<ArtistId>>(&mut self, sort: SORT) {
@ -114,18 +123,26 @@ impl PartialOrd for Artist {
impl Ord for Artist {
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 {
fn merge_in_place(&mut self, other: Self) {
assert_eq!(self.id, other.id);
self.sort = self.sort.take().or(other.sort);
self.musicbrainz = self.musicbrainz.take().or(other.musicbrainz);
self.properties.merge_in_place(other.properties);
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 {
/// 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())?;
Self::new_from_url(url)
}
/// Validate and wrap a MusicBrainz URL.
pub fn new_from_url(url: Url) -> Result<Self, Error> {
if !url
.domain()
.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: &str) -> Result<Self, Self::Error> {
MusicBrainz::new(value)
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;
fn try_from(value: Url) -> Result<Self, Self::Error> {
MusicBrainz::new_from_url(value)
}
}
@ -220,34 +266,35 @@ mod tests {
#[test]
fn musicbrainz() {
let uuid = "d368baa8-21ca-4759-9731-0b2753071ad8";
let url = format!("https://musicbrainz.org/artist/{uuid}");
let mb = MusicBrainz::new(&url).unwrap();
assert_eq!(url, mb.as_ref());
let url_str = format!("https://musicbrainz.org/artist/{uuid}");
let url: Url = url_str.as_str().try_into().unwrap();
let mb: MusicBrainz = url.try_into().unwrap();
assert_eq!(url_str, mb.as_ref());
assert_eq!(uuid, mb.mbid());
let url = "not a url at all".to_string();
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.to_string(), expected_error.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 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.to_string(), expected_error.to_string());
let url = "https://musicbrainz.org/artist".to_string();
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.to_string(), expected_error.to_string());
}
#[test]
fn urls() {
assert!(MusicBrainz::new(MUSICBRAINZ).is_ok());
assert!(MusicBrainz::new(MUSICBUTLER).is_err());
assert!(MusicBrainz::from_str(MUSICBRAINZ).is_ok());
assert!(MusicBrainz::from_str(MUSICBUTLER).is_err());
}
#[test]
@ -256,11 +303,11 @@ mod tests {
let sort_id_1 = ArtistId::new("sort id 1");
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.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_2.clone()));
@ -268,7 +315,7 @@ mod tests {
assert_eq!(artist.id, artist_id);
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(sort_id_2.clone()));
@ -276,7 +323,7 @@ mod tests {
assert_eq!(artist.id, artist_id);
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(sort_id_1.clone()));
@ -284,7 +331,7 @@ mod tests {
assert_eq!(artist.id, artist_id);
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_2.clone()));
}
@ -307,14 +354,14 @@ mod tests {
// Setting a URL on an artist.
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);
artist.set_musicbrainz_url(MUSICBRAINZ.try_into().unwrap());
assert_eq!(artist.musicbrainz, expected);
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);
// 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
/// 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;
mod merge;
pub use merge::Merge;
pub use merge::MergeCollections;
use std::fmt::{self, Display};

View File

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

View File

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

View File

@ -1,5 +1,5 @@
pub static DATABASE_JSON: &str = "{\
\"V20240210\":\
\"V20240302\":\
[\
{\
\"name\":\"Album_Artist A\",\
@ -8,7 +8,11 @@ pub static DATABASE_JSON: &str = "{\
\"properties\":{\
\"MusicButler\":[\"https://www.musicbutler.io/artist-page/000000000\"],\
\"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\",\
@ -21,19 +25,33 @@ pub static DATABASE_JSON: &str = "{\
\"https://www.musicbutler.io/artist-page/111111112\"\
],\
\"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\",\
\"sort\":\"Album_Artist C, The\",\
\"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\",\
\"sort\":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 crate::{
collection::artist::{ArtistId, MusicBrainz},
core::{
collection::{artist::Artist, Collection},
database::{serde::Database, LoadError},
use crate::core::{
collection::{
album::{Album, AlbumDate, AlbumId, AlbumSeq},
artist::{Artist, ArtistId},
Collection,
},
database::LoadError,
};
pub type DeserializeDatabase = Database<DeserializeArtist>;
#[derive(Debug, Deserialize)]
pub enum DeserializeDatabase {
V20240302(Vec<DeserializeArtist>),
}
#[derive(Debug, Deserialize)]
pub struct DeserializeArtist {
@ -18,6 +22,13 @@ pub struct DeserializeArtist {
sort: Option<String>,
musicbrainz: Option<String>,
properties: HashMap<String, Vec<String>>,
albums: Vec<DeserializeAlbum>,
}
#[derive(Debug, Deserialize)]
pub struct DeserializeAlbum {
title: String,
seq: u8,
}
impl TryFrom<DeserializeDatabase> for Collection {
@ -25,7 +36,7 @@ impl TryFrom<DeserializeDatabase> for Collection {
fn try_from(database: DeserializeDatabase) -> Result<Self, Self::Error> {
match database {
Database::V20240210(collection) => collection
DeserializeDatabase::V20240302(collection) => collection
.into_iter()
.map(|artist| artist.try_into())
.collect(),
@ -40,9 +51,20 @@ impl TryFrom<DeserializeArtist> for Artist {
Ok(Artist {
id: ArtistId::new(artist.name),
sort: artist.sort.map(ArtistId::new),
musicbrainz: artist.musicbrainz.map(MusicBrainz::new).transpose()?,
musicbrainz: artist.musicbrainz.map(TryInto::try_into).transpose()?,
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 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 crate::core::{
collection::{artist::Artist, Collection},
database::serde::Database,
};
use crate::core::collection::{album::Album, artist::Artist, Collection};
pub type SerializeDatabase<'a> = Database<SerializeArtist<'a>>;
#[derive(Debug, Serialize)]
pub enum SerializeDatabase<'a> {
V20240302(Vec<SerializeArtist<'a>>),
}
#[derive(Debug, Serialize)]
pub struct SerializeArtist<'a> {
@ -15,11 +15,18 @@ pub struct SerializeArtist<'a> {
sort: Option<&'a str>,
musicbrainz: Option<&'a str>,
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> {
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()
.map(|(k, v)| (k.as_ref(), v))
.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 crate::core::{
collection::track::Format,
collection::track::TrackFormat,
library::{Error, Field, ILibrary, Item, Query},
};
@ -27,6 +27,10 @@ const LIST_FORMAT_ARG: &str = concat!(
list_format_separator!(),
"$year",
list_format_separator!(),
"$month",
list_format_separator!(),
"$day",
list_format_separator!(),
"$album",
list_format_separator!(),
"$track",
@ -42,6 +46,21 @@ const LIST_FORMAT_ARG: &str = concat!(
const TRACK_FORMAT_FLAC: &str = "FLAC";
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 {
fn to_arg(&self, include: bool) -> String;
}
@ -57,10 +76,13 @@ impl ToBeetsArg for Field {
Field::AlbumArtist(ref s) => format!("{negate}albumartist:{s}"),
Field::AlbumArtistSort(ref s) => format!("{negate}albumartist_sort:{s}"),
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::TrackNumber(ref u) => format!("{negate}track:{u}"),
Field::TrackTitle(ref s) => format!("{negate}title:{s}"),
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}"),
}
}
@ -127,36 +149,38 @@ impl<BLE: IBeetsLibraryExecutor> ILibraryPrivate for BeetsLibrary<BLE> {
}
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()));
}
let album_artist = split[0].to_string();
let album_artist_sort = if !split[1].is_empty() {
Some(split[1].to_string())
} else {
None
let album_artist_sort = match !split[1].is_empty() {
true => Some(split[1].to_string()),
false => None,
};
let album_year = split[2].parse::<u32>()?;
let album_title = split[3].to_string();
let track_number = split[4].parse::<u32>()?;
let track_title = split[5].to_string();
let track_artist = split[6]
let album_month = split[3].parse::<u8>()?.into();
let album_day = split[4].parse::<u8>()?;
let album_title = split[5].to_string();
let track_number = split[6].parse::<u32>()?;
let track_title = split[7].to_string();
let track_artist = split[8]
.to_string()
.split("; ")
.map(|s| s.to_owned())
.collect();
let track_format = match split[7].to_string().as_str() {
TRACK_FORMAT_FLAC => Format::Flac,
TRACK_FORMAT_MP3 => Format::Mp3,
_ => return Err(Error::Invalid(line.to_string())),
let track_format = match str_to_format(split[9].to_string().as_str()) {
Some(format) => format,
None => 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 {
album_artist,
album_artist_sort,
album_year,
album_month,
album_day,
album_title,
track_number,
track_title,
@ -177,7 +201,7 @@ mod testmod;
mod tests {
use mockall::predicate;
use crate::core::library::testmod::LIBRARY_ITEMS;
use crate::{collection::album::AlbumMonth, core::library::testmod::LIBRARY_ITEMS};
use super::*;
use testmod::LIBRARY_BEETS;
@ -191,6 +215,7 @@ mod tests {
String::from("some.artist.1"),
String::from("some.artist.2"),
]))
.exclude(Field::TrackFormat(TrackFormat::Mp3))
.exclude(Field::All(String::from("some.all")))
.to_args();
query.sort();
@ -199,6 +224,7 @@ mod tests {
query,
vec![
String::from("^album:some.album"),
String::from("^format:MP3"),
String::from("^some.all"),
String::from("artist:some.artist.1; some.artist.2"),
String::from("track:5"),
@ -209,7 +235,10 @@ mod tests {
.exclude(Field::AlbumArtist(String::from("some.albumartist")))
.exclude(Field::AlbumArtistSort(String::from("some.albumartist")))
.include(Field::AlbumYear(3030))
.include(Field::AlbumMonth(AlbumMonth::April))
.include(Field::AlbumDay(6))
.include(Field::TrackTitle(String::from("some.track")))
.include(Field::TrackFormat(TrackFormat::Flac))
.exclude(Field::TrackArtist(vec![
String::from("some.artist.1"),
String::from("some.artist.2"),
@ -223,6 +252,9 @@ mod tests {
String::from("^albumartist:some.albumartist"),
String::from("^albumartist_sort:some.albumartist"),
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("year:3030"),
]
@ -335,8 +367,8 @@ mod tests {
.split(LIST_FORMAT_SEPARATOR)
.map(|s| s.to_owned())
.collect::<Vec<String>>();
invalid_string[7].clear();
invalid_string[7].push_str("invalid format");
invalid_string[9].clear();
invalid_string[9].push_str("invalid format");
let invalid_string = invalid_string.join(LIST_FORMAT_SEPARATOR);
output[2] = invalid_string.clone();
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> {
vec![
String::from("Album_Artist A -*^- -*^- 1998 -*^- album_title a.a -*^- 1 -*^- track a.a.1 -*^- artist a.a.1 -*^- FLAC -*^- 992"),
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"),
String::from("Album_Artist A -*^- -*^- 1998 -*^- album_title a.a -*^- 3 -*^- track a.a.3 -*^- artist a.a.3 -*^- FLAC -*^- 1061"),
String::from("Album_Artist A -*^- -*^- 1998 -*^- album_title a.a -*^- 4 -*^- track a.a.4 -*^- artist a.a.4 -*^- FLAC -*^- 1042"),
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("Album_Artist B -*^- -*^- 2003 -*^- album_title b.a -*^- 1 -*^- track b.a.1 -*^- artist b.a.1 -*^- MP3 -*^- 190"),
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"),
String::from("Album_Artist B -*^- -*^- 2008 -*^- album_title b.b -*^- 1 -*^- track b.b.1 -*^- artist b.b.1 -*^- FLAC -*^- 1077"),
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("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"),
String::from("Album_Artist B -*^- -*^- 2015 -*^- album_title b.d -*^- 1 -*^- track b.d.1 -*^- artist b.d.1 -*^- MP3 -*^- 190"),
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"),
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("The Album_Artist C -*^- Album_Artist C, The -*^- 2018 -*^- 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 -*^- 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 -*^- album_title d.a -*^- 1 -*^- track d.a.1 -*^- artist d.a.1 -*^- MP3 -*^- 120"),
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("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")
String::from(
"Album_Artist A -*^- -*^- \
1998 -*^- 00 -*^- 00 -*^- album_title a.a -*^- \
1 -*^- track a.a.1 -*^- artist a.a.1 -*^- FLAC -*^- 992",
),
String::from(
"Album_Artist A -*^- -*^- \
1998 -*^- 00 -*^- 00 -*^- album_title a.a -*^- \
2 -*^- track a.a.2 -*^- artist a.a.2.1; artist a.a.2.2 -*^- MP3 -*^- 320",
),
String::from(
"Album_Artist A -*^- -*^- \
1998 -*^- 00 -*^- 00 -*^- album_title a.a -*^- \
3 -*^- track a.a.3 -*^- artist a.a.3 -*^- FLAC -*^- 1061",
),
String::from(
"Album_Artist A -*^- -*^- \
1998 -*^- 00 -*^- 00 -*^- album_title a.a -*^- \
4 -*^- track a.a.4 -*^- artist a.a.4 -*^- FLAC -*^- 1042",
),
String::from(
"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)]
use mockall::automock;
use crate::core::collection::track::Format;
use crate::core::collection::{album::AlbumMonth, track::TrackFormat};
/// Trait for interacting with the music library.
#[cfg_attr(test, automock)]
@ -32,11 +32,13 @@ pub struct Item {
pub album_artist: String,
pub album_artist_sort: Option<String>,
pub album_year: u32,
pub album_month: AlbumMonth,
pub album_day: u8,
pub album_title: String,
pub track_number: u32,
pub track_title: String,
pub track_artist: Vec<String>,
pub track_format: Format,
pub track_format: TrackFormat,
pub track_bitrate: u32,
}
@ -46,10 +48,13 @@ pub enum Field {
AlbumArtist(String),
AlbumArtistSort(String),
AlbumYear(u32),
AlbumMonth(AlbumMonth),
AlbumDay(u8),
AlbumTitle(String),
TrackNumber(u32),
TrackTitle(String),
TrackArtist(Vec<String>),
TrackFormat(TrackFormat),
All(String),
}

View File

@ -1,6 +1,9 @@
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> {
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_sort: None,
album_year: 1998,
album_month: AlbumMonth::None,
album_day: 0,
album_title: String::from("album_title a.a"),
track_number: 1,
track_title: String::from("track a.a.1"),
track_artist: vec![String::from("artist a.a.1")],
track_format: Format::Flac,
track_format: TrackFormat::Flac,
track_bitrate: 992,
},
Item {
album_artist: String::from("Album_Artist A"),
album_artist_sort: None,
album_year: 1998,
album_month: AlbumMonth::None,
album_day: 0,
album_title: String::from("album_title a.a"),
track_number: 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.2"),
],
track_format: Format::Mp3,
track_format: TrackFormat::Mp3,
track_bitrate: 320,
},
Item {
album_artist: String::from("Album_Artist A"),
album_artist_sort: None,
album_year: 1998,
album_month: AlbumMonth::None,
album_day: 0,
album_title: String::from("album_title a.a"),
track_number: 3,
track_title: String::from("track a.a.3"),
track_artist: vec![String::from("artist a.a.3")],
track_format: Format::Flac,
track_format: TrackFormat::Flac,
track_bitrate: 1061,
},
Item {
album_artist: String::from("Album_Artist A"),
album_artist_sort: None,
album_year: 1998,
album_month: AlbumMonth::None,
album_day: 0,
album_title: String::from("album_title a.a"),
track_number: 4,
track_title: String::from("track a.a.4"),
track_artist: vec![String::from("artist a.a.4")],
track_format: Format::Flac,
track_format: TrackFormat::Flac,
track_bitrate: 1042,
},
Item {
album_artist: String::from("Album_Artist A"),
album_artist_sort: None,
album_year: 2015,
album_month: AlbumMonth::April,
album_day: 0,
album_title: String::from("album_title a.b"),
track_number: 1,
track_title: String::from("track a.b.1"),
track_artist: vec![String::from("artist a.b.1")],
track_format: Format::Flac,
track_format: TrackFormat::Flac,
track_bitrate: 1004,
},
Item {
album_artist: String::from("Album_Artist A"),
album_artist_sort: None,
album_year: 2015,
album_month: AlbumMonth::April,
album_day: 0,
album_title: String::from("album_title a.b"),
track_number: 2,
track_title: String::from("track a.b.2"),
track_artist: vec![String::from("artist a.b.2")],
track_format: Format::Flac,
track_format: TrackFormat::Flac,
track_bitrate: 1077,
},
Item {
album_artist: String::from("Album_Artist B"),
album_artist_sort: None,
album_year: 2003,
album_month: AlbumMonth::June,
album_day: 6,
album_title: String::from("album_title b.a"),
track_number: 1,
track_title: String::from("track b.a.1"),
track_artist: vec![String::from("artist b.a.1")],
track_format: Format::Mp3,
track_format: TrackFormat::Mp3,
track_bitrate: 190,
},
Item {
album_artist: String::from("Album_Artist B"),
album_artist_sort: None,
album_year: 2003,
album_month: AlbumMonth::June,
album_day: 6,
album_title: String::from("album_title b.a"),
track_number: 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.2"),
],
track_format: Format::Mp3,
track_format: TrackFormat::Mp3,
track_bitrate: 120,
},
Item {
album_artist: String::from("Album_Artist B"),
album_artist_sort: None,
album_year: 2008,
album_month: AlbumMonth::None,
album_day: 0,
album_title: String::from("album_title b.b"),
track_number: 1,
track_title: String::from("track b.b.1"),
track_artist: vec![String::from("artist b.b.1")],
track_format: Format::Flac,
track_format: TrackFormat::Flac,
track_bitrate: 1077,
},
Item {
album_artist: String::from("Album_Artist B"),
album_artist_sort: None,
album_year: 2008,
album_month: AlbumMonth::None,
album_day: 0,
album_title: String::from("album_title b.b"),
track_number: 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.2"),
],
track_format: Format::Mp3,
track_format: TrackFormat::Mp3,
track_bitrate: 320,
},
Item {
album_artist: String::from("Album_Artist B"),
album_artist_sort: None,
album_year: 2009,
album_month: AlbumMonth::None,
album_day: 0,
album_title: String::from("album_title b.c"),
track_number: 1,
track_title: String::from("track b.c.1"),
track_artist: vec![String::from("artist b.c.1")],
track_format: Format::Mp3,
track_format: TrackFormat::Mp3,
track_bitrate: 190,
},
Item {
album_artist: String::from("Album_Artist B"),
album_artist_sort: None,
album_year: 2009,
album_month: AlbumMonth::None,
album_day: 0,
album_title: String::from("album_title b.c"),
track_number: 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.2"),
],
track_format: Format::Mp3,
track_format: TrackFormat::Mp3,
track_bitrate: 120,
},
Item {
album_artist: String::from("Album_Artist B"),
album_artist_sort: None,
album_year: 2015,
album_month: AlbumMonth::None,
album_day: 0,
album_title: String::from("album_title b.d"),
track_number: 1,
track_title: String::from("track b.d.1"),
track_artist: vec![String::from("artist b.d.1")],
track_format: Format::Mp3,
track_format: TrackFormat::Mp3,
track_bitrate: 190,
},
Item {
album_artist: String::from("Album_Artist B"),
album_artist_sort: None,
album_year: 2015,
album_month: AlbumMonth::None,
album_day: 0,
album_title: String::from("album_title b.d"),
track_number: 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.2"),
],
track_format: Format::Mp3,
track_format: TrackFormat::Mp3,
track_bitrate: 120,
},
Item {
album_artist: String::from("The Album_Artist C"),
album_artist_sort: Some(String::from("Album_Artist C, The")),
album_year: 1985,
album_month: AlbumMonth::None,
album_day: 0,
album_title: String::from("album_title c.a"),
track_number: 1,
track_title: String::from("track c.a.1"),
track_artist: vec![String::from("artist c.a.1")],
track_format: Format::Mp3,
track_format: TrackFormat::Mp3,
track_bitrate: 320,
},
Item {
album_artist: String::from("The Album_Artist C"),
album_artist_sort: Some(String::from("Album_Artist C, The")),
album_year: 1985,
album_month: AlbumMonth::None,
album_day: 0,
album_title: String::from("album_title c.a"),
track_number: 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.2"),
],
track_format: Format::Mp3,
track_format: TrackFormat::Mp3,
track_bitrate: 120,
},
Item {
album_artist: String::from("The Album_Artist C"),
album_artist_sort: Some(String::from("Album_Artist C, The")),
album_year: 2018,
album_month: AlbumMonth::None,
album_day: 0,
album_title: String::from("album_title c.b"),
track_number: 1,
track_title: String::from("track c.b.1"),
track_artist: vec![String::from("artist c.b.1")],
track_format: Format::Flac,
track_format: TrackFormat::Flac,
track_bitrate: 1041,
},
Item {
album_artist: String::from("The Album_Artist C"),
album_artist_sort: Some(String::from("Album_Artist C, The")),
album_year: 2018,
album_month: AlbumMonth::None,
album_day: 0,
album_title: String::from("album_title c.b"),
track_number: 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.2"),
],
track_format: Format::Flac,
track_format: TrackFormat::Flac,
track_bitrate: 756,
},
Item {
album_artist: String::from("Album_Artist D"),
album_artist_sort: None,
album_year: 1995,
album_month: AlbumMonth::None,
album_day: 0,
album_title: String::from("album_title d.a"),
track_number: 1,
track_title: String::from("track d.a.1"),
track_artist: vec![String::from("artist d.a.1")],
track_format: Format::Mp3,
track_format: TrackFormat::Mp3,
track_bitrate: 120,
},
Item {
album_artist: String::from("Album_Artist D"),
album_artist_sort: None,
album_year: 1995,
album_month: AlbumMonth::None,
album_day: 0,
album_title: String::from("album_title d.a"),
track_number: 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.2"),
],
track_format: Format::Mp3,
track_format: TrackFormat::Mp3,
track_bitrate: 120,
},
Item {
album_artist: String::from("Album_Artist D"),
album_artist_sort: None,
album_year: 2028,
album_month: AlbumMonth::None,
album_day: 0,
album_title: String::from("album_title d.b"),
track_number: 1,
track_title: String::from("track d.b.1"),
track_artist: vec![String::from("artist d.b.1")],
track_format: Format::Flac,
track_format: TrackFormat::Flac,
track_bitrate: 841,
},
Item {
album_artist: String::from("Album_Artist D"),
album_artist_sort: None,
album_year: 2028,
album_month: AlbumMonth::None,
album_day: 0,
album_title: String::from("album_title d.b"),
track_number: 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.2"),
],
track_format: Format::Flac,
track_format: TrackFormat::Flac,
track_bitrate: 756,
},
]

View File

@ -2,10 +2,10 @@ use std::collections::HashMap;
use crate::core::{
collection::{
album::{Album, AlbumId},
artist::{Artist, ArtistId},
track::{Quality, Track, TrackId},
Collection, Merge,
album::{Album, AlbumDate, AlbumId, AlbumSeq},
artist::{Artist, ArtistId, MusicBrainz},
track::{Track, TrackId, TrackNum, TrackQuality},
Collection, MergeCollections,
},
database::IDatabase,
library::{ILibrary, Item, Query},
@ -73,19 +73,7 @@ impl<LIB, DB> MusicHoard<LIB, DB> {
}
fn merge_collections(&self) -> Collection {
let mut primary = self.library_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
MergeCollections::merge(self.library_cache.clone(), self.database_cache.clone())
}
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 album_id = AlbumId {
year: item.album_year,
title: item.album_title,
};
let album_date = AlbumDate {
year: item.album_year,
month: item.album_month,
day: item.album_day,
};
let track = Track {
id: TrackId {
number: item.track_number,
title: item.track_title,
},
number: TrackNum(item.track_number),
artist: item.track_artist,
quality: Quality {
quality: TrackQuality {
format: item.track_format,
bitrate: item.track_bitrate,
},
@ -149,6 +142,8 @@ impl<LIB, DB> MusicHoard<LIB, DB> {
Some(album) => album.tracks.push(track),
None => artist.albums.push(Album {
id: album_id,
date: album_date,
seq: AlbumSeq(0),
tracks: vec![track],
}),
}
@ -176,6 +171,22 @@ impl<LIB, DB> MusicHoard<LIB, DB> {
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> {
@ -248,39 +259,61 @@ impl<LIB, DB: IDatabase> MusicHoard<LIB, DB> {
Ok(())
}
fn update_collection<F>(&mut self, func: F) -> Result<(), Error>
fn update_collection<FnColl>(&mut self, fn_coll: FnColl) -> Result<(), Error>
where
F: FnOnce(&mut Collection),
FnColl: FnOnce(&mut Collection),
{
func(&mut self.pre_commit);
fn_coll(&mut self.pre_commit);
self.commit()
}
fn update_artist_and<ID: AsRef<ArtistId>, F1, F2>(
fn update_artist_and<FnArtist, FnColl>(
&mut self,
artist_id: ID,
f1: F1,
f2: F2,
artist_id: &ArtistId,
fn_artist: FnArtist,
fn_coll: FnColl,
) -> Result<(), Error>
where
F1: FnOnce(&mut Artist),
F2: FnOnce(&mut Collection),
FnArtist: FnOnce(&mut Artist),
FnColl: FnOnce(&mut Collection),
{
f1(Self::get_artist_mut_or_err(
&mut self.pre_commit,
artist_id.as_ref(),
)?);
self.update_collection(f2)
let artist = Self::get_artist_mut_or_err(&mut self.pre_commit, artist_id)?;
fn_artist(artist);
self.update_collection(fn_coll)
}
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
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();
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| {
let index_opt = collection.iter().position(|a| &a.id == artist_id.as_ref());
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,
artist_id: ID,
artist_sort: SORT,
artist_id: Id,
artist_sort: IntoId,
) -> Result<(), Error> {
self.update_artist_and(
artist_id,
artist_id.as_ref(),
|artist| artist.set_sort_key(artist_sort),
|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(
artist_id,
artist_id.as_ref(),
|artist| artist.clear_sort_key(),
|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,
artist_id: ID,
url: S,
) -> Result<(), Error> {
let url = url.as_ref().try_into()?;
self.update_artist(artist_id, |artist| artist.set_musicbrainz_url(url))
artist_id: Id,
url: Mb,
) -> Result<(), Error>
where
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,
artist_id: ID,
artist_id: Id,
) -> 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,
artist_id: ID,
artist_id: Id,
property: S,
values: Vec<S>,
) -> 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,
artist_id: ID,
artist_id: Id,
property: S,
values: Vec<S>,
) -> Result<(), Error> {
self.update_artist(artist_id, |artist| {
self.update_artist(artist_id.as_ref(), |artist| {
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,
artist_id: ID,
artist_id: Id,
property: S,
values: Vec<S>,
) -> 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,
artist_id: ID,
artist_id: Id,
property: S,
) -> 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());
let actual_err = music_hoard
.set_musicbrainz_url(&artist_id, MUSICBUTLER)
.set_artist_musicbrainz(&artist_id, MUSICBUTLER)
.unwrap_err();
let expected_err = Error::CollectionError(format!(
"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.
assert!(music_hoard
.set_musicbrainz_url(&artist_id_2, MUSICBRAINZ)
.set_artist_musicbrainz(&artist_id_2, MUSICBRAINZ)
.is_err());
assert_eq!(music_hoard.collection[0].musicbrainz, expected);
// Setting a URL on an artist.
assert!(music_hoard
.set_musicbrainz_url(&artist_id, MUSICBRAINZ)
.set_artist_musicbrainz(&artist_id, MUSICBRAINZ)
.is_ok());
_ = expected.insert(MusicBrainz::new(MUSICBRAINZ).unwrap());
_ = expected.insert(MUSICBRAINZ.try_into().unwrap());
assert_eq!(music_hoard.collection[0].musicbrainz, expected);
// 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);
// Clearing URLs.
assert!(music_hoard.clear_musicbrainz_url(&artist_id).is_ok());
assert!(music_hoard.clear_artist_musicbrainz(&artist_id).is_ok());
_ = expected.take();
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.
assert!(music_hoard
.add_to_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
.add_to_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
.is_err());
assert!(music_hoard.collection[0].properties.is_empty());
// Adding mutliple URLs without clashes.
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());
expected.push(MUSICBUTLER.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.
assert!(music_hoard
.remove_from_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
.remove_from_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
.is_err());
assert_eq!(
music_hoard.collection[0].properties.get("MusicButler"),
@ -613,7 +682,11 @@ mod tests {
// Removing multiple URLs without clashes.
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());
expected.clear();
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.
assert!(music_hoard
.set_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
.set_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
.is_err());
assert!(music_hoard.collection[0].properties.is_empty());
// Set URLs.
assert!(music_hoard
.set_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2])
.set_artist_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2])
.is_ok());
expected.clear();
expected.push(MUSICBUTLER.to_owned());
@ -654,17 +727,62 @@ mod tests {
// Clearing URLs on an artist that does not exist is an error.
assert!(music_hoard
.clear_property(&artist_id_2, "MusicButler")
.clear_artist_property(&artist_id_2, "MusicButler")
.is_err());
// Clear URLs.
assert!(music_hoard
.clear_property(&artist_id, "MusicButler")
.clear_artist_property(&artist_id, "MusicButler")
.is_ok());
expected.clear();
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]
fn merge_collection_no_overlap() {
let half: usize = FULL_COLLECTION.len() / 2;
@ -887,7 +1005,7 @@ mod tests {
}
#[test]
fn rescan_library_album_title_year_clash() {
fn rescan_library_album_id_clash() {
let mut library = MockILibrary::new();
let mut expected = LIBRARY_COLLECTION.to_owned();
@ -895,10 +1013,10 @@ mod tests {
let clashed_album_id = &expected[1].albums[0].id;
let mut items = LIBRARY_ITEMS.to_owned();
for item in items.iter_mut().filter(|it| {
(it.album_year == removed_album_id.year) && (it.album_title == removed_album_id.title)
}) {
item.album_year = clashed_album_id.year;
for item in items
.iter_mut()
.filter(|it| it.album_title == removed_album_id.title)
{
item.album_title = clashed_album_id.title.clone();
}
@ -916,6 +1034,7 @@ mod tests {
let mut music_hoard = MusicHoard::library(library);
music_hoard.rescan_library().unwrap();
assert_eq!(music_hoard.get_collection()[0], expected[0]);
assert_eq!(music_hoard.get_collection(), &expected);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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