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 structopt::{clap::AppSettings, StructOpt};
|
||||||
|
|
||||||
use musichoard::{
|
use musichoard::{
|
||||||
collection::artist::ArtistId,
|
collection::{album::AlbumId, artist::ArtistId},
|
||||||
database::json::{backend::JsonDatabaseFileBackend, JsonDatabase},
|
database::json::{backend::JsonDatabaseFileBackend, JsonDatabase},
|
||||||
MusicHoard, MusicHoardBuilder, NoLibrary,
|
MusicHoard, MusicHoardBuilder, NoLibrary,
|
||||||
};
|
};
|
||||||
@ -22,56 +22,54 @@ struct Opt {
|
|||||||
database_file_path: PathBuf,
|
database_file_path: PathBuf,
|
||||||
|
|
||||||
#[structopt(subcommand)]
|
#[structopt(subcommand)]
|
||||||
category: Category,
|
command: Command,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(StructOpt, Debug)]
|
#[derive(StructOpt, Debug)]
|
||||||
enum Category {
|
enum Command {
|
||||||
#[structopt(about = "Edit artist information")]
|
#[structopt(about = "Modify artist information")]
|
||||||
Artist(ArtistCommand),
|
Artist(ArtistOpt),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Category {
|
#[derive(StructOpt, Debug)]
|
||||||
fn handle(self, music_hoard: &mut MH) {
|
struct ArtistOpt {
|
||||||
match self {
|
// For some reason, not specyfing the artist name with the `long` name makes StructOpt failed
|
||||||
Category::Artist(artist_command) => artist_command.handle(music_hoard),
|
// for inexplicable reason. For example, it won't recognise `Abadde` or `Abadden` as a name and
|
||||||
}
|
// will insteady try to process it as a command.
|
||||||
}
|
#[structopt(long, help = "The name of the artist")]
|
||||||
|
name: String,
|
||||||
|
|
||||||
|
#[structopt(subcommand)]
|
||||||
|
command: ArtistCommand,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(StructOpt, Debug)]
|
#[derive(StructOpt, Debug)]
|
||||||
enum ArtistCommand {
|
enum ArtistCommand {
|
||||||
#[structopt(about = "Add a new artist to the collection")]
|
#[structopt(about = "Add a new artist to the collection")]
|
||||||
Add(ArtistValue),
|
Add,
|
||||||
#[structopt(about = "Remove an artist from the collection")]
|
#[structopt(about = "Remove an artist from the collection")]
|
||||||
Remove(ArtistValue),
|
Remove,
|
||||||
#[structopt(about = "Edit the artist's sort name")]
|
#[structopt(about = "Edit the artist's sort name")]
|
||||||
Sort(SortCommand),
|
Sort(SortCommand),
|
||||||
#[structopt(name = "musicbrainz", about = "Edit the MusicBrainz URL of an artist")]
|
#[structopt(name = "musicbrainz", about = "Edit the MusicBrainz URL of an artist")]
|
||||||
MusicBrainz(MusicBrainzCommand),
|
MusicBrainz(MusicBrainzCommand),
|
||||||
#[structopt(name = "property", about = "Edit a property of an artist")]
|
#[structopt(about = "Edit a property of an artist")]
|
||||||
Property(PropertyCommand),
|
Property(PropertyCommand),
|
||||||
|
#[structopt(about = "Modify the artist's album information")]
|
||||||
|
Album(AlbumOpt),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(StructOpt, Debug)]
|
#[derive(StructOpt, Debug)]
|
||||||
enum SortCommand {
|
enum SortCommand {
|
||||||
#[structopt(about = "Set the provided name as the artist's sort name")]
|
#[structopt(about = "Set the provided name as the artist's sort name")]
|
||||||
Set(ArtistSortValue),
|
Set(SortValue),
|
||||||
#[structopt(about = "Clear the artist's sort name")]
|
#[structopt(about = "Clear the artist's sort name")]
|
||||||
Clear(ArtistValue),
|
Clear,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(StructOpt, Debug)]
|
#[derive(StructOpt, Debug)]
|
||||||
struct ArtistValue {
|
struct SortValue {
|
||||||
#[structopt(help = "The name of the artist")]
|
#[structopt(help = "The sort name")]
|
||||||
artist: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(StructOpt, Debug)]
|
|
||||||
struct ArtistSortValue {
|
|
||||||
#[structopt(help = "The name of the artist")]
|
|
||||||
artist: String,
|
|
||||||
#[structopt(help = "The sort name of the artist")]
|
|
||||||
sort: String,
|
sort: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,13 +78,11 @@ enum MusicBrainzCommand {
|
|||||||
#[structopt(about = "Set the MusicBrainz URL overwriting any existing value")]
|
#[structopt(about = "Set the MusicBrainz URL overwriting any existing value")]
|
||||||
Set(MusicBrainzValue),
|
Set(MusicBrainzValue),
|
||||||
#[structopt(about = "Clear the MusicBrainz URL)")]
|
#[structopt(about = "Clear the MusicBrainz URL)")]
|
||||||
Clear(ArtistValue),
|
Clear,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(StructOpt, Debug)]
|
#[derive(StructOpt, Debug)]
|
||||||
struct MusicBrainzValue {
|
struct MusicBrainzValue {
|
||||||
#[structopt(help = "The name of the artist")]
|
|
||||||
artist: String,
|
|
||||||
#[structopt(help = "The MusicBrainz URL")]
|
#[structopt(help = "The MusicBrainz URL")]
|
||||||
url: String,
|
url: String,
|
||||||
}
|
}
|
||||||
@ -105,8 +101,6 @@ enum PropertyCommand {
|
|||||||
|
|
||||||
#[derive(StructOpt, Debug)]
|
#[derive(StructOpt, Debug)]
|
||||||
struct PropertyValue {
|
struct PropertyValue {
|
||||||
#[structopt(help = "The name of the artist")]
|
|
||||||
artist: String,
|
|
||||||
#[structopt(help = "The name of the property")]
|
#[structopt(help = "The name of the property")]
|
||||||
property: String,
|
property: String,
|
||||||
#[structopt(help = "The list of values")]
|
#[structopt(help = "The list of values")]
|
||||||
@ -115,101 +109,176 @@ struct PropertyValue {
|
|||||||
|
|
||||||
#[derive(StructOpt, Debug)]
|
#[derive(StructOpt, Debug)]
|
||||||
struct PropertyName {
|
struct PropertyName {
|
||||||
#[structopt(help = "The name of the artist")]
|
|
||||||
artist: String,
|
|
||||||
#[structopt(help = "The name of the property")]
|
#[structopt(help = "The name of the property")]
|
||||||
property: String,
|
property: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ArtistCommand {
|
#[derive(StructOpt, Debug)]
|
||||||
|
struct AlbumOpt {
|
||||||
|
// Using `long` for consistency with `ArtistOpt`.
|
||||||
|
#[structopt(long, help = "The title of the album")]
|
||||||
|
title: String,
|
||||||
|
|
||||||
|
#[structopt(subcommand)]
|
||||||
|
command: AlbumCommand,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(StructOpt, Debug)]
|
||||||
|
enum AlbumCommand {
|
||||||
|
#[structopt(about = "Edit the album's sequence value")]
|
||||||
|
Seq(AlbumSeqCommand),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(StructOpt, Debug)]
|
||||||
|
enum AlbumSeqCommand {
|
||||||
|
#[structopt(about = "Set the sequence value overwriting any existing value")]
|
||||||
|
Set(AlbumSeqValue),
|
||||||
|
#[structopt(about = "Clear the sequence value")]
|
||||||
|
Clear,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(StructOpt, Debug)]
|
||||||
|
struct AlbumSeqValue {
|
||||||
|
#[structopt(help = "The new sequence value")]
|
||||||
|
value: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Command {
|
||||||
fn handle(self, music_hoard: &mut MH) {
|
fn handle(self, music_hoard: &mut MH) {
|
||||||
match self {
|
match self {
|
||||||
ArtistCommand::Add(artist_value) => {
|
Command::Artist(artist_opt) => artist_opt.handle(music_hoard),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ArtistOpt {
|
||||||
|
fn handle(self, music_hoard: &mut MH) {
|
||||||
|
self.command.handle(music_hoard, &self.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ArtistCommand {
|
||||||
|
fn handle(self, music_hoard: &mut MH, artist_name: &str) {
|
||||||
|
match self {
|
||||||
|
ArtistCommand::Add => {
|
||||||
music_hoard
|
music_hoard
|
||||||
.add_artist(ArtistId::new(artist_value.artist))
|
.add_artist(ArtistId::new(artist_name))
|
||||||
.expect("failed to add artist");
|
.expect("failed to add artist");
|
||||||
}
|
}
|
||||||
ArtistCommand::Remove(artist_value) => {
|
ArtistCommand::Remove => {
|
||||||
music_hoard
|
music_hoard
|
||||||
.remove_artist(ArtistId::new(artist_value.artist))
|
.remove_artist(ArtistId::new(artist_name))
|
||||||
.expect("failed to remove artist");
|
.expect("failed to remove artist");
|
||||||
}
|
}
|
||||||
ArtistCommand::Sort(sort_command) => {
|
ArtistCommand::Sort(sort_command) => {
|
||||||
sort_command.handle(music_hoard);
|
sort_command.handle(music_hoard, artist_name);
|
||||||
}
|
}
|
||||||
ArtistCommand::MusicBrainz(musicbrainz_command) => {
|
ArtistCommand::MusicBrainz(musicbrainz_command) => {
|
||||||
musicbrainz_command.handle(music_hoard)
|
musicbrainz_command.handle(music_hoard, artist_name)
|
||||||
}
|
}
|
||||||
ArtistCommand::Property(property_command) => {
|
ArtistCommand::Property(property_command) => {
|
||||||
property_command.handle(music_hoard);
|
property_command.handle(music_hoard, artist_name);
|
||||||
|
}
|
||||||
|
ArtistCommand::Album(album_opt) => {
|
||||||
|
album_opt.handle(music_hoard, artist_name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SortCommand {
|
impl SortCommand {
|
||||||
fn handle(self, music_hoard: &mut MH) {
|
fn handle(self, music_hoard: &mut MH, artist_name: &str) {
|
||||||
match self {
|
match self {
|
||||||
SortCommand::Set(artist_sort_value) => music_hoard
|
SortCommand::Set(artist_sort_value) => music_hoard
|
||||||
.set_artist_sort(
|
.set_artist_sort(
|
||||||
ArtistId::new(artist_sort_value.artist),
|
ArtistId::new(artist_name),
|
||||||
ArtistId::new(artist_sort_value.sort),
|
ArtistId::new(artist_sort_value.sort),
|
||||||
)
|
)
|
||||||
.expect("faild to set artist sort name"),
|
.expect("faild to set artist sort name"),
|
||||||
SortCommand::Clear(artist_value) => music_hoard
|
SortCommand::Clear => music_hoard
|
||||||
.clear_artist_sort(ArtistId::new(artist_value.artist))
|
.clear_artist_sort(ArtistId::new(artist_name))
|
||||||
.expect("failed to clear artist sort name"),
|
.expect("failed to clear artist sort name"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MusicBrainzCommand {
|
impl MusicBrainzCommand {
|
||||||
fn handle(self, music_hoard: &mut MH) {
|
fn handle(self, music_hoard: &mut MH, artist_name: &str) {
|
||||||
match self {
|
match self {
|
||||||
MusicBrainzCommand::Set(musicbrainz_value) => music_hoard
|
MusicBrainzCommand::Set(musicbrainz_value) => music_hoard
|
||||||
.set_musicbrainz_url(
|
.set_artist_musicbrainz(ArtistId::new(artist_name), musicbrainz_value.url)
|
||||||
ArtistId::new(musicbrainz_value.artist),
|
|
||||||
musicbrainz_value.url,
|
|
||||||
)
|
|
||||||
.expect("failed to set MusicBrainz URL"),
|
.expect("failed to set MusicBrainz URL"),
|
||||||
MusicBrainzCommand::Clear(artist_value) => music_hoard
|
MusicBrainzCommand::Clear => music_hoard
|
||||||
.clear_musicbrainz_url(ArtistId::new(artist_value.artist))
|
.clear_artist_musicbrainz(ArtistId::new(artist_name))
|
||||||
.expect("failed to clear MusicBrainz URL"),
|
.expect("failed to clear MusicBrainz URL"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PropertyCommand {
|
impl PropertyCommand {
|
||||||
fn handle(self, music_hoard: &mut MH) {
|
fn handle(self, music_hoard: &mut MH, artist_name: &str) {
|
||||||
match self {
|
match self {
|
||||||
PropertyCommand::Add(property_value) => music_hoard
|
PropertyCommand::Add(property_value) => music_hoard
|
||||||
.add_to_property(
|
.add_to_artist_property(
|
||||||
ArtistId::new(property_value.artist),
|
ArtistId::new(artist_name),
|
||||||
property_value.property,
|
property_value.property,
|
||||||
property_value.values,
|
property_value.values,
|
||||||
)
|
)
|
||||||
.expect("failed to add values to property"),
|
.expect("failed to add values to property"),
|
||||||
PropertyCommand::Remove(property_value) => music_hoard
|
PropertyCommand::Remove(property_value) => music_hoard
|
||||||
.remove_from_property(
|
.remove_from_artist_property(
|
||||||
ArtistId::new(property_value.artist),
|
ArtistId::new(artist_name),
|
||||||
property_value.property,
|
property_value.property,
|
||||||
property_value.values,
|
property_value.values,
|
||||||
)
|
)
|
||||||
.expect("failed to remove values from property"),
|
.expect("failed to remove values from property"),
|
||||||
PropertyCommand::Set(property_value) => music_hoard
|
PropertyCommand::Set(property_value) => music_hoard
|
||||||
.set_property(
|
.set_artist_property(
|
||||||
ArtistId::new(property_value.artist),
|
ArtistId::new(artist_name),
|
||||||
property_value.property,
|
property_value.property,
|
||||||
property_value.values,
|
property_value.values,
|
||||||
)
|
)
|
||||||
.expect("failed to set property"),
|
.expect("failed to set property"),
|
||||||
PropertyCommand::Clear(property_name) => music_hoard
|
PropertyCommand::Clear(property_name) => music_hoard
|
||||||
.clear_property(ArtistId::new(property_name.artist), property_name.property)
|
.clear_artist_property(ArtistId::new(artist_name), property_name.property)
|
||||||
.expect("failed to clear property"),
|
.expect("failed to clear property"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AlbumOpt {
|
||||||
|
fn handle(self, music_hoard: &mut MH, artist_name: &str) {
|
||||||
|
self.command.handle(music_hoard, artist_name, &self.title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AlbumCommand {
|
||||||
|
fn handle(self, music_hoard: &mut MH, artist_name: &str, album_name: &str) {
|
||||||
|
match self {
|
||||||
|
AlbumCommand::Seq(seq_command) => {
|
||||||
|
seq_command.handle(music_hoard, artist_name, album_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AlbumSeqCommand {
|
||||||
|
fn handle(self, music_hoard: &mut MH, artist_name: &str, album_name: &str) {
|
||||||
|
match self {
|
||||||
|
AlbumSeqCommand::Set(seq_value) => music_hoard
|
||||||
|
.set_album_seq(
|
||||||
|
ArtistId::new(artist_name),
|
||||||
|
AlbumId::new(album_name),
|
||||||
|
seq_value.value,
|
||||||
|
)
|
||||||
|
.expect("failed to set sequence value"),
|
||||||
|
AlbumSeqCommand::Clear => music_hoard
|
||||||
|
.clear_album_seq(ArtistId::new(artist_name), AlbumId::new(album_name))
|
||||||
|
.expect("failed to clear sequence value"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let opt = Opt::from_args();
|
let opt = Opt::from_args();
|
||||||
|
|
||||||
@ -219,5 +288,5 @@ fn main() {
|
|||||||
.set_database(db)
|
.set_database(db)
|
||||||
.build()
|
.build()
|
||||||
.expect("failed to initialise MusicHoard");
|
.expect("failed to initialise MusicHoard");
|
||||||
opt.category.handle(&mut music_hoard);
|
opt.command.handle(&mut music_hoard);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
use std::mem;
|
use std::{
|
||||||
|
fmt::{self, Display},
|
||||||
|
mem,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::core::collection::{
|
use crate::core::collection::{
|
||||||
merge::{Merge, MergeSorted},
|
merge::{Merge, MergeSorted, WithId},
|
||||||
track::Track,
|
track::Track,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -9,19 +12,106 @@ use crate::core::collection::{
|
|||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct Album {
|
pub struct Album {
|
||||||
pub id: AlbumId,
|
pub id: AlbumId,
|
||||||
|
pub date: AlbumDate,
|
||||||
|
pub seq: AlbumSeq,
|
||||||
pub tracks: Vec<Track>,
|
pub tracks: Vec<Track>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl WithId for Album {
|
||||||
|
type Id = AlbumId;
|
||||||
|
|
||||||
|
fn id(&self) -> &Self::Id {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The album identifier.
|
/// The album identifier.
|
||||||
#[derive(Clone, Debug, PartialEq, PartialOrd, Ord, Eq, Hash)]
|
#[derive(Clone, Debug, PartialEq, PartialOrd, Ord, Eq, Hash)]
|
||||||
pub struct AlbumId {
|
pub struct AlbumId {
|
||||||
pub year: u32,
|
|
||||||
pub title: String,
|
pub title: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// There are crates for handling dates, but we don't need much complexity beyond year-month-day.
|
||||||
|
/// The album's release date.
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, PartialOrd, Ord, Eq, Hash)]
|
||||||
|
pub struct AlbumDate {
|
||||||
|
pub year: u32,
|
||||||
|
pub month: AlbumMonth,
|
||||||
|
pub day: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for AlbumDate {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
if self.month.is_none() {
|
||||||
|
write!(f, "{}", self.year)
|
||||||
|
} else if self.day == 0 {
|
||||||
|
write!(f, "{}‐{:02}", self.year, self.month as u8)
|
||||||
|
} else {
|
||||||
|
write!(f, "{}‐{:02}‐{:02}", self.year, self.month as u8, self.day)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The album's sequence to determine order when two or more albums have the same release date.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd, Ord, Eq, Hash)]
|
||||||
|
pub struct AlbumSeq(pub u8);
|
||||||
|
|
||||||
|
#[repr(u8)]
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd, Ord, Eq, Hash)]
|
||||||
|
pub enum AlbumMonth {
|
||||||
|
#[default]
|
||||||
|
None = 0,
|
||||||
|
January = 1,
|
||||||
|
February = 2,
|
||||||
|
March = 3,
|
||||||
|
April = 4,
|
||||||
|
May = 5,
|
||||||
|
June = 6,
|
||||||
|
July = 7,
|
||||||
|
August = 8,
|
||||||
|
September = 9,
|
||||||
|
October = 10,
|
||||||
|
November = 11,
|
||||||
|
December = 12,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u8> for AlbumMonth {
|
||||||
|
fn from(value: u8) -> Self {
|
||||||
|
match value {
|
||||||
|
1 => AlbumMonth::January,
|
||||||
|
2 => AlbumMonth::February,
|
||||||
|
3 => AlbumMonth::March,
|
||||||
|
4 => AlbumMonth::April,
|
||||||
|
5 => AlbumMonth::May,
|
||||||
|
6 => AlbumMonth::June,
|
||||||
|
7 => AlbumMonth::July,
|
||||||
|
8 => AlbumMonth::August,
|
||||||
|
9 => AlbumMonth::September,
|
||||||
|
10 => AlbumMonth::October,
|
||||||
|
11 => AlbumMonth::November,
|
||||||
|
12 => AlbumMonth::December,
|
||||||
|
_ => AlbumMonth::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AlbumMonth {
|
||||||
|
fn is_none(&self) -> bool {
|
||||||
|
matches!(self, AlbumMonth::None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Album {
|
impl Album {
|
||||||
pub fn get_sort_key(&self) -> &AlbumId {
|
pub fn get_sort_key(&self) -> (&AlbumDate, &AlbumSeq, &AlbumId) {
|
||||||
&self.id
|
(&self.date, &self.seq, &self.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_seq(&mut self, seq: AlbumSeq) {
|
||||||
|
self.seq = seq;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_seq(&mut self) {
|
||||||
|
self.seq = AlbumSeq::default();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,24 +123,140 @@ impl PartialOrd for Album {
|
|||||||
|
|
||||||
impl Ord for Album {
|
impl Ord for Album {
|
||||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
self.id.cmp(&other.id)
|
self.get_sort_key().cmp(&other.get_sort_key())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Merge for Album {
|
impl Merge for Album {
|
||||||
fn merge_in_place(&mut self, other: Self) {
|
fn merge_in_place(&mut self, other: Self) {
|
||||||
assert_eq!(self.id, other.id);
|
assert_eq!(self.id, other.id);
|
||||||
|
self.seq = std::cmp::max(self.seq, other.seq);
|
||||||
let tracks = mem::take(&mut self.tracks);
|
let tracks = mem::take(&mut self.tracks);
|
||||||
self.tracks = MergeSorted::new(tracks.into_iter(), other.tracks.into_iter()).collect();
|
self.tracks = MergeSorted::new(tracks.into_iter(), other.tracks.into_iter()).collect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<S: Into<String>> From<S> for AlbumId {
|
||||||
|
fn from(value: S) -> Self {
|
||||||
|
AlbumId::new(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<AlbumId> for AlbumId {
|
||||||
|
fn as_ref(&self) -> &AlbumId {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AlbumId {
|
||||||
|
pub fn new<S: Into<String>>(name: S) -> AlbumId {
|
||||||
|
AlbumId { title: name.into() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for AlbumId {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::core::testmod::FULL_COLLECTION;
|
use crate::core::testmod::FULL_COLLECTION;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
impl AlbumDate {
|
||||||
|
fn new<M: Into<AlbumMonth>>(year: u32, month: M, day: u8) -> Self {
|
||||||
|
AlbumDate {
|
||||||
|
year,
|
||||||
|
month: month.into(),
|
||||||
|
day,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn album_month() {
|
||||||
|
assert_eq!(<u8 as Into<AlbumMonth>>::into(0), AlbumMonth::None);
|
||||||
|
assert_eq!(<u8 as Into<AlbumMonth>>::into(1), AlbumMonth::January);
|
||||||
|
assert_eq!(<u8 as Into<AlbumMonth>>::into(2), AlbumMonth::February);
|
||||||
|
assert_eq!(<u8 as Into<AlbumMonth>>::into(3), AlbumMonth::March);
|
||||||
|
assert_eq!(<u8 as Into<AlbumMonth>>::into(4), AlbumMonth::April);
|
||||||
|
assert_eq!(<u8 as Into<AlbumMonth>>::into(5), AlbumMonth::May);
|
||||||
|
assert_eq!(<u8 as Into<AlbumMonth>>::into(6), AlbumMonth::June);
|
||||||
|
assert_eq!(<u8 as Into<AlbumMonth>>::into(7), AlbumMonth::July);
|
||||||
|
assert_eq!(<u8 as Into<AlbumMonth>>::into(8), AlbumMonth::August);
|
||||||
|
assert_eq!(<u8 as Into<AlbumMonth>>::into(9), AlbumMonth::September);
|
||||||
|
assert_eq!(<u8 as Into<AlbumMonth>>::into(10), AlbumMonth::October);
|
||||||
|
assert_eq!(<u8 as Into<AlbumMonth>>::into(11), AlbumMonth::November);
|
||||||
|
assert_eq!(<u8 as Into<AlbumMonth>>::into(12), AlbumMonth::December);
|
||||||
|
assert_eq!(<u8 as Into<AlbumMonth>>::into(13), AlbumMonth::None);
|
||||||
|
assert_eq!(<u8 as Into<AlbumMonth>>::into(255), AlbumMonth::None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn album_display() {
|
||||||
|
assert_eq!(AlbumDate::default().to_string(), "0");
|
||||||
|
assert_eq!(AlbumDate::new(1990, 0, 0).to_string(), "1990");
|
||||||
|
assert_eq!(AlbumDate::new(1990, 5, 0).to_string(), "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]
|
#[test]
|
||||||
fn merge_album_no_overlap() {
|
fn merge_album_no_overlap() {
|
||||||
let left = FULL_COLLECTION[0].albums[0].to_owned();
|
let left = FULL_COLLECTION[0].albums[0].to_owned();
|
||||||
@ -64,9 +270,9 @@ mod tests {
|
|||||||
let merged = left.clone().merge(right.clone());
|
let merged = left.clone().merge(right.clone());
|
||||||
assert_eq!(expected, merged);
|
assert_eq!(expected, merged);
|
||||||
|
|
||||||
// Non-overlapping merge should be commutative.
|
// Non-overlapping merge should be commutative in the tracks.
|
||||||
let merged = right.clone().merge(left.clone());
|
let merged = right.clone().merge(left.clone());
|
||||||
assert_eq!(expected, merged);
|
assert_eq!(expected.tracks, merged.tracks);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -2,6 +2,7 @@ use std::{
|
|||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
fmt::{self, Debug, Display},
|
fmt::{self, Debug, Display},
|
||||||
mem,
|
mem,
|
||||||
|
str::FromStr,
|
||||||
};
|
};
|
||||||
|
|
||||||
use url::Url;
|
use url::Url;
|
||||||
@ -9,7 +10,7 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
use crate::core::collection::{
|
use crate::core::collection::{
|
||||||
album::Album,
|
album::Album,
|
||||||
merge::{Merge, MergeSorted},
|
merge::{Merge, MergeCollections, WithId},
|
||||||
Error,
|
Error,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -23,6 +24,14 @@ pub struct Artist {
|
|||||||
pub albums: Vec<Album>,
|
pub albums: Vec<Album>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl WithId for Artist {
|
||||||
|
type Id = ArtistId;
|
||||||
|
|
||||||
|
fn id(&self) -> &Self::Id {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The artist identifier.
|
/// The artist identifier.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub struct ArtistId {
|
pub struct ArtistId {
|
||||||
@ -41,8 +50,8 @@ impl Artist {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_sort_key(&self) -> &ArtistId {
|
pub fn get_sort_key(&self) -> (&ArtistId,) {
|
||||||
self.sort.as_ref().unwrap_or(&self.id)
|
(self.sort.as_ref().unwrap_or(&self.id),)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_sort_key<SORT: Into<ArtistId>>(&mut self, sort: SORT) {
|
pub fn set_sort_key<SORT: Into<ArtistId>>(&mut self, sort: SORT) {
|
||||||
@ -114,18 +123,26 @@ impl PartialOrd for Artist {
|
|||||||
|
|
||||||
impl Ord for Artist {
|
impl Ord for Artist {
|
||||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
self.get_sort_key().cmp(other.get_sort_key())
|
self.get_sort_key().cmp(&other.get_sort_key())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Merge for Artist {
|
impl Merge for Artist {
|
||||||
fn merge_in_place(&mut self, other: Self) {
|
fn merge_in_place(&mut self, other: Self) {
|
||||||
assert_eq!(self.id, other.id);
|
assert_eq!(self.id, other.id);
|
||||||
|
|
||||||
self.sort = self.sort.take().or(other.sort);
|
self.sort = self.sort.take().or(other.sort);
|
||||||
self.musicbrainz = self.musicbrainz.take().or(other.musicbrainz);
|
self.musicbrainz = self.musicbrainz.take().or(other.musicbrainz);
|
||||||
self.properties.merge_in_place(other.properties);
|
self.properties.merge_in_place(other.properties);
|
||||||
|
|
||||||
let albums = mem::take(&mut self.albums);
|
let albums = mem::take(&mut self.albums);
|
||||||
self.albums = MergeSorted::new(albums.into_iter(), other.albums.into_iter()).collect();
|
self.albums = MergeCollections::merge_iter(albums, other.albums);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: Into<String>> From<S> for ArtistId {
|
||||||
|
fn from(value: S) -> Self {
|
||||||
|
ArtistId::new(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,9 +176,13 @@ pub struct MusicBrainz(Url);
|
|||||||
|
|
||||||
impl MusicBrainz {
|
impl MusicBrainz {
|
||||||
/// Validate and wrap a MusicBrainz URL.
|
/// Validate and wrap a MusicBrainz URL.
|
||||||
pub fn new<S: AsRef<str>>(url: S) -> Result<Self, Error> {
|
pub fn new_from_str<S: AsRef<str>>(url: S) -> Result<Self, Error> {
|
||||||
let url = Url::parse(url.as_ref())?;
|
let url = Url::parse(url.as_ref())?;
|
||||||
|
Self::new_from_url(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate and wrap a MusicBrainz URL.
|
||||||
|
pub fn new_from_url(url: Url) -> Result<Self, Error> {
|
||||||
if !url
|
if !url
|
||||||
.domain()
|
.domain()
|
||||||
.map(|u| u.ends_with("musicbrainz.org"))
|
.map(|u| u.ends_with("musicbrainz.org"))
|
||||||
@ -189,11 +210,36 @@ impl AsRef<str> for MusicBrainz {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&str> for MusicBrainz {
|
impl FromStr for MusicBrainz {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
MusicBrainz::new_from_str(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A blanket TryFrom would be better, but https://stackoverflow.com/a/64407892
|
||||||
|
macro_rules! impl_try_from_for_musicbrainz {
|
||||||
|
($from:ty) => {
|
||||||
|
impl TryFrom<$from> for MusicBrainz {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
fn try_from(value: $from) -> Result<Self, Self::Error> {
|
||||||
MusicBrainz::new(value)
|
MusicBrainz::new_from_str(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_try_from_for_musicbrainz!(&str);
|
||||||
|
impl_try_from_for_musicbrainz!(&String);
|
||||||
|
impl_try_from_for_musicbrainz!(String);
|
||||||
|
|
||||||
|
impl TryFrom<Url> for MusicBrainz {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(value: Url) -> Result<Self, Self::Error> {
|
||||||
|
MusicBrainz::new_from_url(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,34 +266,35 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn musicbrainz() {
|
fn musicbrainz() {
|
||||||
let uuid = "d368baa8-21ca-4759-9731-0b2753071ad8";
|
let uuid = "d368baa8-21ca-4759-9731-0b2753071ad8";
|
||||||
let url = format!("https://musicbrainz.org/artist/{uuid}");
|
let url_str = format!("https://musicbrainz.org/artist/{uuid}");
|
||||||
let mb = MusicBrainz::new(&url).unwrap();
|
let url: Url = url_str.as_str().try_into().unwrap();
|
||||||
assert_eq!(url, mb.as_ref());
|
let mb: MusicBrainz = url.try_into().unwrap();
|
||||||
|
assert_eq!(url_str, mb.as_ref());
|
||||||
assert_eq!(uuid, mb.mbid());
|
assert_eq!(uuid, mb.mbid());
|
||||||
|
|
||||||
let url = "not a url at all".to_string();
|
let url = "not a url at all".to_string();
|
||||||
let expected_error: Error = url::ParseError::RelativeUrlWithoutBase.into();
|
let expected_error: Error = url::ParseError::RelativeUrlWithoutBase.into();
|
||||||
let actual_error = MusicBrainz::new(url).unwrap_err();
|
let actual_error = MusicBrainz::from_str(&url).unwrap_err();
|
||||||
assert_eq!(actual_error, expected_error);
|
assert_eq!(actual_error, expected_error);
|
||||||
assert_eq!(actual_error.to_string(), expected_error.to_string());
|
assert_eq!(actual_error.to_string(), expected_error.to_string());
|
||||||
|
|
||||||
let url = "https://musicbrainz.org/artist/i-am-not-a-uuid".to_string();
|
let url = "https://musicbrainz.org/artist/i-am-not-a-uuid".to_string();
|
||||||
let expected_error: Error = Uuid::try_parse("i-am-not-a-uuid").unwrap_err().into();
|
let expected_error: Error = Uuid::try_parse("i-am-not-a-uuid").unwrap_err().into();
|
||||||
let actual_error = MusicBrainz::new(url).unwrap_err();
|
let actual_error = MusicBrainz::from_str(&url).unwrap_err();
|
||||||
assert_eq!(actual_error, expected_error);
|
assert_eq!(actual_error, expected_error);
|
||||||
assert_eq!(actual_error.to_string(), expected_error.to_string());
|
assert_eq!(actual_error.to_string(), expected_error.to_string());
|
||||||
|
|
||||||
let url = "https://musicbrainz.org/artist".to_string();
|
let url = "https://musicbrainz.org/artist".to_string();
|
||||||
let expected_error = Error::UrlError(format!("invalid MusicBrainz URL: {url}"));
|
let expected_error = Error::UrlError(format!("invalid MusicBrainz URL: {url}"));
|
||||||
let actual_error = MusicBrainz::new(&url).unwrap_err();
|
let actual_error = MusicBrainz::from_str(&url).unwrap_err();
|
||||||
assert_eq!(actual_error, expected_error);
|
assert_eq!(actual_error, expected_error);
|
||||||
assert_eq!(actual_error.to_string(), expected_error.to_string());
|
assert_eq!(actual_error.to_string(), expected_error.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn urls() {
|
fn urls() {
|
||||||
assert!(MusicBrainz::new(MUSICBRAINZ).is_ok());
|
assert!(MusicBrainz::from_str(MUSICBRAINZ).is_ok());
|
||||||
assert!(MusicBrainz::new(MUSICBUTLER).is_err());
|
assert!(MusicBrainz::from_str(MUSICBUTLER).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -256,11 +303,11 @@ mod tests {
|
|||||||
let sort_id_1 = ArtistId::new("sort id 1");
|
let sort_id_1 = ArtistId::new("sort id 1");
|
||||||
let sort_id_2 = ArtistId::new("sort id 2");
|
let sort_id_2 = ArtistId::new("sort id 2");
|
||||||
|
|
||||||
let mut artist = Artist::new(artist_id.clone());
|
let mut artist = Artist::new(&artist_id.name);
|
||||||
|
|
||||||
assert_eq!(artist.id, artist_id);
|
assert_eq!(artist.id, artist_id);
|
||||||
assert_eq!(artist.sort, None);
|
assert_eq!(artist.sort, None);
|
||||||
assert_eq!(artist.get_sort_key(), &artist_id);
|
assert_eq!(artist.get_sort_key(), (&artist_id,));
|
||||||
assert!(artist < Artist::new(sort_id_1.clone()));
|
assert!(artist < Artist::new(sort_id_1.clone()));
|
||||||
assert!(artist < Artist::new(sort_id_2.clone()));
|
assert!(artist < Artist::new(sort_id_2.clone()));
|
||||||
|
|
||||||
@ -268,7 +315,7 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(artist.id, artist_id);
|
assert_eq!(artist.id, artist_id);
|
||||||
assert_eq!(artist.sort.as_ref(), Some(&sort_id_1));
|
assert_eq!(artist.sort.as_ref(), Some(&sort_id_1));
|
||||||
assert_eq!(artist.get_sort_key(), &sort_id_1);
|
assert_eq!(artist.get_sort_key(), (&sort_id_1,));
|
||||||
assert!(artist > Artist::new(artist_id.clone()));
|
assert!(artist > Artist::new(artist_id.clone()));
|
||||||
assert!(artist < Artist::new(sort_id_2.clone()));
|
assert!(artist < Artist::new(sort_id_2.clone()));
|
||||||
|
|
||||||
@ -276,7 +323,7 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(artist.id, artist_id);
|
assert_eq!(artist.id, artist_id);
|
||||||
assert_eq!(artist.sort.as_ref(), Some(&sort_id_2));
|
assert_eq!(artist.sort.as_ref(), Some(&sort_id_2));
|
||||||
assert_eq!(artist.get_sort_key(), &sort_id_2);
|
assert_eq!(artist.get_sort_key(), (&sort_id_2,));
|
||||||
assert!(artist > Artist::new(artist_id.clone()));
|
assert!(artist > Artist::new(artist_id.clone()));
|
||||||
assert!(artist > Artist::new(sort_id_1.clone()));
|
assert!(artist > Artist::new(sort_id_1.clone()));
|
||||||
|
|
||||||
@ -284,7 +331,7 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(artist.id, artist_id);
|
assert_eq!(artist.id, artist_id);
|
||||||
assert_eq!(artist.sort, None);
|
assert_eq!(artist.sort, None);
|
||||||
assert_eq!(artist.get_sort_key(), &artist_id);
|
assert_eq!(artist.get_sort_key(), (&artist_id,));
|
||||||
assert!(artist < Artist::new(sort_id_1.clone()));
|
assert!(artist < Artist::new(sort_id_1.clone()));
|
||||||
assert!(artist < Artist::new(sort_id_2.clone()));
|
assert!(artist < Artist::new(sort_id_2.clone()));
|
||||||
}
|
}
|
||||||
@ -307,14 +354,14 @@ mod tests {
|
|||||||
|
|
||||||
// Setting a URL on an artist.
|
// Setting a URL on an artist.
|
||||||
artist.set_musicbrainz_url(MUSICBRAINZ.try_into().unwrap());
|
artist.set_musicbrainz_url(MUSICBRAINZ.try_into().unwrap());
|
||||||
_ = expected.insert(MusicBrainz::new(MUSICBRAINZ).unwrap());
|
_ = expected.insert(MUSICBRAINZ.try_into().unwrap());
|
||||||
assert_eq!(artist.musicbrainz, expected);
|
assert_eq!(artist.musicbrainz, expected);
|
||||||
|
|
||||||
artist.set_musicbrainz_url(MUSICBRAINZ.try_into().unwrap());
|
artist.set_musicbrainz_url(MUSICBRAINZ.try_into().unwrap());
|
||||||
assert_eq!(artist.musicbrainz, expected);
|
assert_eq!(artist.musicbrainz, expected);
|
||||||
|
|
||||||
artist.set_musicbrainz_url(MUSICBRAINZ_2.try_into().unwrap());
|
artist.set_musicbrainz_url(MUSICBRAINZ_2.try_into().unwrap());
|
||||||
_ = expected.insert(MusicBrainz::new(MUSICBRAINZ_2).unwrap());
|
_ = expected.insert(MUSICBRAINZ_2.try_into().unwrap());
|
||||||
assert_eq!(artist.musicbrainz, expected);
|
assert_eq!(artist.musicbrainz, expected);
|
||||||
|
|
||||||
// Clearing URLs.
|
// Clearing URLs.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use std::{cmp::Ordering, collections::HashMap, hash::Hash, iter::Peekable};
|
use std::{cmp::Ordering, collections::HashMap, hash::Hash, iter::Peekable, marker::PhantomData};
|
||||||
|
|
||||||
/// A trait for merging two objects. The merge is asymmetric with the left argument considered to be
|
/// A trait for merging two objects. The merge is asymmetric with the left argument considered to be
|
||||||
/// the primary whose properties are to be kept in case of collisions.
|
/// the primary whose properties are to be kept in case of collisions.
|
||||||
@ -79,3 +79,45 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait WithId {
|
||||||
|
type Id;
|
||||||
|
|
||||||
|
fn id(&self) -> &Self::Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MergeCollections<ID, T, IT> {
|
||||||
|
_id: PhantomData<ID>,
|
||||||
|
_t: PhantomData<T>,
|
||||||
|
_it: PhantomData<IT>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<ID, T, IT> MergeCollections<ID, T, IT>
|
||||||
|
where
|
||||||
|
ID: Eq + Hash + Clone,
|
||||||
|
T: WithId<Id = ID> + Merge + Ord,
|
||||||
|
IT: IntoIterator<Item = T>,
|
||||||
|
{
|
||||||
|
pub fn merge_iter(primary: IT, secondary: IT) -> Vec<T> {
|
||||||
|
let primary = primary
|
||||||
|
.into_iter()
|
||||||
|
.map(|item| (item.id().clone(), item))
|
||||||
|
.collect();
|
||||||
|
Self::merge(primary, secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn merge(mut primary: HashMap<ID, T>, secondary: IT) -> Vec<T> {
|
||||||
|
for secondary_item in secondary {
|
||||||
|
if let Some(ref mut primary_item) = primary.get_mut(secondary_item.id()) {
|
||||||
|
primary_item.merge_in_place(secondary_item);
|
||||||
|
} else {
|
||||||
|
primary.insert(secondary_item.id().clone(), secondary_item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut collection: Vec<T> = primary.into_values().collect();
|
||||||
|
collection.sort_unstable();
|
||||||
|
|
||||||
|
collection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -5,7 +5,7 @@ pub mod artist;
|
|||||||
pub mod track;
|
pub mod track;
|
||||||
|
|
||||||
mod merge;
|
mod merge;
|
||||||
pub use merge::Merge;
|
pub use merge::MergeCollections;
|
||||||
|
|
||||||
use std::fmt::{self, Display};
|
use std::fmt::{self, Display};
|
||||||
|
|
||||||
|
@ -4,33 +4,37 @@ use crate::core::collection::merge::Merge;
|
|||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct Track {
|
pub struct Track {
|
||||||
pub id: TrackId,
|
pub id: TrackId,
|
||||||
|
pub number: TrackNum,
|
||||||
pub artist: Vec<String>,
|
pub artist: Vec<String>,
|
||||||
pub quality: Quality,
|
pub quality: TrackQuality,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The track identifier.
|
/// The track identifier.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub struct TrackId {
|
pub struct TrackId {
|
||||||
pub number: u32,
|
|
||||||
pub title: String,
|
pub title: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The track number.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct TrackNum(pub u32);
|
||||||
|
|
||||||
/// The track quality. Combines format and bitrate information.
|
/// The track quality. Combines format and bitrate information.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub struct Quality {
|
pub struct TrackQuality {
|
||||||
pub format: Format,
|
pub format: TrackFormat,
|
||||||
pub bitrate: u32,
|
pub bitrate: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Track {
|
impl Track {
|
||||||
pub fn get_sort_key(&self) -> &TrackId {
|
pub fn get_sort_key(&self) -> (&TrackNum, &TrackId) {
|
||||||
&self.id
|
(&self.number, &self.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The track file format.
|
/// The track file format.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
pub enum Format {
|
pub enum TrackFormat {
|
||||||
Flac,
|
Flac,
|
||||||
Mp3,
|
Mp3,
|
||||||
}
|
}
|
||||||
@ -43,7 +47,7 @@ impl PartialOrd for Track {
|
|||||||
|
|
||||||
impl Ord for Track {
|
impl Ord for Track {
|
||||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
self.id.cmp(&other.id)
|
self.get_sort_key().cmp(&other.get_sort_key())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,20 +65,21 @@ mod tests {
|
|||||||
fn merge_track() {
|
fn merge_track() {
|
||||||
let left = Track {
|
let left = Track {
|
||||||
id: TrackId {
|
id: TrackId {
|
||||||
number: 4,
|
|
||||||
title: String::from("a title"),
|
title: String::from("a title"),
|
||||||
},
|
},
|
||||||
|
number: TrackNum(4),
|
||||||
artist: vec![String::from("left artist")],
|
artist: vec![String::from("left artist")],
|
||||||
quality: Quality {
|
quality: TrackQuality {
|
||||||
format: Format::Flac,
|
format: TrackFormat::Flac,
|
||||||
bitrate: 1411,
|
bitrate: 1411,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let right = Track {
|
let right = Track {
|
||||||
id: left.id.clone(),
|
id: left.id.clone(),
|
||||||
|
number: left.number,
|
||||||
artist: vec![String::from("right artist")],
|
artist: vec![String::from("right artist")],
|
||||||
quality: Quality {
|
quality: TrackQuality {
|
||||||
format: Format::Mp3,
|
format: TrackFormat::Mp3,
|
||||||
bitrate: 320,
|
bitrate: 320,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -72,7 +72,7 @@ mod tests {
|
|||||||
use mockall::predicate;
|
use mockall::predicate;
|
||||||
|
|
||||||
use crate::core::{
|
use crate::core::{
|
||||||
collection::{artist::Artist, Collection},
|
collection::{album::AlbumDate, artist::Artist, Collection},
|
||||||
testmod::FULL_COLLECTION,
|
testmod::FULL_COLLECTION,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -82,7 +82,10 @@ mod tests {
|
|||||||
fn expected() -> Collection {
|
fn expected() -> Collection {
|
||||||
let mut expected = FULL_COLLECTION.to_owned();
|
let mut expected = FULL_COLLECTION.to_owned();
|
||||||
for artist in expected.iter_mut() {
|
for artist in expected.iter_mut() {
|
||||||
artist.albums.clear();
|
for album in artist.albums.iter_mut() {
|
||||||
|
album.date = AlbumDate::default();
|
||||||
|
album.tracks.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
expected
|
expected
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
pub static DATABASE_JSON: &str = "{\
|
pub static DATABASE_JSON: &str = "{\
|
||||||
\"V20240210\":\
|
\"V20240302\":\
|
||||||
[\
|
[\
|
||||||
{\
|
{\
|
||||||
\"name\":\"Album_Artist ‘A’\",\
|
\"name\":\"Album_Artist ‘A’\",\
|
||||||
@ -8,7 +8,11 @@ pub static DATABASE_JSON: &str = "{\
|
|||||||
\"properties\":{\
|
\"properties\":{\
|
||||||
\"MusicButler\":[\"https://www.musicbutler.io/artist-page/000000000\"],\
|
\"MusicButler\":[\"https://www.musicbutler.io/artist-page/000000000\"],\
|
||||||
\"Qobuz\":[\"https://www.qobuz.com/nl-nl/interpreter/artist-a/download-streaming-albums\"]\
|
\"Qobuz\":[\"https://www.qobuz.com/nl-nl/interpreter/artist-a/download-streaming-albums\"]\
|
||||||
}\
|
},\
|
||||||
|
\"albums\":[\
|
||||||
|
{\"title\":\"album_title a.a\",\"seq\":1},\
|
||||||
|
{\"title\":\"album_title a.b\",\"seq\":1}\
|
||||||
|
]\
|
||||||
},\
|
},\
|
||||||
{\
|
{\
|
||||||
\"name\":\"Album_Artist ‘B’\",\
|
\"name\":\"Album_Artist ‘B’\",\
|
||||||
@ -21,19 +25,33 @@ pub static DATABASE_JSON: &str = "{\
|
|||||||
\"https://www.musicbutler.io/artist-page/111111112\"\
|
\"https://www.musicbutler.io/artist-page/111111112\"\
|
||||||
],\
|
],\
|
||||||
\"Qobuz\":[\"https://www.qobuz.com/nl-nl/interpreter/artist-b/download-streaming-albums\"]\
|
\"Qobuz\":[\"https://www.qobuz.com/nl-nl/interpreter/artist-b/download-streaming-albums\"]\
|
||||||
}\
|
},\
|
||||||
|
\"albums\":[\
|
||||||
|
{\"title\":\"album_title b.a\",\"seq\":1},\
|
||||||
|
{\"title\":\"album_title b.b\",\"seq\":3},\
|
||||||
|
{\"title\":\"album_title b.c\",\"seq\":2},\
|
||||||
|
{\"title\":\"album_title b.d\",\"seq\":4}\
|
||||||
|
]\
|
||||||
},\
|
},\
|
||||||
{\
|
{\
|
||||||
\"name\":\"The Album_Artist ‘C’\",\
|
\"name\":\"The Album_Artist ‘C’\",\
|
||||||
\"sort\":\"Album_Artist ‘C’, The\",\
|
\"sort\":\"Album_Artist ‘C’, The\",\
|
||||||
\"musicbrainz\":\"https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111\",\
|
\"musicbrainz\":\"https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111\",\
|
||||||
\"properties\":{}\
|
\"properties\":{},\
|
||||||
|
\"albums\":[\
|
||||||
|
{\"title\":\"album_title c.a\",\"seq\":0},\
|
||||||
|
{\"title\":\"album_title c.b\",\"seq\":0}\
|
||||||
|
]\
|
||||||
},\
|
},\
|
||||||
{\
|
{\
|
||||||
\"name\":\"Album_Artist ‘D’\",\
|
\"name\":\"Album_Artist ‘D’\",\
|
||||||
\"sort\":null,\
|
\"sort\":null,\
|
||||||
\"musicbrainz\":null,\
|
\"musicbrainz\":null,\
|
||||||
\"properties\":{}\
|
\"properties\":{},\
|
||||||
|
\"albums\":[\
|
||||||
|
{\"title\":\"album_title d.a\",\"seq\":0},\
|
||||||
|
{\"title\":\"album_title d.b\",\"seq\":0}\
|
||||||
|
]\
|
||||||
}\
|
}\
|
||||||
]\
|
]\
|
||||||
}";
|
}";
|
||||||
|
@ -2,15 +2,19 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::{
|
use crate::core::{
|
||||||
collection::artist::{ArtistId, MusicBrainz},
|
collection::{
|
||||||
core::{
|
album::{Album, AlbumDate, AlbumId, AlbumSeq},
|
||||||
collection::{artist::Artist, Collection},
|
artist::{Artist, ArtistId},
|
||||||
database::{serde::Database, LoadError},
|
Collection,
|
||||||
},
|
},
|
||||||
|
database::LoadError,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub type DeserializeDatabase = Database<DeserializeArtist>;
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub enum DeserializeDatabase {
|
||||||
|
V20240302(Vec<DeserializeArtist>),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct DeserializeArtist {
|
pub struct DeserializeArtist {
|
||||||
@ -18,6 +22,13 @@ pub struct DeserializeArtist {
|
|||||||
sort: Option<String>,
|
sort: Option<String>,
|
||||||
musicbrainz: Option<String>,
|
musicbrainz: Option<String>,
|
||||||
properties: HashMap<String, Vec<String>>,
|
properties: HashMap<String, Vec<String>>,
|
||||||
|
albums: Vec<DeserializeAlbum>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct DeserializeAlbum {
|
||||||
|
title: String,
|
||||||
|
seq: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<DeserializeDatabase> for Collection {
|
impl TryFrom<DeserializeDatabase> for Collection {
|
||||||
@ -25,7 +36,7 @@ impl TryFrom<DeserializeDatabase> for Collection {
|
|||||||
|
|
||||||
fn try_from(database: DeserializeDatabase) -> Result<Self, Self::Error> {
|
fn try_from(database: DeserializeDatabase) -> Result<Self, Self::Error> {
|
||||||
match database {
|
match database {
|
||||||
Database::V20240210(collection) => collection
|
DeserializeDatabase::V20240302(collection) => collection
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|artist| artist.try_into())
|
.map(|artist| artist.try_into())
|
||||||
.collect(),
|
.collect(),
|
||||||
@ -40,9 +51,20 @@ impl TryFrom<DeserializeArtist> for Artist {
|
|||||||
Ok(Artist {
|
Ok(Artist {
|
||||||
id: ArtistId::new(artist.name),
|
id: ArtistId::new(artist.name),
|
||||||
sort: artist.sort.map(ArtistId::new),
|
sort: artist.sort.map(ArtistId::new),
|
||||||
musicbrainz: artist.musicbrainz.map(MusicBrainz::new).transpose()?,
|
musicbrainz: artist.musicbrainz.map(TryInto::try_into).transpose()?,
|
||||||
properties: artist.properties,
|
properties: artist.properties,
|
||||||
albums: vec![],
|
albums: artist.albums.into_iter().map(Into::into).collect(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<DeserializeAlbum> for Album {
|
||||||
|
fn from(album: DeserializeAlbum) -> Self {
|
||||||
|
Album {
|
||||||
|
id: AlbumId { title: album.title },
|
||||||
|
date: AlbumDate::default(),
|
||||||
|
seq: AlbumSeq(album.seq),
|
||||||
|
tracks: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -2,10 +2,3 @@
|
|||||||
|
|
||||||
pub mod deserialize;
|
pub mod deserialize;
|
||||||
pub mod serialize;
|
pub mod serialize;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub enum Database<ARTIST> {
|
|
||||||
V20240210(Vec<ARTIST>),
|
|
||||||
}
|
|
||||||
|
@ -2,12 +2,12 @@ use std::collections::BTreeMap;
|
|||||||
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::core::{
|
use crate::core::collection::{album::Album, artist::Artist, Collection};
|
||||||
collection::{artist::Artist, Collection},
|
|
||||||
database::serde::Database,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub type SerializeDatabase<'a> = Database<SerializeArtist<'a>>;
|
#[derive(Debug, Serialize)]
|
||||||
|
pub enum SerializeDatabase<'a> {
|
||||||
|
V20240302(Vec<SerializeArtist<'a>>),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct SerializeArtist<'a> {
|
pub struct SerializeArtist<'a> {
|
||||||
@ -15,11 +15,18 @@ pub struct SerializeArtist<'a> {
|
|||||||
sort: Option<&'a str>,
|
sort: Option<&'a str>,
|
||||||
musicbrainz: Option<&'a str>,
|
musicbrainz: Option<&'a str>,
|
||||||
properties: BTreeMap<&'a str, &'a Vec<String>>,
|
properties: BTreeMap<&'a str, &'a Vec<String>>,
|
||||||
|
albums: Vec<SerializeAlbum<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct SerializeAlbum<'a> {
|
||||||
|
title: &'a str,
|
||||||
|
seq: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> From<&'a Collection> for SerializeDatabase<'a> {
|
impl<'a> From<&'a Collection> for SerializeDatabase<'a> {
|
||||||
fn from(collection: &'a Collection) -> Self {
|
fn from(collection: &'a Collection) -> Self {
|
||||||
Database::V20240210(collection.iter().map(|artist| artist.into()).collect())
|
SerializeDatabase::V20240302(collection.iter().map(Into::into).collect())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,6 +41,16 @@ impl<'a> From<&'a Artist> for SerializeArtist<'a> {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|(k, v)| (k.as_ref(), v))
|
.map(|(k, v)| (k.as_ref(), v))
|
||||||
.collect(),
|
.collect(),
|
||||||
|
albums: artist.albums.iter().map(Into::into).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a Album> for SerializeAlbum<'a> {
|
||||||
|
fn from(album: &'a Album) -> Self {
|
||||||
|
SerializeAlbum {
|
||||||
|
title: &album.id.title,
|
||||||
|
seq: album.seq.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ pub mod executor;
|
|||||||
use mockall::automock;
|
use mockall::automock;
|
||||||
|
|
||||||
use crate::core::{
|
use crate::core::{
|
||||||
collection::track::Format,
|
collection::track::TrackFormat,
|
||||||
library::{Error, Field, ILibrary, Item, Query},
|
library::{Error, Field, ILibrary, Item, Query},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -27,6 +27,10 @@ const LIST_FORMAT_ARG: &str = concat!(
|
|||||||
list_format_separator!(),
|
list_format_separator!(),
|
||||||
"$year",
|
"$year",
|
||||||
list_format_separator!(),
|
list_format_separator!(),
|
||||||
|
"$month",
|
||||||
|
list_format_separator!(),
|
||||||
|
"$day",
|
||||||
|
list_format_separator!(),
|
||||||
"$album",
|
"$album",
|
||||||
list_format_separator!(),
|
list_format_separator!(),
|
||||||
"$track",
|
"$track",
|
||||||
@ -42,6 +46,21 @@ const LIST_FORMAT_ARG: &str = concat!(
|
|||||||
const TRACK_FORMAT_FLAC: &str = "FLAC";
|
const TRACK_FORMAT_FLAC: &str = "FLAC";
|
||||||
const TRACK_FORMAT_MP3: &str = "MP3";
|
const TRACK_FORMAT_MP3: &str = "MP3";
|
||||||
|
|
||||||
|
fn format_to_str(format: &TrackFormat) -> &'static str {
|
||||||
|
match format {
|
||||||
|
TrackFormat::Flac => TRACK_FORMAT_FLAC,
|
||||||
|
TrackFormat::Mp3 => TRACK_FORMAT_MP3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn str_to_format(format: &str) -> Option<TrackFormat> {
|
||||||
|
match format {
|
||||||
|
TRACK_FORMAT_FLAC => Some(TrackFormat::Flac),
|
||||||
|
TRACK_FORMAT_MP3 => Some(TrackFormat::Mp3),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
trait ToBeetsArg {
|
trait ToBeetsArg {
|
||||||
fn to_arg(&self, include: bool) -> String;
|
fn to_arg(&self, include: bool) -> String;
|
||||||
}
|
}
|
||||||
@ -57,10 +76,13 @@ impl ToBeetsArg for Field {
|
|||||||
Field::AlbumArtist(ref s) => format!("{negate}albumartist:{s}"),
|
Field::AlbumArtist(ref s) => format!("{negate}albumartist:{s}"),
|
||||||
Field::AlbumArtistSort(ref s) => format!("{negate}albumartist_sort:{s}"),
|
Field::AlbumArtistSort(ref s) => format!("{negate}albumartist_sort:{s}"),
|
||||||
Field::AlbumYear(ref u) => format!("{negate}year:{u}"),
|
Field::AlbumYear(ref u) => format!("{negate}year:{u}"),
|
||||||
|
Field::AlbumMonth(ref e) => format!("{negate}month:{}", *e as u8),
|
||||||
|
Field::AlbumDay(ref u) => format!("{negate}day:{u}"),
|
||||||
Field::AlbumTitle(ref s) => format!("{negate}album:{s}"),
|
Field::AlbumTitle(ref s) => format!("{negate}album:{s}"),
|
||||||
Field::TrackNumber(ref u) => format!("{negate}track:{u}"),
|
Field::TrackNumber(ref u) => format!("{negate}track:{u}"),
|
||||||
Field::TrackTitle(ref s) => format!("{negate}title:{s}"),
|
Field::TrackTitle(ref s) => format!("{negate}title:{s}"),
|
||||||
Field::TrackArtist(ref v) => format!("{negate}artist:{}", v.join("; ")),
|
Field::TrackArtist(ref v) => format!("{negate}artist:{}", v.join("; ")),
|
||||||
|
Field::TrackFormat(ref f) => format!("{negate}format:{}", format_to_str(f)),
|
||||||
Field::All(ref s) => format!("{negate}{s}"),
|
Field::All(ref s) => format!("{negate}{s}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -127,36 +149,38 @@ impl<BLE: IBeetsLibraryExecutor> ILibraryPrivate for BeetsLibrary<BLE> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let split: Vec<&str> = line.split(LIST_FORMAT_SEPARATOR).collect();
|
let split: Vec<&str> = line.split(LIST_FORMAT_SEPARATOR).collect();
|
||||||
if split.len() != 9 {
|
if split.len() != 11 {
|
||||||
return Err(Error::Invalid(line.to_string()));
|
return Err(Error::Invalid(line.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let album_artist = split[0].to_string();
|
let album_artist = split[0].to_string();
|
||||||
let album_artist_sort = if !split[1].is_empty() {
|
let album_artist_sort = match !split[1].is_empty() {
|
||||||
Some(split[1].to_string())
|
true => Some(split[1].to_string()),
|
||||||
} else {
|
false => None,
|
||||||
None
|
|
||||||
};
|
};
|
||||||
let album_year = split[2].parse::<u32>()?;
|
let album_year = split[2].parse::<u32>()?;
|
||||||
let album_title = split[3].to_string();
|
let album_month = split[3].parse::<u8>()?.into();
|
||||||
let track_number = split[4].parse::<u32>()?;
|
let album_day = split[4].parse::<u8>()?;
|
||||||
let track_title = split[5].to_string();
|
let album_title = split[5].to_string();
|
||||||
let track_artist = split[6]
|
let track_number = split[6].parse::<u32>()?;
|
||||||
|
let track_title = split[7].to_string();
|
||||||
|
let track_artist = split[8]
|
||||||
.to_string()
|
.to_string()
|
||||||
.split("; ")
|
.split("; ")
|
||||||
.map(|s| s.to_owned())
|
.map(|s| s.to_owned())
|
||||||
.collect();
|
.collect();
|
||||||
let track_format = match split[7].to_string().as_str() {
|
let track_format = match str_to_format(split[9].to_string().as_str()) {
|
||||||
TRACK_FORMAT_FLAC => Format::Flac,
|
Some(format) => format,
|
||||||
TRACK_FORMAT_MP3 => Format::Mp3,
|
None => return Err(Error::Invalid(line.to_string())),
|
||||||
_ => return Err(Error::Invalid(line.to_string())),
|
|
||||||
};
|
};
|
||||||
let track_bitrate = split[8].trim_end_matches("kbps").parse::<u32>()?;
|
let track_bitrate = split[10].trim_end_matches("kbps").parse::<u32>()?;
|
||||||
|
|
||||||
items.push(Item {
|
items.push(Item {
|
||||||
album_artist,
|
album_artist,
|
||||||
album_artist_sort,
|
album_artist_sort,
|
||||||
album_year,
|
album_year,
|
||||||
|
album_month,
|
||||||
|
album_day,
|
||||||
album_title,
|
album_title,
|
||||||
track_number,
|
track_number,
|
||||||
track_title,
|
track_title,
|
||||||
@ -177,7 +201,7 @@ mod testmod;
|
|||||||
mod tests {
|
mod tests {
|
||||||
use mockall::predicate;
|
use mockall::predicate;
|
||||||
|
|
||||||
use crate::core::library::testmod::LIBRARY_ITEMS;
|
use crate::{collection::album::AlbumMonth, core::library::testmod::LIBRARY_ITEMS};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use testmod::LIBRARY_BEETS;
|
use testmod::LIBRARY_BEETS;
|
||||||
@ -191,6 +215,7 @@ mod tests {
|
|||||||
String::from("some.artist.1"),
|
String::from("some.artist.1"),
|
||||||
String::from("some.artist.2"),
|
String::from("some.artist.2"),
|
||||||
]))
|
]))
|
||||||
|
.exclude(Field::TrackFormat(TrackFormat::Mp3))
|
||||||
.exclude(Field::All(String::from("some.all")))
|
.exclude(Field::All(String::from("some.all")))
|
||||||
.to_args();
|
.to_args();
|
||||||
query.sort();
|
query.sort();
|
||||||
@ -199,6 +224,7 @@ mod tests {
|
|||||||
query,
|
query,
|
||||||
vec![
|
vec![
|
||||||
String::from("^album:some.album"),
|
String::from("^album:some.album"),
|
||||||
|
String::from("^format:MP3"),
|
||||||
String::from("^some.all"),
|
String::from("^some.all"),
|
||||||
String::from("artist:some.artist.1; some.artist.2"),
|
String::from("artist:some.artist.1; some.artist.2"),
|
||||||
String::from("track:5"),
|
String::from("track:5"),
|
||||||
@ -209,7 +235,10 @@ mod tests {
|
|||||||
.exclude(Field::AlbumArtist(String::from("some.albumartist")))
|
.exclude(Field::AlbumArtist(String::from("some.albumartist")))
|
||||||
.exclude(Field::AlbumArtistSort(String::from("some.albumartist")))
|
.exclude(Field::AlbumArtistSort(String::from("some.albumartist")))
|
||||||
.include(Field::AlbumYear(3030))
|
.include(Field::AlbumYear(3030))
|
||||||
|
.include(Field::AlbumMonth(AlbumMonth::April))
|
||||||
|
.include(Field::AlbumDay(6))
|
||||||
.include(Field::TrackTitle(String::from("some.track")))
|
.include(Field::TrackTitle(String::from("some.track")))
|
||||||
|
.include(Field::TrackFormat(TrackFormat::Flac))
|
||||||
.exclude(Field::TrackArtist(vec![
|
.exclude(Field::TrackArtist(vec![
|
||||||
String::from("some.artist.1"),
|
String::from("some.artist.1"),
|
||||||
String::from("some.artist.2"),
|
String::from("some.artist.2"),
|
||||||
@ -223,6 +252,9 @@ mod tests {
|
|||||||
String::from("^albumartist:some.albumartist"),
|
String::from("^albumartist:some.albumartist"),
|
||||||
String::from("^albumartist_sort:some.albumartist"),
|
String::from("^albumartist_sort:some.albumartist"),
|
||||||
String::from("^artist:some.artist.1; some.artist.2"),
|
String::from("^artist:some.artist.1; some.artist.2"),
|
||||||
|
String::from("day:6"),
|
||||||
|
String::from("format:FLAC"),
|
||||||
|
String::from("month:4"),
|
||||||
String::from("title:some.track"),
|
String::from("title:some.track"),
|
||||||
String::from("year:3030"),
|
String::from("year:3030"),
|
||||||
]
|
]
|
||||||
@ -335,8 +367,8 @@ mod tests {
|
|||||||
.split(LIST_FORMAT_SEPARATOR)
|
.split(LIST_FORMAT_SEPARATOR)
|
||||||
.map(|s| s.to_owned())
|
.map(|s| s.to_owned())
|
||||||
.collect::<Vec<String>>();
|
.collect::<Vec<String>>();
|
||||||
invalid_string[7].clear();
|
invalid_string[9].clear();
|
||||||
invalid_string[7].push_str("invalid format");
|
invalid_string[9].push_str("invalid format");
|
||||||
let invalid_string = invalid_string.join(LIST_FORMAT_SEPARATOR);
|
let invalid_string = invalid_string.join(LIST_FORMAT_SEPARATOR);
|
||||||
output[2] = invalid_string.clone();
|
output[2] = invalid_string.clone();
|
||||||
let result = Ok(output);
|
let result = Ok(output);
|
||||||
|
@ -2,27 +2,115 @@ use once_cell::sync::Lazy;
|
|||||||
|
|
||||||
pub static LIBRARY_BEETS: Lazy<Vec<String>> = Lazy::new(|| -> Vec<String> {
|
pub static LIBRARY_BEETS: Lazy<Vec<String>> = Lazy::new(|| -> Vec<String> {
|
||||||
vec![
|
vec![
|
||||||
String::from("Album_Artist ‘A’ -*^- -*^- 1998 -*^- album_title a.a -*^- 1 -*^- track a.a.1 -*^- artist a.a.1 -*^- FLAC -*^- 992"),
|
String::from(
|
||||||
String::from("Album_Artist ‘A’ -*^- -*^- 1998 -*^- album_title a.a -*^- 2 -*^- track a.a.2 -*^- artist a.a.2.1; artist a.a.2.2 -*^- MP3 -*^- 320"),
|
"Album_Artist ‘A’ -*^- -*^- \
|
||||||
String::from("Album_Artist ‘A’ -*^- -*^- 1998 -*^- album_title a.a -*^- 3 -*^- track a.a.3 -*^- artist a.a.3 -*^- FLAC -*^- 1061"),
|
1998 -*^- 00 -*^- 00 -*^- album_title a.a -*^- \
|
||||||
String::from("Album_Artist ‘A’ -*^- -*^- 1998 -*^- album_title a.a -*^- 4 -*^- track a.a.4 -*^- artist a.a.4 -*^- FLAC -*^- 1042"),
|
1 -*^- track a.a.1 -*^- artist a.a.1 -*^- FLAC -*^- 992",
|
||||||
String::from("Album_Artist ‘A’ -*^- -*^- 2015 -*^- album_title a.b -*^- 1 -*^- track a.b.1 -*^- artist a.b.1 -*^- FLAC -*^- 1004"),
|
),
|
||||||
String::from("Album_Artist ‘A’ -*^- -*^- 2015 -*^- album_title a.b -*^- 2 -*^- track a.b.2 -*^- artist a.b.2 -*^- FLAC -*^- 1077"),
|
String::from(
|
||||||
String::from("Album_Artist ‘B’ -*^- -*^- 2003 -*^- album_title b.a -*^- 1 -*^- track b.a.1 -*^- artist b.a.1 -*^- MP3 -*^- 190"),
|
"Album_Artist ‘A’ -*^- -*^- \
|
||||||
String::from("Album_Artist ‘B’ -*^- -*^- 2003 -*^- album_title b.a -*^- 2 -*^- track b.a.2 -*^- artist b.a.2.1; artist b.a.2.2 -*^- MP3 -*^- 120"),
|
1998 -*^- 00 -*^- 00 -*^- album_title a.a -*^- \
|
||||||
String::from("Album_Artist ‘B’ -*^- -*^- 2008 -*^- album_title b.b -*^- 1 -*^- track b.b.1 -*^- artist b.b.1 -*^- FLAC -*^- 1077"),
|
2 -*^- track a.a.2 -*^- artist a.a.2.1; artist a.a.2.2 -*^- MP3 -*^- 320",
|
||||||
String::from("Album_Artist ‘B’ -*^- -*^- 2008 -*^- album_title b.b -*^- 2 -*^- track b.b.2 -*^- artist b.b.2.1; artist b.b.2.2 -*^- MP3 -*^- 320"),
|
),
|
||||||
String::from("Album_Artist ‘B’ -*^- -*^- 2009 -*^- album_title b.c -*^- 1 -*^- track b.c.1 -*^- artist b.c.1 -*^- MP3 -*^- 190"),
|
String::from(
|
||||||
String::from("Album_Artist ‘B’ -*^- -*^- 2009 -*^- album_title b.c -*^- 2 -*^- track b.c.2 -*^- artist b.c.2.1; artist b.c.2.2 -*^- MP3 -*^- 120"),
|
"Album_Artist ‘A’ -*^- -*^- \
|
||||||
String::from("Album_Artist ‘B’ -*^- -*^- 2015 -*^- album_title b.d -*^- 1 -*^- track b.d.1 -*^- artist b.d.1 -*^- MP3 -*^- 190"),
|
1998 -*^- 00 -*^- 00 -*^- album_title a.a -*^- \
|
||||||
String::from("Album_Artist ‘B’ -*^- -*^- 2015 -*^- album_title b.d -*^- 2 -*^- track b.d.2 -*^- artist b.d.2.1; artist b.d.2.2 -*^- MP3 -*^- 120"),
|
3 -*^- track a.a.3 -*^- artist a.a.3 -*^- FLAC -*^- 1061",
|
||||||
String::from("The Album_Artist ‘C’ -*^- Album_Artist ‘C’, The -*^- 1985 -*^- album_title c.a -*^- 1 -*^- track c.a.1 -*^- artist c.a.1 -*^- MP3 -*^- 320"),
|
),
|
||||||
String::from("The Album_Artist ‘C’ -*^- Album_Artist ‘C’, The -*^- 1985 -*^- album_title c.a -*^- 2 -*^- track c.a.2 -*^- artist c.a.2.1; artist c.a.2.2 -*^- MP3 -*^- 120"),
|
String::from(
|
||||||
String::from("The Album_Artist ‘C’ -*^- Album_Artist ‘C’, The -*^- 2018 -*^- album_title c.b -*^- 1 -*^- track c.b.1 -*^- artist c.b.1 -*^- FLAC -*^- 1041"),
|
"Album_Artist ‘A’ -*^- -*^- \
|
||||||
String::from("The Album_Artist ‘C’ -*^- Album_Artist ‘C’, The -*^- 2018 -*^- album_title c.b -*^- 2 -*^- track c.b.2 -*^- artist c.b.2.1; artist c.b.2.2 -*^- FLAC -*^- 756"),
|
1998 -*^- 00 -*^- 00 -*^- album_title a.a -*^- \
|
||||||
String::from("Album_Artist ‘D’ -*^- -*^- 1995 -*^- album_title d.a -*^- 1 -*^- track d.a.1 -*^- artist d.a.1 -*^- MP3 -*^- 120"),
|
4 -*^- track a.a.4 -*^- artist a.a.4 -*^- FLAC -*^- 1042",
|
||||||
String::from("Album_Artist ‘D’ -*^- -*^- 1995 -*^- album_title d.a -*^- 2 -*^- track d.a.2 -*^- artist d.a.2.1; artist d.a.2.2 -*^- MP3 -*^- 120"),
|
),
|
||||||
String::from("Album_Artist ‘D’ -*^- -*^- 2028 -*^- album_title d.b -*^- 1 -*^- track d.b.1 -*^- artist d.b.1 -*^- FLAC -*^- 841"),
|
String::from(
|
||||||
String::from("Album_Artist ‘D’ -*^- -*^- 2028 -*^- album_title d.b -*^- 2 -*^- track d.b.2 -*^- artist d.b.2.1; artist d.b.2.2 -*^- FLAC -*^- 756")
|
"Album_Artist ‘A’ -*^- -*^- \
|
||||||
|
2015 -*^- 04 -*^- 00 -*^- album_title a.b -*^- \
|
||||||
|
1 -*^- track a.b.1 -*^- artist a.b.1 -*^- FLAC -*^- 1004",
|
||||||
|
),
|
||||||
|
String::from(
|
||||||
|
"Album_Artist ‘A’ -*^- -*^- \
|
||||||
|
2015 -*^- 04 -*^- 00 -*^- album_title a.b -*^- \
|
||||||
|
2 -*^- track a.b.2 -*^- artist a.b.2 -*^- FLAC -*^- 1077",
|
||||||
|
),
|
||||||
|
String::from(
|
||||||
|
"Album_Artist ‘B’ -*^- -*^- \
|
||||||
|
2003 -*^- 06 -*^- 06 -*^- album_title b.a -*^- \
|
||||||
|
1 -*^- track b.a.1 -*^- artist b.a.1 -*^- MP3 -*^- 190",
|
||||||
|
),
|
||||||
|
String::from(
|
||||||
|
"Album_Artist ‘B’ -*^- -*^- \
|
||||||
|
2003 -*^- 06 -*^- 06 -*^- album_title b.a -*^- \
|
||||||
|
2 -*^- track b.a.2 -*^- artist b.a.2.1; artist b.a.2.2 -*^- MP3 -*^- 120",
|
||||||
|
),
|
||||||
|
String::from(
|
||||||
|
"Album_Artist ‘B’ -*^- -*^- \
|
||||||
|
2008 -*^- 00 -*^- 00 -*^- album_title b.b -*^- \
|
||||||
|
1 -*^- track b.b.1 -*^- artist b.b.1 -*^- FLAC -*^- 1077",
|
||||||
|
),
|
||||||
|
String::from(
|
||||||
|
"Album_Artist ‘B’ -*^- -*^- \
|
||||||
|
2008 -*^- 00 -*^- 00 -*^- album_title b.b -*^- \
|
||||||
|
2 -*^- track b.b.2 -*^- artist b.b.2.1; artist b.b.2.2 -*^- MP3 -*^- 320",
|
||||||
|
),
|
||||||
|
String::from(
|
||||||
|
"Album_Artist ‘B’ -*^- -*^- \
|
||||||
|
2009 -*^- 00 -*^- 00 -*^- album_title b.c -*^- \
|
||||||
|
1 -*^- track b.c.1 -*^- artist b.c.1 -*^- MP3 -*^- 190",
|
||||||
|
),
|
||||||
|
String::from(
|
||||||
|
"Album_Artist ‘B’ -*^- -*^- \
|
||||||
|
2009 -*^- 00 -*^- 00 -*^- album_title b.c -*^- \
|
||||||
|
2 -*^- track b.c.2 -*^- artist b.c.2.1; artist b.c.2.2 -*^- MP3 -*^- 120",
|
||||||
|
),
|
||||||
|
String::from(
|
||||||
|
"Album_Artist ‘B’ -*^- -*^- \
|
||||||
|
2015 -*^- 00 -*^- 00 -*^- album_title b.d -*^- \
|
||||||
|
1 -*^- track b.d.1 -*^- artist b.d.1 -*^- MP3 -*^- 190",
|
||||||
|
),
|
||||||
|
String::from(
|
||||||
|
"Album_Artist ‘B’ -*^- -*^- \
|
||||||
|
2015 -*^- 00 -*^- 00 -*^- album_title b.d -*^- \
|
||||||
|
2 -*^- track b.d.2 -*^- artist b.d.2.1; artist b.d.2.2 -*^- MP3 -*^- 120",
|
||||||
|
),
|
||||||
|
String::from(
|
||||||
|
"The Album_Artist ‘C’ -*^- Album_Artist ‘C’, The -*^- \
|
||||||
|
1985 -*^- 00 -*^- 00 -*^- album_title c.a -*^- \
|
||||||
|
1 -*^- track c.a.1 -*^- artist c.a.1 -*^- MP3 -*^- 320",
|
||||||
|
),
|
||||||
|
String::from(
|
||||||
|
"The Album_Artist ‘C’ -*^- Album_Artist ‘C’, The -*^- \
|
||||||
|
1985 -*^- 00 -*^- 00 -*^- album_title c.a -*^- \
|
||||||
|
2 -*^- track c.a.2 -*^- artist c.a.2.1; artist c.a.2.2 -*^- MP3 -*^- 120",
|
||||||
|
),
|
||||||
|
String::from(
|
||||||
|
"The Album_Artist ‘C’ -*^- Album_Artist ‘C’, The -*^- \
|
||||||
|
2018 -*^- 00 -*^- 00 -*^- album_title c.b -*^- \
|
||||||
|
1 -*^- track c.b.1 -*^- artist c.b.1 -*^- FLAC -*^- 1041",
|
||||||
|
),
|
||||||
|
String::from(
|
||||||
|
"The Album_Artist ‘C’ -*^- Album_Artist ‘C’, The -*^- \
|
||||||
|
2018 -*^- 00 -*^- 00 -*^- album_title c.b -*^- \
|
||||||
|
2 -*^- track c.b.2 -*^- artist c.b.2.1; artist c.b.2.2 -*^- FLAC -*^- 756",
|
||||||
|
),
|
||||||
|
String::from(
|
||||||
|
"Album_Artist ‘D’ -*^- -*^- \
|
||||||
|
1995 -*^- 00 -*^- 00 -*^- album_title d.a -*^- \
|
||||||
|
1 -*^- track d.a.1 -*^- artist d.a.1 -*^- MP3 -*^- 120",
|
||||||
|
),
|
||||||
|
String::from(
|
||||||
|
"Album_Artist ‘D’ -*^- -*^- \
|
||||||
|
1995 -*^- 00 -*^- 00 -*^- album_title d.a -*^- \
|
||||||
|
2 -*^- track d.a.2 -*^- artist d.a.2.1; artist d.a.2.2 -*^- MP3 -*^- 120",
|
||||||
|
),
|
||||||
|
String::from(
|
||||||
|
"Album_Artist ‘D’ -*^- -*^- \
|
||||||
|
2028 -*^- 00 -*^- 00 -*^- album_title d.b -*^- \
|
||||||
|
1 -*^- track d.b.1 -*^- artist d.b.1 -*^- FLAC -*^- 841",
|
||||||
|
),
|
||||||
|
String::from(
|
||||||
|
"Album_Artist ‘D’ -*^- -*^- \
|
||||||
|
2028 -*^- 00 -*^- 00 -*^- album_title d.b -*^- \
|
||||||
|
2 -*^- track d.b.2 -*^- artist d.b.2.1; artist d.b.2.2 -*^- FLAC -*^- 756",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
@ -8,7 +8,7 @@ use std::{collections::HashSet, fmt, num::ParseIntError, str::Utf8Error};
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use mockall::automock;
|
use mockall::automock;
|
||||||
|
|
||||||
use crate::core::collection::track::Format;
|
use crate::core::collection::{album::AlbumMonth, track::TrackFormat};
|
||||||
|
|
||||||
/// Trait for interacting with the music library.
|
/// Trait for interacting with the music library.
|
||||||
#[cfg_attr(test, automock)]
|
#[cfg_attr(test, automock)]
|
||||||
@ -32,11 +32,13 @@ pub struct Item {
|
|||||||
pub album_artist: String,
|
pub album_artist: String,
|
||||||
pub album_artist_sort: Option<String>,
|
pub album_artist_sort: Option<String>,
|
||||||
pub album_year: u32,
|
pub album_year: u32,
|
||||||
|
pub album_month: AlbumMonth,
|
||||||
|
pub album_day: u8,
|
||||||
pub album_title: String,
|
pub album_title: String,
|
||||||
pub track_number: u32,
|
pub track_number: u32,
|
||||||
pub track_title: String,
|
pub track_title: String,
|
||||||
pub track_artist: Vec<String>,
|
pub track_artist: Vec<String>,
|
||||||
pub track_format: Format,
|
pub track_format: TrackFormat,
|
||||||
pub track_bitrate: u32,
|
pub track_bitrate: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,10 +48,13 @@ pub enum Field {
|
|||||||
AlbumArtist(String),
|
AlbumArtist(String),
|
||||||
AlbumArtistSort(String),
|
AlbumArtistSort(String),
|
||||||
AlbumYear(u32),
|
AlbumYear(u32),
|
||||||
|
AlbumMonth(AlbumMonth),
|
||||||
|
AlbumDay(u8),
|
||||||
AlbumTitle(String),
|
AlbumTitle(String),
|
||||||
TrackNumber(u32),
|
TrackNumber(u32),
|
||||||
TrackTitle(String),
|
TrackTitle(String),
|
||||||
TrackArtist(Vec<String>),
|
TrackArtist(Vec<String>),
|
||||||
|
TrackFormat(TrackFormat),
|
||||||
All(String),
|
All(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
use crate::core::{collection::track::Format, library::Item};
|
use crate::core::{
|
||||||
|
collection::{album::AlbumMonth, track::TrackFormat},
|
||||||
|
library::Item,
|
||||||
|
};
|
||||||
|
|
||||||
pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
|
pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
|
||||||
vec![
|
vec![
|
||||||
@ -8,17 +11,21 @@ pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
|
|||||||
album_artist: String::from("Album_Artist ‘A’"),
|
album_artist: String::from("Album_Artist ‘A’"),
|
||||||
album_artist_sort: None,
|
album_artist_sort: None,
|
||||||
album_year: 1998,
|
album_year: 1998,
|
||||||
|
album_month: AlbumMonth::None,
|
||||||
|
album_day: 0,
|
||||||
album_title: String::from("album_title a.a"),
|
album_title: String::from("album_title a.a"),
|
||||||
track_number: 1,
|
track_number: 1,
|
||||||
track_title: String::from("track a.a.1"),
|
track_title: String::from("track a.a.1"),
|
||||||
track_artist: vec![String::from("artist a.a.1")],
|
track_artist: vec![String::from("artist a.a.1")],
|
||||||
track_format: Format::Flac,
|
track_format: TrackFormat::Flac,
|
||||||
track_bitrate: 992,
|
track_bitrate: 992,
|
||||||
},
|
},
|
||||||
Item {
|
Item {
|
||||||
album_artist: String::from("Album_Artist ‘A’"),
|
album_artist: String::from("Album_Artist ‘A’"),
|
||||||
album_artist_sort: None,
|
album_artist_sort: None,
|
||||||
album_year: 1998,
|
album_year: 1998,
|
||||||
|
album_month: AlbumMonth::None,
|
||||||
|
album_day: 0,
|
||||||
album_title: String::from("album_title a.a"),
|
album_title: String::from("album_title a.a"),
|
||||||
track_number: 2,
|
track_number: 2,
|
||||||
track_title: String::from("track a.a.2"),
|
track_title: String::from("track a.a.2"),
|
||||||
@ -26,68 +33,80 @@ pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
|
|||||||
String::from("artist a.a.2.1"),
|
String::from("artist a.a.2.1"),
|
||||||
String::from("artist a.a.2.2"),
|
String::from("artist a.a.2.2"),
|
||||||
],
|
],
|
||||||
track_format: Format::Mp3,
|
track_format: TrackFormat::Mp3,
|
||||||
track_bitrate: 320,
|
track_bitrate: 320,
|
||||||
},
|
},
|
||||||
Item {
|
Item {
|
||||||
album_artist: String::from("Album_Artist ‘A’"),
|
album_artist: String::from("Album_Artist ‘A’"),
|
||||||
album_artist_sort: None,
|
album_artist_sort: None,
|
||||||
album_year: 1998,
|
album_year: 1998,
|
||||||
|
album_month: AlbumMonth::None,
|
||||||
|
album_day: 0,
|
||||||
album_title: String::from("album_title a.a"),
|
album_title: String::from("album_title a.a"),
|
||||||
track_number: 3,
|
track_number: 3,
|
||||||
track_title: String::from("track a.a.3"),
|
track_title: String::from("track a.a.3"),
|
||||||
track_artist: vec![String::from("artist a.a.3")],
|
track_artist: vec![String::from("artist a.a.3")],
|
||||||
track_format: Format::Flac,
|
track_format: TrackFormat::Flac,
|
||||||
track_bitrate: 1061,
|
track_bitrate: 1061,
|
||||||
},
|
},
|
||||||
Item {
|
Item {
|
||||||
album_artist: String::from("Album_Artist ‘A’"),
|
album_artist: String::from("Album_Artist ‘A’"),
|
||||||
album_artist_sort: None,
|
album_artist_sort: None,
|
||||||
album_year: 1998,
|
album_year: 1998,
|
||||||
|
album_month: AlbumMonth::None,
|
||||||
|
album_day: 0,
|
||||||
album_title: String::from("album_title a.a"),
|
album_title: String::from("album_title a.a"),
|
||||||
track_number: 4,
|
track_number: 4,
|
||||||
track_title: String::from("track a.a.4"),
|
track_title: String::from("track a.a.4"),
|
||||||
track_artist: vec![String::from("artist a.a.4")],
|
track_artist: vec![String::from("artist a.a.4")],
|
||||||
track_format: Format::Flac,
|
track_format: TrackFormat::Flac,
|
||||||
track_bitrate: 1042,
|
track_bitrate: 1042,
|
||||||
},
|
},
|
||||||
Item {
|
Item {
|
||||||
album_artist: String::from("Album_Artist ‘A’"),
|
album_artist: String::from("Album_Artist ‘A’"),
|
||||||
album_artist_sort: None,
|
album_artist_sort: None,
|
||||||
album_year: 2015,
|
album_year: 2015,
|
||||||
|
album_month: AlbumMonth::April,
|
||||||
|
album_day: 0,
|
||||||
album_title: String::from("album_title a.b"),
|
album_title: String::from("album_title a.b"),
|
||||||
track_number: 1,
|
track_number: 1,
|
||||||
track_title: String::from("track a.b.1"),
|
track_title: String::from("track a.b.1"),
|
||||||
track_artist: vec![String::from("artist a.b.1")],
|
track_artist: vec![String::from("artist a.b.1")],
|
||||||
track_format: Format::Flac,
|
track_format: TrackFormat::Flac,
|
||||||
track_bitrate: 1004,
|
track_bitrate: 1004,
|
||||||
},
|
},
|
||||||
Item {
|
Item {
|
||||||
album_artist: String::from("Album_Artist ‘A’"),
|
album_artist: String::from("Album_Artist ‘A’"),
|
||||||
album_artist_sort: None,
|
album_artist_sort: None,
|
||||||
album_year: 2015,
|
album_year: 2015,
|
||||||
|
album_month: AlbumMonth::April,
|
||||||
|
album_day: 0,
|
||||||
album_title: String::from("album_title a.b"),
|
album_title: String::from("album_title a.b"),
|
||||||
track_number: 2,
|
track_number: 2,
|
||||||
track_title: String::from("track a.b.2"),
|
track_title: String::from("track a.b.2"),
|
||||||
track_artist: vec![String::from("artist a.b.2")],
|
track_artist: vec![String::from("artist a.b.2")],
|
||||||
track_format: Format::Flac,
|
track_format: TrackFormat::Flac,
|
||||||
track_bitrate: 1077,
|
track_bitrate: 1077,
|
||||||
},
|
},
|
||||||
Item {
|
Item {
|
||||||
album_artist: String::from("Album_Artist ‘B’"),
|
album_artist: String::from("Album_Artist ‘B’"),
|
||||||
album_artist_sort: None,
|
album_artist_sort: None,
|
||||||
album_year: 2003,
|
album_year: 2003,
|
||||||
|
album_month: AlbumMonth::June,
|
||||||
|
album_day: 6,
|
||||||
album_title: String::from("album_title b.a"),
|
album_title: String::from("album_title b.a"),
|
||||||
track_number: 1,
|
track_number: 1,
|
||||||
track_title: String::from("track b.a.1"),
|
track_title: String::from("track b.a.1"),
|
||||||
track_artist: vec![String::from("artist b.a.1")],
|
track_artist: vec![String::from("artist b.a.1")],
|
||||||
track_format: Format::Mp3,
|
track_format: TrackFormat::Mp3,
|
||||||
track_bitrate: 190,
|
track_bitrate: 190,
|
||||||
},
|
},
|
||||||
Item {
|
Item {
|
||||||
album_artist: String::from("Album_Artist ‘B’"),
|
album_artist: String::from("Album_Artist ‘B’"),
|
||||||
album_artist_sort: None,
|
album_artist_sort: None,
|
||||||
album_year: 2003,
|
album_year: 2003,
|
||||||
|
album_month: AlbumMonth::June,
|
||||||
|
album_day: 6,
|
||||||
album_title: String::from("album_title b.a"),
|
album_title: String::from("album_title b.a"),
|
||||||
track_number: 2,
|
track_number: 2,
|
||||||
track_title: String::from("track b.a.2"),
|
track_title: String::from("track b.a.2"),
|
||||||
@ -95,24 +114,28 @@ pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
|
|||||||
String::from("artist b.a.2.1"),
|
String::from("artist b.a.2.1"),
|
||||||
String::from("artist b.a.2.2"),
|
String::from("artist b.a.2.2"),
|
||||||
],
|
],
|
||||||
track_format: Format::Mp3,
|
track_format: TrackFormat::Mp3,
|
||||||
track_bitrate: 120,
|
track_bitrate: 120,
|
||||||
},
|
},
|
||||||
Item {
|
Item {
|
||||||
album_artist: String::from("Album_Artist ‘B’"),
|
album_artist: String::from("Album_Artist ‘B’"),
|
||||||
album_artist_sort: None,
|
album_artist_sort: None,
|
||||||
album_year: 2008,
|
album_year: 2008,
|
||||||
|
album_month: AlbumMonth::None,
|
||||||
|
album_day: 0,
|
||||||
album_title: String::from("album_title b.b"),
|
album_title: String::from("album_title b.b"),
|
||||||
track_number: 1,
|
track_number: 1,
|
||||||
track_title: String::from("track b.b.1"),
|
track_title: String::from("track b.b.1"),
|
||||||
track_artist: vec![String::from("artist b.b.1")],
|
track_artist: vec![String::from("artist b.b.1")],
|
||||||
track_format: Format::Flac,
|
track_format: TrackFormat::Flac,
|
||||||
track_bitrate: 1077,
|
track_bitrate: 1077,
|
||||||
},
|
},
|
||||||
Item {
|
Item {
|
||||||
album_artist: String::from("Album_Artist ‘B’"),
|
album_artist: String::from("Album_Artist ‘B’"),
|
||||||
album_artist_sort: None,
|
album_artist_sort: None,
|
||||||
album_year: 2008,
|
album_year: 2008,
|
||||||
|
album_month: AlbumMonth::None,
|
||||||
|
album_day: 0,
|
||||||
album_title: String::from("album_title b.b"),
|
album_title: String::from("album_title b.b"),
|
||||||
track_number: 2,
|
track_number: 2,
|
||||||
track_title: String::from("track b.b.2"),
|
track_title: String::from("track b.b.2"),
|
||||||
@ -120,24 +143,28 @@ pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
|
|||||||
String::from("artist b.b.2.1"),
|
String::from("artist b.b.2.1"),
|
||||||
String::from("artist b.b.2.2"),
|
String::from("artist b.b.2.2"),
|
||||||
],
|
],
|
||||||
track_format: Format::Mp3,
|
track_format: TrackFormat::Mp3,
|
||||||
track_bitrate: 320,
|
track_bitrate: 320,
|
||||||
},
|
},
|
||||||
Item {
|
Item {
|
||||||
album_artist: String::from("Album_Artist ‘B’"),
|
album_artist: String::from("Album_Artist ‘B’"),
|
||||||
album_artist_sort: None,
|
album_artist_sort: None,
|
||||||
album_year: 2009,
|
album_year: 2009,
|
||||||
|
album_month: AlbumMonth::None,
|
||||||
|
album_day: 0,
|
||||||
album_title: String::from("album_title b.c"),
|
album_title: String::from("album_title b.c"),
|
||||||
track_number: 1,
|
track_number: 1,
|
||||||
track_title: String::from("track b.c.1"),
|
track_title: String::from("track b.c.1"),
|
||||||
track_artist: vec![String::from("artist b.c.1")],
|
track_artist: vec![String::from("artist b.c.1")],
|
||||||
track_format: Format::Mp3,
|
track_format: TrackFormat::Mp3,
|
||||||
track_bitrate: 190,
|
track_bitrate: 190,
|
||||||
},
|
},
|
||||||
Item {
|
Item {
|
||||||
album_artist: String::from("Album_Artist ‘B’"),
|
album_artist: String::from("Album_Artist ‘B’"),
|
||||||
album_artist_sort: None,
|
album_artist_sort: None,
|
||||||
album_year: 2009,
|
album_year: 2009,
|
||||||
|
album_month: AlbumMonth::None,
|
||||||
|
album_day: 0,
|
||||||
album_title: String::from("album_title b.c"),
|
album_title: String::from("album_title b.c"),
|
||||||
track_number: 2,
|
track_number: 2,
|
||||||
track_title: String::from("track b.c.2"),
|
track_title: String::from("track b.c.2"),
|
||||||
@ -145,24 +172,28 @@ pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
|
|||||||
String::from("artist b.c.2.1"),
|
String::from("artist b.c.2.1"),
|
||||||
String::from("artist b.c.2.2"),
|
String::from("artist b.c.2.2"),
|
||||||
],
|
],
|
||||||
track_format: Format::Mp3,
|
track_format: TrackFormat::Mp3,
|
||||||
track_bitrate: 120,
|
track_bitrate: 120,
|
||||||
},
|
},
|
||||||
Item {
|
Item {
|
||||||
album_artist: String::from("Album_Artist ‘B’"),
|
album_artist: String::from("Album_Artist ‘B’"),
|
||||||
album_artist_sort: None,
|
album_artist_sort: None,
|
||||||
album_year: 2015,
|
album_year: 2015,
|
||||||
|
album_month: AlbumMonth::None,
|
||||||
|
album_day: 0,
|
||||||
album_title: String::from("album_title b.d"),
|
album_title: String::from("album_title b.d"),
|
||||||
track_number: 1,
|
track_number: 1,
|
||||||
track_title: String::from("track b.d.1"),
|
track_title: String::from("track b.d.1"),
|
||||||
track_artist: vec![String::from("artist b.d.1")],
|
track_artist: vec![String::from("artist b.d.1")],
|
||||||
track_format: Format::Mp3,
|
track_format: TrackFormat::Mp3,
|
||||||
track_bitrate: 190,
|
track_bitrate: 190,
|
||||||
},
|
},
|
||||||
Item {
|
Item {
|
||||||
album_artist: String::from("Album_Artist ‘B’"),
|
album_artist: String::from("Album_Artist ‘B’"),
|
||||||
album_artist_sort: None,
|
album_artist_sort: None,
|
||||||
album_year: 2015,
|
album_year: 2015,
|
||||||
|
album_month: AlbumMonth::None,
|
||||||
|
album_day: 0,
|
||||||
album_title: String::from("album_title b.d"),
|
album_title: String::from("album_title b.d"),
|
||||||
track_number: 2,
|
track_number: 2,
|
||||||
track_title: String::from("track b.d.2"),
|
track_title: String::from("track b.d.2"),
|
||||||
@ -170,24 +201,28 @@ pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
|
|||||||
String::from("artist b.d.2.1"),
|
String::from("artist b.d.2.1"),
|
||||||
String::from("artist b.d.2.2"),
|
String::from("artist b.d.2.2"),
|
||||||
],
|
],
|
||||||
track_format: Format::Mp3,
|
track_format: TrackFormat::Mp3,
|
||||||
track_bitrate: 120,
|
track_bitrate: 120,
|
||||||
},
|
},
|
||||||
Item {
|
Item {
|
||||||
album_artist: String::from("The Album_Artist ‘C’"),
|
album_artist: String::from("The Album_Artist ‘C’"),
|
||||||
album_artist_sort: Some(String::from("Album_Artist ‘C’, The")),
|
album_artist_sort: Some(String::from("Album_Artist ‘C’, The")),
|
||||||
album_year: 1985,
|
album_year: 1985,
|
||||||
|
album_month: AlbumMonth::None,
|
||||||
|
album_day: 0,
|
||||||
album_title: String::from("album_title c.a"),
|
album_title: String::from("album_title c.a"),
|
||||||
track_number: 1,
|
track_number: 1,
|
||||||
track_title: String::from("track c.a.1"),
|
track_title: String::from("track c.a.1"),
|
||||||
track_artist: vec![String::from("artist c.a.1")],
|
track_artist: vec![String::from("artist c.a.1")],
|
||||||
track_format: Format::Mp3,
|
track_format: TrackFormat::Mp3,
|
||||||
track_bitrate: 320,
|
track_bitrate: 320,
|
||||||
},
|
},
|
||||||
Item {
|
Item {
|
||||||
album_artist: String::from("The Album_Artist ‘C’"),
|
album_artist: String::from("The Album_Artist ‘C’"),
|
||||||
album_artist_sort: Some(String::from("Album_Artist ‘C’, The")),
|
album_artist_sort: Some(String::from("Album_Artist ‘C’, The")),
|
||||||
album_year: 1985,
|
album_year: 1985,
|
||||||
|
album_month: AlbumMonth::None,
|
||||||
|
album_day: 0,
|
||||||
album_title: String::from("album_title c.a"),
|
album_title: String::from("album_title c.a"),
|
||||||
track_number: 2,
|
track_number: 2,
|
||||||
track_title: String::from("track c.a.2"),
|
track_title: String::from("track c.a.2"),
|
||||||
@ -195,24 +230,28 @@ pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
|
|||||||
String::from("artist c.a.2.1"),
|
String::from("artist c.a.2.1"),
|
||||||
String::from("artist c.a.2.2"),
|
String::from("artist c.a.2.2"),
|
||||||
],
|
],
|
||||||
track_format: Format::Mp3,
|
track_format: TrackFormat::Mp3,
|
||||||
track_bitrate: 120,
|
track_bitrate: 120,
|
||||||
},
|
},
|
||||||
Item {
|
Item {
|
||||||
album_artist: String::from("The Album_Artist ‘C’"),
|
album_artist: String::from("The Album_Artist ‘C’"),
|
||||||
album_artist_sort: Some(String::from("Album_Artist ‘C’, The")),
|
album_artist_sort: Some(String::from("Album_Artist ‘C’, The")),
|
||||||
album_year: 2018,
|
album_year: 2018,
|
||||||
|
album_month: AlbumMonth::None,
|
||||||
|
album_day: 0,
|
||||||
album_title: String::from("album_title c.b"),
|
album_title: String::from("album_title c.b"),
|
||||||
track_number: 1,
|
track_number: 1,
|
||||||
track_title: String::from("track c.b.1"),
|
track_title: String::from("track c.b.1"),
|
||||||
track_artist: vec![String::from("artist c.b.1")],
|
track_artist: vec![String::from("artist c.b.1")],
|
||||||
track_format: Format::Flac,
|
track_format: TrackFormat::Flac,
|
||||||
track_bitrate: 1041,
|
track_bitrate: 1041,
|
||||||
},
|
},
|
||||||
Item {
|
Item {
|
||||||
album_artist: String::from("The Album_Artist ‘C’"),
|
album_artist: String::from("The Album_Artist ‘C’"),
|
||||||
album_artist_sort: Some(String::from("Album_Artist ‘C’, The")),
|
album_artist_sort: Some(String::from("Album_Artist ‘C’, The")),
|
||||||
album_year: 2018,
|
album_year: 2018,
|
||||||
|
album_month: AlbumMonth::None,
|
||||||
|
album_day: 0,
|
||||||
album_title: String::from("album_title c.b"),
|
album_title: String::from("album_title c.b"),
|
||||||
track_number: 2,
|
track_number: 2,
|
||||||
track_title: String::from("track c.b.2"),
|
track_title: String::from("track c.b.2"),
|
||||||
@ -220,24 +259,28 @@ pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
|
|||||||
String::from("artist c.b.2.1"),
|
String::from("artist c.b.2.1"),
|
||||||
String::from("artist c.b.2.2"),
|
String::from("artist c.b.2.2"),
|
||||||
],
|
],
|
||||||
track_format: Format::Flac,
|
track_format: TrackFormat::Flac,
|
||||||
track_bitrate: 756,
|
track_bitrate: 756,
|
||||||
},
|
},
|
||||||
Item {
|
Item {
|
||||||
album_artist: String::from("Album_Artist ‘D’"),
|
album_artist: String::from("Album_Artist ‘D’"),
|
||||||
album_artist_sort: None,
|
album_artist_sort: None,
|
||||||
album_year: 1995,
|
album_year: 1995,
|
||||||
|
album_month: AlbumMonth::None,
|
||||||
|
album_day: 0,
|
||||||
album_title: String::from("album_title d.a"),
|
album_title: String::from("album_title d.a"),
|
||||||
track_number: 1,
|
track_number: 1,
|
||||||
track_title: String::from("track d.a.1"),
|
track_title: String::from("track d.a.1"),
|
||||||
track_artist: vec![String::from("artist d.a.1")],
|
track_artist: vec![String::from("artist d.a.1")],
|
||||||
track_format: Format::Mp3,
|
track_format: TrackFormat::Mp3,
|
||||||
track_bitrate: 120,
|
track_bitrate: 120,
|
||||||
},
|
},
|
||||||
Item {
|
Item {
|
||||||
album_artist: String::from("Album_Artist ‘D’"),
|
album_artist: String::from("Album_Artist ‘D’"),
|
||||||
album_artist_sort: None,
|
album_artist_sort: None,
|
||||||
album_year: 1995,
|
album_year: 1995,
|
||||||
|
album_month: AlbumMonth::None,
|
||||||
|
album_day: 0,
|
||||||
album_title: String::from("album_title d.a"),
|
album_title: String::from("album_title d.a"),
|
||||||
track_number: 2,
|
track_number: 2,
|
||||||
track_title: String::from("track d.a.2"),
|
track_title: String::from("track d.a.2"),
|
||||||
@ -245,24 +288,28 @@ pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
|
|||||||
String::from("artist d.a.2.1"),
|
String::from("artist d.a.2.1"),
|
||||||
String::from("artist d.a.2.2"),
|
String::from("artist d.a.2.2"),
|
||||||
],
|
],
|
||||||
track_format: Format::Mp3,
|
track_format: TrackFormat::Mp3,
|
||||||
track_bitrate: 120,
|
track_bitrate: 120,
|
||||||
},
|
},
|
||||||
Item {
|
Item {
|
||||||
album_artist: String::from("Album_Artist ‘D’"),
|
album_artist: String::from("Album_Artist ‘D’"),
|
||||||
album_artist_sort: None,
|
album_artist_sort: None,
|
||||||
album_year: 2028,
|
album_year: 2028,
|
||||||
|
album_month: AlbumMonth::None,
|
||||||
|
album_day: 0,
|
||||||
album_title: String::from("album_title d.b"),
|
album_title: String::from("album_title d.b"),
|
||||||
track_number: 1,
|
track_number: 1,
|
||||||
track_title: String::from("track d.b.1"),
|
track_title: String::from("track d.b.1"),
|
||||||
track_artist: vec![String::from("artist d.b.1")],
|
track_artist: vec![String::from("artist d.b.1")],
|
||||||
track_format: Format::Flac,
|
track_format: TrackFormat::Flac,
|
||||||
track_bitrate: 841,
|
track_bitrate: 841,
|
||||||
},
|
},
|
||||||
Item {
|
Item {
|
||||||
album_artist: String::from("Album_Artist ‘D’"),
|
album_artist: String::from("Album_Artist ‘D’"),
|
||||||
album_artist_sort: None,
|
album_artist_sort: None,
|
||||||
album_year: 2028,
|
album_year: 2028,
|
||||||
|
album_month: AlbumMonth::None,
|
||||||
|
album_day: 0,
|
||||||
album_title: String::from("album_title d.b"),
|
album_title: String::from("album_title d.b"),
|
||||||
track_number: 2,
|
track_number: 2,
|
||||||
track_title: String::from("track d.b.2"),
|
track_title: String::from("track d.b.2"),
|
||||||
@ -270,7 +317,7 @@ pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
|
|||||||
String::from("artist d.b.2.1"),
|
String::from("artist d.b.2.1"),
|
||||||
String::from("artist d.b.2.2"),
|
String::from("artist d.b.2.2"),
|
||||||
],
|
],
|
||||||
track_format: Format::Flac,
|
track_format: TrackFormat::Flac,
|
||||||
track_bitrate: 756,
|
track_bitrate: 756,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@ -2,10 +2,10 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
use crate::core::{
|
use crate::core::{
|
||||||
collection::{
|
collection::{
|
||||||
album::{Album, AlbumId},
|
album::{Album, AlbumDate, AlbumId, AlbumSeq},
|
||||||
artist::{Artist, ArtistId},
|
artist::{Artist, ArtistId, MusicBrainz},
|
||||||
track::{Quality, Track, TrackId},
|
track::{Track, TrackId, TrackNum, TrackQuality},
|
||||||
Collection, Merge,
|
Collection, MergeCollections,
|
||||||
},
|
},
|
||||||
database::IDatabase,
|
database::IDatabase,
|
||||||
library::{ILibrary, Item, Query},
|
library::{ILibrary, Item, Query},
|
||||||
@ -73,19 +73,7 @@ impl<LIB, DB> MusicHoard<LIB, DB> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn merge_collections(&self) -> Collection {
|
fn merge_collections(&self) -> Collection {
|
||||||
let mut primary = self.library_cache.clone();
|
MergeCollections::merge(self.library_cache.clone(), self.database_cache.clone())
|
||||||
for secondary_artist in self.database_cache.iter().cloned() {
|
|
||||||
if let Some(ref mut primary_artist) = primary.get_mut(&secondary_artist.id) {
|
|
||||||
primary_artist.merge_in_place(secondary_artist);
|
|
||||||
} else {
|
|
||||||
primary.insert(secondary_artist.id.clone(), secondary_artist);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut collection: Collection = primary.into_values().collect();
|
|
||||||
Self::sort_artists(&mut collection);
|
|
||||||
|
|
||||||
collection
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn items_to_artists(items: Vec<Item>) -> Result<HashMap<ArtistId, Artist>, Error> {
|
fn items_to_artists(items: Vec<Item>) -> Result<HashMap<ArtistId, Artist>, Error> {
|
||||||
@ -99,17 +87,22 @@ impl<LIB, DB> MusicHoard<LIB, DB> {
|
|||||||
let artist_sort = item.album_artist_sort.map(|s| ArtistId { name: s });
|
let artist_sort = item.album_artist_sort.map(|s| ArtistId { name: s });
|
||||||
|
|
||||||
let album_id = AlbumId {
|
let album_id = AlbumId {
|
||||||
year: item.album_year,
|
|
||||||
title: item.album_title,
|
title: item.album_title,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let album_date = AlbumDate {
|
||||||
|
year: item.album_year,
|
||||||
|
month: item.album_month,
|
||||||
|
day: item.album_day,
|
||||||
|
};
|
||||||
|
|
||||||
let track = Track {
|
let track = Track {
|
||||||
id: TrackId {
|
id: TrackId {
|
||||||
number: item.track_number,
|
|
||||||
title: item.track_title,
|
title: item.track_title,
|
||||||
},
|
},
|
||||||
|
number: TrackNum(item.track_number),
|
||||||
artist: item.track_artist,
|
artist: item.track_artist,
|
||||||
quality: Quality {
|
quality: TrackQuality {
|
||||||
format: item.track_format,
|
format: item.track_format,
|
||||||
bitrate: item.track_bitrate,
|
bitrate: item.track_bitrate,
|
||||||
},
|
},
|
||||||
@ -149,6 +142,8 @@ impl<LIB, DB> MusicHoard<LIB, DB> {
|
|||||||
Some(album) => album.tracks.push(track),
|
Some(album) => album.tracks.push(track),
|
||||||
None => artist.albums.push(Album {
|
None => artist.albums.push(Album {
|
||||||
id: album_id,
|
id: album_id,
|
||||||
|
date: album_date,
|
||||||
|
seq: AlbumSeq(0),
|
||||||
tracks: vec![track],
|
tracks: vec![track],
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
@ -176,6 +171,22 @@ impl<LIB, DB> MusicHoard<LIB, DB> {
|
|||||||
Error::CollectionError(format!("artist '{}' is not in the collection", artist_id))
|
Error::CollectionError(format!("artist '{}' is not in the collection", artist_id))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_album_mut<'a>(artist: &'a mut Artist, album_id: &AlbumId) -> Option<&'a mut Album> {
|
||||||
|
artist.albums.iter_mut().find(|a| &a.id == album_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_album_mut_or_err<'a>(
|
||||||
|
artist: &'a mut Artist,
|
||||||
|
album_id: &AlbumId,
|
||||||
|
) -> Result<&'a mut Album, Error> {
|
||||||
|
Self::get_album_mut(artist, album_id).ok_or_else(|| {
|
||||||
|
Error::CollectionError(format!(
|
||||||
|
"album '{}' does not belong to the artist",
|
||||||
|
album_id
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<LIB: ILibrary> MusicHoard<LIB, NoDatabase> {
|
impl<LIB: ILibrary> MusicHoard<LIB, NoDatabase> {
|
||||||
@ -248,39 +259,61 @@ impl<LIB, DB: IDatabase> MusicHoard<LIB, DB> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_collection<F>(&mut self, func: F) -> Result<(), Error>
|
fn update_collection<FnColl>(&mut self, fn_coll: FnColl) -> Result<(), Error>
|
||||||
where
|
where
|
||||||
F: FnOnce(&mut Collection),
|
FnColl: FnOnce(&mut Collection),
|
||||||
{
|
{
|
||||||
func(&mut self.pre_commit);
|
fn_coll(&mut self.pre_commit);
|
||||||
self.commit()
|
self.commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_artist_and<ID: AsRef<ArtistId>, F1, F2>(
|
fn update_artist_and<FnArtist, FnColl>(
|
||||||
&mut self,
|
&mut self,
|
||||||
artist_id: ID,
|
artist_id: &ArtistId,
|
||||||
f1: F1,
|
fn_artist: FnArtist,
|
||||||
f2: F2,
|
fn_coll: FnColl,
|
||||||
) -> Result<(), Error>
|
) -> Result<(), Error>
|
||||||
where
|
where
|
||||||
F1: FnOnce(&mut Artist),
|
FnArtist: FnOnce(&mut Artist),
|
||||||
F2: FnOnce(&mut Collection),
|
FnColl: FnOnce(&mut Collection),
|
||||||
{
|
{
|
||||||
f1(Self::get_artist_mut_or_err(
|
let artist = Self::get_artist_mut_or_err(&mut self.pre_commit, artist_id)?;
|
||||||
&mut self.pre_commit,
|
fn_artist(artist);
|
||||||
artist_id.as_ref(),
|
self.update_collection(fn_coll)
|
||||||
)?);
|
|
||||||
self.update_collection(f2)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_artist<ID: AsRef<ArtistId>, F>(&mut self, artist_id: ID, func: F) -> Result<(), Error>
|
fn update_artist<FnArtist>(
|
||||||
|
&mut self,
|
||||||
|
artist_id: &ArtistId,
|
||||||
|
fn_artist: FnArtist,
|
||||||
|
) -> Result<(), Error>
|
||||||
where
|
where
|
||||||
F: FnOnce(&mut Artist),
|
FnArtist: FnOnce(&mut Artist),
|
||||||
{
|
{
|
||||||
self.update_artist_and(artist_id, func, |_| {})
|
self.update_artist_and(artist_id, fn_artist, |_| {})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_artist<ID: Into<ArtistId>>(&mut self, artist_id: ID) -> Result<(), Error> {
|
fn update_album_and<FnAlbum, FnArtist, FnColl>(
|
||||||
|
&mut self,
|
||||||
|
artist_id: &ArtistId,
|
||||||
|
album_id: &AlbumId,
|
||||||
|
fn_album: FnAlbum,
|
||||||
|
fn_artist: FnArtist,
|
||||||
|
fn_coll: FnColl,
|
||||||
|
) -> Result<(), Error>
|
||||||
|
where
|
||||||
|
FnAlbum: FnOnce(&mut Album),
|
||||||
|
FnArtist: FnOnce(&mut Artist),
|
||||||
|
FnColl: FnOnce(&mut Collection),
|
||||||
|
{
|
||||||
|
let artist = Self::get_artist_mut_or_err(&mut self.pre_commit, artist_id)?;
|
||||||
|
let album = Self::get_album_mut_or_err(artist, album_id)?;
|
||||||
|
fn_album(album);
|
||||||
|
fn_artist(artist);
|
||||||
|
self.update_collection(fn_coll)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_artist<IntoId: Into<ArtistId>>(&mut self, artist_id: IntoId) -> Result<(), Error> {
|
||||||
let artist_id: ArtistId = artist_id.into();
|
let artist_id: ArtistId = artist_id.into();
|
||||||
|
|
||||||
self.update_collection(|collection| {
|
self.update_collection(|collection| {
|
||||||
@ -291,7 +324,7 @@ impl<LIB, DB: IDatabase> MusicHoard<LIB, DB> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_artist<ID: AsRef<ArtistId>>(&mut self, artist_id: ID) -> Result<(), Error> {
|
pub fn remove_artist<Id: AsRef<ArtistId>>(&mut self, artist_id: Id) -> Result<(), Error> {
|
||||||
self.update_collection(|collection| {
|
self.update_collection(|collection| {
|
||||||
let index_opt = collection.iter().position(|a| &a.id == artist_id.as_ref());
|
let index_opt = collection.iter().position(|a| &a.id == artist_id.as_ref());
|
||||||
if let Some(index) = index_opt {
|
if let Some(index) = index_opt {
|
||||||
@ -300,77 +333,113 @@ impl<LIB, DB: IDatabase> MusicHoard<LIB, DB> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_artist_sort<ID: AsRef<ArtistId>, SORT: Into<ArtistId>>(
|
pub fn set_artist_sort<Id: AsRef<ArtistId>, IntoId: Into<ArtistId>>(
|
||||||
&mut self,
|
&mut self,
|
||||||
artist_id: ID,
|
artist_id: Id,
|
||||||
artist_sort: SORT,
|
artist_sort: IntoId,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
self.update_artist_and(
|
self.update_artist_and(
|
||||||
artist_id,
|
artist_id.as_ref(),
|
||||||
|artist| artist.set_sort_key(artist_sort),
|
|artist| artist.set_sort_key(artist_sort),
|
||||||
|collection| Self::sort_artists(collection),
|
|collection| Self::sort_artists(collection),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear_artist_sort<ID: AsRef<ArtistId>>(&mut self, artist_id: ID) -> Result<(), Error> {
|
pub fn clear_artist_sort<Id: AsRef<ArtistId>>(&mut self, artist_id: Id) -> Result<(), Error> {
|
||||||
self.update_artist_and(
|
self.update_artist_and(
|
||||||
artist_id,
|
artist_id.as_ref(),
|
||||||
|artist| artist.clear_sort_key(),
|
|artist| artist.clear_sort_key(),
|
||||||
|collection| Self::sort_artists(collection),
|
|collection| Self::sort_artists(collection),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_musicbrainz_url<ID: AsRef<ArtistId>, S: AsRef<str>>(
|
pub fn set_artist_musicbrainz<Id: AsRef<ArtistId>, Mb: TryInto<MusicBrainz, Error = E>, E>(
|
||||||
&mut self,
|
&mut self,
|
||||||
artist_id: ID,
|
artist_id: Id,
|
||||||
url: S,
|
url: Mb,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error>
|
||||||
let url = url.as_ref().try_into()?;
|
where
|
||||||
self.update_artist(artist_id, |artist| artist.set_musicbrainz_url(url))
|
Error: From<E>,
|
||||||
|
{
|
||||||
|
let mb = url.try_into()?;
|
||||||
|
self.update_artist(artist_id.as_ref(), |artist| artist.set_musicbrainz_url(mb))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear_musicbrainz_url<ID: AsRef<ArtistId>>(
|
pub fn clear_artist_musicbrainz<Id: AsRef<ArtistId>>(
|
||||||
&mut self,
|
&mut self,
|
||||||
artist_id: ID,
|
artist_id: Id,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
self.update_artist(artist_id, |artist| artist.clear_musicbrainz_url())
|
self.update_artist(artist_id.as_ref(), |artist| artist.clear_musicbrainz_url())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_to_property<ID: AsRef<ArtistId>, S: AsRef<str> + Into<String>>(
|
pub fn add_to_artist_property<Id: AsRef<ArtistId>, S: AsRef<str> + Into<String>>(
|
||||||
&mut self,
|
&mut self,
|
||||||
artist_id: ID,
|
artist_id: Id,
|
||||||
property: S,
|
property: S,
|
||||||
values: Vec<S>,
|
values: Vec<S>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
self.update_artist(artist_id, |artist| artist.add_to_property(property, values))
|
self.update_artist(artist_id.as_ref(), |artist| {
|
||||||
|
artist.add_to_property(property, values)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_from_property<ID: AsRef<ArtistId>, S: AsRef<str>>(
|
pub fn remove_from_artist_property<Id: AsRef<ArtistId>, S: AsRef<str>>(
|
||||||
&mut self,
|
&mut self,
|
||||||
artist_id: ID,
|
artist_id: Id,
|
||||||
property: S,
|
property: S,
|
||||||
values: Vec<S>,
|
values: Vec<S>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
self.update_artist(artist_id, |artist| {
|
self.update_artist(artist_id.as_ref(), |artist| {
|
||||||
artist.remove_from_property(property, values)
|
artist.remove_from_property(property, values)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_property<ID: AsRef<ArtistId>, S: AsRef<str> + Into<String>>(
|
pub fn set_artist_property<Id: AsRef<ArtistId>, S: AsRef<str> + Into<String>>(
|
||||||
&mut self,
|
&mut self,
|
||||||
artist_id: ID,
|
artist_id: Id,
|
||||||
property: S,
|
property: S,
|
||||||
values: Vec<S>,
|
values: Vec<S>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
self.update_artist(artist_id, |artist| artist.set_property(property, values))
|
self.update_artist(artist_id.as_ref(), |artist| {
|
||||||
|
artist.set_property(property, values)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear_property<ID: AsRef<ArtistId>, S: AsRef<str>>(
|
pub fn clear_artist_property<Id: AsRef<ArtistId>, S: AsRef<str>>(
|
||||||
&mut self,
|
&mut self,
|
||||||
artist_id: ID,
|
artist_id: Id,
|
||||||
property: S,
|
property: S,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
self.update_artist(artist_id, |artist| artist.clear_property(property))
|
self.update_artist(artist_id.as_ref(), |artist| artist.clear_property(property))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_album_seq<ArtistIdRef: AsRef<ArtistId>, AlbumIdRef: AsRef<AlbumId>>(
|
||||||
|
&mut self,
|
||||||
|
artist_id: ArtistIdRef,
|
||||||
|
album_id: AlbumIdRef,
|
||||||
|
seq: u8,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
self.update_album_and(
|
||||||
|
artist_id.as_ref(),
|
||||||
|
album_id.as_ref(),
|
||||||
|
|album| album.set_seq(AlbumSeq(seq)),
|
||||||
|
|artist| artist.albums.sort_unstable(),
|
||||||
|
|_| {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_album_seq<ArtistIdRef: AsRef<ArtistId>, AlbumIdRef: AsRef<AlbumId>>(
|
||||||
|
&mut self,
|
||||||
|
artist_id: ArtistIdRef,
|
||||||
|
album_id: AlbumIdRef,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
self.update_album_and(
|
||||||
|
artist_id.as_ref(),
|
||||||
|
album_id.as_ref(),
|
||||||
|
|album| album.clear_seq(),
|
||||||
|
|artist| artist.albums.sort_unstable(),
|
||||||
|
|_| {},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -523,7 +592,7 @@ mod tests {
|
|||||||
assert!(music_hoard.add_artist(artist_id.clone()).is_ok());
|
assert!(music_hoard.add_artist(artist_id.clone()).is_ok());
|
||||||
|
|
||||||
let actual_err = music_hoard
|
let actual_err = music_hoard
|
||||||
.set_musicbrainz_url(&artist_id, MUSICBUTLER)
|
.set_artist_musicbrainz(&artist_id, MUSICBUTLER)
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
let expected_err = Error::CollectionError(format!(
|
let expected_err = Error::CollectionError(format!(
|
||||||
"an error occurred when processing a URL: invalid MusicBrainz URL: {MUSICBUTLER}"
|
"an error occurred when processing a URL: invalid MusicBrainz URL: {MUSICBUTLER}"
|
||||||
@ -549,23 +618,23 @@ mod tests {
|
|||||||
|
|
||||||
// Setting a URL on an artist not in the collection is an error.
|
// Setting a URL on an artist not in the collection is an error.
|
||||||
assert!(music_hoard
|
assert!(music_hoard
|
||||||
.set_musicbrainz_url(&artist_id_2, MUSICBRAINZ)
|
.set_artist_musicbrainz(&artist_id_2, MUSICBRAINZ)
|
||||||
.is_err());
|
.is_err());
|
||||||
assert_eq!(music_hoard.collection[0].musicbrainz, expected);
|
assert_eq!(music_hoard.collection[0].musicbrainz, expected);
|
||||||
|
|
||||||
// Setting a URL on an artist.
|
// Setting a URL on an artist.
|
||||||
assert!(music_hoard
|
assert!(music_hoard
|
||||||
.set_musicbrainz_url(&artist_id, MUSICBRAINZ)
|
.set_artist_musicbrainz(&artist_id, MUSICBRAINZ)
|
||||||
.is_ok());
|
.is_ok());
|
||||||
_ = expected.insert(MusicBrainz::new(MUSICBRAINZ).unwrap());
|
_ = expected.insert(MUSICBRAINZ.try_into().unwrap());
|
||||||
assert_eq!(music_hoard.collection[0].musicbrainz, expected);
|
assert_eq!(music_hoard.collection[0].musicbrainz, expected);
|
||||||
|
|
||||||
// Clearing URLs on an artist that does not exist is an error.
|
// Clearing URLs on an artist that does not exist is an error.
|
||||||
assert!(music_hoard.clear_musicbrainz_url(&artist_id_2).is_err());
|
assert!(music_hoard.clear_artist_musicbrainz(&artist_id_2).is_err());
|
||||||
assert_eq!(music_hoard.collection[0].musicbrainz, expected);
|
assert_eq!(music_hoard.collection[0].musicbrainz, expected);
|
||||||
|
|
||||||
// Clearing URLs.
|
// Clearing URLs.
|
||||||
assert!(music_hoard.clear_musicbrainz_url(&artist_id).is_ok());
|
assert!(music_hoard.clear_artist_musicbrainz(&artist_id).is_ok());
|
||||||
_ = expected.take();
|
_ = expected.take();
|
||||||
assert_eq!(music_hoard.collection[0].musicbrainz, expected);
|
assert_eq!(music_hoard.collection[0].musicbrainz, expected);
|
||||||
}
|
}
|
||||||
@ -587,13 +656,13 @@ mod tests {
|
|||||||
|
|
||||||
// Adding URLs to an artist not in the collection is an error.
|
// Adding URLs to an artist not in the collection is an error.
|
||||||
assert!(music_hoard
|
assert!(music_hoard
|
||||||
.add_to_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
|
.add_to_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
|
||||||
.is_err());
|
.is_err());
|
||||||
assert!(music_hoard.collection[0].properties.is_empty());
|
assert!(music_hoard.collection[0].properties.is_empty());
|
||||||
|
|
||||||
// Adding mutliple URLs without clashes.
|
// Adding mutliple URLs without clashes.
|
||||||
assert!(music_hoard
|
assert!(music_hoard
|
||||||
.add_to_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2])
|
.add_to_artist_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2])
|
||||||
.is_ok());
|
.is_ok());
|
||||||
expected.push(MUSICBUTLER.to_owned());
|
expected.push(MUSICBUTLER.to_owned());
|
||||||
expected.push(MUSICBUTLER_2.to_owned());
|
expected.push(MUSICBUTLER_2.to_owned());
|
||||||
@ -604,7 +673,7 @@ mod tests {
|
|||||||
|
|
||||||
// Removing URLs from an artist not in the collection is an error.
|
// Removing URLs from an artist not in the collection is an error.
|
||||||
assert!(music_hoard
|
assert!(music_hoard
|
||||||
.remove_from_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
|
.remove_from_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
|
||||||
.is_err());
|
.is_err());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
music_hoard.collection[0].properties.get("MusicButler"),
|
music_hoard.collection[0].properties.get("MusicButler"),
|
||||||
@ -613,7 +682,11 @@ mod tests {
|
|||||||
|
|
||||||
// Removing multiple URLs without clashes.
|
// Removing multiple URLs without clashes.
|
||||||
assert!(music_hoard
|
assert!(music_hoard
|
||||||
.remove_from_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2])
|
.remove_from_artist_property(
|
||||||
|
&artist_id,
|
||||||
|
"MusicButler",
|
||||||
|
vec![MUSICBUTLER, MUSICBUTLER_2]
|
||||||
|
)
|
||||||
.is_ok());
|
.is_ok());
|
||||||
expected.clear();
|
expected.clear();
|
||||||
assert!(music_hoard.collection[0].properties.is_empty());
|
assert!(music_hoard.collection[0].properties.is_empty());
|
||||||
@ -636,13 +709,13 @@ mod tests {
|
|||||||
|
|
||||||
// Seting URL on an artist not in the collection is an error.
|
// Seting URL on an artist not in the collection is an error.
|
||||||
assert!(music_hoard
|
assert!(music_hoard
|
||||||
.set_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
|
.set_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
|
||||||
.is_err());
|
.is_err());
|
||||||
assert!(music_hoard.collection[0].properties.is_empty());
|
assert!(music_hoard.collection[0].properties.is_empty());
|
||||||
|
|
||||||
// Set URLs.
|
// Set URLs.
|
||||||
assert!(music_hoard
|
assert!(music_hoard
|
||||||
.set_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2])
|
.set_artist_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2])
|
||||||
.is_ok());
|
.is_ok());
|
||||||
expected.clear();
|
expected.clear();
|
||||||
expected.push(MUSICBUTLER.to_owned());
|
expected.push(MUSICBUTLER.to_owned());
|
||||||
@ -654,17 +727,62 @@ mod tests {
|
|||||||
|
|
||||||
// Clearing URLs on an artist that does not exist is an error.
|
// Clearing URLs on an artist that does not exist is an error.
|
||||||
assert!(music_hoard
|
assert!(music_hoard
|
||||||
.clear_property(&artist_id_2, "MusicButler")
|
.clear_artist_property(&artist_id_2, "MusicButler")
|
||||||
.is_err());
|
.is_err());
|
||||||
|
|
||||||
// Clear URLs.
|
// Clear URLs.
|
||||||
assert!(music_hoard
|
assert!(music_hoard
|
||||||
.clear_property(&artist_id, "MusicButler")
|
.clear_artist_property(&artist_id, "MusicButler")
|
||||||
.is_ok());
|
.is_ok());
|
||||||
expected.clear();
|
expected.clear();
|
||||||
assert!(music_hoard.collection[0].properties.is_empty());
|
assert!(music_hoard.collection[0].properties.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_clear_album_seq() {
|
||||||
|
let mut database = MockIDatabase::new();
|
||||||
|
|
||||||
|
let artist_id = ArtistId::new("an artist");
|
||||||
|
let album_id = AlbumId::new("an album");
|
||||||
|
let album_id_2 = AlbumId::new("another album");
|
||||||
|
|
||||||
|
let mut database_result = vec![Artist::new(artist_id.clone())];
|
||||||
|
database_result[0].albums.push(Album {
|
||||||
|
id: album_id.clone(),
|
||||||
|
date: AlbumDate::default(),
|
||||||
|
seq: AlbumSeq::default(),
|
||||||
|
tracks: vec![],
|
||||||
|
});
|
||||||
|
|
||||||
|
database
|
||||||
|
.expect_load()
|
||||||
|
.times(1)
|
||||||
|
.return_once(|| Ok(database_result));
|
||||||
|
database.expect_save().times(2).returning(|_| Ok(()));
|
||||||
|
|
||||||
|
let mut music_hoard = MusicHoard::database(database).unwrap();
|
||||||
|
assert_eq!(music_hoard.collection[0].albums[0].seq, AlbumSeq(0));
|
||||||
|
|
||||||
|
// Seting seq on an album not belonging to the artist is an error.
|
||||||
|
assert!(music_hoard
|
||||||
|
.set_album_seq(&artist_id, &album_id_2, 6)
|
||||||
|
.is_err());
|
||||||
|
assert_eq!(music_hoard.collection[0].albums[0].seq, AlbumSeq(0));
|
||||||
|
|
||||||
|
// Set seq.
|
||||||
|
assert!(music_hoard.set_album_seq(&artist_id, &album_id, 6).is_ok());
|
||||||
|
assert_eq!(music_hoard.collection[0].albums[0].seq, AlbumSeq(6));
|
||||||
|
|
||||||
|
// Clearing seq on an album that does not exist is an error.
|
||||||
|
assert!(music_hoard
|
||||||
|
.clear_album_seq(&artist_id, &album_id_2)
|
||||||
|
.is_err());
|
||||||
|
|
||||||
|
// Clear seq.
|
||||||
|
assert!(music_hoard.clear_album_seq(&artist_id, &album_id).is_ok());
|
||||||
|
assert_eq!(music_hoard.collection[0].albums[0].seq, AlbumSeq(0));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn merge_collection_no_overlap() {
|
fn merge_collection_no_overlap() {
|
||||||
let half: usize = FULL_COLLECTION.len() / 2;
|
let half: usize = FULL_COLLECTION.len() / 2;
|
||||||
@ -887,7 +1005,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rescan_library_album_title_year_clash() {
|
fn rescan_library_album_id_clash() {
|
||||||
let mut library = MockILibrary::new();
|
let mut library = MockILibrary::new();
|
||||||
|
|
||||||
let mut expected = LIBRARY_COLLECTION.to_owned();
|
let mut expected = LIBRARY_COLLECTION.to_owned();
|
||||||
@ -895,10 +1013,10 @@ mod tests {
|
|||||||
let clashed_album_id = &expected[1].albums[0].id;
|
let clashed_album_id = &expected[1].albums[0].id;
|
||||||
|
|
||||||
let mut items = LIBRARY_ITEMS.to_owned();
|
let mut items = LIBRARY_ITEMS.to_owned();
|
||||||
for item in items.iter_mut().filter(|it| {
|
for item in items
|
||||||
(it.album_year == removed_album_id.year) && (it.album_title == removed_album_id.title)
|
.iter_mut()
|
||||||
}) {
|
.filter(|it| it.album_title == removed_album_id.title)
|
||||||
item.album_year = clashed_album_id.year;
|
{
|
||||||
item.album_title = clashed_album_id.title.clone();
|
item.album_title = clashed_album_id.title.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -916,6 +1034,7 @@ mod tests {
|
|||||||
let mut music_hoard = MusicHoard::library(library);
|
let mut music_hoard = MusicHoard::library(library);
|
||||||
|
|
||||||
music_hoard.rescan_library().unwrap();
|
music_hoard.rescan_library().unwrap();
|
||||||
|
assert_eq!(music_hoard.get_collection()[0], expected[0]);
|
||||||
assert_eq!(music_hoard.get_collection(), &expected);
|
assert_eq!(music_hoard.get_collection(), &expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use std::collections::HashMap;
|
use std::{collections::HashMap, str::FromStr};
|
||||||
|
|
||||||
use crate::core::collection::{
|
use crate::core::collection::{
|
||||||
album::{Album, AlbumId},
|
album::{Album, AlbumDate, AlbumId, AlbumMonth, AlbumSeq},
|
||||||
artist::{Artist, ArtistId, MusicBrainz},
|
artist::{Artist, ArtistId, MusicBrainz},
|
||||||
track::{Format, Quality, Track, TrackId},
|
track::{Track, TrackFormat, TrackId, TrackNum, TrackQuality},
|
||||||
};
|
};
|
||||||
use crate::tests::*;
|
use crate::tests::*;
|
||||||
|
|
||||||
|
229
src/tests.rs
229
src/tests.rs
@ -11,54 +11,59 @@ macro_rules! library_collection {
|
|||||||
albums: vec![
|
albums: vec![
|
||||||
Album {
|
Album {
|
||||||
id: AlbumId {
|
id: AlbumId {
|
||||||
year: 1998,
|
|
||||||
title: "album_title a.a".to_string(),
|
title: "album_title a.a".to_string(),
|
||||||
},
|
},
|
||||||
|
date: AlbumDate {
|
||||||
|
year: 1998,
|
||||||
|
month: AlbumMonth::None,
|
||||||
|
day: 0,
|
||||||
|
},
|
||||||
|
seq: AlbumSeq(0),
|
||||||
tracks: vec![
|
tracks: vec![
|
||||||
Track {
|
Track {
|
||||||
id: TrackId {
|
id: TrackId {
|
||||||
number: 1,
|
|
||||||
title: "track a.a.1".to_string(),
|
title: "track a.a.1".to_string(),
|
||||||
},
|
},
|
||||||
|
number: TrackNum(1),
|
||||||
artist: vec!["artist a.a.1".to_string()],
|
artist: vec!["artist a.a.1".to_string()],
|
||||||
quality: Quality {
|
quality: TrackQuality {
|
||||||
format: Format::Flac,
|
format: TrackFormat::Flac,
|
||||||
bitrate: 992,
|
bitrate: 992,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Track {
|
Track {
|
||||||
id: TrackId {
|
id: TrackId {
|
||||||
number: 2,
|
|
||||||
title: "track a.a.2".to_string(),
|
title: "track a.a.2".to_string(),
|
||||||
},
|
},
|
||||||
|
number: TrackNum(2),
|
||||||
artist: vec![
|
artist: vec![
|
||||||
"artist a.a.2.1".to_string(),
|
"artist a.a.2.1".to_string(),
|
||||||
"artist a.a.2.2".to_string(),
|
"artist a.a.2.2".to_string(),
|
||||||
],
|
],
|
||||||
quality: Quality {
|
quality: TrackQuality {
|
||||||
format: Format::Mp3,
|
format: TrackFormat::Mp3,
|
||||||
bitrate: 320,
|
bitrate: 320,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Track {
|
Track {
|
||||||
id: TrackId {
|
id: TrackId {
|
||||||
number: 3,
|
|
||||||
title: "track a.a.3".to_string(),
|
title: "track a.a.3".to_string(),
|
||||||
},
|
},
|
||||||
|
number: TrackNum(3),
|
||||||
artist: vec!["artist a.a.3".to_string()],
|
artist: vec!["artist a.a.3".to_string()],
|
||||||
quality: Quality {
|
quality: TrackQuality {
|
||||||
format: Format::Flac,
|
format: TrackFormat::Flac,
|
||||||
bitrate: 1061,
|
bitrate: 1061,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Track {
|
Track {
|
||||||
id: TrackId {
|
id: TrackId {
|
||||||
number: 4,
|
|
||||||
title: "track a.a.4".to_string(),
|
title: "track a.a.4".to_string(),
|
||||||
},
|
},
|
||||||
|
number: TrackNum(4),
|
||||||
artist: vec!["artist a.a.4".to_string()],
|
artist: vec!["artist a.a.4".to_string()],
|
||||||
quality: Quality {
|
quality: TrackQuality {
|
||||||
format: Format::Flac,
|
format: TrackFormat::Flac,
|
||||||
bitrate: 1042,
|
bitrate: 1042,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -66,29 +71,34 @@ macro_rules! library_collection {
|
|||||||
},
|
},
|
||||||
Album {
|
Album {
|
||||||
id: AlbumId {
|
id: AlbumId {
|
||||||
year: 2015,
|
|
||||||
title: "album_title a.b".to_string(),
|
title: "album_title a.b".to_string(),
|
||||||
},
|
},
|
||||||
|
date: AlbumDate {
|
||||||
|
year: 2015,
|
||||||
|
month: AlbumMonth::April,
|
||||||
|
day: 0,
|
||||||
|
},
|
||||||
|
seq: AlbumSeq(0),
|
||||||
tracks: vec![
|
tracks: vec![
|
||||||
Track {
|
Track {
|
||||||
id: TrackId {
|
id: TrackId {
|
||||||
number: 1,
|
|
||||||
title: "track a.b.1".to_string(),
|
title: "track a.b.1".to_string(),
|
||||||
},
|
},
|
||||||
|
number: TrackNum(1),
|
||||||
artist: vec!["artist a.b.1".to_string()],
|
artist: vec!["artist a.b.1".to_string()],
|
||||||
quality: Quality {
|
quality: TrackQuality {
|
||||||
format: Format::Flac,
|
format: TrackFormat::Flac,
|
||||||
bitrate: 1004,
|
bitrate: 1004,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Track {
|
Track {
|
||||||
id: TrackId {
|
id: TrackId {
|
||||||
number: 2,
|
|
||||||
title: "track a.b.2".to_string(),
|
title: "track a.b.2".to_string(),
|
||||||
},
|
},
|
||||||
|
number: TrackNum(2),
|
||||||
artist: vec!["artist a.b.2".to_string()],
|
artist: vec!["artist a.b.2".to_string()],
|
||||||
quality: Quality {
|
quality: TrackQuality {
|
||||||
format: Format::Flac,
|
format: TrackFormat::Flac,
|
||||||
bitrate: 1077,
|
bitrate: 1077,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -106,32 +116,37 @@ macro_rules! library_collection {
|
|||||||
albums: vec![
|
albums: vec![
|
||||||
Album {
|
Album {
|
||||||
id: AlbumId {
|
id: AlbumId {
|
||||||
year: 2003,
|
|
||||||
title: "album_title b.a".to_string(),
|
title: "album_title b.a".to_string(),
|
||||||
},
|
},
|
||||||
|
date: AlbumDate {
|
||||||
|
year: 2003,
|
||||||
|
month: AlbumMonth::June,
|
||||||
|
day: 6,
|
||||||
|
},
|
||||||
|
seq: AlbumSeq(0),
|
||||||
tracks: vec![
|
tracks: vec![
|
||||||
Track {
|
Track {
|
||||||
id: TrackId {
|
id: TrackId {
|
||||||
number: 1,
|
|
||||||
title: "track b.a.1".to_string(),
|
title: "track b.a.1".to_string(),
|
||||||
},
|
},
|
||||||
|
number: TrackNum(1),
|
||||||
artist: vec!["artist b.a.1".to_string()],
|
artist: vec!["artist b.a.1".to_string()],
|
||||||
quality: Quality {
|
quality: TrackQuality {
|
||||||
format: Format::Mp3,
|
format: TrackFormat::Mp3,
|
||||||
bitrate: 190,
|
bitrate: 190,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Track {
|
Track {
|
||||||
id: TrackId {
|
id: TrackId {
|
||||||
number: 2,
|
|
||||||
title: "track b.a.2".to_string(),
|
title: "track b.a.2".to_string(),
|
||||||
},
|
},
|
||||||
|
number: TrackNum(2),
|
||||||
artist: vec![
|
artist: vec![
|
||||||
"artist b.a.2.1".to_string(),
|
"artist b.a.2.1".to_string(),
|
||||||
"artist b.a.2.2".to_string(),
|
"artist b.a.2.2".to_string(),
|
||||||
],
|
],
|
||||||
quality: Quality {
|
quality: TrackQuality {
|
||||||
format: Format::Mp3,
|
format: TrackFormat::Mp3,
|
||||||
bitrate: 120,
|
bitrate: 120,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -139,32 +154,37 @@ macro_rules! library_collection {
|
|||||||
},
|
},
|
||||||
Album {
|
Album {
|
||||||
id: AlbumId {
|
id: AlbumId {
|
||||||
year: 2008,
|
|
||||||
title: "album_title b.b".to_string(),
|
title: "album_title b.b".to_string(),
|
||||||
},
|
},
|
||||||
|
date: AlbumDate {
|
||||||
|
year: 2008,
|
||||||
|
month: AlbumMonth::None,
|
||||||
|
day: 0,
|
||||||
|
},
|
||||||
|
seq: AlbumSeq(0),
|
||||||
tracks: vec![
|
tracks: vec![
|
||||||
Track {
|
Track {
|
||||||
id: TrackId {
|
id: TrackId {
|
||||||
number: 1,
|
|
||||||
title: "track b.b.1".to_string(),
|
title: "track b.b.1".to_string(),
|
||||||
},
|
},
|
||||||
|
number: TrackNum(1),
|
||||||
artist: vec!["artist b.b.1".to_string()],
|
artist: vec!["artist b.b.1".to_string()],
|
||||||
quality: Quality {
|
quality: TrackQuality {
|
||||||
format: Format::Flac,
|
format: TrackFormat::Flac,
|
||||||
bitrate: 1077,
|
bitrate: 1077,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Track {
|
Track {
|
||||||
id: TrackId {
|
id: TrackId {
|
||||||
number: 2,
|
|
||||||
title: "track b.b.2".to_string(),
|
title: "track b.b.2".to_string(),
|
||||||
},
|
},
|
||||||
|
number: TrackNum(2),
|
||||||
artist: vec![
|
artist: vec![
|
||||||
"artist b.b.2.1".to_string(),
|
"artist b.b.2.1".to_string(),
|
||||||
"artist b.b.2.2".to_string(),
|
"artist b.b.2.2".to_string(),
|
||||||
],
|
],
|
||||||
quality: Quality {
|
quality: TrackQuality {
|
||||||
format: Format::Mp3,
|
format: TrackFormat::Mp3,
|
||||||
bitrate: 320,
|
bitrate: 320,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -172,32 +192,37 @@ macro_rules! library_collection {
|
|||||||
},
|
},
|
||||||
Album {
|
Album {
|
||||||
id: AlbumId {
|
id: AlbumId {
|
||||||
year: 2009,
|
|
||||||
title: "album_title b.c".to_string(),
|
title: "album_title b.c".to_string(),
|
||||||
},
|
},
|
||||||
|
date: AlbumDate {
|
||||||
|
year: 2009,
|
||||||
|
month: AlbumMonth::None,
|
||||||
|
day: 0,
|
||||||
|
},
|
||||||
|
seq: AlbumSeq(0),
|
||||||
tracks: vec![
|
tracks: vec![
|
||||||
Track {
|
Track {
|
||||||
id: TrackId {
|
id: TrackId {
|
||||||
number: 1,
|
|
||||||
title: "track b.c.1".to_string(),
|
title: "track b.c.1".to_string(),
|
||||||
},
|
},
|
||||||
|
number: TrackNum(1),
|
||||||
artist: vec!["artist b.c.1".to_string()],
|
artist: vec!["artist b.c.1".to_string()],
|
||||||
quality: Quality {
|
quality: TrackQuality {
|
||||||
format: Format::Mp3,
|
format: TrackFormat::Mp3,
|
||||||
bitrate: 190,
|
bitrate: 190,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Track {
|
Track {
|
||||||
id: TrackId {
|
id: TrackId {
|
||||||
number: 2,
|
|
||||||
title: "track b.c.2".to_string(),
|
title: "track b.c.2".to_string(),
|
||||||
},
|
},
|
||||||
|
number: TrackNum(2),
|
||||||
artist: vec![
|
artist: vec![
|
||||||
"artist b.c.2.1".to_string(),
|
"artist b.c.2.1".to_string(),
|
||||||
"artist b.c.2.2".to_string(),
|
"artist b.c.2.2".to_string(),
|
||||||
],
|
],
|
||||||
quality: Quality {
|
quality: TrackQuality {
|
||||||
format: Format::Mp3,
|
format: TrackFormat::Mp3,
|
||||||
bitrate: 120,
|
bitrate: 120,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -205,32 +230,37 @@ macro_rules! library_collection {
|
|||||||
},
|
},
|
||||||
Album {
|
Album {
|
||||||
id: AlbumId {
|
id: AlbumId {
|
||||||
year: 2015,
|
|
||||||
title: "album_title b.d".to_string(),
|
title: "album_title b.d".to_string(),
|
||||||
},
|
},
|
||||||
|
date: AlbumDate {
|
||||||
|
year: 2015,
|
||||||
|
month: AlbumMonth::None,
|
||||||
|
day: 0,
|
||||||
|
},
|
||||||
|
seq: AlbumSeq(0),
|
||||||
tracks: vec![
|
tracks: vec![
|
||||||
Track {
|
Track {
|
||||||
id: TrackId {
|
id: TrackId {
|
||||||
number: 1,
|
|
||||||
title: "track b.d.1".to_string(),
|
title: "track b.d.1".to_string(),
|
||||||
},
|
},
|
||||||
|
number: TrackNum(1),
|
||||||
artist: vec!["artist b.d.1".to_string()],
|
artist: vec!["artist b.d.1".to_string()],
|
||||||
quality: Quality {
|
quality: TrackQuality {
|
||||||
format: Format::Mp3,
|
format: TrackFormat::Mp3,
|
||||||
bitrate: 190,
|
bitrate: 190,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Track {
|
Track {
|
||||||
id: TrackId {
|
id: TrackId {
|
||||||
number: 2,
|
|
||||||
title: "track b.d.2".to_string(),
|
title: "track b.d.2".to_string(),
|
||||||
},
|
},
|
||||||
|
number: TrackNum(2),
|
||||||
artist: vec![
|
artist: vec![
|
||||||
"artist b.d.2.1".to_string(),
|
"artist b.d.2.1".to_string(),
|
||||||
"artist b.d.2.2".to_string(),
|
"artist b.d.2.2".to_string(),
|
||||||
],
|
],
|
||||||
quality: Quality {
|
quality: TrackQuality {
|
||||||
format: Format::Mp3,
|
format: TrackFormat::Mp3,
|
||||||
bitrate: 120,
|
bitrate: 120,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -250,32 +280,37 @@ macro_rules! library_collection {
|
|||||||
albums: vec![
|
albums: vec![
|
||||||
Album {
|
Album {
|
||||||
id: AlbumId {
|
id: AlbumId {
|
||||||
year: 1985,
|
|
||||||
title: "album_title c.a".to_string(),
|
title: "album_title c.a".to_string(),
|
||||||
},
|
},
|
||||||
|
date: AlbumDate {
|
||||||
|
year: 1985,
|
||||||
|
month: AlbumMonth::None,
|
||||||
|
day: 0,
|
||||||
|
},
|
||||||
|
seq: AlbumSeq(0),
|
||||||
tracks: vec![
|
tracks: vec![
|
||||||
Track {
|
Track {
|
||||||
id: TrackId {
|
id: TrackId {
|
||||||
number: 1,
|
|
||||||
title: "track c.a.1".to_string(),
|
title: "track c.a.1".to_string(),
|
||||||
},
|
},
|
||||||
|
number: TrackNum(1),
|
||||||
artist: vec!["artist c.a.1".to_string()],
|
artist: vec!["artist c.a.1".to_string()],
|
||||||
quality: Quality {
|
quality: TrackQuality {
|
||||||
format: Format::Mp3,
|
format: TrackFormat::Mp3,
|
||||||
bitrate: 320,
|
bitrate: 320,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Track {
|
Track {
|
||||||
id: TrackId {
|
id: TrackId {
|
||||||
number: 2,
|
|
||||||
title: "track c.a.2".to_string(),
|
title: "track c.a.2".to_string(),
|
||||||
},
|
},
|
||||||
|
number: TrackNum(2),
|
||||||
artist: vec![
|
artist: vec![
|
||||||
"artist c.a.2.1".to_string(),
|
"artist c.a.2.1".to_string(),
|
||||||
"artist c.a.2.2".to_string(),
|
"artist c.a.2.2".to_string(),
|
||||||
],
|
],
|
||||||
quality: Quality {
|
quality: TrackQuality {
|
||||||
format: Format::Mp3,
|
format: TrackFormat::Mp3,
|
||||||
bitrate: 120,
|
bitrate: 120,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -283,32 +318,37 @@ macro_rules! library_collection {
|
|||||||
},
|
},
|
||||||
Album {
|
Album {
|
||||||
id: AlbumId {
|
id: AlbumId {
|
||||||
year: 2018,
|
|
||||||
title: "album_title c.b".to_string(),
|
title: "album_title c.b".to_string(),
|
||||||
},
|
},
|
||||||
|
date: AlbumDate {
|
||||||
|
year: 2018,
|
||||||
|
month: AlbumMonth::None,
|
||||||
|
day: 0,
|
||||||
|
},
|
||||||
|
seq: AlbumSeq(0),
|
||||||
tracks: vec![
|
tracks: vec![
|
||||||
Track {
|
Track {
|
||||||
id: TrackId {
|
id: TrackId {
|
||||||
number: 1,
|
|
||||||
title: "track c.b.1".to_string(),
|
title: "track c.b.1".to_string(),
|
||||||
},
|
},
|
||||||
|
number: TrackNum(1),
|
||||||
artist: vec!["artist c.b.1".to_string()],
|
artist: vec!["artist c.b.1".to_string()],
|
||||||
quality: Quality {
|
quality: TrackQuality {
|
||||||
format: Format::Flac,
|
format: TrackFormat::Flac,
|
||||||
bitrate: 1041,
|
bitrate: 1041,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Track {
|
Track {
|
||||||
id: TrackId {
|
id: TrackId {
|
||||||
number: 2,
|
|
||||||
title: "track c.b.2".to_string(),
|
title: "track c.b.2".to_string(),
|
||||||
},
|
},
|
||||||
|
number: TrackNum(2),
|
||||||
artist: vec![
|
artist: vec![
|
||||||
"artist c.b.2.1".to_string(),
|
"artist c.b.2.1".to_string(),
|
||||||
"artist c.b.2.2".to_string(),
|
"artist c.b.2.2".to_string(),
|
||||||
],
|
],
|
||||||
quality: Quality {
|
quality: TrackQuality {
|
||||||
format: Format::Flac,
|
format: TrackFormat::Flac,
|
||||||
bitrate: 756,
|
bitrate: 756,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -326,32 +366,37 @@ macro_rules! library_collection {
|
|||||||
albums: vec![
|
albums: vec![
|
||||||
Album {
|
Album {
|
||||||
id: AlbumId {
|
id: AlbumId {
|
||||||
year: 1995,
|
|
||||||
title: "album_title d.a".to_string(),
|
title: "album_title d.a".to_string(),
|
||||||
},
|
},
|
||||||
|
date: AlbumDate {
|
||||||
|
year: 1995,
|
||||||
|
month: AlbumMonth::None,
|
||||||
|
day: 0,
|
||||||
|
},
|
||||||
|
seq: AlbumSeq(0),
|
||||||
tracks: vec![
|
tracks: vec![
|
||||||
Track {
|
Track {
|
||||||
id: TrackId {
|
id: TrackId {
|
||||||
number: 1,
|
|
||||||
title: "track d.a.1".to_string(),
|
title: "track d.a.1".to_string(),
|
||||||
},
|
},
|
||||||
|
number: TrackNum(1),
|
||||||
artist: vec!["artist d.a.1".to_string()],
|
artist: vec!["artist d.a.1".to_string()],
|
||||||
quality: Quality {
|
quality: TrackQuality {
|
||||||
format: Format::Mp3,
|
format: TrackFormat::Mp3,
|
||||||
bitrate: 120,
|
bitrate: 120,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Track {
|
Track {
|
||||||
id: TrackId {
|
id: TrackId {
|
||||||
number: 2,
|
|
||||||
title: "track d.a.2".to_string(),
|
title: "track d.a.2".to_string(),
|
||||||
},
|
},
|
||||||
|
number: TrackNum(2),
|
||||||
artist: vec![
|
artist: vec![
|
||||||
"artist d.a.2.1".to_string(),
|
"artist d.a.2.1".to_string(),
|
||||||
"artist d.a.2.2".to_string(),
|
"artist d.a.2.2".to_string(),
|
||||||
],
|
],
|
||||||
quality: Quality {
|
quality: TrackQuality {
|
||||||
format: Format::Mp3,
|
format: TrackFormat::Mp3,
|
||||||
bitrate: 120,
|
bitrate: 120,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -359,32 +404,37 @@ macro_rules! library_collection {
|
|||||||
},
|
},
|
||||||
Album {
|
Album {
|
||||||
id: AlbumId {
|
id: AlbumId {
|
||||||
year: 2028,
|
|
||||||
title: "album_title d.b".to_string(),
|
title: "album_title d.b".to_string(),
|
||||||
},
|
},
|
||||||
|
date: AlbumDate {
|
||||||
|
year: 2028,
|
||||||
|
month: AlbumMonth::None,
|
||||||
|
day: 0,
|
||||||
|
},
|
||||||
|
seq: AlbumSeq(0),
|
||||||
tracks: vec![
|
tracks: vec![
|
||||||
Track {
|
Track {
|
||||||
id: TrackId {
|
id: TrackId {
|
||||||
number: 1,
|
|
||||||
title: "track d.b.1".to_string(),
|
title: "track d.b.1".to_string(),
|
||||||
},
|
},
|
||||||
|
number: TrackNum(1),
|
||||||
artist: vec!["artist d.b.1".to_string()],
|
artist: vec!["artist d.b.1".to_string()],
|
||||||
quality: Quality {
|
quality: TrackQuality {
|
||||||
format: Format::Flac,
|
format: TrackFormat::Flac,
|
||||||
bitrate: 841,
|
bitrate: 841,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Track {
|
Track {
|
||||||
id: TrackId {
|
id: TrackId {
|
||||||
number: 2,
|
|
||||||
title: "track d.b.2".to_string(),
|
title: "track d.b.2".to_string(),
|
||||||
},
|
},
|
||||||
|
number: TrackNum(2),
|
||||||
artist: vec![
|
artist: vec![
|
||||||
"artist d.b.2.1".to_string(),
|
"artist d.b.2.1".to_string(),
|
||||||
"artist d.b.2.2".to_string(),
|
"artist d.b.2.2".to_string(),
|
||||||
],
|
],
|
||||||
quality: Quality {
|
quality: TrackQuality {
|
||||||
format: Format::Flac,
|
format: TrackFormat::Flac,
|
||||||
bitrate: 756,
|
bitrate: 756,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -404,12 +454,9 @@ macro_rules! full_collection {
|
|||||||
let artist_a = iter.next().unwrap();
|
let artist_a = iter.next().unwrap();
|
||||||
assert_eq!(artist_a.id.name, "Album_Artist ‘A’");
|
assert_eq!(artist_a.id.name, "Album_Artist ‘A’");
|
||||||
|
|
||||||
artist_a.musicbrainz = Some(
|
artist_a.musicbrainz = Some(MusicBrainz::from_str(
|
||||||
MusicBrainz::new(
|
|
||||||
"https://musicbrainz.org/artist/00000000-0000-0000-0000-000000000000",
|
"https://musicbrainz.org/artist/00000000-0000-0000-0000-000000000000",
|
||||||
)
|
).unwrap());
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
|
|
||||||
artist_a.properties = HashMap::from([
|
artist_a.properties = HashMap::from([
|
||||||
(String::from("MusicButler"), vec![
|
(String::from("MusicButler"), vec![
|
||||||
@ -422,14 +469,15 @@ macro_rules! full_collection {
|
|||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
artist_a.albums[0].seq = AlbumSeq(1);
|
||||||
|
artist_a.albums[1].seq = AlbumSeq(1);
|
||||||
|
|
||||||
let artist_b = iter.next().unwrap();
|
let artist_b = iter.next().unwrap();
|
||||||
assert_eq!(artist_b.id.name, "Album_Artist ‘B’");
|
assert_eq!(artist_b.id.name, "Album_Artist ‘B’");
|
||||||
|
|
||||||
artist_b.musicbrainz = Some(
|
artist_b.musicbrainz = Some(MusicBrainz::from_str(
|
||||||
MusicBrainz::new(
|
|
||||||
"https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111",
|
"https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111",
|
||||||
).unwrap(),
|
).unwrap());
|
||||||
);
|
|
||||||
|
|
||||||
artist_b.properties = HashMap::from([
|
artist_b.properties = HashMap::from([
|
||||||
(String::from("MusicButler"), vec![
|
(String::from("MusicButler"), vec![
|
||||||
@ -444,14 +492,17 @@ macro_rules! full_collection {
|
|||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
artist_b.albums[0].seq = AlbumSeq(1);
|
||||||
|
artist_b.albums[1].seq = AlbumSeq(3);
|
||||||
|
artist_b.albums[2].seq = AlbumSeq(2);
|
||||||
|
artist_b.albums[3].seq = AlbumSeq(4);
|
||||||
|
|
||||||
let artist_c = iter.next().unwrap();
|
let artist_c = iter.next().unwrap();
|
||||||
assert_eq!(artist_c.id.name, "The Album_Artist ‘C’");
|
assert_eq!(artist_c.id.name, "The Album_Artist ‘C’");
|
||||||
|
|
||||||
artist_c.musicbrainz = Some(
|
artist_c.musicbrainz = Some(MusicBrainz::from_str(
|
||||||
MusicBrainz::new(
|
|
||||||
"https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111",
|
"https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111",
|
||||||
).unwrap(),
|
).unwrap());
|
||||||
);
|
|
||||||
|
|
||||||
// Nothing for artist_d
|
// Nothing for artist_d
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use crate::tui::{
|
use crate::tui::{
|
||||||
app::{
|
app::{
|
||||||
machine::{App, AppInner, AppMachine},
|
machine::{App, AppInner, AppMachine},
|
||||||
selection::IdSelection,
|
selection::KeySelection,
|
||||||
AppPublic, AppState, IAppInteractReload,
|
AppPublic, AppState, IAppInteractReload,
|
||||||
},
|
},
|
||||||
lib::IMusicHoard,
|
lib::IMusicHoard,
|
||||||
@ -36,7 +36,7 @@ impl<MH: IMusicHoard> IAppInteractReload for AppMachine<MH, AppReload> {
|
|||||||
type APP = App<MH>;
|
type APP = App<MH>;
|
||||||
|
|
||||||
fn reload_library(mut self) -> Self::APP {
|
fn reload_library(mut self) -> Self::APP {
|
||||||
let previous = IdSelection::get(
|
let previous = KeySelection::get(
|
||||||
self.inner.music_hoard.get_collection(),
|
self.inner.music_hoard.get_collection(),
|
||||||
&self.inner.selection,
|
&self.inner.selection,
|
||||||
);
|
);
|
||||||
@ -45,7 +45,7 @@ impl<MH: IMusicHoard> IAppInteractReload for AppMachine<MH, AppReload> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn reload_database(mut self) -> Self::APP {
|
fn reload_database(mut self) -> Self::APP {
|
||||||
let previous = IdSelection::get(
|
let previous = KeySelection::get(
|
||||||
self.inner.music_hoard.get_collection(),
|
self.inner.music_hoard.get_collection(),
|
||||||
&self.inner.selection,
|
&self.inner.selection,
|
||||||
);
|
);
|
||||||
@ -63,11 +63,11 @@ impl<MH: IMusicHoard> IAppInteractReload for AppMachine<MH, AppReload> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
trait IAppInteractReloadPrivate<MH: IMusicHoard> {
|
trait IAppInteractReloadPrivate<MH: IMusicHoard> {
|
||||||
fn refresh(self, previous: IdSelection, result: Result<(), musichoard::Error>) -> App<MH>;
|
fn refresh(self, previous: KeySelection, result: Result<(), musichoard::Error>) -> App<MH>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<MH: IMusicHoard> IAppInteractReloadPrivate<MH> for AppMachine<MH, AppReload> {
|
impl<MH: IMusicHoard> IAppInteractReloadPrivate<MH> for AppMachine<MH, AppReload> {
|
||||||
fn refresh(mut self, previous: IdSelection, result: Result<(), musichoard::Error>) -> App<MH> {
|
fn refresh(mut self, previous: KeySelection, result: Result<(), musichoard::Error>) -> App<MH> {
|
||||||
match result {
|
match result {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
self.inner
|
self.inner
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
use std::cmp;
|
use std::cmp;
|
||||||
|
|
||||||
use musichoard::collection::{
|
use musichoard::collection::{
|
||||||
album::{Album, AlbumId},
|
album::{Album, AlbumDate, AlbumId, AlbumSeq},
|
||||||
track::Track,
|
track::Track,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::tui::app::selection::{
|
use crate::tui::app::selection::{
|
||||||
track::{IdSelectTrack, TrackSelection},
|
track::{KeySelectTrack, TrackSelection},
|
||||||
Delta, SelectionState, WidgetState,
|
Delta, SelectionState, WidgetState,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -26,9 +26,9 @@ impl AlbumSelection {
|
|||||||
selection
|
selection
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reinitialise(&mut self, albums: &[Album], album: Option<IdSelectAlbum>) {
|
pub fn reinitialise(&mut self, albums: &[Album], album: Option<KeySelectAlbum>) {
|
||||||
if let Some(album) = album {
|
if let Some(album) = album {
|
||||||
let result = albums.binary_search_by(|a| a.get_sort_key().cmp(&album.album_id));
|
let result = albums.binary_search_by(|a| a.get_sort_key().cmp(&album.get_sort_key()));
|
||||||
match result {
|
match result {
|
||||||
Ok(index) => self.reinitialise_with_index(albums, index, album.track),
|
Ok(index) => self.reinitialise_with_index(albums, index, album.track),
|
||||||
Err(index) => self.reinitialise_with_index(albums, index, None),
|
Err(index) => self.reinitialise_with_index(albums, index, None),
|
||||||
@ -42,7 +42,7 @@ impl AlbumSelection {
|
|||||||
&mut self,
|
&mut self,
|
||||||
albums: &[Album],
|
albums: &[Album],
|
||||||
index: usize,
|
index: usize,
|
||||||
active_track: Option<IdSelectTrack>,
|
active_track: Option<KeySelectTrack>,
|
||||||
) {
|
) {
|
||||||
if albums.is_empty() {
|
if albums.is_empty() {
|
||||||
self.state.list.select(None);
|
self.state.list.select(None);
|
||||||
@ -160,21 +160,26 @@ impl AlbumSelection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct IdSelectAlbum {
|
pub struct KeySelectAlbum {
|
||||||
album_id: AlbumId,
|
key: (AlbumDate, AlbumSeq, AlbumId),
|
||||||
track: Option<IdSelectTrack>,
|
track: Option<KeySelectTrack>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IdSelectAlbum {
|
impl KeySelectAlbum {
|
||||||
pub fn get(albums: &[Album], selection: &AlbumSelection) -> Option<Self> {
|
pub fn get(albums: &[Album], selection: &AlbumSelection) -> Option<Self> {
|
||||||
selection.state.list.selected().map(|index| {
|
selection.state.list.selected().map(|index| {
|
||||||
let album = &albums[index];
|
let album = &albums[index];
|
||||||
IdSelectAlbum {
|
let key = album.get_sort_key();
|
||||||
album_id: album.get_sort_key().clone(),
|
KeySelectAlbum {
|
||||||
track: IdSelectTrack::get(&album.tracks, &selection.track),
|
key: (key.0.to_owned(), key.1.to_owned(), key.2.to_owned()),
|
||||||
|
track: KeySelectTrack::get(&album.tracks, &selection.track),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_sort_key(&self) -> (&AlbumDate, &AlbumSeq, &AlbumId) {
|
||||||
|
(&self.key.0, &self.key.1, &self.key.2)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -330,20 +335,20 @@ mod tests {
|
|||||||
|
|
||||||
// Re-initialise.
|
// Re-initialise.
|
||||||
let expected = sel.clone();
|
let expected = sel.clone();
|
||||||
let active_album = IdSelectAlbum::get(albums, &sel);
|
let active_album = KeySelectAlbum::get(albums, &sel);
|
||||||
sel.reinitialise(albums, active_album);
|
sel.reinitialise(albums, active_album);
|
||||||
assert_eq!(sel, expected);
|
assert_eq!(sel, expected);
|
||||||
|
|
||||||
// Re-initialise out-of-bounds.
|
// Re-initialise out-of-bounds.
|
||||||
let mut expected = sel.clone();
|
let mut expected = sel.clone();
|
||||||
expected.decrement(albums, Delta::Line);
|
expected.decrement(albums, Delta::Line);
|
||||||
let active_album = IdSelectAlbum::get(albums, &sel);
|
let active_album = KeySelectAlbum::get(albums, &sel);
|
||||||
sel.reinitialise(&albums[..(albums.len() - 1)], active_album);
|
sel.reinitialise(&albums[..(albums.len() - 1)], active_album);
|
||||||
assert_eq!(sel, expected);
|
assert_eq!(sel, expected);
|
||||||
|
|
||||||
// Re-initialise empty.
|
// Re-initialise empty.
|
||||||
let expected = AlbumSelection::initialise(&[]);
|
let expected = AlbumSelection::initialise(&[]);
|
||||||
let active_album = IdSelectAlbum::get(albums, &sel);
|
let active_album = KeySelectAlbum::get(albums, &sel);
|
||||||
sel.reinitialise(&[], active_album);
|
sel.reinitialise(&[], active_album);
|
||||||
assert_eq!(sel, expected);
|
assert_eq!(sel, expected);
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ use musichoard::collection::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::tui::app::selection::{
|
use crate::tui::app::selection::{
|
||||||
album::{AlbumSelection, IdSelectAlbum},
|
album::{AlbumSelection, KeySelectAlbum},
|
||||||
Delta, SelectionState, WidgetState,
|
Delta, SelectionState, WidgetState,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -27,9 +27,9 @@ impl ArtistSelection {
|
|||||||
selection
|
selection
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reinitialise(&mut self, artists: &[Artist], active: Option<IdSelectArtist>) {
|
pub fn reinitialise(&mut self, artists: &[Artist], active: Option<KeySelectArtist>) {
|
||||||
if let Some(active) = active {
|
if let Some(active) = active {
|
||||||
let result = artists.binary_search_by(|a| a.get_sort_key().cmp(&active.artist_id));
|
let result = artists.binary_search_by(|a| a.get_sort_key().cmp(&active.get_sort_key()));
|
||||||
match result {
|
match result {
|
||||||
Ok(index) => self.reinitialise_with_index(artists, index, active.album),
|
Ok(index) => self.reinitialise_with_index(artists, index, active.album),
|
||||||
Err(index) => self.reinitialise_with_index(artists, index, None),
|
Err(index) => self.reinitialise_with_index(artists, index, None),
|
||||||
@ -43,7 +43,7 @@ impl ArtistSelection {
|
|||||||
&mut self,
|
&mut self,
|
||||||
artists: &[Artist],
|
artists: &[Artist],
|
||||||
index: usize,
|
index: usize,
|
||||||
active_album: Option<IdSelectAlbum>,
|
active_album: Option<KeySelectAlbum>,
|
||||||
) {
|
) {
|
||||||
if artists.is_empty() {
|
if artists.is_empty() {
|
||||||
self.state.list.select(None);
|
self.state.list.select(None);
|
||||||
@ -193,21 +193,26 @@ impl ArtistSelection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct IdSelectArtist {
|
pub struct KeySelectArtist {
|
||||||
artist_id: ArtistId,
|
key: (ArtistId,),
|
||||||
album: Option<IdSelectAlbum>,
|
album: Option<KeySelectAlbum>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IdSelectArtist {
|
impl KeySelectArtist {
|
||||||
pub fn get(artists: &[Artist], selection: &ArtistSelection) -> Option<Self> {
|
pub fn get(artists: &[Artist], selection: &ArtistSelection) -> Option<Self> {
|
||||||
selection.state.list.selected().map(|index| {
|
selection.state.list.selected().map(|index| {
|
||||||
let artist = &artists[index];
|
let artist = &artists[index];
|
||||||
IdSelectArtist {
|
let key = artist.get_sort_key();
|
||||||
artist_id: artist.get_sort_key().clone(),
|
KeySelectArtist {
|
||||||
album: IdSelectAlbum::get(&artist.albums, &selection.album),
|
key: (key.0.to_owned(),),
|
||||||
|
album: KeySelectAlbum::get(&artist.albums, &selection.album),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_sort_key(&self) -> (&ArtistId,) {
|
||||||
|
(&self.key.0,)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -385,20 +390,20 @@ mod tests {
|
|||||||
|
|
||||||
// Re-initialise.
|
// Re-initialise.
|
||||||
let expected = sel.clone();
|
let expected = sel.clone();
|
||||||
let active_artist = IdSelectArtist::get(artists, &sel);
|
let active_artist = KeySelectArtist::get(artists, &sel);
|
||||||
sel.reinitialise(artists, active_artist);
|
sel.reinitialise(artists, active_artist);
|
||||||
assert_eq!(sel, expected);
|
assert_eq!(sel, expected);
|
||||||
|
|
||||||
// Re-initialise out-of-bounds.
|
// Re-initialise out-of-bounds.
|
||||||
let mut expected = sel.clone();
|
let mut expected = sel.clone();
|
||||||
expected.decrement(artists, Delta::Line);
|
expected.decrement(artists, Delta::Line);
|
||||||
let active_artist = IdSelectArtist::get(artists, &sel);
|
let active_artist = KeySelectArtist::get(artists, &sel);
|
||||||
sel.reinitialise(&artists[..(artists.len() - 1)], active_artist);
|
sel.reinitialise(&artists[..(artists.len() - 1)], active_artist);
|
||||||
assert_eq!(sel, expected);
|
assert_eq!(sel, expected);
|
||||||
|
|
||||||
// Re-initialise empty.
|
// Re-initialise empty.
|
||||||
let expected = ArtistSelection::initialise(&[]);
|
let expected = ArtistSelection::initialise(&[]);
|
||||||
let active_artist = IdSelectArtist::get(artists, &sel);
|
let active_artist = KeySelectArtist::get(artists, &sel);
|
||||||
sel.reinitialise(&[], active_artist);
|
sel.reinitialise(&[], active_artist);
|
||||||
assert_eq!(sel, expected);
|
assert_eq!(sel, expected);
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ mod track;
|
|||||||
use musichoard::collection::{album::Album, artist::Artist, track::Track, Collection};
|
use musichoard::collection::{album::Album, artist::Artist, track::Track, Collection};
|
||||||
use ratatui::widgets::ListState;
|
use ratatui::widgets::ListState;
|
||||||
|
|
||||||
use artist::{ArtistSelection, IdSelectArtist};
|
use artist::{ArtistSelection, KeySelectArtist};
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum Category {
|
pub enum Category {
|
||||||
@ -64,7 +64,7 @@ impl Selection {
|
|||||||
self.artist.album.track.state.list = selected.track;
|
self.artist.album.track.state.list = selected.track;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_by_id(&mut self, artists: &[Artist], selected: IdSelection) {
|
pub fn select_by_id(&mut self, artists: &[Artist], selected: KeySelection) {
|
||||||
self.artist.reinitialise(artists, selected.artist);
|
self.artist.reinitialise(artists, selected.artist);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -229,14 +229,14 @@ impl ListSelection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct IdSelection {
|
pub struct KeySelection {
|
||||||
artist: Option<IdSelectArtist>,
|
artist: Option<KeySelectArtist>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IdSelection {
|
impl KeySelection {
|
||||||
pub fn get(collection: &Collection, selection: &Selection) -> Self {
|
pub fn get(collection: &Collection, selection: &Selection) -> Self {
|
||||||
IdSelection {
|
KeySelection {
|
||||||
artist: IdSelectArtist::get(collection, &selection.artist),
|
artist: KeySelectArtist::get(collection, &selection.artist),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use std::cmp;
|
use std::cmp;
|
||||||
|
|
||||||
use musichoard::collection::track::{Track, TrackId};
|
use musichoard::collection::track::{Track, TrackId, TrackNum};
|
||||||
|
|
||||||
use crate::tui::app::selection::{Delta, SelectionState, WidgetState};
|
use crate::tui::app::selection::{Delta, SelectionState, WidgetState};
|
||||||
|
|
||||||
@ -18,9 +18,9 @@ impl TrackSelection {
|
|||||||
selection
|
selection
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reinitialise(&mut self, tracks: &[Track], track: Option<IdSelectTrack>) {
|
pub fn reinitialise(&mut self, tracks: &[Track], track: Option<KeySelectTrack>) {
|
||||||
if let Some(track) = track {
|
if let Some(track) = track {
|
||||||
let result = tracks.binary_search_by(|t| t.get_sort_key().cmp(&track.track_id));
|
let result = tracks.binary_search_by(|t| t.get_sort_key().cmp(&track.get_sort_key()));
|
||||||
match result {
|
match result {
|
||||||
Ok(index) | Err(index) => self.reinitialise_with_index(tracks, index),
|
Ok(index) | Err(index) => self.reinitialise_with_index(tracks, index),
|
||||||
}
|
}
|
||||||
@ -100,19 +100,24 @@ impl TrackSelection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct IdSelectTrack {
|
pub struct KeySelectTrack {
|
||||||
track_id: TrackId,
|
key: (TrackNum, TrackId),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IdSelectTrack {
|
impl KeySelectTrack {
|
||||||
pub fn get(tracks: &[Track], selection: &TrackSelection) -> Option<Self> {
|
pub fn get(tracks: &[Track], selection: &TrackSelection) -> Option<Self> {
|
||||||
selection.state.list.selected().map(|index| {
|
selection.state.list.selected().map(|index| {
|
||||||
let track = &tracks[index];
|
let track = &tracks[index];
|
||||||
IdSelectTrack {
|
let key = track.get_sort_key();
|
||||||
track_id: track.get_sort_key().clone(),
|
KeySelectTrack {
|
||||||
|
key: (key.0.to_owned(), key.1.to_owned()),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_sort_key(&self) -> (&TrackNum, &TrackId) {
|
||||||
|
(&self.key.0, &self.key.1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -210,20 +215,20 @@ mod tests {
|
|||||||
|
|
||||||
// Re-initialise.
|
// Re-initialise.
|
||||||
let expected = sel.clone();
|
let expected = sel.clone();
|
||||||
let active_track = IdSelectTrack::get(tracks, &sel);
|
let active_track = KeySelectTrack::get(tracks, &sel);
|
||||||
sel.reinitialise(tracks, active_track);
|
sel.reinitialise(tracks, active_track);
|
||||||
assert_eq!(sel, expected);
|
assert_eq!(sel, expected);
|
||||||
|
|
||||||
// Re-initialise out-of-bounds.
|
// Re-initialise out-of-bounds.
|
||||||
let mut expected = sel.clone();
|
let mut expected = sel.clone();
|
||||||
expected.decrement(tracks, Delta::Line);
|
expected.decrement(tracks, Delta::Line);
|
||||||
let active_track = IdSelectTrack::get(tracks, &sel);
|
let active_track = KeySelectTrack::get(tracks, &sel);
|
||||||
sel.reinitialise(&tracks[..(tracks.len() - 1)], active_track);
|
sel.reinitialise(&tracks[..(tracks.len() - 1)], active_track);
|
||||||
assert_eq!(sel, expected);
|
assert_eq!(sel, expected);
|
||||||
|
|
||||||
// Re-initialise empty.
|
// Re-initialise empty.
|
||||||
let expected = TrackSelection::initialise(&[]);
|
let expected = TrackSelection::initialise(&[]);
|
||||||
let active_track = IdSelectTrack::get(tracks, &sel);
|
let active_track = KeySelectTrack::get(tracks, &sel);
|
||||||
sel.reinitialise(&[], active_track);
|
sel.reinitialise(&[], active_track);
|
||||||
assert_eq!(sel, expected);
|
assert_eq!(sel, expected);
|
||||||
}
|
}
|
||||||
|
@ -11,12 +11,12 @@ pub use handler::EventHandler;
|
|||||||
pub use listener::EventListener;
|
pub use listener::EventListener;
|
||||||
pub use ui::Ui;
|
pub use ui::Ui;
|
||||||
|
|
||||||
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
|
use crossterm::{
|
||||||
use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen};
|
event::{DisableMouseCapture, EnableMouseCapture},
|
||||||
use ratatui::backend::Backend;
|
terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
use ratatui::Terminal;
|
};
|
||||||
use std::io;
|
use ratatui::{backend::Backend, Terminal};
|
||||||
use std::marker::PhantomData;
|
use std::{io, marker::PhantomData};
|
||||||
|
|
||||||
use crate::tui::{
|
use crate::tui::{
|
||||||
app::{IAppAccess, IAppInteract},
|
app::{IAppAccess, IAppInteract},
|
||||||
@ -26,7 +26,7 @@ use crate::tui::{
|
|||||||
ui::IUi,
|
ui::IUi,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
Io(String),
|
Io(String),
|
||||||
Event(String),
|
Event(String),
|
||||||
@ -112,8 +112,8 @@ impl<B: Backend, UI: IUi, APP: IAppInteract + IAppAccess> Tui<B, UI, APP> {
|
|||||||
match listener_handle.join() {
|
match listener_handle.join() {
|
||||||
Ok(err) => return Err(err.into()),
|
Ok(err) => return Err(err.into()),
|
||||||
// Calling std::panic::resume_unwind(err) as recommended by the Rust docs
|
// Calling std::panic::resume_unwind(err) as recommended by the Rust docs
|
||||||
// will not produce an error message. The panic error message is printed at
|
// will not produce an error message. This may be due to the panic simply
|
||||||
// the location of the panic which at the time is hidden by the TUI.
|
// causing the process to abort in which case there is nothing to unwind.
|
||||||
Err(_) => return Err(Error::ListenerPanic),
|
Err(_) => return Err(Error::ListenerPanic),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -251,10 +251,9 @@ mod tests {
|
|||||||
|
|
||||||
let result = Tui::main(terminal, app, ui, handler, listener);
|
let result = Tui::main(terminal, app, ui, handler, listener);
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
assert_eq!(
|
|
||||||
result.unwrap_err(),
|
let error = EventError::Recv;
|
||||||
Error::Event(EventError::Recv.to_string())
|
assert_eq!(result.unwrap_err(), Error::Event(error.to_string()));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
|
use std::{collections::HashMap, str::FromStr};
|
||||||
|
|
||||||
use musichoard::collection::{
|
use musichoard::collection::{
|
||||||
album::{Album, AlbumId},
|
album::{Album, AlbumDate, AlbumId, AlbumMonth, AlbumSeq},
|
||||||
artist::{Artist, ArtistId, MusicBrainz},
|
artist::{Artist, ArtistId, MusicBrainz},
|
||||||
track::{Format, Quality, Track, TrackId},
|
track::{Track, TrackFormat, TrackId, TrackNum, TrackQuality},
|
||||||
};
|
};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use crate::tests::*;
|
use crate::tests::*;
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ use std::collections::HashMap;
|
|||||||
use musichoard::collection::{
|
use musichoard::collection::{
|
||||||
album::Album,
|
album::Album,
|
||||||
artist::Artist,
|
artist::Artist,
|
||||||
track::{Format, Track},
|
track::{Track, TrackFormat},
|
||||||
Collection,
|
Collection,
|
||||||
};
|
};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
@ -287,9 +287,13 @@ impl<'a, 'b> AlbumState<'a, 'b> {
|
|||||||
let album = state.list.selected().map(|i| &albums[i]);
|
let album = state.list.selected().map(|i| &albums[i]);
|
||||||
let info = Paragraph::new(format!(
|
let info = Paragraph::new(format!(
|
||||||
"Title: {}\n\
|
"Title: {}\n\
|
||||||
Year: {}",
|
Date: {}{}",
|
||||||
album.map(|a| a.id.title.as_str()).unwrap_or(""),
|
album.map(|a| a.id.title.as_str()).unwrap_or(""),
|
||||||
album.map(|a| a.id.year.to_string()).unwrap_or_default(),
|
album.map(|a| a.date.to_string()).unwrap_or_default(),
|
||||||
|
album
|
||||||
|
.filter(|a| a.seq.0 > 0)
|
||||||
|
.map(|a| format!(" ({})", a.seq.0))
|
||||||
|
.unwrap_or_default()
|
||||||
));
|
));
|
||||||
|
|
||||||
AlbumState {
|
AlbumState {
|
||||||
@ -323,13 +327,13 @@ impl<'a, 'b> TrackState<'a, 'b> {
|
|||||||
Title: {}\n\
|
Title: {}\n\
|
||||||
Artist: {}\n\
|
Artist: {}\n\
|
||||||
Quality: {}",
|
Quality: {}",
|
||||||
track.map(|t| t.id.number.to_string()).unwrap_or_default(),
|
track.map(|t| t.number.0.to_string()).unwrap_or_default(),
|
||||||
track.map(|t| t.id.title.as_str()).unwrap_or(""),
|
track.map(|t| t.id.title.as_str()).unwrap_or(""),
|
||||||
track.map(|t| t.artist.join("; ")).unwrap_or_default(),
|
track.map(|t| t.artist.join("; ")).unwrap_or_default(),
|
||||||
track
|
track
|
||||||
.map(|t| match t.quality.format {
|
.map(|t| match t.quality.format {
|
||||||
Format::Flac => "FLAC".to_string(),
|
TrackFormat::Flac => "FLAC".to_string(),
|
||||||
Format::Mp3 => format!("MP3 {}kbps", t.quality.bitrate),
|
TrackFormat::Mp3 => format!("MP3 {}kbps", t.quality.bitrate),
|
||||||
})
|
})
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
));
|
));
|
||||||
|
@ -4,7 +4,7 @@ use once_cell::sync::Lazy;
|
|||||||
use tempfile::NamedTempFile;
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
use musichoard::{
|
use musichoard::{
|
||||||
collection::{artist::Artist, Collection},
|
collection::{album::AlbumDate, artist::Artist, Collection},
|
||||||
database::{
|
database::{
|
||||||
json::{backend::JsonDatabaseFileBackend, JsonDatabase},
|
json::{backend::JsonDatabaseFileBackend, JsonDatabase},
|
||||||
IDatabase,
|
IDatabase,
|
||||||
@ -19,7 +19,10 @@ pub static DATABASE_TEST_FILE: Lazy<PathBuf> =
|
|||||||
fn expected() -> Collection {
|
fn expected() -> Collection {
|
||||||
let mut expected = COLLECTION.to_owned();
|
let mut expected = COLLECTION.to_owned();
|
||||||
for artist in expected.iter_mut() {
|
for artist in expected.iter_mut() {
|
||||||
artist.albums.clear();
|
for album in artist.albums.iter_mut() {
|
||||||
|
album.date = AlbumDate::default();
|
||||||
|
album.tracks.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
expected
|
expected
|
||||||
}
|
}
|
||||||
|
@ -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