Sort albums by month if two releases of the same artist happen in the same year (#155)
Closes #106 Reviewed-on: #155
This commit is contained in:
parent
4dc56f66c6
commit
c015f4c112
@ -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);
|
||||
}
|
||||
|
@ -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(), "1990‐05");
|
||||
assert_eq!(AlbumDate::new(1990, 5, 6).to_string(), "1990‐05‐06");
|
||||
}
|
||||
|
||||
#[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]
|
||||
|
@ -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: $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: &str) -> Result<Self, Self::Error> {
|
||||
MusicBrainz::new(value)
|
||||
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.
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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};
|
||||
|
||||
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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}\
|
||||
]\
|
||||
}\
|
||||
]\
|
||||
}";
|
||||
|
@ -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![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>),
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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",
|
||||
),
|
||||
]
|
||||
});
|
||||
|
@ -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),
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
},
|
||||
]
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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::*;
|
||||
|
||||
|
235
src/tests.rs
235
src/tests.rs
@ -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(
|
||||
"https://musicbrainz.org/artist/00000000-0000-0000-0000-000000000000",
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
artist_a.musicbrainz = Some(MusicBrainz::from_str(
|
||||
"https://musicbrainz.org/artist/00000000-0000-0000-0000-000000000000",
|
||||
).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(
|
||||
"https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111",
|
||||
).unwrap(),
|
||||
);
|
||||
artist_b.musicbrainz = Some(MusicBrainz::from_str(
|
||||
"https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111",
|
||||
).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(
|
||||
"https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111",
|
||||
).unwrap(),
|
||||
);
|
||||
artist_c.musicbrainz = Some(MusicBrainz::from_str(
|
||||
"https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111",
|
||||
).unwrap());
|
||||
|
||||
// Nothing for artist_d
|
||||
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -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::*;
|
||||
|
||||
|
@ -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(),
|
||||
));
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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":"Heaven’s Basement","sort":"Heaven’s 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 [re‐recorded]","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":"Heaven’s Basement","sort":"Heaven’s 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
554
tests/testlib.rs
554
tests/testlib.rs
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user