WIP: Refactor the IDatabase calls to write directly to the database #271

Draft
wojtek wants to merge 12 commits from 268---refactor-the-idatabase-calls-to-write-directly-to-the-database into main
43 changed files with 1258 additions and 1462 deletions

View File

@ -22,11 +22,11 @@ jobs:
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- run: cargo build --no-default-features --all-targets - run: cargo build --no-default-features --all-targets
- run: cargo test --no-default-features --all-targets --no-fail-fast - run: cargo test --no-default-features --all-targets --no-fail-fast -- --include-ignored
- run: cargo build --all-targets - run: cargo build --all-targets
- run: cargo test --all-targets --no-fail-fast - run: cargo test --all-targets --no-fail-fast -- --include-ignored
- run: cargo build --all-features --all-targets - run: cargo build --all-features --all-targets
- run: cargo test --all-features --all-targets --no-fail-fast - run: cargo test --all-features --all-targets --no-fail-fast -- --include-ignored
- run: >- - run: >-
grcov target/debug/profraw grcov target/debug/profraw
--binary-path target/debug/ --binary-path target/debug/

View File

@ -8,7 +8,7 @@
This feature requires the `sqlite` library. This feature requires the `sqlite` library.
Either install system libraries: with Either install system libraries with:
On Fedora: On Fedora:
``` sh ``` sh
@ -17,6 +17,13 @@ sudo dnf install sqlite-devel
Or use a bundled version by enabling the `database-sqlite-bundled` feature. Or use a bundled version by enabling the `database-sqlite-bundled` feature.
To run the tests you will also need `sqldiff`.
On Fedora:
``` sh
sudo dnf install sqlite-tools
```
#### musicbrainz-api #### musicbrainz-api
This feature requires the `openssl` system library. This feature requires the `openssl` system library.

View File

@ -1,7 +1,7 @@
use std::mem; use std::mem;
use crate::core::collection::{ use crate::core::collection::{
merge::{Merge, MergeSorted}, merge::{IntoId, Merge, MergeSorted, WithId},
musicbrainz::{MbAlbumRef, MbRefOption}, musicbrainz::{MbAlbumRef, MbRefOption},
track::{Track, TrackFormat}, track::{Track, TrackFormat},
}; };
@ -19,14 +19,14 @@ pub struct Album {
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct AlbumMeta { pub struct AlbumMeta {
pub id: AlbumId, pub id: AlbumId,
pub date: AlbumDate,
pub seq: AlbumSeq,
pub info: AlbumInfo, pub info: AlbumInfo,
} }
/// Album non-identifier metadata. /// Album non-identifier metadata.
#[derive(Clone, Debug, Default, PartialEq, Eq)] #[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct AlbumInfo { pub struct AlbumInfo {
pub date: AlbumDate,
pub seq: AlbumSeq,
pub primary_type: Option<AlbumPrimaryType>, pub primary_type: Option<AlbumPrimaryType>,
pub secondary_types: Vec<AlbumSecondaryType>, pub secondary_types: Vec<AlbumSecondaryType>,
} }
@ -93,6 +93,12 @@ impl From<(u32, u8, u8)> for AlbumDate {
#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd, Ord, Eq, Hash)] #[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd, Ord, Eq, Hash)]
pub struct AlbumSeq(pub u8); pub struct AlbumSeq(pub u8);
impl From<u8> for AlbumSeq {
fn from(value: u8) -> Self {
AlbumSeq(value)
}
}
/// Based on [MusicBrainz types](https://musicbrainz.org/doc/Release_Group/Type). /// Based on [MusicBrainz types](https://musicbrainz.org/doc/Release_Group/Type).
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum AlbumPrimaryType { pub enum AlbumPrimaryType {
@ -181,7 +187,7 @@ impl Album {
} }
pub fn with_date<Date: Into<AlbumDate>>(mut self, date: Date) -> Self { pub fn with_date<Date: Into<AlbumDate>>(mut self, date: Date) -> Self {
self.meta.date = date.into(); self.meta.info.date = date.into();
self self
} }
@ -202,6 +208,23 @@ impl Ord for Album {
} }
} }
impl WithId for Album {
type Id = AlbumId;
fn id(&self) -> &Self::Id {
&self.meta.id
}
}
impl IntoId for Album {
type Id = AlbumId;
type IdSelf = Album;
fn into_id(self, _: &Self::Id) -> Self::IdSelf {
self
}
}
impl Merge for Album { impl Merge for Album {
fn merge_in_place(&mut self, other: Self) { fn merge_in_place(&mut self, other: Self) {
self.meta.merge_in_place(other.meta); self.meta.merge_in_place(other.meta);
@ -214,14 +237,12 @@ impl AlbumMeta {
pub fn new<Id: Into<AlbumId>>(id: Id) -> Self { pub fn new<Id: Into<AlbumId>>(id: Id) -> Self {
AlbumMeta { AlbumMeta {
id: id.into(), id: id.into(),
date: AlbumDate::default(),
seq: AlbumSeq::default(),
info: AlbumInfo::default(), info: AlbumInfo::default(),
} }
} }
pub fn with_date<Date: Into<AlbumDate>>(mut self, date: Date) -> Self { pub fn with_date<Date: Into<AlbumDate>>(mut self, date: Date) -> Self {
self.date = date.into(); self.info.date = date.into();
self self
} }
@ -232,8 +253,8 @@ impl AlbumMeta {
pub fn get_sort_key(&self) -> (&AlbumDate, &AlbumSeq, &str, &Option<AlbumPrimaryType>) { pub fn get_sort_key(&self) -> (&AlbumDate, &AlbumSeq, &str, &Option<AlbumPrimaryType>) {
( (
&self.date, &self.info.date,
&self.seq, &self.info.seq,
&self.id.title, &self.id.title,
&self.info.primary_type, &self.info.primary_type,
) )
@ -247,6 +268,36 @@ impl AlbumMeta {
self.id.clear_mb_ref(); self.id.clear_mb_ref();
} }
pub fn set_seq(&mut self, seq: AlbumSeq) {
self.info.set_seq(seq);
}
pub fn clear_seq(&mut self) {
self.info.clear_seq();
}
}
impl AlbumInfo {
pub fn with_date<Date: Into<AlbumDate>>(mut self, date: Date) -> Self {
self.date = date.into();
self
}
pub fn with_seq<Seq: Into<AlbumSeq>>(mut self, seq: Seq) -> Self {
self.seq = seq.into();
self
}
pub fn with_primary_type(mut self, primary_type: AlbumPrimaryType) -> Self {
self.primary_type = Some(primary_type);
self
}
pub fn with_secondary_types(mut self, secondary_types: Vec<AlbumSecondaryType>) -> Self {
self.secondary_types = secondary_types;
self
}
pub fn set_seq(&mut self, seq: AlbumSeq) { pub fn set_seq(&mut self, seq: AlbumSeq) {
self.seq = seq; self.seq = seq;
} }
@ -256,18 +307,6 @@ impl AlbumMeta {
} }
} }
impl AlbumInfo {
pub fn new(
primary_type: Option<AlbumPrimaryType>,
secondary_types: Vec<AlbumSecondaryType>,
) -> Self {
AlbumInfo {
primary_type,
secondary_types,
}
}
}
impl PartialOrd for AlbumMeta { impl PartialOrd for AlbumMeta {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other)) Some(self.cmp(other))
@ -284,16 +323,16 @@ impl Merge for AlbumMeta {
fn merge_in_place(&mut self, other: Self) { fn merge_in_place(&mut self, other: Self) {
assert!(self.id.compatible(&other.id)); assert!(self.id.compatible(&other.id));
self.id.mb_ref = self.id.mb_ref.take().or(other.id.mb_ref); self.id.mb_ref = self.id.mb_ref.take().or(other.id.mb_ref);
if self.date.year.is_none() && other.date.year.is_some() {
self.date = other.date;
}
self.seq = std::cmp::max(self.seq, other.seq);
self.info.merge_in_place(other.info); self.info.merge_in_place(other.info);
} }
} }
impl Merge for AlbumInfo { impl Merge for AlbumInfo {
fn merge_in_place(&mut self, other: Self) { fn merge_in_place(&mut self, other: Self) {
if self.date.year.is_none() && other.date.year.is_some() {
self.date = other.date;
}
self.seq = std::cmp::max(self.seq, other.seq);
self.primary_type = self.primary_type.take().or(other.primary_type); self.primary_type = self.primary_type.take().or(other.primary_type);
if self.secondary_types.is_empty() { if self.secondary_types.is_empty() {
self.secondary_types = other.secondary_types; self.secondary_types = other.secondary_types;
@ -385,21 +424,21 @@ mod tests {
fn set_clear_seq() { fn set_clear_seq() {
let mut album = Album::new("An album"); let mut album = Album::new("An album");
assert_eq!(album.meta.seq, AlbumSeq(0)); assert_eq!(album.meta.info.seq, AlbumSeq(0));
// Setting a seq on an album. // Setting a seq on an album.
album.meta.set_seq(AlbumSeq(6)); album.meta.set_seq(AlbumSeq(6));
assert_eq!(album.meta.seq, AlbumSeq(6)); assert_eq!(album.meta.info.seq, AlbumSeq(6));
album.meta.set_seq(AlbumSeq(6)); album.meta.set_seq(AlbumSeq(6));
assert_eq!(album.meta.seq, AlbumSeq(6)); assert_eq!(album.meta.info.seq, AlbumSeq(6));
album.meta.set_seq(AlbumSeq(8)); album.meta.set_seq(AlbumSeq(8));
assert_eq!(album.meta.seq, AlbumSeq(8)); assert_eq!(album.meta.info.seq, AlbumSeq(8));
// Clearing seq. // Clearing seq.
album.meta.clear_seq(); album.meta.clear_seq();
assert_eq!(album.meta.seq, AlbumSeq(0)); assert_eq!(album.meta.info.seq, AlbumSeq(0));
} }
#[test] #[test]
@ -439,7 +478,7 @@ mod tests {
#[test] #[test]
fn merge_album_dates() { fn merge_album_dates() {
let meta = AlbumMeta::new(AlbumId::new("An album")); let meta = AlbumInfo::default();
// No merge if years are different. // No merge if years are different.
let left = meta.clone().with_date((2000, 1, 6)); let left = meta.clone().with_date((2000, 1, 6));

View File

@ -11,56 +11,57 @@ use crate::core::collection::{
string::{self, NormalString}, string::{self, NormalString},
}; };
use super::merge::WithId;
/// An artist. /// An artist.
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct Artist { pub struct Artist {
pub id: ArtistId,
pub meta: ArtistMeta, pub meta: ArtistMeta,
pub albums: Vec<Album>, pub albums: Vec<Album>,
} }
/// Artist metadata.
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct ArtistMeta { pub struct ArtistMeta {
pub id: ArtistId, pub name: ArtistName,
pub sort: Option<String>, pub sort: Option<ArtistName>,
pub info: ArtistInfo, pub info: ArtistInfo,
} }
/// Artist non-identifier metadata.
#[derive(Clone, Debug, Default, PartialEq, Eq)] #[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ArtistInfo { pub struct ArtistInfo {
pub mb_ref: ArtistMbRef,
pub properties: HashMap<String, Vec<String>>, pub properties: HashMap<String, Vec<String>>,
} }
/// The artist identifier. /// The artist identifier.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ArtistId { pub struct ArtistId(pub usize);
pub name: String,
pub mb_ref: ArtistMbRef, impl From<usize> for ArtistId {
fn from(value: usize) -> Self {
ArtistId(value)
} }
}
impl AsRef<ArtistId> for ArtistId {
fn as_ref(&self) -> &ArtistId {
self
}
}
impl Display for ArtistId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
/// The artist name.
pub type ArtistName = String;
/// Unique database identifier. Use MBID for this purpose. /// Unique database identifier. Use MBID for this purpose.
pub type ArtistMbRef = MbRefOption<MbArtistRef>; pub type ArtistMbRef = MbRefOption<MbArtistRef>;
impl PartialOrd for Artist {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Artist {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.meta.cmp(&other.meta)
}
}
impl Merge for Artist {
fn merge_in_place(&mut self, other: Self) {
self.meta.merge_in_place(other.meta);
self.albums = MergeAlbums::merge_albums(mem::take(&mut self.albums), other.albums);
}
}
#[derive(Debug, Default)] #[derive(Debug, Default)]
struct MergeAlbums { struct MergeAlbums {
primary_by_lib_id: HashMap<u32, Album>, primary_by_lib_id: HashMap<u32, Album>,
@ -74,10 +75,10 @@ impl MergeAlbums {
fn merge_albums(primary_albums: Vec<Album>, secondary_albums: Vec<Album>) -> Vec<Album> { fn merge_albums(primary_albums: Vec<Album>, secondary_albums: Vec<Album>) -> Vec<Album> {
let mut cache = MergeAlbums::new(primary_albums); let mut cache = MergeAlbums::new(primary_albums);
cache.merge_albums_by_lib_id(secondary_albums); cache.merge_albums_by_lib_id(secondary_albums);
cache.merged.extend(MergeCollections::merge_by_name( let (merged, left) =
cache.primary_by_title, MergeCollections::merge_by_name(cache.primary_by_title, cache.secondary_by_title);
cache.secondary_by_title, cache.merged.extend(merged);
)); cache.merged.extend(left);
cache.merged.sort_unstable(); cache.merged.sort_unstable();
cache.merged cache.merged
} }
@ -140,49 +141,65 @@ impl MergeAlbums {
impl Artist { impl Artist {
/// Create new [`Artist`] with the given [`ArtistId`]. /// Create new [`Artist`] with the given [`ArtistId`].
pub fn new<Id: Into<ArtistId>>(id: Id) -> Self { pub fn new<Id: Into<ArtistId>, Name: Into<ArtistName>>(id: Id, name: Name) -> Self {
Artist { Artist {
meta: ArtistMeta::new(id), id: id.into(),
meta: ArtistMeta::new(name),
albums: vec![], albums: vec![],
} }
} }
} }
impl ArtistMeta { impl ArtistMeta {
pub fn new<Id: Into<ArtistId>>(id: Id) -> Self { pub fn new<Name: Into<ArtistName>>(name: Name) -> Self {
ArtistMeta { ArtistMeta {
id: id.into(), name: name.into(),
sort: None, sort: None,
info: ArtistInfo::default(), info: ArtistInfo::default(),
} }
} }
pub fn set_mb_ref(&mut self, mb_ref: ArtistMbRef) { pub fn compatible(&self, other: &ArtistMeta) -> bool {
self.id.set_mb_ref(mb_ref); let names_compatible =
} string::normalize_string(&self.name) == string::normalize_string(&other.name);
let mb_ref_compatible = self.info.mb_ref.is_none()
pub fn clear_mb_ref(&mut self) { || other.info.mb_ref.is_none()
self.id.clear_mb_ref(); || (self.info.mb_ref == other.info.mb_ref);
names_compatible && mb_ref_compatible
} }
pub fn get_sort_key(&self) -> (&str,) { pub fn get_sort_key(&self) -> (&str,) {
(self.sort.as_ref().unwrap_or(&self.id.name),) (self.sort.as_ref().unwrap_or(&self.name),)
} }
pub fn set_sort_key<S: Into<String>>(&mut self, sort: S) { pub fn with_sort<S: Into<String>>(mut self, name: S) -> Self {
self.sort = Some(sort.into()); self.sort = Some(name.into());
self
} }
pub fn clear_sort_key(&mut self) { pub fn with_mb_ref(mut self, mb_ref: ArtistMbRef) -> Self {
self.sort.take(); self.info.set_mb_ref(mb_ref);
self
} }
} }
impl ArtistInfo { impl ArtistInfo {
pub fn with_mb_ref(mut self, mb_ref: ArtistMbRef) -> Self {
self.set_mb_ref(mb_ref);
self
}
pub fn set_mb_ref(&mut self, mb_ref: ArtistMbRef) {
self.mb_ref = mb_ref;
}
pub fn clear_mb_ref(&mut self) {
self.mb_ref.take();
}
// In the functions below, it would be better to use `contains` instead of `iter().any`, but for // In the functions below, it would be better to use `contains` instead of `iter().any`, but for
// type reasons that does not work: // type reasons that does not work:
// https://stackoverflow.com/questions/48985924/why-does-a-str-not-coerce-to-a-string-when-using-veccontains // https://stackoverflow.com/questions/48985924/why-does-a-str-not-coerce-to-a-string-when-using-veccontains
pub fn add_to_property<S: AsRef<str> + Into<String>>(&mut self, property: S, values: Vec<S>) { pub fn add_to_property<S: AsRef<str> + Into<String>>(&mut self, property: S, values: Vec<S>) {
match self.properties.get_mut(property.as_ref()) { match self.properties.get_mut(property.as_ref()) {
Some(container) => { Some(container) => {
@ -224,6 +241,49 @@ impl ArtistInfo {
} }
} }
impl WithId for Artist {
type Id = ArtistId;
fn id(&self) -> &Self::Id {
&self.id
}
}
impl Merge for Artist {
fn merge_in_place(&mut self, other: Self) {
assert_eq!(self.id, other.id);
self.meta.merge_in_place(other.meta);
self.albums = MergeAlbums::merge_albums(mem::take(&mut self.albums), other.albums);
}
}
impl Merge for ArtistMeta {
fn merge_in_place(&mut self, other: Self) {
assert!(self.compatible(&other));
// No merge for name - always keep original.
self.info.merge_in_place(other.info);
}
}
impl Merge for ArtistInfo {
fn merge_in_place(&mut self, other: Self) {
self.mb_ref = self.mb_ref.take().or(other.mb_ref);
self.properties.merge_in_place(other.properties);
}
}
impl PartialOrd for Artist {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Artist {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.meta.cmp(&other.meta)
}
}
impl PartialOrd for ArtistMeta { impl PartialOrd for ArtistMeta {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other)) Some(self.cmp(other))
@ -236,69 +296,6 @@ impl Ord for ArtistMeta {
} }
} }
impl Merge for ArtistMeta {
fn merge_in_place(&mut self, other: Self) {
assert!(self.id.compatible(&other.id));
self.id.mb_ref = self.id.mb_ref.take().or(other.id.mb_ref);
self.sort = self.sort.take().or(other.sort);
self.info.merge_in_place(other.info);
}
}
impl Merge for ArtistInfo {
fn merge_in_place(&mut self, other: Self) {
self.properties.merge_in_place(other.properties);
}
}
impl<S: Into<String>> From<S> for ArtistId {
fn from(value: S) -> Self {
ArtistId::new(value)
}
}
impl AsRef<ArtistId> for ArtistId {
fn as_ref(&self) -> &ArtistId {
self
}
}
impl ArtistId {
pub fn new<S: Into<String>>(name: S) -> ArtistId {
ArtistId {
name: name.into(),
mb_ref: ArtistMbRef::None,
}
}
pub fn with_mb_ref(mut self, mb_ref: ArtistMbRef) -> Self {
self.mb_ref = mb_ref;
self
}
pub fn set_mb_ref(&mut self, mb_ref: ArtistMbRef) {
self.mb_ref = mb_ref;
}
pub fn clear_mb_ref(&mut self) {
self.mb_ref.take();
}
pub fn compatible(&self, other: &ArtistId) -> bool {
let names_compatible =
string::normalize_string(&self.name) == string::normalize_string(&other.name);
let mb_ref_compatible =
self.mb_ref.is_none() || other.mb_ref.is_none() || (self.mb_ref == other.mb_ref);
names_compatible && mb_ref_compatible
}
}
impl Display for ArtistId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.name)
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{ use crate::{
@ -318,89 +315,41 @@ mod tests {
static MUSICBUTLER: &str = "https://www.musicbutler.io/artist-page/483340948"; static MUSICBUTLER: &str = "https://www.musicbutler.io/artist-page/483340948";
static MUSICBUTLER_2: &str = "https://www.musicbutler.io/artist-page/658903042/"; static MUSICBUTLER_2: &str = "https://www.musicbutler.io/artist-page/658903042/";
#[test]
fn artist_sort_set_clear() {
let artist_id = ArtistId::new("an artist");
let sort_id_1 = String::from("sort id 1");
let sort_id_2 = String::from("sort id 2");
let mut artist = Artist::new(&artist_id.name);
assert_eq!(artist.meta.id, artist_id);
assert_eq!(artist.meta.sort, None);
assert_eq!(artist.meta.get_sort_key(), (artist_id.name.as_str(),));
assert!(artist.meta < ArtistMeta::new(sort_id_1.clone()));
assert!(artist.meta < ArtistMeta::new(sort_id_2.clone()));
assert!(artist < Artist::new(sort_id_1.clone()));
assert!(artist < Artist::new(sort_id_2.clone()));
artist.meta.set_sort_key(sort_id_1.clone());
assert_eq!(artist.meta.id, artist_id);
assert_eq!(artist.meta.sort.as_ref(), Some(&sort_id_1));
assert_eq!(artist.meta.get_sort_key(), (sort_id_1.as_str(),));
assert!(artist.meta > ArtistMeta::new(artist_id.clone()));
assert!(artist.meta < ArtistMeta::new(sort_id_2.clone()));
assert!(artist > Artist::new(artist_id.clone()));
assert!(artist < Artist::new(sort_id_2.clone()));
artist.meta.set_sort_key(sort_id_2.clone());
assert_eq!(artist.meta.id, artist_id);
assert_eq!(artist.meta.sort.as_ref(), Some(&sort_id_2));
assert_eq!(artist.meta.get_sort_key(), (sort_id_2.as_str(),));
assert!(artist.meta > ArtistMeta::new(artist_id.clone()));
assert!(artist.meta > ArtistMeta::new(sort_id_1.clone()));
assert!(artist > Artist::new(artist_id.clone()));
assert!(artist > Artist::new(sort_id_1.clone()));
artist.meta.clear_sort_key();
assert_eq!(artist.meta.id, artist_id);
assert_eq!(artist.meta.sort, None);
assert_eq!(artist.meta.get_sort_key(), (artist_id.name.as_str(),));
assert!(artist.meta < ArtistMeta::new(sort_id_1.clone()));
assert!(artist.meta < ArtistMeta::new(sort_id_2.clone()));
assert!(artist < Artist::new(sort_id_1.clone()));
assert!(artist < Artist::new(sort_id_2.clone()));
}
#[test] #[test]
fn set_clear_musicbrainz_url() { fn set_clear_musicbrainz_url() {
let mut artist = Artist::new(ArtistId::new("an artist")); let mut info = ArtistInfo::default();
let mut expected: MbRefOption<MbArtistRef> = MbRefOption::None; let mut expected: MbRefOption<MbArtistRef> = MbRefOption::None;
assert_eq!(artist.meta.id.mb_ref, expected); assert_eq!(info.mb_ref, expected);
// Setting a URL on an artist. // Setting a URL on an info.
artist.meta.id.set_mb_ref(MbRefOption::Some( info.set_mb_ref(MbRefOption::Some(
MbArtistRef::from_url_str(MUSICBRAINZ).unwrap(), MbArtistRef::from_url_str(MUSICBRAINZ).unwrap(),
)); ));
expected.replace(MbArtistRef::from_url_str(MUSICBRAINZ).unwrap()); expected.replace(MbArtistRef::from_url_str(MUSICBRAINZ).unwrap());
assert_eq!(artist.meta.id.mb_ref, expected); assert_eq!(info.mb_ref, expected);
artist.meta.id.set_mb_ref(MbRefOption::Some( info.set_mb_ref(MbRefOption::Some(
MbArtistRef::from_url_str(MUSICBRAINZ).unwrap(), MbArtistRef::from_url_str(MUSICBRAINZ).unwrap(),
)); ));
assert_eq!(artist.meta.id.mb_ref, expected); assert_eq!(info.mb_ref, expected);
artist.meta.id.set_mb_ref(MbRefOption::Some( info.set_mb_ref(MbRefOption::Some(
MbArtistRef::from_url_str(MUSICBRAINZ_2).unwrap(), MbArtistRef::from_url_str(MUSICBRAINZ_2).unwrap(),
)); ));
expected.replace(MbArtistRef::from_url_str(MUSICBRAINZ_2).unwrap()); expected.replace(MbArtistRef::from_url_str(MUSICBRAINZ_2).unwrap());
assert_eq!(artist.meta.id.mb_ref, expected); assert_eq!(info.mb_ref, expected);
// Clearing URLs. // Clearing URLs.
artist.meta.id.clear_mb_ref(); info.clear_mb_ref();
expected.take(); expected.take();
assert_eq!(artist.meta.id.mb_ref, expected); assert_eq!(info.mb_ref, expected);
} }
#[test] #[test]
fn add_to_remove_from_property() { fn add_to_remove_from_property() {
let mut artist = Artist::new(ArtistId::new("an artist")); let mut info = ArtistInfo::default();
let info = &mut artist.meta.info;
let mut expected: Vec<String> = vec![]; let mut expected: Vec<String> = vec![];
assert!(info.properties.is_empty()); assert!(info.properties.is_empty());
@ -470,9 +419,8 @@ mod tests {
#[test] #[test]
fn set_clear_musicbutler_urls() { fn set_clear_musicbutler_urls() {
let mut artist = Artist::new(ArtistId::new("an artist")); let mut info = ArtistInfo::default();
let info = &mut artist.meta.info;
let mut expected: Vec<String> = vec![]; let mut expected: Vec<String> = vec![];
assert!(info.properties.is_empty()); assert!(info.properties.is_empty());
@ -508,8 +456,9 @@ mod tests {
fn merge_artist_no_overlap() { fn merge_artist_no_overlap() {
let left = FULL_COLLECTION[0].to_owned(); let left = FULL_COLLECTION[0].to_owned();
let mut right = FULL_COLLECTION[1].to_owned(); let mut right = FULL_COLLECTION[1].to_owned();
right.meta.id = left.meta.id.clone(); right.id = left.id;
right.meta.id.mb_ref = MbRefOption::None; right.meta.name = left.meta.name.clone();
right.meta.info.mb_ref = MbRefOption::None;
right.meta.info.properties = HashMap::new(); right.meta.info.properties = HashMap::new();
let mut expected = left.clone(); let mut expected = left.clone();
@ -545,7 +494,9 @@ mod tests {
fn merge_artist_overlap() { fn merge_artist_overlap() {
let mut left = FULL_COLLECTION[0].to_owned(); let mut left = FULL_COLLECTION[0].to_owned();
let mut right = FULL_COLLECTION[1].to_owned(); let mut right = FULL_COLLECTION[1].to_owned();
right.meta.id = left.meta.id.clone(); right.id = left.id;
right.meta.name = left.meta.name.clone();
right.meta.info.mb_ref = left.meta.info.mb_ref.clone();
// The right collection needs more albums than we modify to make sure some do not overlap. // The right collection needs more albums than we modify to make sure some do not overlap.
assert!(right.albums.len() > 2); assert!(right.albums.len() > 2);
@ -581,7 +532,7 @@ mod tests {
#[test] #[test]
#[should_panic(expected = "multiple secondaries unsupported")] #[should_panic(expected = "multiple secondaries unsupported")]
fn merge_two_db_albums_to_one_lib_album() { fn merge_two_db_albums_to_one_lib_album() {
let mut left = Artist::new(ArtistId::new("Artist")); let mut left = Artist::new(0, "Artist");
let mut right = left.clone(); let mut right = left.clone();
let album = Album::new(AlbumId::new("Album")); let album = Album::new(AlbumId::new("Album"));
@ -598,7 +549,7 @@ mod tests {
#[test] #[test]
#[should_panic(expected = "multiple primaries unsupported")] #[should_panic(expected = "multiple primaries unsupported")]
fn merge_one_db_album_to_two_lib_albums() { fn merge_one_db_album_to_two_lib_albums() {
let mut left = Artist::new(ArtistId::new("Artist")); let mut left = Artist::new(0, "Artist");
let mut right = left.clone(); let mut right = left.clone();
let album = Album::new(AlbumId::new("Album")); let album = Album::new(AlbumId::new("Album"));
@ -615,7 +566,7 @@ mod tests {
#[test] #[test]
fn merge_normalized_album_titles() { fn merge_normalized_album_titles() {
let mut left = Artist::new(ArtistId::new("Artist")); let mut left = Artist::new(0, "Artist");
let mut right = left.clone(); let mut right = left.clone();
left.albums left.albums
@ -640,7 +591,7 @@ mod tests {
#[test] #[test]
fn merge_multiple_singletons() { fn merge_multiple_singletons() {
let mut left = Artist::new(ArtistId::new("Artist")); let mut left = Artist::new(0, "Artist");
let mut right = left.clone(); let mut right = left.clone();
left.albums.push(Album::new(AlbumId::new("Singleton 1"))); left.albums.push(Album::new(AlbumId::new("Singleton 1")));
@ -666,7 +617,7 @@ mod tests {
#[test] #[test]
fn merge_two_db_albums_to_one_lib_album_with_ids() { fn merge_two_db_albums_to_one_lib_album_with_ids() {
let mut left = Artist::new(ArtistId::new("Artist")); let mut left = Artist::new(0, "Artist");
let mut right = left.clone(); let mut right = left.clone();
let album = Album::new(AlbumId::new("Album")); let album = Album::new(AlbumId::new("Album"));

View File

@ -105,10 +105,26 @@ impl<T> NormalMap<T> {
} }
} }
pub trait WithId {
type Id;
fn id(&self) -> &Self::Id;
}
pub trait IntoId {
type Id;
type IdSelf;
fn into_id(self, id: &Self::Id) -> Self::IdSelf;
}
pub struct MergeCollections; pub struct MergeCollections;
impl MergeCollections { impl MergeCollections {
pub fn merge_by_name<T: Merge>(mut primary: NormalMap<T>, secondary: NormalMap<T>) -> Vec<T> { pub fn merge_by_name<Id, T1: Merge + WithId<Id = Id>, T2: IntoId<Id = Id, IdSelf = T1>>(
mut primary: NormalMap<T2>,
secondary: NormalMap<T1>,
) -> (Vec<T1>, Vec<T2>) {
let mut merged = vec![]; let mut merged = vec![];
for (title, mut secondary_items) in secondary.0.into_iter() { for (title, mut secondary_items) in secondary.0.into_iter() {
match primary.remove(&title) { match primary.remove(&title) {
@ -117,14 +133,17 @@ impl MergeCollections {
// added once encountered in the wild. // added once encountered in the wild.
assert_eq!(primary_items.len(), 1, "multiple primaries unsupported"); assert_eq!(primary_items.len(), 1, "multiple primaries unsupported");
assert_eq!(secondary_items.len(), 1, "multiple secondaries unsupported"); assert_eq!(secondary_items.len(), 1, "multiple secondaries unsupported");
let mut primary_item = primary_items.pop().unwrap();
primary_item.merge_in_place(secondary_items.pop().unwrap()); let secondary_item = secondary_items.pop().unwrap();
let id = secondary_item.id();
let mut primary_item = primary_items.pop().unwrap().into_id(id);
primary_item.merge_in_place(secondary_item);
merged.push(primary_item); merged.push(primary_item);
} }
None => merged.extend(secondary_items), None => merged.extend(secondary_items),
} }
} }
merged.extend(primary.0.into_values().flatten()); (merged, primary.0.into_values().flatten().collect())
merged
} }
} }

View File

@ -45,6 +45,12 @@ pub enum MbRefOption<T> {
None, None,
} }
impl<T> Default for MbRefOption<T> {
fn default() -> Self {
MbRefOption::None
}
}
impl<T> MbRefOption<T> { impl<T> MbRefOption<T> {
pub fn is_some(&self) -> bool { pub fn is_some(&self) -> bool {
matches!(self, MbRefOption::Some(_)) matches!(self, MbRefOption::Some(_))

View File

@ -5,22 +5,32 @@ use std::fmt;
#[cfg(test)] #[cfg(test)]
use mockall::automock; use mockall::automock;
use crate::core::collection::{self, Collection}; use crate::{collection::artist::{ArtistId, ArtistMeta}, core::collection::{self, Collection}};
/// Trait for interacting with the database. /// Trait for interacting with the database.
#[cfg_attr(test, automock)] #[cfg_attr(test, automock)]
pub trait IDatabase { pub trait IDatabase {
/// Reset all content.
fn reset(&mut self) -> Result<(), SaveError>;
/// Load collection from the database. /// Load collection from the database.
fn load(&mut self) -> Result<Collection, LoadError>; fn load(&mut self) -> Result<Collection, LoadError>;
/// Save collection to the database. /// Save collection to the database.
fn save(&mut self, collection: &Collection) -> Result<(), SaveError>; fn save(&mut self, collection: &Collection) -> Result<(), SaveError>;
/// Insert an artist into the database and return its assigned ID.
fn insert_artist(&mut self, artist: &ArtistMeta) -> Result<ArtistId, SaveError>;
} }
/// Null database implementation of [`IDatabase`]. /// Null database implementation of [`IDatabase`].
pub struct NullDatabase; pub struct NullDatabase;
impl IDatabase for NullDatabase { impl IDatabase for NullDatabase {
fn reset(&mut self) -> Result<(), SaveError> {
Ok(())
}
fn load(&mut self) -> Result<Collection, LoadError> { fn load(&mut self) -> Result<Collection, LoadError> {
Ok(vec![]) Ok(vec![])
} }
@ -28,6 +38,10 @@ impl IDatabase for NullDatabase {
fn save(&mut self, _collection: &Collection) -> Result<(), SaveError> { fn save(&mut self, _collection: &Collection) -> Result<(), SaveError> {
Ok(()) Ok(())
} }
fn insert_artist(&mut self, _: &ArtistMeta) -> Result<ArtistId,SaveError> {
Ok(ArtistId(0))
}
} }
/// Error type for database calls. /// Error type for database calls.
@ -98,6 +112,12 @@ mod tests {
use super::*; use super::*;
#[test]
fn null_database_reset() {
let mut database = NullDatabase;
assert!(database.reset().is_ok());
}
#[test] #[test]
fn null_database_load() { fn null_database_load() {
let mut database = NullDatabase; let mut database = NullDatabase;

View File

@ -2,8 +2,7 @@ use crate::core::{
collection::{ collection::{
album::{Album, AlbumId}, album::{Album, AlbumId},
artist::{Artist, ArtistId}, artist::{Artist, ArtistId},
merge::{MergeCollections, NormalMap}, Collection,
string, Collection,
}, },
musichoard::{filter::CollectionFilter, Error, MusicHoard}, musichoard::{filter::CollectionFilter, Error, MusicHoard},
}; };
@ -30,14 +29,11 @@ impl<Database, Library> IMusicHoardBase for MusicHoard<Database, Library> {
} }
pub trait IMusicHoardBasePrivate { pub trait IMusicHoardBasePrivate {
fn sort_artists(collection: &mut [Artist]); fn sort_albums_and_tracks<'a, COL: Iterator<Item = &'a mut Vec<Album>>>(collection: COL);
fn sort_albums_and_tracks<'a, C: Iterator<Item = &'a mut Artist>>(collection: C);
fn merge_collections<It: IntoIterator<Item = Artist>>(&self, database: It) -> Collection;
fn filter_collection(&self) -> Collection; fn filter_collection(&self) -> Collection;
fn filter_artist(&self, artist: &Artist) -> Option<Artist>; fn filter_artist(&self, artist: &Artist) -> Option<Artist>;
fn get_artist<'a>(collection: &'a Collection, artist_id: &ArtistId) -> Option<&'a Artist>;
fn get_artist_mut<'a>( fn get_artist_mut<'a>(
collection: &'a mut Collection, collection: &'a mut Collection,
artist_id: &ArtistId, artist_id: &ArtistId,
@ -56,37 +52,15 @@ pub trait IMusicHoardBasePrivate {
} }
impl<Database, Library> IMusicHoardBasePrivate for MusicHoard<Database, Library> { impl<Database, Library> IMusicHoardBasePrivate for MusicHoard<Database, Library> {
fn sort_artists(collection: &mut [Artist]) { fn sort_albums_and_tracks<'a, COL: Iterator<Item = &'a mut Vec<Album>>>(collection: COL) {
collection.sort_unstable(); for albums in collection {
} albums.sort_unstable();
for album in albums.iter_mut() {
fn sort_albums_and_tracks<'a, COL: Iterator<Item = &'a mut Artist>>(collection: COL) {
for artist in collection {
artist.albums.sort_unstable();
for album in artist.albums.iter_mut() {
album.tracks.sort_unstable(); album.tracks.sort_unstable();
} }
} }
} }
fn merge_collections<It: IntoIterator<Item = Artist>>(&self, database: It) -> Collection {
let mut primary = NormalMap::<Artist>::new();
let mut secondary = NormalMap::<Artist>::new();
for artist in self.library_cache.iter().cloned() {
primary.insert(string::normalize_string(&artist.meta.id.name), artist);
}
for artist in database.into_iter() {
secondary.insert(string::normalize_string(&artist.meta.id.name), artist);
}
let mut collection = MergeCollections::merge_by_name(primary, secondary);
collection.sort_unstable();
collection
}
fn filter_collection(&self) -> Collection { fn filter_collection(&self) -> Collection {
let iter = self.collection.iter(); let iter = self.collection.iter();
iter.flat_map(|a| self.filter_artist(a)).collect() iter.flat_map(|a| self.filter_artist(a)).collect()
@ -101,19 +75,18 @@ impl<Database, Library> IMusicHoardBasePrivate for MusicHoard<Database, Library>
return None; return None;
} }
let meta = artist.meta.clone(); Some(Artist {
Some(Artist { meta, albums }) id: artist.id,
} meta: artist.meta.clone(),
albums,
fn get_artist<'a>(collection: &'a Collection, artist_id: &ArtistId) -> Option<&'a Artist> { })
collection.iter().find(|a| &a.meta.id == artist_id)
} }
fn get_artist_mut<'a>( fn get_artist_mut<'a>(
collection: &'a mut Collection, collection: &'a mut Collection,
artist_id: &ArtistId, artist_id: &ArtistId,
) -> Option<&'a mut Artist> { ) -> Option<&'a mut Artist> {
collection.iter_mut().find(|a| &a.meta.id == artist_id) collection.iter_mut().find(|a| &a.id == artist_id)
} }
fn get_artist_mut_or_err<'a>( fn get_artist_mut_or_err<'a>(
@ -148,180 +121,224 @@ impl<Database, Library> IMusicHoardBasePrivate for MusicHoard<Database, Library>
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::collections::HashMap;
use crate::{ use crate::{
collection::{album::AlbumPrimaryType, artist::ArtistMeta}, collection::{
core::testmod::FULL_COLLECTION, album::AlbumPrimaryType,
artist::{ArtistMeta, ArtistName},
},
core::{musichoard::LibArtist, testmod::FULL_COLLECTION},
filter::AlbumField, filter::AlbumField,
}; };
use super::*; use super::*;
#[test] #[test]
#[ignore]
// TODO: figure out how to do a merge
fn merge_collection_no_overlap() { fn merge_collection_no_overlap() {
let half: usize = FULL_COLLECTION.len() / 2; // let half: usize = FULL_COLLECTION.len() / 2;
let left = FULL_COLLECTION[..half].to_owned(); // let left = FULL_COLLECTION[..half].to_owned();
let right = FULL_COLLECTION[half..].to_owned(); // let right = FULL_COLLECTION[half..].to_owned();
let mut expected = FULL_COLLECTION.to_owned(); // let mut expected = FULL_COLLECTION.to_owned();
expected.sort_unstable(); // expected.sort_unstable();
let mut mh = MusicHoard { // let mut mh = MusicHoard {
library_cache: left.clone(), // library_cache: left.clone(),
..Default::default() // ..Default::default()
}; // };
mh.collection = mh.merge_collections(right.clone()); // mh.collection = mh.merge_collections(right.clone());
assert_eq!(expected, mh.collection); // assert_eq!(expected, mh.collection);
// The merge is completely non-overlapping so it should be commutative. // // The merge is completely non-overlapping so it should be commutative.
let mut mh = MusicHoard { // let mut mh = MusicHoard {
library_cache: right.clone(), // library_cache: right.clone(),
..Default::default() // ..Default::default()
}; // };
mh.collection = mh.merge_collections(left.clone()); // mh.collection = mh.merge_collections(left.clone());
assert_eq!(expected, mh.collection); // assert_eq!(expected, mh.collection);
} }
#[test] #[test]
#[ignore]
// TODO: figure out how to do a merge
fn merge_collection_overlap() { fn merge_collection_overlap() {
let half: usize = FULL_COLLECTION.len() / 2; // let half: usize = FULL_COLLECTION.len() / 2;
let left = FULL_COLLECTION[..(half + 1)].to_owned(); // let left = FULL_COLLECTION[..(half + 1)].to_owned();
let right = FULL_COLLECTION[half..].to_owned(); // let right = FULL_COLLECTION[half..].to_owned();
let mut expected = FULL_COLLECTION.to_owned(); // let mut expected = FULL_COLLECTION.to_owned();
expected.sort_unstable(); // expected.sort_unstable();
let mut mh = MusicHoard { // let mut mh = MusicHoard {
library_cache: left.clone(), // library_cache: left.clone(),
..Default::default() // ..Default::default()
}; // };
mh.collection = mh.merge_collections(right.clone()); // mh.collection = mh.merge_collections(right.clone());
assert_eq!(expected, mh.collection); // assert_eq!(expected, mh.collection);
// The merge does not overwrite any data so it should be commutative. // // The merge does not overwrite any data so it should be commutative.
let mut mh = MusicHoard { // let mut mh = MusicHoard {
library_cache: right.clone(), // library_cache: right.clone(),
..Default::default() // ..Default::default()
}; // };
mh.collection = mh.merge_collections(left.clone()); // mh.collection = mh.merge_collections(left.clone());
assert_eq!(expected, mh.collection); // assert_eq!(expected, mh.collection);
} }
#[test] #[test]
#[ignore]
// TODO: figure out how to do a merge
fn merge_collection_incompatible_sorting() { fn merge_collection_incompatible_sorting() {
// It may be that the same artist in one collection has a "sort" field defined while the // // It may be that the same artist in one collection has a "sort" field defined while the
// same artist in the other collection does not. This means that the two collections are not // // same artist in the other collection does not. This means that the two collections are not
// sorted consistently. If the merge assumes they are sorted consistently this will lead to // // sorted consistently. If the merge assumes they are sorted consistently this will lead to
// the same artist appearing twice in the final list. This should not be the case. // // the same artist appearing twice in the final list. This should not be the case.
// We will mimic this situation by taking the last artist from FULL_COLLECTION and giving it // // We will mimic this situation by taking the last artist from FULL_COLLECTION and giving it
// a sorting name that would place it in the beginning. // // a sorting name that would place it in the beginning.
let left = FULL_COLLECTION.to_owned(); // let left = FULL_COLLECTION.to_owned();
let mut right: Vec<Artist> = vec![left.last().unwrap().clone()]; // let mut right: Vec<Artist> = vec![left.last().unwrap().clone()];
assert!(right.first().unwrap() > left.first().unwrap()); // assert!(right.first().unwrap() > left.first().unwrap());
let artist_sort = Some(String::from("Album_Artist 0")); // let artist_sort = Some(String::from("Album_Artist 0"));
right[0].meta.sort = artist_sort.clone(); // right[0].meta.info.sort = artist_sort.clone();
assert!(right.first().unwrap() < left.first().unwrap()); // assert!(right.first().unwrap() < left.first().unwrap());
// The result of the merge should be the same list of artists, but with the last artist now // // The result of the merge should be the same list of artists, but with the last artist now
// in first place. // // in first place.
let mut expected = left.to_owned(); // let mut expected = left.to_owned();
expected.last_mut().as_mut().unwrap().meta.sort = artist_sort.clone(); // expected.last_mut().as_mut().unwrap().meta.info.sort = artist_sort.clone();
expected.rotate_right(1); // expected.rotate_right(1);
let mut mh = MusicHoard { // let mut mh = MusicHoard {
library_cache: left.clone(), // library_cache: left.clone(),
..Default::default() // ..Default::default()
}; // };
mh.collection = mh.merge_collections(right.clone()); // mh.collection = mh.merge_collections(right.clone());
assert_eq!(expected, mh.collection); // assert_eq!(expected, mh.collection);
// The merge overwrites the sort data, but no data is erased so it should be commutative. // // The merge overwrites the sort data, but no data is erased so it should be commutative.
let mut mh = MusicHoard { // let mut mh = MusicHoard {
library_cache: right.clone(), // library_cache: right.clone(),
..Default::default() // ..Default::default()
}; // };
mh.collection = mh.merge_collections(left.clone()); // mh.collection = mh.merge_collections(left.clone());
assert_eq!(expected, mh.collection); // assert_eq!(expected, mh.collection);
} }
#[test] #[test]
#[ignore]
// TODO: figure out how to do a merge
#[should_panic(expected = "multiple secondaries unsupported")] #[should_panic(expected = "multiple secondaries unsupported")]
fn merge_two_db_artists_to_one_lib_artist() { fn merge_two_db_artists_to_one_lib_artist() {
let mut left = Collection::new(); // let mut left = HashMap::<String, LibArtist>::new();
let mut right = Collection::new(); // let mut right = Collection::new();
let artist = Artist::new(ArtistId::new("Artist")); // let name = ArtistName::new("Artist");
left.push(artist.clone()); // left.insert(
right.push(artist.clone()); // name.official.clone(),
right.push(artist.clone()); // LibArtist {
// meta: ArtistMeta::new(name.clone()),
// albums: vec![],
// },
// );
// right.push(Artist::new(1, name.clone()));
// right.push(Artist::new(2, name.clone()));
let mut mh = MusicHoard { // let mut mh = MusicHoard {
library_cache: left.clone(), // library_cache: left.clone(),
..Default::default() // ..Default::default()
}; // };
mh.collection = mh.merge_collections(right.clone()); // mh.collection = mh.merge_collections(right.clone());
} }
#[test] #[test]
#[ignore]
// TODO: change to albums - primary clash is not possible for artists since without a lib id
#[should_panic(expected = "multiple primaries unsupported")] #[should_panic(expected = "multiple primaries unsupported")]
fn merge_one_db_artist_to_two_lib_artists() { fn merge_one_db_artist_to_two_lib_artists() {
let mut left = Collection::new(); // let mut left = Collection::new();
let mut right = Collection::new(); // let mut right = Collection::new();
let artist = Artist::new(ArtistId::new("Artist")); // let artist = Artist::new(ArtistId::new("Artist"));
left.push(artist.clone()); // left.insert(
left.push(artist.clone()); // name.official.clone(),
right.push(artist.clone()); // LibArtist {
// name: name.clone(),
// meta: ArtistMeta::default(),
// albums: vec![],
// },
// );
// left.insert(
// name.official.clone(),
// LibArtist {
// name: name.clone(),
// meta: ArtistMeta::default(),
// albums: vec![],
// },
// );
// right.push(artist.clone());
let mut mh = MusicHoard { // let mut mh = MusicHoard {
library_cache: left.clone(), // library_cache: left.clone(),
..Default::default() // ..Default::default()
}; // };
mh.collection = mh.merge_collections(right.clone()); // mh.collection = mh.merge_collections(right.clone());
} }
#[test] #[test]
#[ignore]
// TODO: figue out how to do a merge
fn merge_normalized_artist_names() { fn merge_normalized_artist_names() {
let mut left = Collection::new(); // let mut left = HashMap::<String, LibArtist>::new();
let mut right = Collection::new(); // let mut right = Collection::new();
left.push(Artist::new(ArtistId::new("ArtistName Name"))); // let left_name = "ArtistName Name";
// left.insert(
// String::from(left_name),
// LibArtist {
// meta: ArtistMeta::new(left_name.into()),
// albums: vec![],
// },
// );
right.push(Artist::new(ArtistId::new("arTist—naMe 'name"))); // right.push(Artist::new(1, "arTist—naMe 'name"));
right.push(Artist::new(ArtistId::new("ArtistName “Name”"))); // right.push(Artist::new(2, "ArtistName “Name”"));
// The first artist will be merged, the second will be added. // // The first artist will be merged preserving the name, the second will be added.
let mut expected = left.clone(); // let mut expected = right.clone();
expected.push(right.last().unwrap().clone()); // expected.first_mut().unwrap().meta.name = left["ArtistName Name"].meta.name.clone();
expected.sort_unstable();
let mut mh = MusicHoard { // let mut mh = MusicHoard {
library_cache: left.clone(), // library_cache: left.clone(),
..Default::default() // ..Default::default()
}; // };
mh.collection = mh.merge_collections(right.clone()); // mh.collection = mh.merge_collections(right.clone());
assert_eq!(expected, mh.collection); // assert_eq!(expected, mh.collection);
} }
#[test] #[test]
fn filtered() { fn filtered() {
let mut mh = MusicHoard { let mut mh = MusicHoard {
collection: vec![Artist { collection: vec![Artist {
meta: ArtistMeta::new(ArtistId::new("Artist")), id: 0.into(),
meta: ArtistMeta::new("Artist"),
albums: vec![ albums: vec![
Album::new(AlbumId::new("Album 1")), Album::new(AlbumId::new("Album 1")),
Album::new(AlbumId::new("Album 2")), Album::new(AlbumId::new("Album 2")),

View File

@ -1,3 +1,5 @@
use std::collections::HashMap;
use crate::core::{ use crate::core::{
interface::{database::IDatabase, library::ILibrary}, interface::{database::IDatabase, library::ILibrary},
musichoard::{CollectionFilter, MusicHoard, NoDatabase, NoLibrary}, musichoard::{CollectionFilter, MusicHoard, NoDatabase, NoLibrary},
@ -65,10 +67,9 @@ impl MusicHoard<NoDatabase, NoLibrary> {
filter: CollectionFilter::default(), filter: CollectionFilter::default(),
filtered: vec![], filtered: vec![],
collection: vec![], collection: vec![],
pre_commit: vec![],
database: NoDatabase, database: NoDatabase,
library: NoLibrary, library: NoLibrary,
library_cache: vec![], library_cache: HashMap::new(),
} }
} }
} }
@ -87,10 +88,9 @@ impl<Library: ILibrary> MusicHoard<NoDatabase, Library> {
filter: CollectionFilter::default(), filter: CollectionFilter::default(),
filtered: vec![], filtered: vec![],
collection: vec![], collection: vec![],
pre_commit: vec![],
database: NoDatabase, database: NoDatabase,
library, library,
library_cache: vec![], library_cache: HashMap::new(),
} }
} }
} }
@ -109,10 +109,9 @@ impl<Database: IDatabase> MusicHoard<Database, NoLibrary> {
filter: CollectionFilter::default(), filter: CollectionFilter::default(),
filtered: vec![], filtered: vec![],
collection: vec![], collection: vec![],
pre_commit: vec![],
database, database,
library: NoLibrary, library: NoLibrary,
library_cache: vec![], library_cache: HashMap::new(),
} }
} }
} }
@ -131,10 +130,9 @@ impl<Database: IDatabase, Library: ILibrary> MusicHoard<Database, Library> {
filter: CollectionFilter::default(), filter: CollectionFilter::default(),
filtered: vec![], filtered: vec![],
collection: vec![], collection: vec![],
pre_commit: vec![],
database, database,
library, library,
library_cache: vec![], library_cache: HashMap::new(),
} }
} }
} }

View File

@ -3,40 +3,28 @@ use std::mem;
use crate::{ use crate::{
collection::{ collection::{
album::{AlbumInfo, AlbumMbRef, AlbumMeta}, album::{AlbumInfo, AlbumMbRef, AlbumMeta},
artist::{ArtistInfo, ArtistMbRef}, artist::ArtistInfo,
merge::Merge, merge::{Merge, MergeCollections, NormalMap},
string,
}, },
core::{ core::{
collection::{ collection::{
album::{Album, AlbumId, AlbumSeq}, album::{Album, AlbumId},
artist::{Artist, ArtistId}, artist::{Artist, ArtistId},
Collection, Collection,
}, },
interface::database::IDatabase, interface::database::IDatabase,
musichoard::{base::IMusicHoardBasePrivate, Error, MusicHoard, NoDatabase}, musichoard::{
base::IMusicHoardBasePrivate, Error, IntoId, LibArtist, MusicHoard, NoDatabase,
},
}, },
}; };
pub trait IMusicHoardDatabase { pub trait IMusicHoardDatabase {
fn merge_collections(&mut self) -> Result<Collection, Error>;
fn reload_database(&mut self) -> Result<(), Error>; fn reload_database(&mut self) -> Result<(), Error>;
fn add_artist<IntoId: Into<ArtistId>>(&mut self, artist_id: IntoId) -> Result<(), Error>;
fn remove_artist<Id: AsRef<ArtistId>>(&mut self, artist_id: Id) -> Result<(), Error>;
fn set_artist_mb_ref<Id: AsRef<ArtistId>>(
&mut self,
artist_id: Id,
mb_ref: ArtistMbRef,
) -> Result<(), Error>;
fn clear_artist_mb_ref<Id: AsRef<ArtistId>>(&mut self, artist_id: Id) -> Result<(), Error>;
fn set_artist_sort<Id: AsRef<ArtistId>, S: Into<String>>(
&mut self,
artist_id: Id,
artist_sort: S,
) -> Result<(), Error>;
fn clear_artist_sort<Id: AsRef<ArtistId>>(&mut self, artist_id: Id) -> Result<(), Error>;
fn merge_artist_info<Id: AsRef<ArtistId>>( fn merge_artist_info<Id: AsRef<ArtistId>>(
&mut self, &mut self,
artist_id: Id, artist_id: Id,
@ -44,30 +32,6 @@ pub trait IMusicHoardDatabase {
) -> Result<(), Error>; ) -> Result<(), Error>;
fn clear_artist_info<Id: AsRef<ArtistId>>(&mut self, artist_id: Id) -> Result<(), Error>; fn clear_artist_info<Id: AsRef<ArtistId>>(&mut self, artist_id: Id) -> Result<(), Error>;
fn add_to_artist_property<Id: AsRef<ArtistId>, S: AsRef<str> + Into<String>>(
&mut self,
artist_id: Id,
property: S,
values: Vec<S>,
) -> Result<(), Error>;
fn remove_from_artist_property<Id: AsRef<ArtistId>, S: AsRef<str>>(
&mut self,
artist_id: Id,
property: S,
values: Vec<S>,
) -> Result<(), Error>;
fn set_artist_property<Id: AsRef<ArtistId>, S: AsRef<str> + Into<String>>(
&mut self,
artist_id: Id,
property: S,
values: Vec<S>,
) -> Result<(), Error>;
fn clear_artist_property<Id: AsRef<ArtistId>, S: AsRef<str>>(
&mut self,
artist_id: Id,
property: S,
) -> Result<(), Error>;
fn add_album<ArtistIdRef: AsRef<ArtistId>>( fn add_album<ArtistIdRef: AsRef<ArtistId>>(
&mut self, &mut self,
artist_id: ArtistIdRef, artist_id: ArtistIdRef,
@ -90,17 +54,7 @@ pub trait IMusicHoardDatabase {
artist_id: ArtistIdRef, artist_id: ArtistIdRef,
album_id: AlbumIdRef, album_id: AlbumIdRef,
) -> Result<(), Error>; ) -> Result<(), Error>;
fn set_album_seq<ArtistIdRef: AsRef<ArtistId>, AlbumIdRef: AsRef<AlbumId>>(
&mut self,
artist_id: ArtistIdRef,
album_id: AlbumIdRef,
seq: u8,
) -> Result<(), Error>;
fn clear_album_seq<ArtistIdRef: AsRef<ArtistId>, AlbumIdRef: AsRef<AlbumId>>(
&mut self,
artist_id: ArtistIdRef,
album_id: AlbumIdRef,
) -> Result<(), Error>;
fn merge_album_info<ArtistIdRef: AsRef<ArtistId>, AlbumIdRef: AsRef<AlbumId>>( fn merge_album_info<ArtistIdRef: AsRef<ArtistId>, AlbumIdRef: AsRef<AlbumId>>(
&mut self, &mut self,
artist_id: ArtistIdRef, artist_id: ArtistIdRef,
@ -115,80 +69,39 @@ pub trait IMusicHoardDatabase {
} }
impl<Database: IDatabase, Library> IMusicHoardDatabase for MusicHoard<Database, Library> { impl<Database: IDatabase, Library> IMusicHoardDatabase for MusicHoard<Database, Library> {
fn reload_database(&mut self) -> Result<(), Error> { fn merge_collections(&mut self) -> Result<Collection, Error> {
let mut database_cache = self.database.load()?; let mut database = self.database.load()?;
Self::sort_albums_and_tracks(database_cache.iter_mut()); Self::sort_albums_and_tracks(database.iter_mut().map(|a| &mut a.albums));
self.collection = self.merge_collections(database_cache); let mut primary = NormalMap::<LibArtist>::new();
for (normal_name, artist) in self.library_cache.clone().into_iter() {
primary.insert(normal_name, artist);
}
let mut secondary = NormalMap::<Artist>::new();
for artist in database.into_iter() {
secondary.insert(string::normalize_string(&artist.meta.name), artist);
}
let (mut collection, lib_artists) = MergeCollections::merge_by_name(primary, secondary);
for lib_artist in lib_artists.into_iter() {
let id = self.database.insert_artist(&lib_artist.meta)?;
collection.push(lib_artist.into_id(&id));
}
collection.sort_unstable();
Ok(collection)
}
fn reload_database(&mut self) -> Result<(), Error> {
self.collection = self.merge_collections()?;
self.filtered = self.filter_collection(); self.filtered = self.filter_collection();
self.pre_commit = self.collection.clone();
Ok(()) Ok(())
} }
fn add_artist<IntoId: Into<ArtistId>>(&mut self, artist_id: IntoId) -> Result<(), Error> {
let artist_id: ArtistId = artist_id.into();
self.update_collection(|collection| {
if Self::get_artist(collection, &artist_id).is_none() {
collection.push(Artist::new(artist_id));
Self::sort_artists(collection);
}
})
}
fn remove_artist<Id: AsRef<ArtistId>>(&mut self, artist_id: Id) -> Result<(), Error> {
self.update_collection(|collection| {
let index_opt = collection
.iter()
.position(|a| &a.meta.id == artist_id.as_ref());
if let Some(index) = index_opt {
collection.remove(index);
}
})
}
fn set_artist_mb_ref<Id: AsRef<ArtistId>>(
&mut self,
artist_id: Id,
mb_ref: ArtistMbRef,
) -> Result<(), Error> {
self.update_artist_and(
artist_id.as_ref(),
|artist| artist.meta.set_mb_ref(mb_ref),
|collection| Self::sort_artists(collection),
)
}
fn clear_artist_mb_ref<Id: AsRef<ArtistId>>(&mut self, artist_id: Id) -> Result<(), Error> {
self.update_artist_and(
artist_id.as_ref(),
|artist| artist.meta.clear_mb_ref(),
|collection| Self::sort_artists(collection),
)
}
fn set_artist_sort<Id: AsRef<ArtistId>, S: Into<String>>(
&mut self,
artist_id: Id,
artist_sort: S,
) -> Result<(), Error> {
self.update_artist_and(
artist_id.as_ref(),
|artist| artist.meta.set_sort_key(artist_sort),
|collection| Self::sort_artists(collection),
)
}
fn clear_artist_sort<Id: AsRef<ArtistId>>(&mut self, artist_id: Id) -> Result<(), Error> {
self.update_artist_and(
artist_id.as_ref(),
|artist| artist.meta.clear_sort_key(),
|collection| Self::sort_artists(collection),
)
}
fn merge_artist_info<Id: AsRef<ArtistId>>( fn merge_artist_info<Id: AsRef<ArtistId>>(
&mut self, &mut self,
artist_id: Id, artist_id: Id,
@ -206,49 +119,6 @@ impl<Database: IDatabase, Library> IMusicHoardDatabase for MusicHoard<Database,
}) })
} }
fn add_to_artist_property<Id: AsRef<ArtistId>, S: AsRef<str> + Into<String>>(
&mut self,
artist_id: Id,
property: S,
values: Vec<S>,
) -> Result<(), Error> {
self.update_artist(artist_id.as_ref(), |artist| {
artist.meta.info.add_to_property(property, values)
})
}
fn remove_from_artist_property<Id: AsRef<ArtistId>, S: AsRef<str>>(
&mut self,
artist_id: Id,
property: S,
values: Vec<S>,
) -> Result<(), Error> {
self.update_artist(artist_id.as_ref(), |artist| {
artist.meta.info.remove_from_property(property, values)
})
}
fn set_artist_property<Id: AsRef<ArtistId>, S: AsRef<str> + Into<String>>(
&mut self,
artist_id: Id,
property: S,
values: Vec<S>,
) -> Result<(), Error> {
self.update_artist(artist_id.as_ref(), |artist| {
artist.meta.info.set_property(property, values)
})
}
fn clear_artist_property<Id: AsRef<ArtistId>, S: AsRef<str>>(
&mut self,
artist_id: Id,
property: S,
) -> Result<(), Error> {
self.update_artist(artist_id.as_ref(), |artist| {
artist.meta.info.clear_property(property)
})
}
fn add_album<ArtistIdRef: AsRef<ArtistId>>( fn add_album<ArtistIdRef: AsRef<ArtistId>>(
&mut self, &mut self,
artist_id: ArtistIdRef, artist_id: ArtistIdRef,
@ -288,12 +158,9 @@ impl<Database: IDatabase, Library> IMusicHoardDatabase for MusicHoard<Database,
album_id: AlbumIdRef, album_id: AlbumIdRef,
mb_ref: AlbumMbRef, mb_ref: AlbumMbRef,
) -> Result<(), Error> { ) -> Result<(), Error> {
self.update_album_and( self.update_album(artist_id.as_ref(), album_id.as_ref(), |album| {
artist_id.as_ref(), album.meta.set_mb_ref(mb_ref)
album_id.as_ref(), })
|album| album.meta.set_mb_ref(mb_ref),
|artist| artist.albums.sort_unstable(),
)
} }
fn clear_album_mb_ref<ArtistIdRef: AsRef<ArtistId>, AlbumIdRef: AsRef<AlbumId>>( fn clear_album_mb_ref<ArtistIdRef: AsRef<ArtistId>, AlbumIdRef: AsRef<AlbumId>>(
@ -301,39 +168,9 @@ impl<Database: IDatabase, Library> IMusicHoardDatabase for MusicHoard<Database,
artist_id: ArtistIdRef, artist_id: ArtistIdRef,
album_id: AlbumIdRef, album_id: AlbumIdRef,
) -> Result<(), Error> { ) -> Result<(), Error> {
self.update_album_and( self.update_album(artist_id.as_ref(), album_id.as_ref(), |album| {
artist_id.as_ref(), album.meta.clear_mb_ref()
album_id.as_ref(), })
|album| album.meta.clear_mb_ref(),
|artist| artist.albums.sort_unstable(),
)
}
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.meta.set_seq(AlbumSeq(seq)),
|artist| artist.albums.sort_unstable(),
)
}
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.meta.clear_seq(),
|artist| artist.albums.sort_unstable(),
)
} }
fn merge_album_info<Id: AsRef<ArtistId>, AlbumIdRef: AsRef<AlbumId>>( fn merge_album_info<Id: AsRef<ArtistId>, AlbumIdRef: AsRef<AlbumId>>(
@ -342,7 +179,7 @@ impl<Database: IDatabase, Library> IMusicHoardDatabase for MusicHoard<Database,
album_id: AlbumIdRef, album_id: AlbumIdRef,
mut info: AlbumInfo, mut info: AlbumInfo,
) -> Result<(), Error> { ) -> Result<(), Error> {
self.update_album(artist_id.as_ref(), album_id.as_ref(), |album| { self.update_album_and_sort(artist_id.as_ref(), album_id.as_ref(), |album| {
mem::swap(&mut album.meta.info, &mut info); mem::swap(&mut album.meta.info, &mut info);
album.meta.info.merge_in_place(info); album.meta.info.merge_in_place(info);
}) })
@ -353,7 +190,7 @@ impl<Database: IDatabase, Library> IMusicHoardDatabase for MusicHoard<Database,
artist_id: Id, artist_id: Id,
album_id: AlbumIdRef, album_id: AlbumIdRef,
) -> Result<(), Error> { ) -> Result<(), Error> {
self.update_album(artist_id.as_ref(), album_id.as_ref(), |album| { self.update_album_and_sort(artist_id.as_ref(), album_id.as_ref(), |album| {
album.meta.info = AlbumInfo::default() album.meta.info = AlbumInfo::default()
}) })
} }
@ -365,7 +202,6 @@ pub trait IMusicHoardDatabasePrivate {
impl<Library> IMusicHoardDatabasePrivate for MusicHoard<NoDatabase, Library> { impl<Library> IMusicHoardDatabasePrivate for MusicHoard<NoDatabase, Library> {
fn commit(&mut self) -> Result<(), Error> { fn commit(&mut self) -> Result<(), Error> {
self.collection = self.pre_commit.clone();
self.filtered = self.filter_collection(); self.filtered = self.filter_collection();
Ok(()) Ok(())
} }
@ -373,14 +209,11 @@ impl<Library> IMusicHoardDatabasePrivate for MusicHoard<NoDatabase, Library> {
impl<Database: IDatabase, Library> IMusicHoardDatabasePrivate for MusicHoard<Database, Library> { impl<Database: IDatabase, Library> IMusicHoardDatabasePrivate for MusicHoard<Database, Library> {
fn commit(&mut self) -> Result<(), Error> { fn commit(&mut self) -> Result<(), Error> {
if self.collection != self.pre_commit { if let Err(err) = self.database.save(&self.collection) {
if let Err(err) = self.database.save(&self.pre_commit) { self.reload_database()?;
self.pre_commit = self.collection.clone();
return Err(err.into()); return Err(err.into());
} }
self.collection = self.pre_commit.clone();
self.filtered = self.filter_collection(); self.filtered = self.filter_collection();
}
Ok(()) Ok(())
} }
} }
@ -390,7 +223,7 @@ impl<Database: IDatabase, Library> MusicHoard<Database, Library> {
where where
FnColl: FnOnce(&mut Collection), FnColl: FnOnce(&mut Collection),
{ {
fn_coll(&mut self.pre_commit); fn_coll(&mut self.collection);
self.commit() self.commit()
} }
@ -404,7 +237,7 @@ impl<Database: IDatabase, Library> MusicHoard<Database, Library> {
FnArtist: FnOnce(&mut Artist), FnArtist: FnOnce(&mut Artist),
FnColl: FnOnce(&mut Collection), FnColl: FnOnce(&mut Collection),
{ {
let artist = Self::get_artist_mut_or_err(&mut self.pre_commit, artist_id)?; let artist = Self::get_artist_mut_or_err(&mut self.collection, artist_id)?;
fn_artist(artist); fn_artist(artist);
self.update_collection(fn_coll) self.update_collection(fn_coll)
} }
@ -431,13 +264,27 @@ impl<Database: IDatabase, Library> MusicHoard<Database, Library> {
FnAlbum: FnOnce(&mut Album), FnAlbum: FnOnce(&mut Album),
FnArtist: FnOnce(&mut Artist), FnArtist: FnOnce(&mut Artist),
{ {
let artist = Self::get_artist_mut_or_err(&mut self.pre_commit, artist_id)?; let artist = Self::get_artist_mut_or_err(&mut self.collection, artist_id)?;
let album = Self::get_album_mut_or_err(artist, album_id)?; let album = Self::get_album_mut_or_err(artist, album_id)?;
fn_album(album); fn_album(album);
fn_artist(artist); fn_artist(artist);
self.update_collection(|_| {}) self.update_collection(|_| {})
} }
fn update_album_and_sort<FnAlbum>(
&mut self,
artist_id: &ArtistId,
album_id: &AlbumId,
fn_album: FnAlbum,
) -> Result<(), Error>
where
FnAlbum: FnOnce(&mut Album),
{
self.update_album_and(artist_id, album_id, fn_album, |artist| {
artist.albums.sort_unstable()
})
}
fn update_album<FnAlbum>( fn update_album<FnAlbum>(
&mut self, &mut self,
artist_id: &ArtistId, artist_id: &ArtistId,
@ -458,12 +305,13 @@ mod tests {
use crate::{ use crate::{
collection::{ collection::{
album::{AlbumPrimaryType, AlbumSecondaryType}, album::{AlbumPrimaryType, AlbumSecondaryType},
artist::ArtistMbRef,
musicbrainz::MbArtistRef, musicbrainz::MbArtistRef,
}, },
core::{ core::{
collection::artist::ArtistId, collection::artist::ArtistId,
interface::database::{self, MockIDatabase}, interface::database::{self, MockIDatabase},
musichoard::{base::IMusicHoardBase, NoLibrary}, musichoard::base::IMusicHoardBase,
testmod::FULL_COLLECTION, testmod::FULL_COLLECTION,
}, },
}; };
@ -471,113 +319,13 @@ mod tests {
use super::*; use super::*;
static MBID: &str = "d368baa8-21ca-4759-9731-0b2753071ad8"; static MBID: &str = "d368baa8-21ca-4759-9731-0b2753071ad8";
static MUSICBUTLER: &str = "https://www.musicbutler.io/artist-page/483340948";
static MUSICBUTLER_2: &str = "https://www.musicbutler.io/artist-page/658903042/";
#[test]
fn artist_new_delete() {
let artist_id = ArtistId::new("an artist");
let artist_id_2 = ArtistId::new("another artist");
let collection = FULL_COLLECTION.to_owned();
let mut with_artist = collection.clone();
with_artist.push(Artist::new(artist_id.clone()));
let mut database = MockIDatabase::new();
let mut seq = Sequence::new();
database
.expect_load()
.times(1)
.times(1)
.in_sequence(&mut seq)
.returning(|| Ok(FULL_COLLECTION.to_owned()));
database
.expect_save()
.times(1)
.in_sequence(&mut seq)
.with(predicate::eq(with_artist.clone()))
.returning(|_| Ok(()));
database
.expect_save()
.times(1)
.in_sequence(&mut seq)
.with(predicate::eq(collection.clone()))
.returning(|_| Ok(()));
let mut music_hoard = MusicHoard::database(database);
music_hoard.reload_database().unwrap();
assert_eq!(music_hoard.collection, collection);
assert!(music_hoard.add_artist(artist_id.clone()).is_ok());
assert_eq!(music_hoard.collection, with_artist);
assert!(music_hoard.add_artist(artist_id.clone()).is_ok());
assert_eq!(music_hoard.collection, with_artist);
assert!(music_hoard.remove_artist(&artist_id_2).is_ok());
assert_eq!(music_hoard.collection, with_artist);
assert!(music_hoard.remove_artist(&artist_id).is_ok());
assert_eq!(music_hoard.collection, collection);
}
#[test]
fn artist_sort_set_clear() {
let mut database = MockIDatabase::new();
database.expect_save().times(4).returning(|_| Ok(()));
type MH = MusicHoard<MockIDatabase, NoLibrary>;
let mut music_hoard: MH = MusicHoard::database(database);
let artist_1_id = ArtistId::new("the artist");
let artist_1_sort = String::from("artist, the");
// Must be after "artist, the", but before "the artist"
let artist_2_id = ArtistId::new("b-artist");
assert!(artist_1_sort < artist_2_id.name);
assert!(artist_2_id < artist_1_id);
assert!(music_hoard.add_artist(artist_1_id.clone()).is_ok());
assert!(music_hoard.add_artist(artist_2_id.clone()).is_ok());
let artist_1: &Artist = MH::get_artist(&music_hoard.collection, &artist_1_id).unwrap();
let artist_2: &Artist = MH::get_artist(&music_hoard.collection, &artist_2_id).unwrap();
assert!(artist_2 < artist_1);
assert_eq!(artist_1, &music_hoard.collection[1]);
assert_eq!(artist_2, &music_hoard.collection[0]);
music_hoard
.set_artist_sort(artist_1_id.as_ref(), artist_1_sort.clone())
.unwrap();
let artist_1: &Artist = MH::get_artist(&music_hoard.collection, &artist_1_id).unwrap();
let artist_2: &Artist = MH::get_artist(&music_hoard.collection, &artist_2_id).unwrap();
assert!(artist_1 < artist_2);
assert_eq!(artist_1, &music_hoard.collection[0]);
assert_eq!(artist_2, &music_hoard.collection[1]);
music_hoard.clear_artist_sort(artist_1_id.as_ref()).unwrap();
let artist_1: &Artist = MH::get_artist(&music_hoard.collection, &artist_1_id).unwrap();
let artist_2: &Artist = MH::get_artist(&music_hoard.collection, &artist_2_id).unwrap();
assert!(artist_2 < artist_1);
assert_eq!(artist_1, &music_hoard.collection[1]);
assert_eq!(artist_2, &music_hoard.collection[0]);
}
#[test] #[test]
fn collection_error() { fn collection_error() {
let database = MockIDatabase::new(); let database = MockIDatabase::new();
let mut music_hoard = MusicHoard::database(database); let mut music_hoard = MusicHoard::database(database);
let artist_id = ArtistId::new("an artist"); let artist_id = ArtistId(1);
let actual_err = music_hoard let actual_err = music_hoard
.merge_artist_info(&artist_id, ArtistInfo::default()) .merge_artist_info(&artist_id, ArtistInfo::default())
.unwrap_err(); .unwrap_err();
@ -587,78 +335,36 @@ mod tests {
assert_eq!(actual_err.to_string(), expected_err.to_string()); assert_eq!(actual_err.to_string(), expected_err.to_string());
} }
#[test]
fn set_clear_artist_mb_ref() {
let mut database = MockIDatabase::new();
database.expect_save().times(3).returning(|_| Ok(()));
let mut artist_id = ArtistId::new("an artist");
let artist_id_2 = ArtistId::new("another artist");
let mut music_hoard = MusicHoard::database(database);
assert!(music_hoard.add_artist(artist_id.clone()).is_ok());
let mut expected = ArtistMbRef::None;
assert_eq!(music_hoard.collection[0].meta.id.mb_ref, expected);
let mb_ref = ArtistMbRef::Some(MbArtistRef::from_uuid_str(MBID).unwrap());
// Setting a mb_ref on an artist not in the collection is an error.
assert!(music_hoard
.set_artist_mb_ref(&artist_id_2, mb_ref.clone())
.is_err());
assert_eq!(music_hoard.collection[0].meta.id.mb_ref, expected);
// Setting a mb_ref on an artist.
assert!(music_hoard
.set_artist_mb_ref(&artist_id, mb_ref.clone())
.is_ok());
expected.replace(MbArtistRef::from_uuid_str(MBID).unwrap());
assert_eq!(music_hoard.collection[0].meta.id.mb_ref, expected);
// Clearing mb_ref on an artist that does not exist is an error.
assert!(music_hoard.clear_artist_mb_ref(&artist_id_2).is_err());
assert_eq!(music_hoard.collection[0].meta.id.mb_ref, expected);
// Clearing mb_ref from an artist without the mb_ref set is an error. Effectively the album
// does not exist.
assert!(music_hoard.clear_artist_mb_ref(&artist_id).is_err());
assert_eq!(music_hoard.collection[0].meta.id.mb_ref, expected);
// Clearing mb_ref.
artist_id.set_mb_ref(mb_ref);
assert!(music_hoard.clear_artist_mb_ref(&artist_id).is_ok());
expected.take();
assert_eq!(music_hoard.collection[0].meta.id.mb_ref, expected);
}
#[test] #[test]
fn set_clear_artist_info() { fn set_clear_artist_info() {
let mut database = MockIDatabase::new(); let mut database = MockIDatabase::new();
database.expect_save().times(3).returning(|_| Ok(())); database.expect_save().times(2).returning(|_| Ok(()));
let artist_id = ArtistId::new("an artist"); let artist = Artist::new(1, "an artist");
let artist_id_2 = ArtistId::new("another artist"); let artist_2 = Artist::new(2, "another artist");
let mb_ref = ArtistMbRef::Some(MbArtistRef::from_uuid_str(MBID).unwrap());
let mut music_hoard = MusicHoard::database(database); let mut music_hoard = MusicHoard::database(database);
assert!(music_hoard.add_artist(artist_id.clone()).is_ok()); music_hoard.collection.push(artist.clone());
music_hoard.collection.sort_unstable();
let mut expected = ArtistInfo::default(); let mut expected = ArtistInfo::default();
assert_eq!(music_hoard.collection[0].meta.info, expected); assert_eq!(music_hoard.collection[0].meta.info, expected);
let mut info = ArtistInfo::default(); let mut info = ArtistInfo::default().with_mb_ref(mb_ref.clone());
info.add_to_property("property", vec!["value-1", "value-2"]); info.add_to_property("property", vec!["value-1", "value-2"]);
// Setting info on an artist not in the collection is an error. // Setting info on an artist not in the collection is an error.
assert!(music_hoard assert!(music_hoard
.merge_artist_info(&artist_id_2, info.clone()) .merge_artist_info(&artist_2.id, info.clone())
.is_err()); .is_err());
assert_eq!(music_hoard.collection[0].meta.info, expected); assert_eq!(music_hoard.collection[0].meta.info, expected);
// Setting info on an artist. // Setting info on an artist.
assert!(music_hoard assert!(music_hoard
.merge_artist_info(&artist_id, info.clone()) .merge_artist_info(&artist.id, info.clone())
.is_ok()); .is_ok());
expected.mb_ref = mb_ref.clone();
expected.properties.insert( expected.properties.insert(
String::from("property"), String::from("property"),
vec![String::from("value-1"), String::from("value-2")], vec![String::from("value-1"), String::from("value-2")],
@ -666,106 +372,16 @@ mod tests {
assert_eq!(music_hoard.collection[0].meta.info, expected); assert_eq!(music_hoard.collection[0].meta.info, expected);
// Clearing info on an artist that does not exist is an error. // Clearing info on an artist that does not exist is an error.
assert!(music_hoard.clear_artist_info(&artist_id_2).is_err()); assert!(music_hoard.clear_artist_info(&artist_2.id).is_err());
assert_eq!(music_hoard.collection[0].meta.info, expected); assert_eq!(music_hoard.collection[0].meta.info, expected);
// Clearing info. // Clearing info.
assert!(music_hoard.clear_artist_info(&artist_id).is_ok()); assert!(music_hoard.clear_artist_info(&artist.id).is_ok());
expected.mb_ref.take();
expected.properties.clear(); expected.properties.clear();
assert_eq!(music_hoard.collection[0].meta.info, expected); assert_eq!(music_hoard.collection[0].meta.info, expected);
} }
#[test]
fn add_to_remove_from_property() {
let mut database = MockIDatabase::new();
database.expect_save().times(3).returning(|_| Ok(()));
let artist_id = ArtistId::new("an artist");
let artist_id_2 = ArtistId::new("another artist");
let mut music_hoard = MusicHoard::database(database);
assert!(music_hoard.add_artist(artist_id.clone()).is_ok());
let mut expected: Vec<String> = vec![];
assert!(music_hoard.collection[0].meta.info.properties.is_empty());
// Adding URLs to an artist not in the collection is an error.
assert!(music_hoard
.add_to_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
.is_err());
assert!(music_hoard.collection[0].meta.info.properties.is_empty());
// Adding mutliple URLs without clashes.
assert!(music_hoard
.add_to_artist_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2])
.is_ok());
expected.push(MUSICBUTLER.to_owned());
expected.push(MUSICBUTLER_2.to_owned());
let info = &music_hoard.collection[0].meta.info;
assert_eq!(info.properties.get("MusicButler"), Some(&expected));
// Removing URLs from an artist not in the collection is an error.
assert!(music_hoard
.remove_from_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
.is_err());
let info = &music_hoard.collection[0].meta.info;
assert_eq!(info.properties.get("MusicButler"), Some(&expected));
// Removing multiple URLs without clashes.
assert!(music_hoard
.remove_from_artist_property(
&artist_id,
"MusicButler",
vec![MUSICBUTLER, MUSICBUTLER_2]
)
.is_ok());
expected.clear();
assert!(music_hoard.collection[0].meta.info.properties.is_empty());
}
#[test]
fn set_clear_property() {
let mut database = MockIDatabase::new();
database.expect_save().times(3).returning(|_| Ok(()));
let artist_id = ArtistId::new("an artist");
let artist_id_2 = ArtistId::new("another artist");
let mut music_hoard = MusicHoard::database(database);
assert!(music_hoard.add_artist(artist_id.clone()).is_ok());
let mut expected: Vec<String> = vec![];
assert!(music_hoard.collection[0].meta.info.properties.is_empty());
// Seting URL on an artist not in the collection is an error.
assert!(music_hoard
.set_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
.is_err());
assert!(music_hoard.collection[0].meta.info.properties.is_empty());
// Set URLs.
assert!(music_hoard
.set_artist_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2])
.is_ok());
expected.clear();
expected.push(MUSICBUTLER.to_owned());
expected.push(MUSICBUTLER_2.to_owned());
let info = &music_hoard.collection[0].meta.info;
assert_eq!(info.properties.get("MusicButler"), Some(&expected));
// Clearing URLs on an artist that does not exist is an error.
assert!(music_hoard
.clear_artist_property(&artist_id_2, "MusicButler")
.is_err());
// Clear URLs.
assert!(music_hoard
.clear_artist_property(&artist_id, "MusicButler")
.is_ok());
expected.clear();
assert!(music_hoard.collection[0].meta.info.properties.is_empty());
}
#[test] #[test]
fn album_new_delete() { fn album_new_delete() {
let album_id = AlbumId::new("an album"); let album_id = AlbumId::new("an album");
@ -774,7 +390,7 @@ mod tests {
let album_meta_2 = AlbumMeta::new(album_id_2); let album_meta_2 = AlbumMeta::new(album_id_2);
let collection = FULL_COLLECTION.to_owned(); let collection = FULL_COLLECTION.to_owned();
let artist_id = collection[0].meta.id.clone(); let artist_id = collection[0].id;
let mut with_album = collection.clone(); let mut with_album = collection.clone();
with_album[0].albums.push(Album::new(album_id)); with_album[0].albums.push(Album::new(album_id));
with_album[0].albums.sort_unstable(); with_album[0].albums.sort_unstable();
@ -789,7 +405,7 @@ mod tests {
.returning(|| Ok(FULL_COLLECTION.to_owned())); .returning(|| Ok(FULL_COLLECTION.to_owned()));
database database
.expect_save() .expect_save()
.times(1) .times(3)
.in_sequence(&mut seq) .in_sequence(&mut seq)
.with(predicate::eq(with_album.clone())) .with(predicate::eq(with_album.clone()))
.returning(|_| Ok(())); .returning(|_| Ok(()));
@ -827,11 +443,11 @@ mod tests {
fn set_clear_album_mb_ref() { fn set_clear_album_mb_ref() {
let mut database = MockIDatabase::new(); let mut database = MockIDatabase::new();
let artist_id = ArtistId::new("an artist"); let artist = Artist::new(1, "an artist");
let mut album_id = AlbumId::new("an album"); let mut album_id = AlbumId::new("an album");
let album_id_2 = AlbumId::new("another album"); let album_id_2 = AlbumId::new("another album");
let mut database_result = vec![Artist::new(artist_id.clone())]; let mut database_result = vec![artist.clone()];
database_result[0].albums.push(Album::new(album_id.clone())); database_result[0].albums.push(Album::new(album_id.clone()));
database database
@ -847,27 +463,27 @@ mod tests {
// Seting mb_ref on an album not belonging to the artist is an error. // Seting mb_ref on an album not belonging to the artist is an error.
assert!(music_hoard assert!(music_hoard
.set_album_mb_ref(&artist_id, &album_id_2, AlbumMbRef::CannotHaveMbid) .set_album_mb_ref(&artist.id, &album_id_2, AlbumMbRef::CannotHaveMbid)
.is_err()); .is_err());
let album = &music_hoard.collection[0].albums[0]; let album = &music_hoard.collection[0].albums[0];
assert_eq!(album.meta.id.mb_ref, AlbumMbRef::None); assert_eq!(album.meta.id.mb_ref, AlbumMbRef::None);
// Set mb_ref. // Set mb_ref.
assert!(music_hoard assert!(music_hoard
.set_album_mb_ref(&artist_id, &album_id, AlbumMbRef::CannotHaveMbid) .set_album_mb_ref(&artist.id, &album_id, AlbumMbRef::CannotHaveMbid)
.is_ok()); .is_ok());
let album = &music_hoard.collection[0].albums[0]; let album = &music_hoard.collection[0].albums[0];
assert_eq!(album.meta.id.mb_ref, AlbumMbRef::CannotHaveMbid); assert_eq!(album.meta.id.mb_ref, AlbumMbRef::CannotHaveMbid);
// Clearing mb_ref on an album that does not exist is an error. // Clearing mb_ref on an album that does not exist is an error.
assert!(music_hoard assert!(music_hoard
.clear_album_mb_ref(&artist_id, &album_id_2) .clear_album_mb_ref(&artist.id, &album_id_2)
.is_err()); .is_err());
// Clearing mb_ref from an album without the mb_ref set is an error. Effectively the album // Clearing mb_ref from an album without the mb_ref set is an error. Effectively the album
// does not exist. // does not exist.
assert!(music_hoard assert!(music_hoard
.clear_album_mb_ref(&artist_id, &album_id) .clear_album_mb_ref(&artist.id, &album_id)
.is_err()); .is_err());
let album = &music_hoard.collection[0].albums[0]; let album = &music_hoard.collection[0].albums[0];
assert_eq!(album.meta.id.mb_ref, AlbumMbRef::CannotHaveMbid); assert_eq!(album.meta.id.mb_ref, AlbumMbRef::CannotHaveMbid);
@ -876,62 +492,21 @@ mod tests {
// album. // album.
album_id.set_mb_ref(AlbumMbRef::CannotHaveMbid); album_id.set_mb_ref(AlbumMbRef::CannotHaveMbid);
assert!(music_hoard assert!(music_hoard
.clear_album_mb_ref(&artist_id, &album_id) .clear_album_mb_ref(&artist.id, &album_id)
.is_ok()); .is_ok());
let album = &music_hoard.collection[0].albums[0]; let album = &music_hoard.collection[0].albums[0];
assert_eq!(album.meta.id.mb_ref, AlbumMbRef::None); assert_eq!(album.meta.id.mb_ref, AlbumMbRef::None);
} }
#[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::new(album_id.clone()));
database
.expect_load()
.times(1)
.return_once(|| Ok(database_result));
database.expect_save().times(2).returning(|_| Ok(()));
let mut music_hoard = MusicHoard::database(database);
music_hoard.reload_database().unwrap();
assert_eq!(music_hoard.collection[0].albums[0].meta.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].meta.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].meta.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].meta.seq, AlbumSeq(0));
}
#[test] #[test]
fn set_clear_album_info() { fn set_clear_album_info() {
let mut database = MockIDatabase::new(); let mut database = MockIDatabase::new();
let artist_id = ArtistId::new("an artist"); let artist = Artist::new(1, "an artist");
let album_id = AlbumId::new("an album"); let album_id = AlbumId::new("an album");
let album_id_2 = AlbumId::new("another album"); let album_id_2 = AlbumId::new("another album");
let mut database_result = vec![Artist::new(artist_id.clone())]; let mut database_result = vec![artist.clone()];
database_result[0].albums.push(Album::new(album_id.clone())); database_result[0].albums.push(Album::new(album_id.clone()));
database database
@ -946,32 +521,31 @@ mod tests {
assert_eq!(meta.info.primary_type, None); assert_eq!(meta.info.primary_type, None);
assert_eq!(meta.info.secondary_types, Vec::new()); assert_eq!(meta.info.secondary_types, Vec::new());
let info = AlbumInfo::new( let info = AlbumInfo::default()
Some(AlbumPrimaryType::Album), .with_primary_type(AlbumPrimaryType::Album)
vec![AlbumSecondaryType::Live], .with_secondary_types(vec![AlbumSecondaryType::Live]);
);
// Seting info on an album not belonging to the artist is an error. // Seting info on an album not belonging to the artist is an error.
assert!(music_hoard assert!(music_hoard
.merge_album_info(&artist_id, &album_id_2, info.clone()) .merge_album_info(&artist.id, &album_id_2, info.clone())
.is_err()); .is_err());
let meta = &music_hoard.collection[0].albums[0].meta; let meta = &music_hoard.collection[0].albums[0].meta;
assert_eq!(meta.info, AlbumInfo::default()); assert_eq!(meta.info, AlbumInfo::default());
// Set info. // Set info.
assert!(music_hoard assert!(music_hoard
.merge_album_info(&artist_id, &album_id, info.clone()) .merge_album_info(&artist.id, &album_id, info.clone())
.is_ok()); .is_ok());
let meta = &music_hoard.collection[0].albums[0].meta; let meta = &music_hoard.collection[0].albums[0].meta;
assert_eq!(meta.info, info); assert_eq!(meta.info, info);
// Clearing info on an album that does not exist is an error. // Clearing info on an album that does not exist is an error.
assert!(music_hoard assert!(music_hoard
.clear_album_info(&artist_id, &album_id_2) .clear_album_info(&artist.id, &album_id_2)
.is_err()); .is_err());
// Clear info. // Clear info.
assert!(music_hoard.clear_album_info(&artist_id, &album_id).is_ok()); assert!(music_hoard.clear_album_info(&artist.id, &album_id).is_ok());
let meta = &music_hoard.collection[0].albums[0].meta; let meta = &music_hoard.collection[0].albums[0].meta;
assert_eq!(meta.info, AlbumInfo::default()); assert_eq!(meta.info, AlbumInfo::default());
} }
@ -1018,17 +592,30 @@ mod tests {
let database_result = Err(database::SaveError::IoError(String::from("I/O error"))); let database_result = Err(database::SaveError::IoError(String::from("I/O error")));
database.expect_load().return_once(|| Ok(vec![])); let mut seq = Sequence::new();
database
.expect_load()
.times(1)
.in_sequence(&mut seq)
.return_once(|| Ok(vec![]));
database database
.expect_save() .expect_save()
.times(1) .times(1)
.return_once(|_: &Collection| database_result); .return_once(|_: &Collection| database_result);
database
.expect_load()
.times(1)
.in_sequence(&mut seq)
.return_once(|| Ok(vec![]));
let mut music_hoard = MusicHoard::database(database); let mut music_hoard = MusicHoard::database(database);
music_hoard.reload_database().unwrap(); music_hoard.reload_database().unwrap();
let artist = Artist::new(1, "an artist");
music_hoard.collection.push(artist.clone());
let actual_err = music_hoard let actual_err = music_hoard
.add_artist(ArtistId::new("an artist")) .add_album(artist.id, AlbumMeta::new("an album"))
.unwrap_err(); .unwrap_err();
let expected_err = Error::DatabaseError( let expected_err = Error::DatabaseError(
database::SaveError::IoError(String::from("I/O error")).to_string(), database::SaveError::IoError(String::from("I/O error")).to_string(),

View File

@ -1,19 +1,25 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::core::{ use crate::{
collection::{
artist::ArtistId,
merge::IntoId,
string::{self, NormalString},
},
core::{
collection::{ collection::{
album::{Album, AlbumDate, AlbumId, AlbumMbRef}, album::{Album, AlbumDate, AlbumId, AlbumMbRef},
artist::{Artist, ArtistId, ArtistMbRef},
track::{Track, TrackId, TrackNum, TrackQuality}, track::{Track, TrackId, TrackNum, TrackQuality},
Collection,
}, },
interface::{ interface::{
database::IDatabase, database::IDatabase,
library::{ILibrary, Item, Query}, library::{ILibrary, Item, Query},
}, },
musichoard::{ musichoard::{
base::IMusicHoardBasePrivate, database::IMusicHoardDatabasePrivate, Error, MusicHoard, base::IMusicHoardBasePrivate,
NoDatabase, database::{IMusicHoardDatabase, IMusicHoardDatabasePrivate},
Error, LibArtist, MusicHoard, NoDatabase,
},
}, },
}; };
@ -23,40 +29,42 @@ pub trait IMusicHoardLibrary {
impl<Library: ILibrary> IMusicHoardLibrary for MusicHoard<NoDatabase, Library> { impl<Library: ILibrary> IMusicHoardLibrary for MusicHoard<NoDatabase, Library> {
fn rescan_library(&mut self) -> Result<(), Error> { fn rescan_library(&mut self) -> Result<(), Error> {
self.pre_commit = self.rescan_library_inner(vec![])?; self.rescan_library_inner()?;
self.collection = self
.library_cache
.values()
.cloned()
.enumerate()
.map(|(ix, la)| la.into_id(&ArtistId(ix)))
.collect();
self.collection.sort_unstable();
self.commit() self.commit()
} }
} }
impl<Database: IDatabase, Library: ILibrary> IMusicHoardLibrary for MusicHoard<Database, Library> { impl<Database: IDatabase, Library: ILibrary> IMusicHoardLibrary for MusicHoard<Database, Library> {
fn rescan_library(&mut self) -> Result<(), Error> { fn rescan_library(&mut self) -> Result<(), Error> {
let mut database_cache = self.database.load()?; self.rescan_library_inner()?;
Self::sort_albums_and_tracks(database_cache.iter_mut()); self.collection = self.merge_collections()?;
self.pre_commit = self.rescan_library_inner(database_cache)?;
self.commit() self.commit()
} }
} }
impl<Database, Library: ILibrary> MusicHoard<Database, Library> { impl<Database, Library: ILibrary> MusicHoard<Database, Library> {
fn rescan_library_inner(&mut self, database: Collection) -> Result<Collection, Error> { fn rescan_library_inner(&mut self) -> Result<(), Error> {
let items = self.library.list(&Query::new())?; let items = self.library.list(&Query::new())?;
self.library_cache = Self::items_to_artists(items)?; self.library_cache = Self::items_to_artists(items)?;
Self::sort_albums_and_tracks(self.library_cache.iter_mut()); Self::sort_albums_and_tracks(self.library_cache.values_mut().map(|la| &mut la.albums));
Ok(())
Ok(self.merge_collections(database))
} }
fn items_to_artists(items: Vec<Item>) -> Result<Collection, Error> { fn items_to_artists(items: Vec<Item>) -> Result<HashMap<NormalString, LibArtist>, Error> {
let mut collection = HashMap::<ArtistId, Artist>::new(); let mut collection = HashMap::<NormalString, LibArtist>::new();
for item in items.into_iter() { for item in items.into_iter() {
let artist_id = ArtistId { let artist_name_official = item.album_artist;
name: item.album_artist, let artist_name_sort = item.album_artist_sort;
mb_ref: ArtistMbRef::None,
};
let artist_sort = item.album_artist_sort;
let album_id = AlbumId { let album_id = AlbumId {
title: item.album_title, title: item.album_title,
@ -85,24 +93,25 @@ impl<Database, Library: ILibrary> MusicHoard<Database, Library> {
// There are usually many entries per artist. Therefore, we avoid simply calling // There are usually many entries per artist. Therefore, we avoid simply calling
// .entry(artist_id.clone()).or_insert_with(..), because of the clone. The flipside is // .entry(artist_id.clone()).or_insert_with(..), because of the clone. The flipside is
// that insertions will thus do an additional lookup. // that insertions will thus do an additional lookup.
let artist = match collection.get_mut(&artist_id) { let normal_name = string::normalize_string(&artist_name_official);
let artist = match collection.get_mut(&normal_name) {
Some(artist) => artist, Some(artist) => artist,
None => collection None => collection
.entry(artist_id.clone()) .entry(normal_name)
.or_insert_with(|| Artist::new(artist_id)), .or_insert_with(|| LibArtist::new(artist_name_official)),
}; };
if artist.meta.sort.is_some() { if artist.meta.sort.is_some() {
if artist_sort.is_some() && (artist.meta.sort != artist_sort) { if artist_name_sort.is_some() && (artist.meta.sort != artist_name_sort) {
return Err(Error::CollectionError(format!( return Err(Error::CollectionError(format!(
"multiple album_artist_sort found for artist '{}': '{}' != '{}'", "multiple album_artist_sort found for artist '{}': '{}' != '{}'",
artist.meta.id, artist.meta.name,
artist.meta.sort.as_ref().unwrap(), artist.meta.sort.as_ref().unwrap(),
artist_sort.as_ref().unwrap() artist_name_sort.as_ref().unwrap()
))); )));
} }
} else if artist_sort.is_some() { } else if artist_name_sort.is_some() {
artist.meta.sort = artist_sort; artist.meta.sort = artist_name_sort;
} }
// Do a linear search as few artists have more than a handful of albums. Search from the // Do a linear search as few artists have more than a handful of albums. Search from the
@ -122,7 +131,7 @@ impl<Database, Library: ILibrary> MusicHoard<Database, Library> {
} }
} }
Ok(collection.into_values().collect()) Ok(collection)
} }
} }

View File

@ -12,10 +12,19 @@ pub use database::IMusicHoardDatabase;
pub use filter::CollectionFilter; pub use filter::CollectionFilter;
pub use library::IMusicHoardLibrary; pub use library::IMusicHoardLibrary;
use std::fmt::{self, Display}; use std::{
collections::HashMap,
fmt::{self, Display},
};
use crate::core::{ use crate::core::{
collection::Collection, collection::{
album::Album,
artist::{Artist, ArtistId, ArtistMeta, ArtistName},
merge::IntoId,
string::NormalString,
Collection,
},
interface::{ interface::{
database::{LoadError as DatabaseLoadError, SaveError as DatabaseSaveError}, database::{LoadError as DatabaseLoadError, SaveError as DatabaseSaveError},
library::Error as LibraryError, library::Error as LibraryError,
@ -24,16 +33,42 @@ use crate::core::{
/// The Music Hoard. It is responsible for pulling information from both the library and the /// The Music Hoard. It is responsible for pulling information from both the library and the
/// database, ensuring its consistent and writing back any changes. /// database, ensuring its consistent and writing back any changes.
// TODO: Split into inner and external/interfaces to facilitate building.
#[derive(Debug)] #[derive(Debug)]
pub struct MusicHoard<Database, Library> { pub struct MusicHoard<Database, Library> {
filter: CollectionFilter, filter: CollectionFilter,
filtered: Collection, filtered: Collection,
collection: Collection, collection: Collection,
pre_commit: Collection,
database: Database, database: Database,
library: Library, library: Library,
library_cache: Collection, library_cache: HashMap<NormalString, LibArtist>,
}
#[derive(Clone, Debug)]
struct LibArtist {
meta: ArtistMeta,
albums: Vec<Album>,
}
impl LibArtist {
fn new<Name: Into<ArtistName>>(name: Name) -> Self {
LibArtist {
meta: ArtistMeta::new(name),
albums: vec![],
}
}
}
impl IntoId for LibArtist {
type Id = ArtistId;
type IdSelf = Artist;
fn into_id(self, id: &Self::Id) -> Self::IdSelf {
Artist {
id: *id,
meta: self.meta,
albums: self.albums,
}
}
} }
/// Phantom type for when a library implementation is not needed. /// Phantom type for when a library implementation is not needed.

View File

@ -2,9 +2,7 @@ use once_cell::sync::Lazy;
use std::collections::HashMap; use std::collections::HashMap;
use crate::core::collection::{ use crate::core::collection::{
album::{ album::{Album, AlbumId, AlbumInfo, AlbumLibId, AlbumMbRef, AlbumMeta, AlbumPrimaryType},
Album, AlbumId, AlbumInfo, AlbumLibId, AlbumMbRef, AlbumMeta, AlbumPrimaryType, AlbumSeq,
},
artist::{Artist, ArtistId, ArtistInfo, ArtistMbRef, ArtistMeta}, artist::{Artist, ArtistId, ArtistInfo, ArtistMbRef, ArtistMeta},
musicbrainz::{MbAlbumRef, MbArtistRef}, musicbrainz::{MbAlbumRef, MbArtistRef},
track::{Track, TrackFormat, TrackId, TrackNum, TrackQuality}, track::{Track, TrackFormat, TrackId, TrackNum, TrackQuality},

View File

@ -34,6 +34,7 @@ impl From<DeserializeDatabase> for Collection {
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
pub struct DeserializeArtist { pub struct DeserializeArtist {
pub id: usize,
pub name: String, pub name: String,
pub mb_ref: DeserializeMbRefOption, pub mb_ref: DeserializeMbRefOption,
pub sort: Option<String>, pub sort: Option<String>,
@ -117,13 +118,12 @@ impl From<DeserializeArtist> for Artist {
let mut albums: Vec<Album> = artist.albums.into_iter().map(Into::into).collect(); let mut albums: Vec<Album> = artist.albums.into_iter().map(Into::into).collect();
albums.sort_unstable(); albums.sort_unstable();
Artist { Artist {
id: ArtistId(artist.id),
meta: ArtistMeta { meta: ArtistMeta {
id: ArtistId {
name: artist.name, name: artist.name,
mb_ref: artist.mb_ref.into(),
},
sort: artist.sort, sort: artist.sort,
info: ArtistInfo { info: ArtistInfo {
mb_ref: artist.mb_ref.into(),
properties: artist.properties, properties: artist.properties,
}, },
}, },
@ -141,9 +141,9 @@ impl From<DeserializeAlbum> for Album {
lib_id: album.lib_id.into(), lib_id: album.lib_id.into(),
mb_ref: album.mb_ref.into(), mb_ref: album.mb_ref.into(),
}, },
info: AlbumInfo {
date: album.date.into(), date: album.date.into(),
seq: AlbumSeq(album.seq), seq: AlbumSeq(album.seq),
info: AlbumInfo {
primary_type: album.primary_type.map(Into::into), primary_type: album.primary_type.map(Into::into),
secondary_types: album.secondary_types.into_iter().map(Into::into).collect(), secondary_types: album.secondary_types.into_iter().map(Into::into).collect(),
}, },

View File

@ -4,7 +4,12 @@ use serde::Serialize;
use crate::{ use crate::{
collection::musicbrainz::{MbRefOption, Mbid}, collection::musicbrainz::{MbRefOption, Mbid},
core::collection::{album::Album, artist::Artist, musicbrainz::IMusicBrainzRef, Collection}, core::collection::{
album::Album,
artist::{Artist, ArtistMeta},
musicbrainz::IMusicBrainzRef,
Collection,
},
external::database::serde::common::{ external::database::serde::common::{
MbRefOptionDef, SerdeAlbumDate, SerdeAlbumLibId, SerdeAlbumPrimaryType, MbRefOptionDef, SerdeAlbumDate, SerdeAlbumLibId, SerdeAlbumPrimaryType,
SerdeAlbumSecondaryType, SerdeAlbumSecondaryType,
@ -73,12 +78,20 @@ impl Serialize for SerializeMbid<'_> {
impl<'a> From<&'a Artist> for SerializeArtist<'a> { impl<'a> From<&'a Artist> for SerializeArtist<'a> {
fn from(artist: &'a Artist) -> Self { fn from(artist: &'a Artist) -> Self {
let mut sa: SerializeArtist = (&artist.meta).into();
sa.albums = artist.albums.iter().map(Into::into).collect();
sa
}
}
impl<'a> From<&'a ArtistMeta> for SerializeArtist<'a> {
fn from(meta: &'a ArtistMeta) -> Self {
SerializeArtist { SerializeArtist {
name: &artist.meta.id.name, name: &meta.name,
mb_ref: (&artist.meta.id.mb_ref).into(), mb_ref: (&meta.info.mb_ref).into(),
sort: &artist.meta.sort, sort: &meta.sort,
properties: &artist.meta.info.properties, properties: &meta.info.properties,
albums: artist.albums.iter().map(Into::into).collect(), albums: vec![],
} }
} }
} }
@ -89,8 +102,8 @@ impl<'a> From<&'a Album> for SerializeAlbum<'a> {
title: &album.meta.id.title, title: &album.meta.id.title,
lib_id: album.meta.id.lib_id.into(), lib_id: album.meta.id.lib_id.into(),
mb_ref: (&album.meta.id.mb_ref).into(), mb_ref: (&album.meta.id.mb_ref).into(),
date: album.meta.date.into(), date: album.meta.info.date.into(),
seq: album.meta.seq.0, seq: album.meta.info.seq.0,
primary_type: album.meta.info.primary_type.map(Into::into), primary_type: album.meta.info.primary_type.map(Into::into),
secondary_types: album secondary_types: album
.meta .meta

View File

@ -108,7 +108,8 @@ impl ISqlTransactionBackend for SqlTransactionSqliteBackend<'_> {
name TEXT NOT NULL, name TEXT NOT NULL,
mbid JSON NOT NULL DEFAULT '\"None\"', mbid JSON NOT NULL DEFAULT '\"None\"',
sort TEXT NULL, sort TEXT NULL,
properties JSON NOT NULL DEFAULT '{}' properties JSON NOT NULL DEFAULT '{}',
UNIQUE(name, mbid)
)", )",
); );
Self::execute(&mut stmt, ()) Self::execute(&mut stmt, ())
@ -131,7 +132,8 @@ impl ISqlTransactionBackend for SqlTransactionSqliteBackend<'_> {
day INT NULL, day INT NULL,
seq INT NOT NULL, seq INT NOT NULL,
primary_type JSON NOT NULL DEFAULT 'null', primary_type JSON NOT NULL DEFAULT 'null',
secondary_types JSON NOT NULL DEFAULT '[]' secondary_types JSON NOT NULL DEFAULT '[]',
UNIQUE(title, lib_id, mbid)
)", )",
); );
Self::execute(&mut stmt, ()) Self::execute(&mut stmt, ())
@ -145,7 +147,11 @@ impl ISqlTransactionBackend for SqlTransactionSqliteBackend<'_> {
fn insert_database_version(&self, version: &str) -> Result<(), Error> { fn insert_database_version(&self, version: &str) -> Result<(), Error> {
let mut stmt = self.prepare_cached( let mut stmt = self.prepare_cached(
"INSERT INTO database_metadata (name, value) "INSERT INTO database_metadata (name, value)
VALUES (?1, ?2)", VALUES (?1, ?2)
ON CONFLICT(name) DO UPDATE SET value = ?2
WHERE EXISTS (
SELECT 1 EXCEPT SELECT 1 WHERE value = ?2
)",
); );
Self::execute(&mut stmt, ("version", version)) Self::execute(&mut stmt, ("version", version))
} }
@ -160,7 +166,7 @@ impl ISqlTransactionBackend for SqlTransactionSqliteBackend<'_> {
.transpose() .transpose()
} }
fn insert_artist(&self, artist: &SerializeArtist<'_>) -> Result<(), Error> { fn insert_artist(&self, artist: &SerializeArtist<'_>) -> Result<i64, Error> {
let mut stmt = self.prepare_cached( let mut stmt = self.prepare_cached(
"INSERT INTO artists (name, mbid, sort, properties) "INSERT INTO artists (name, mbid, sort, properties)
VALUES (?1, ?2, ?3, ?4)", VALUES (?1, ?2, ?3, ?4)",
@ -173,20 +179,43 @@ impl ISqlTransactionBackend for SqlTransactionSqliteBackend<'_> {
artist.sort, artist.sort,
serde_json::to_string(&artist.properties)?, serde_json::to_string(&artist.properties)?,
), ),
)?;
Ok(self.tx.last_insert_rowid())
}
fn update_artist(&self, oid: i64, artist: &SerializeArtist<'_>) -> Result<(), Error> {
let mut stmt = self.prepare_cached(
"UPDATE SET name = ?2, mbid = ?3, sort = ?4, properties = ?5
WHERE rowid = ?1 EXISTS (
SELECT 1 EXCEPT SELECT 1 WHERE
name = ?2 AND mbid = ?3 AND sort = ?4 AND properties = ?5
)",
);
Self::execute(
&mut stmt,
(
oid,
artist.name,
serde_json::to_string(&artist.mb_ref)?,
artist.sort,
serde_json::to_string(&artist.properties)?,
),
) )
} }
fn select_all_artists(&self) -> Result<Vec<DeserializeArtist>, Error> { fn select_all_artists(&self) -> Result<Vec<DeserializeArtist>, Error> {
let mut stmt = self.prepare_cached("SELECT name, mbid, sort, properties FROM artists"); let mut stmt =
self.prepare_cached("SELECT rowid, name, mbid, sort, properties FROM artists");
let mut rows = Self::query(&mut stmt, ())?; let mut rows = Self::query(&mut stmt, ())?;
let mut artists = vec![]; let mut artists = vec![];
while let Some(row) = Self::next_row(&mut rows)? { while let Some(row) = Self::next_row(&mut rows)? {
artists.push(DeserializeArtist { artists.push(DeserializeArtist {
name: Self::get_value(row, 0)?, id: Self::get_value::<i64>(row, 0)? as usize,
mb_ref: serde_json::from_str(&Self::get_value::<String>(row, 1)?)?, name: Self::get_value(row, 1)?,
sort: Self::get_value(row, 2)?, mb_ref: serde_json::from_str(&Self::get_value::<String>(row, 2)?)?,
properties: serde_json::from_str(&Self::get_value::<String>(row, 3)?)?, sort: Self::get_value(row, 3)?,
properties: serde_json::from_str(&Self::get_value::<String>(row, 4)?)?,
albums: vec![], albums: vec![],
}); });
} }
@ -198,7 +227,15 @@ impl ISqlTransactionBackend for SqlTransactionSqliteBackend<'_> {
let mut stmt = self.prepare_cached( let mut stmt = self.prepare_cached(
"INSERT INTO albums (title, lib_id, mbid, artist_name, "INSERT INTO albums (title, lib_id, mbid, artist_name,
year, month, day, seq, primary_type, secondary_types) year, month, day, seq, primary_type, secondary_types)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)", VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)
ON CONFLICT(title, lib_id, mbid) DO UPDATE SET
artist_name = ?4, year = ?5, month = ?6, day = ?7, seq = ?8, primary_type = ?9,
secondary_types = ?10
WHERE EXISTS (
SELECT 1 EXCEPT SELECT 1 WHERE
artist_name = ?4 AND year = ?5 AND month = ?6 AND day = ?7 AND seq = ?8 AND
primary_type = ?9 AND secondary_types = ?10
)",
); );
Self::execute( Self::execute(
&mut stmt, &mut stmt,

View File

@ -9,7 +9,10 @@ use mockall::automock;
use crate::{ use crate::{
core::{ core::{
collection::Collection, collection::{
artist::{ArtistId, ArtistMeta},
Collection,
},
interface::database::{IDatabase, LoadError, SaveError}, interface::database::{IDatabase, LoadError, SaveError},
}, },
external::database::serde::{ external::database::serde::{
@ -60,7 +63,11 @@ pub trait ISqlTransactionBackend {
/// Insert an artist into the artist table. /// Insert an artist into the artist table.
#[allow(clippy::needless_lifetimes)] // Conflicts with automock. #[allow(clippy::needless_lifetimes)] // Conflicts with automock.
fn insert_artist<'a>(&self, artist: &SerializeArtist<'a>) -> Result<(), Error>; fn insert_artist<'a>(&self, artist: &SerializeArtist<'a>) -> Result<i64, Error>;
/// Update an artist in the artist table.
#[allow(clippy::needless_lifetimes)] // Conflicts with automock.
fn update_artist<'a>(&self, oid: i64, artist: &SerializeArtist<'a>) -> Result<(), Error>;
/// Get all artists from the artist table. /// Get all artists from the artist table.
fn select_all_artists(&self) -> Result<Vec<DeserializeArtist>, Error>; fn select_all_artists(&self) -> Result<Vec<DeserializeArtist>, Error>;
@ -152,6 +159,15 @@ impl<SDB: for<'conn> ISqlDatabaseBackend<'conn>> SqlDatabase<SDB> {
} }
impl<SDB: for<'conn> ISqlDatabaseBackend<'conn>> IDatabase for SqlDatabase<SDB> { impl<SDB: for<'conn> ISqlDatabaseBackend<'conn>> IDatabase for SqlDatabase<SDB> {
fn reset(&mut self) -> Result<(), SaveError> {
let tx = self.backend.transaction()?;
Self::drop_tables(&tx)?;
Self::create_tables(&tx)?;
Ok(tx.commit()?)
}
fn load(&mut self) -> Result<Collection, LoadError> { fn load(&mut self) -> Result<Collection, LoadError> {
let tx = self.backend.transaction()?; let tx = self.backend.transaction()?;
@ -178,7 +194,6 @@ impl<SDB: for<'conn> ISqlDatabaseBackend<'conn>> IDatabase for SqlDatabase<SDB>
let database: SerializeDatabase = collection.into(); let database: SerializeDatabase = collection.into();
let tx = self.backend.transaction()?; let tx = self.backend.transaction()?;
Self::drop_tables(&tx)?;
Self::create_tables(&tx)?; Self::create_tables(&tx)?;
match database { match database {
@ -196,6 +211,16 @@ impl<SDB: for<'conn> ISqlDatabaseBackend<'conn>> IDatabase for SqlDatabase<SDB>
tx.commit()?; tx.commit()?;
Ok(()) Ok(())
} }
fn insert_artist(&mut self, artist: &ArtistMeta) -> Result<ArtistId, SaveError> {
let tx = self.backend.transaction()?;
let sa: SerializeArtist = artist.into();
let oid = tx.insert_artist(&sa)?;
tx.commit()?;
Ok(ArtistId(oid as usize))
}
} }
#[cfg(test)] #[cfg(test)]
@ -203,7 +228,7 @@ pub mod testmod;
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::collections::VecDeque; use std::{collections::VecDeque, ops::AddAssign};
use mockall::{predicate, Sequence}; use mockall::{predicate, Sequence};
@ -300,21 +325,33 @@ mod tests {
SqlDatabase::new(backend).unwrap() SqlDatabase::new(backend).unwrap()
} }
#[test]
fn reset() {
let mut tx = MockISqlTransactionBackend::new();
let mut seq = Sequence::new();
expect_drop!(tx, seq);
expect_create!(tx, seq);
then0!(tx, seq, expect_commit);
assert!(database(VecDeque::from([tx])).reset().is_ok());
}
#[test] #[test]
fn save() { fn save() {
let write_data = FULL_COLLECTION.to_owned(); let write_data = FULL_COLLECTION.to_owned();
let mut tx = MockISqlTransactionBackend::new(); let mut tx = MockISqlTransactionBackend::new();
let mut seq = Sequence::new(); let mut seq = Sequence::new();
expect_drop!(tx, seq);
expect_create!(tx, seq); expect_create!(tx, seq);
then1!(tx, seq, expect_insert_database_version).with(predicate::eq(V20250103)); then1!(tx, seq, expect_insert_database_version).with(predicate::eq(V20250103));
let mut rowid: i64 = 0;
for artist in write_data.iter() { for artist in write_data.iter() {
let ac = artist.clone(); let ac = artist.clone();
then1!(tx, seq, expect_insert_artist) rowid.add_assign(1);
then!(tx, seq, expect_insert_artist)
.return_once(move |_| Ok(rowid))
.withf(move |a| a == &Into::<SerializeArtist>::into(&ac)); .withf(move |a| a == &Into::<SerializeArtist>::into(&ac));
for album in artist.albums.iter() { for album in artist.albums.iter() {
let (nc, ac) = (artist.meta.id.name.clone(), album.clone()); let (nc, ac) = (artist.meta.name.clone(), album.clone());
then2!(tx, seq, expect_insert_album) then2!(tx, seq, expect_insert_album)
.withf(move |n, a| n == nc && a == &Into::<SerializeAlbum>::into(&ac)); .withf(move |n, a| n == nc && a == &Into::<SerializeAlbum>::into(&ac));
} }
@ -337,9 +374,9 @@ mod tests {
then!(tx, seq, expect_select_all_artists).return_once(|| Ok(de_artists)); then!(tx, seq, expect_select_all_artists).return_once(|| Ok(de_artists));
for artist in artists.iter() { for artist in artists.iter() {
let de_albums = DATABASE_SQL_ALBUMS.get(&artist.meta.id.name).unwrap(); let de_albums = DATABASE_SQL_ALBUMS.get(&artist.meta.name).unwrap();
then!(tx, seq, expect_select_artist_albums) then!(tx, seq, expect_select_artist_albums)
.with(predicate::eq(artist.meta.id.name.clone())) .with(predicate::eq(artist.meta.name.clone()))
.return_once(|_| Ok(de_albums.to_owned())); .return_once(|_| Ok(de_albums.to_owned()));
} }
@ -396,7 +433,6 @@ mod tests {
fn save_backend_exec_error() { fn save_backend_exec_error() {
let mut tx = MockISqlTransactionBackend::new(); let mut tx = MockISqlTransactionBackend::new();
let mut seq = Sequence::new(); let mut seq = Sequence::new();
expect_drop!(tx, seq);
expect_create!(tx, seq); expect_create!(tx, seq);
then!(tx, seq, expect_insert_database_version) then!(tx, seq, expect_insert_database_version)
.with(predicate::eq(V20250103)) .with(predicate::eq(V20250103))
@ -426,7 +462,6 @@ mod tests {
let mut tx = MockISqlTransactionBackend::new(); let mut tx = MockISqlTransactionBackend::new();
let mut seq = Sequence::new(); let mut seq = Sequence::new();
expect_drop!(tx, seq);
expect_create!(tx, seq); expect_create!(tx, seq);
then1!(tx, seq, expect_insert_database_version).with(predicate::eq(V20250103)); then1!(tx, seq, expect_insert_database_version).with(predicate::eq(V20250103));
then!(tx, seq, expect_insert_artist) then!(tx, seq, expect_insert_artist)

View File

@ -20,6 +20,7 @@ pub static DATABASE_SQL_VERSION: &str = "V20250103";
pub static DATABASE_SQL_ARTISTS: Lazy<Vec<DeserializeArtist>> = Lazy::new(|| { pub static DATABASE_SQL_ARTISTS: Lazy<Vec<DeserializeArtist>> = Lazy::new(|| {
vec![ vec![
DeserializeArtist { DeserializeArtist {
id: 1,
name: String::from("Album_Artist A"), name: String::from("Album_Artist A"),
mb_ref: DeserializeMbRefOption(MbRefOption::Some(DeserializeMbid( mb_ref: DeserializeMbRefOption(MbRefOption::Some(DeserializeMbid(
"00000000-0000-0000-0000-000000000000".try_into().unwrap(), "00000000-0000-0000-0000-000000000000".try_into().unwrap(),
@ -42,6 +43,7 @@ pub static DATABASE_SQL_ARTISTS: Lazy<Vec<DeserializeArtist>> = Lazy::new(|| {
albums: vec![], albums: vec![],
}, },
DeserializeArtist { DeserializeArtist {
id: 2,
name: String::from("Album_Artist B"), name: String::from("Album_Artist B"),
mb_ref: DeserializeMbRefOption(MbRefOption::Some(DeserializeMbid( mb_ref: DeserializeMbRefOption(MbRefOption::Some(DeserializeMbid(
"11111111-1111-1111-1111-111111111111".try_into().unwrap(), "11111111-1111-1111-1111-111111111111".try_into().unwrap(),
@ -64,6 +66,7 @@ pub static DATABASE_SQL_ARTISTS: Lazy<Vec<DeserializeArtist>> = Lazy::new(|| {
albums: vec![], albums: vec![],
}, },
DeserializeArtist { DeserializeArtist {
id: 3,
name: String::from("The Album_Artist C"), name: String::from("The Album_Artist C"),
mb_ref: DeserializeMbRefOption(MbRefOption::CannotHaveMbid), mb_ref: DeserializeMbRefOption(MbRefOption::CannotHaveMbid),
sort: Some(String::from("Album_Artist C, The")), sort: Some(String::from("Album_Artist C, The")),
@ -71,6 +74,7 @@ pub static DATABASE_SQL_ARTISTS: Lazy<Vec<DeserializeArtist>> = Lazy::new(|| {
albums: vec![], albums: vec![],
}, },
DeserializeArtist { DeserializeArtist {
id: 4,
name: String::from("Album_Artist D"), name: String::from("Album_Artist D"),
mb_ref: DeserializeMbRefOption(MbRefOption::None), mb_ref: DeserializeMbRefOption(MbRefOption::None),
sort: None, sort: None,

View File

@ -66,7 +66,7 @@ struct DbOpt {
#[structopt( #[structopt(
long = "database", long = "database",
help = "Database file path", help = "Database file path",
default_value = "database.json" default_value = "database.db"
)] )]
database_file_path: PathBuf, database_file_path: PathBuf,

View File

@ -2,15 +2,14 @@ macro_rules! full_collection {
() => { () => {
vec![ vec![
Artist { Artist {
id: ArtistId(1),
meta: ArtistMeta { meta: ArtistMeta {
id: ArtistId {
name: "Album_Artist A".to_string(), name: "Album_Artist A".to_string(),
sort: None,
info: ArtistInfo {
mb_ref: ArtistMbRef::Some(MbArtistRef::from_url_str( mb_ref: ArtistMbRef::Some(MbArtistRef::from_url_str(
"https://musicbrainz.org/artist/00000000-0000-0000-0000-000000000000" "https://musicbrainz.org/artist/00000000-0000-0000-0000-000000000000"
).unwrap()), ).unwrap()),
},
sort: None,
info: ArtistInfo {
properties: HashMap::from([ properties: HashMap::from([
(String::from("MusicButler"), vec![ (String::from("MusicButler"), vec![
String::from("https://www.musicbutler.io/artist-page/000000000"), String::from("https://www.musicbutler.io/artist-page/000000000"),
@ -33,12 +32,10 @@ macro_rules! full_collection {
"https://musicbrainz.org/release-group/00000000-0000-0000-0000-000000000000" "https://musicbrainz.org/release-group/00000000-0000-0000-0000-000000000000"
).unwrap()), ).unwrap()),
}, },
date: 1998.into(), info: AlbumInfo::default()
seq: AlbumSeq(1), .with_date(1998)
info: AlbumInfo { .with_seq(1)
primary_type: Some(AlbumPrimaryType::Album), .with_primary_type(AlbumPrimaryType::Album),
secondary_types: vec![],
},
}, },
tracks: vec![ tracks: vec![
Track { Track {
@ -97,12 +94,10 @@ macro_rules! full_collection {
lib_id: AlbumLibId::Value(2), lib_id: AlbumLibId::Value(2),
mb_ref: AlbumMbRef::None, mb_ref: AlbumMbRef::None,
}, },
date: (2015, 4).into(), info: AlbumInfo::default()
seq: AlbumSeq(1), .with_date((2015, 4))
info: AlbumInfo { .with_seq(1)
primary_type: Some(AlbumPrimaryType::Album), .with_primary_type(AlbumPrimaryType::Album),
secondary_types: vec![],
},
}, },
tracks: vec![ tracks: vec![
Track { Track {
@ -132,15 +127,14 @@ macro_rules! full_collection {
], ],
}, },
Artist { Artist {
id: ArtistId(2),
meta: ArtistMeta { meta: ArtistMeta {
id: ArtistId {
name: "Album_Artist B".to_string(), name: "Album_Artist B".to_string(),
sort: None,
info: ArtistInfo {
mb_ref: ArtistMbRef::Some(MbArtistRef::from_url_str( mb_ref: ArtistMbRef::Some(MbArtistRef::from_url_str(
"https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111" "https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111"
).unwrap()), ).unwrap()),
},
sort: None,
info: ArtistInfo {
properties: HashMap::from([ properties: HashMap::from([
(String::from("MusicButler"), vec![ (String::from("MusicButler"), vec![
String::from("https://www.musicbutler.io/artist-page/111111111"), String::from("https://www.musicbutler.io/artist-page/111111111"),
@ -165,12 +159,10 @@ macro_rules! full_collection {
lib_id: AlbumLibId::Value(3), lib_id: AlbumLibId::Value(3),
mb_ref: AlbumMbRef::None, mb_ref: AlbumMbRef::None,
}, },
date: (2003, 6, 6).into(), info: AlbumInfo::default()
seq: AlbumSeq(1), .with_date((2003, 6, 6))
info: AlbumInfo { .with_seq(1)
primary_type: Some(AlbumPrimaryType::Album), .with_primary_type(AlbumPrimaryType::Album),
secondary_types: vec![],
},
}, },
tracks: vec![ tracks: vec![
Track { Track {
@ -209,12 +201,10 @@ macro_rules! full_collection {
"https://musicbrainz.org/release-group/11111111-1111-1111-1111-111111111111" "https://musicbrainz.org/release-group/11111111-1111-1111-1111-111111111111"
).unwrap()), ).unwrap()),
}, },
date: 2008.into(), info: AlbumInfo::default()
seq: AlbumSeq(3), .with_date(2008)
info: AlbumInfo { .with_seq(3)
primary_type: Some(AlbumPrimaryType::Album), .with_primary_type(AlbumPrimaryType::Album),
secondary_types: vec![],
},
}, },
tracks: vec![ tracks: vec![
Track { Track {
@ -253,12 +243,10 @@ macro_rules! full_collection {
"https://musicbrainz.org/release-group/11111111-1111-1111-1111-111111111112" "https://musicbrainz.org/release-group/11111111-1111-1111-1111-111111111112"
).unwrap()), ).unwrap()),
}, },
date: 2009.into(), info: AlbumInfo::default()
seq: AlbumSeq(2), .with_date(2009)
info: AlbumInfo { .with_seq(2)
primary_type: Some(AlbumPrimaryType::Album), .with_primary_type(AlbumPrimaryType::Album),
secondary_types: vec![],
},
}, },
tracks: vec![ tracks: vec![
Track { Track {
@ -295,12 +283,10 @@ macro_rules! full_collection {
lib_id: AlbumLibId::Value(6), lib_id: AlbumLibId::Value(6),
mb_ref: AlbumMbRef::None, mb_ref: AlbumMbRef::None,
}, },
date: 2015.into(), info: AlbumInfo::default()
seq: AlbumSeq(4), .with_date(2015)
info: AlbumInfo { .with_seq(4)
primary_type: Some(AlbumPrimaryType::Album), .with_primary_type(AlbumPrimaryType::Album),
secondary_types: vec![],
},
}, },
tracks: vec![ tracks: vec![
Track { Track {
@ -333,13 +319,12 @@ macro_rules! full_collection {
], ],
}, },
Artist { Artist {
id: ArtistId(3),
meta: ArtistMeta { meta: ArtistMeta {
id: ArtistId {
name: "The Album_Artist C".to_string(), name: "The Album_Artist C".to_string(),
mb_ref: ArtistMbRef::CannotHaveMbid,
},
sort: Some("Album_Artist C, The".to_string()), sort: Some("Album_Artist C, The".to_string()),
info: ArtistInfo { info: ArtistInfo {
mb_ref: ArtistMbRef::CannotHaveMbid,
properties: HashMap::new(), properties: HashMap::new(),
}, },
}, },
@ -351,12 +336,9 @@ macro_rules! full_collection {
lib_id: AlbumLibId::Value(7), lib_id: AlbumLibId::Value(7),
mb_ref: AlbumMbRef::None, mb_ref: AlbumMbRef::None,
}, },
date: 1985.into(), info: AlbumInfo::default()
seq: AlbumSeq(0), .with_date(1985)
info: AlbumInfo { .with_primary_type(AlbumPrimaryType::Album),
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
},
}, },
tracks: vec![ tracks: vec![
Track { Track {
@ -393,12 +375,9 @@ macro_rules! full_collection {
lib_id: AlbumLibId::Value(8), lib_id: AlbumLibId::Value(8),
mb_ref: AlbumMbRef::None, mb_ref: AlbumMbRef::None,
}, },
date: 2018.into(), info: AlbumInfo::default()
seq: AlbumSeq(0), .with_date(2018)
info: AlbumInfo { .with_primary_type(AlbumPrimaryType::Album),
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
},
}, },
tracks: vec![ tracks: vec![
Track { Track {
@ -431,13 +410,12 @@ macro_rules! full_collection {
], ],
}, },
Artist { Artist {
id: ArtistId(4),
meta: ArtistMeta { meta: ArtistMeta {
id: ArtistId {
name: "Album_Artist D".to_string(), name: "Album_Artist D".to_string(),
mb_ref: ArtistMbRef::None,
},
sort: None, sort: None,
info: ArtistInfo { info: ArtistInfo {
mb_ref: ArtistMbRef::None,
properties: HashMap::new(), properties: HashMap::new(),
}, },
}, },
@ -449,12 +427,9 @@ macro_rules! full_collection {
lib_id: AlbumLibId::Value(9), lib_id: AlbumLibId::Value(9),
mb_ref: AlbumMbRef::None, mb_ref: AlbumMbRef::None,
}, },
date: 1995.into(), info: AlbumInfo::default()
seq: AlbumSeq(0), .with_date(1995)
info: AlbumInfo { .with_primary_type(AlbumPrimaryType::Album),
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
},
}, },
tracks: vec![ tracks: vec![
Track { Track {
@ -491,12 +466,9 @@ macro_rules! full_collection {
lib_id: AlbumLibId::Value(10), lib_id: AlbumLibId::Value(10),
mb_ref: AlbumMbRef::None, mb_ref: AlbumMbRef::None,
}, },
date: 2028.into(), info: AlbumInfo::default()
seq: AlbumSeq(0), .with_date(2028)
info: AlbumInfo { .with_primary_type(AlbumPrimaryType::Album),
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
},
}, },
tracks: vec![ tracks: vec![
Track { Track {

View File

@ -3,13 +3,12 @@ macro_rules! library_collection {
() => { () => {
vec![ vec![
Artist { Artist {
id: ArtistId(0),
meta: ArtistMeta { meta: ArtistMeta {
id: ArtistId {
name: "Album_Artist A".to_string(), name: "Album_Artist A".to_string(),
mb_ref: ArtistMbRef::None,
},
sort: None, sort: None,
info: ArtistInfo { info: ArtistInfo {
mb_ref: ArtistMbRef::None,
properties: HashMap::new(), properties: HashMap::new(),
}, },
}, },
@ -21,9 +20,7 @@ macro_rules! library_collection {
lib_id: AlbumLibId::Value(1), lib_id: AlbumLibId::Value(1),
mb_ref: AlbumMbRef::None, mb_ref: AlbumMbRef::None,
}, },
date: 1998.into(), info: AlbumInfo::default().with_date(1998),
seq: AlbumSeq(0),
info: AlbumInfo::default(),
}, },
tracks: vec![ tracks: vec![
Track { Track {
@ -82,9 +79,7 @@ macro_rules! library_collection {
lib_id: AlbumLibId::Value(2), lib_id: AlbumLibId::Value(2),
mb_ref: AlbumMbRef::None, mb_ref: AlbumMbRef::None,
}, },
date: (2015, 4).into(), info: AlbumInfo::default().with_date((2015, 4)),
seq: AlbumSeq(0),
info: AlbumInfo::default(),
}, },
tracks: vec![ tracks: vec![
Track { Track {
@ -114,13 +109,12 @@ macro_rules! library_collection {
], ],
}, },
Artist { Artist {
id: ArtistId(0),
meta: ArtistMeta { meta: ArtistMeta {
id: ArtistId {
name: "Album_Artist B".to_string(), name: "Album_Artist B".to_string(),
mb_ref: ArtistMbRef::None,
},
sort: None, sort: None,
info: ArtistInfo { info: ArtistInfo {
mb_ref: ArtistMbRef::None,
properties: HashMap::new(), properties: HashMap::new(),
}, },
}, },
@ -132,9 +126,7 @@ macro_rules! library_collection {
lib_id: AlbumLibId::Value(3), lib_id: AlbumLibId::Value(3),
mb_ref: AlbumMbRef::None, mb_ref: AlbumMbRef::None,
}, },
date: (2003, 6, 6).into(), info: AlbumInfo::default().with_date((2003, 6, 6)),
seq: AlbumSeq(0),
info: AlbumInfo::default(),
}, },
tracks: vec![ tracks: vec![
Track { Track {
@ -171,9 +163,7 @@ macro_rules! library_collection {
lib_id: AlbumLibId::Value(4), lib_id: AlbumLibId::Value(4),
mb_ref: AlbumMbRef::None, mb_ref: AlbumMbRef::None,
}, },
date: 2008.into(), info: AlbumInfo::default().with_date(2008),
seq: AlbumSeq(0),
info: AlbumInfo::default(),
}, },
tracks: vec![ tracks: vec![
Track { Track {
@ -210,9 +200,7 @@ macro_rules! library_collection {
lib_id: AlbumLibId::Value(5), lib_id: AlbumLibId::Value(5),
mb_ref: AlbumMbRef::None, mb_ref: AlbumMbRef::None,
}, },
date: 2009.into(), info: AlbumInfo::default().with_date(2009),
seq: AlbumSeq(0),
info: AlbumInfo::default(),
}, },
tracks: vec![ tracks: vec![
Track { Track {
@ -249,9 +237,7 @@ macro_rules! library_collection {
lib_id: AlbumLibId::Value(6), lib_id: AlbumLibId::Value(6),
mb_ref: AlbumMbRef::None, mb_ref: AlbumMbRef::None,
}, },
date: 2015.into(), info: AlbumInfo::default().with_date(2015),
seq: AlbumSeq(0),
info: AlbumInfo::default(),
}, },
tracks: vec![ tracks: vec![
Track { Track {
@ -284,13 +270,12 @@ macro_rules! library_collection {
], ],
}, },
Artist { Artist {
id: ArtistId(0),
meta: ArtistMeta { meta: ArtistMeta {
id: ArtistId {
name: "The Album_Artist C".to_string(), name: "The Album_Artist C".to_string(),
mb_ref: ArtistMbRef::None,
},
sort: Some("Album_Artist C, The".to_string()), sort: Some("Album_Artist C, The".to_string()),
info: ArtistInfo { info: ArtistInfo {
mb_ref: ArtistMbRef::None,
properties: HashMap::new(), properties: HashMap::new(),
}, },
}, },
@ -302,9 +287,7 @@ macro_rules! library_collection {
lib_id: AlbumLibId::Value(7), lib_id: AlbumLibId::Value(7),
mb_ref: AlbumMbRef::None, mb_ref: AlbumMbRef::None,
}, },
date: 1985.into(), info: AlbumInfo::default().with_date(1985),
seq: AlbumSeq(0),
info: AlbumInfo::default(),
}, },
tracks: vec![ tracks: vec![
Track { Track {
@ -341,9 +324,7 @@ macro_rules! library_collection {
lib_id: AlbumLibId::Value(8), lib_id: AlbumLibId::Value(8),
mb_ref: AlbumMbRef::None, mb_ref: AlbumMbRef::None,
}, },
date: 2018.into(), info: AlbumInfo::default().with_date(2018),
seq: AlbumSeq(0),
info: AlbumInfo::default(),
}, },
tracks: vec![ tracks: vec![
Track { Track {
@ -376,13 +357,12 @@ macro_rules! library_collection {
], ],
}, },
Artist { Artist {
id: ArtistId(0),
meta: ArtistMeta { meta: ArtistMeta {
id: ArtistId {
name: "Album_Artist D".to_string(), name: "Album_Artist D".to_string(),
mb_ref: ArtistMbRef::None,
},
sort: None, sort: None,
info: ArtistInfo { info: ArtistInfo {
mb_ref: ArtistMbRef::None,
properties: HashMap::new(), properties: HashMap::new(),
}, },
}, },
@ -394,9 +374,7 @@ macro_rules! library_collection {
lib_id: AlbumLibId::Value(9), lib_id: AlbumLibId::Value(9),
mb_ref: AlbumMbRef::None, mb_ref: AlbumMbRef::None,
}, },
date: 1995.into(), info: AlbumInfo::default().with_date(1995),
seq: AlbumSeq(0),
info: AlbumInfo::default(),
}, },
tracks: vec![ tracks: vec![
Track { Track {
@ -433,9 +411,7 @@ macro_rules! library_collection {
lib_id: AlbumLibId::Value(10), lib_id: AlbumLibId::Value(10),
mb_ref: AlbumMbRef::None, mb_ref: AlbumMbRef::None,
}, },
date: 2028.into(), info: AlbumInfo::default().with_date(2028),
seq: AlbumSeq(0),
info: AlbumInfo::default(),
}, },
tracks: vec![ tracks: vec![
Track { Track {

View File

@ -6,7 +6,7 @@ use std::{
use musichoard::collection::{ use musichoard::collection::{
album::{Album, AlbumId, AlbumMeta}, album::{Album, AlbumId, AlbumMeta},
artist::{Artist, ArtistId, ArtistMeta}, artist::{Artist, ArtistId},
musicbrainz::{IMusicBrainzRef, MbArtistRef, MbRefOption, Mbid}, musicbrainz::{IMusicBrainzRef, MbArtistRef, MbRefOption, Mbid},
}; };
@ -14,7 +14,7 @@ use crate::tui::{
app::{ app::{
machine::{match_state::MatchState, App, AppInner, AppMachine}, machine::{match_state::MatchState, App, AppInner, AppMachine},
selection::KeySelection, selection::KeySelection,
AppPublicState, AppState, Category, IAppEventFetch, IAppInteractFetch, AppPublicState, AppState, ArtistMatching, Category, IAppEventFetch, IAppInteractFetch,
}, },
lib::interface::musicbrainz::daemon::{ lib::interface::musicbrainz::daemon::{
EntityList, Error as DaemonError, IMbJobSender, MbApiResult, MbParams, MbReturn, EntityList, Error as DaemonError, IMbJobSender, MbApiResult, MbParams, MbReturn,
@ -115,14 +115,14 @@ impl AppMachine<FetchState> {
let mut requests = Self::search_artist_job(artist); let mut requests = Self::search_artist_job(artist);
if requests.is_empty() { if requests.is_empty() {
fetch = FetchState::fetch(rx); fetch = FetchState::fetch(rx);
requests = Self::browse_release_group_job(&artist.meta.id.mb_ref); requests = Self::browse_release_group_job(artist.id, &artist.meta.info.mb_ref);
} else { } else {
fetch = FetchState::search(rx); fetch = FetchState::search(rx);
} }
SubmitJob { fetch, requests } SubmitJob { fetch, requests }
} }
_ => { _ => {
let arid = match artist.meta.id.mb_ref { let arid = match artist.meta.info.mb_ref {
MbRefOption::Some(ref mbref) => mbref, MbRefOption::Some(ref mbref) => mbref,
_ => return Err("cannot fetch album: artist has no MBID"), _ => return Err("cannot fetch album: artist has no MBID"),
}; };
@ -130,10 +130,9 @@ impl AppMachine<FetchState> {
Some(album_state) => &artist.albums[album_state.index], Some(album_state) => &artist.albums[album_state.index],
None => return Err("cannot fetch album: no album selected"), None => return Err("cannot fetch album: no album selected"),
}; };
let artist_id = &artist.meta.id;
SubmitJob { SubmitJob {
fetch: FetchState::search(rx), fetch: FetchState::search(rx),
requests: Self::search_release_group_job(artist_id, arid, album), requests: Self::search_release_group_job(artist.id, arid, album),
} }
} }
}; };
@ -191,9 +190,9 @@ impl AppMachine<FetchState> {
let selection = KeySelection::get(coll, &inner.selection); let selection = KeySelection::get(coll, &inner.selection);
// Find the artist in the full collection to correctly identify already existing albums. // Find the artist in the full collection to correctly identify already existing albums.
let artist_id = artist.meta.id.clone(); let artist_id = artist.id.clone();
let coll = inner.music_hoard.get_collection(); let coll = inner.music_hoard.get_collection();
let artist = coll.iter().find(|a| a.meta.id == artist_id).unwrap(); let artist = coll.iter().find(|a| a.id == artist_id).unwrap();
for new in Self::new_albums(fetch_albums, &artist.albums).into_iter() { for new in Self::new_albums(fetch_albums, &artist.albums).into_iter() {
inner.music_hoard.add_album(&artist_id, new)?; inner.music_hoard.add_album(&artist_id, new)?;
@ -223,11 +222,11 @@ impl AppMachine<FetchState> {
pub fn app_lookup_artist( pub fn app_lookup_artist(
inner: AppInner, inner: AppInner,
fetch: FetchState, fetch: FetchState,
artist: &ArtistMeta, matching: ArtistMatching,
mbid: Mbid, mbid: Mbid,
) -> App { ) -> App {
let f = Self::submit_lookup_artist_job; let f = Self::submit_lookup_artist_job;
Self::app_lookup(f, inner, fetch, artist, mbid) Self::app_lookup(f, inner, fetch, matching, mbid)
} }
pub fn app_lookup_album( pub fn app_lookup_album(
@ -243,18 +242,18 @@ impl AppMachine<FetchState> {
Self::app_lookup(f, inner, fetch, album_id, mbid) Self::app_lookup(f, inner, fetch, album_id, mbid)
} }
fn app_lookup<F, Meta>( fn app_lookup<F, Matching>(
submit: F, submit: F,
inner: AppInner, inner: AppInner,
mut fetch: FetchState, mut fetch: FetchState,
meta: Meta, matching: Matching,
mbid: Mbid, mbid: Mbid,
) -> App ) -> App
where where
F: FnOnce(&dyn IMbJobSender, ResultSender, Meta, Mbid) -> Result<(), DaemonError>, F: FnOnce(&dyn IMbJobSender, ResultSender, Matching, Mbid) -> Result<(), DaemonError>,
{ {
let (lookup_tx, lookup_rx) = mpsc::channel::<MbApiResult>(); let (lookup_tx, lookup_rx) = mpsc::channel::<MbApiResult>();
if let Err(err) = submit(&*inner.musicbrainz, lookup_tx, meta, mbid) { if let Err(err) = submit(&*inner.musicbrainz, lookup_tx, matching, mbid) {
return AppMachine::error_state(inner, err.to_string()).into(); return AppMachine::error_state(inner, err.to_string()).into();
} }
fetch.lookup_rx.replace(lookup_rx); fetch.lookup_rx.replace(lookup_rx);
@ -262,60 +261,72 @@ impl AppMachine<FetchState> {
} }
fn search_artist_job(artist: &Artist) -> VecDeque<MbParams> { fn search_artist_job(artist: &Artist) -> VecDeque<MbParams> {
match artist.meta.id.mb_ref { match artist.meta.info.mb_ref {
MbRefOption::Some(ref arid) => { MbRefOption::Some(ref arid) => {
Self::search_albums_requests(&artist.meta.id, arid, &artist.albums) Self::search_albums_requests(artist.id, arid, &artist.albums)
} }
MbRefOption::CannotHaveMbid => VecDeque::new(), MbRefOption::CannotHaveMbid => VecDeque::new(),
MbRefOption::None => Self::search_artist_request(&artist.meta), MbRefOption::None => Self::search_artist_request(ArtistMatching::new(
artist.id,
artist.meta.name.clone(),
)),
} }
} }
fn search_release_group_job( fn search_release_group_job(
artist_id: &ArtistId, artist_id: ArtistId,
artist_mbid: &MbArtistRef, artist_mbid: &MbArtistRef,
album: &Album, album: &Album,
) -> VecDeque<MbParams> { ) -> VecDeque<MbParams> {
Self::search_albums_requests(artist_id, artist_mbid, slice::from_ref(album)) Self::search_albums_requests(artist_id, artist_mbid, slice::from_ref(album))
} }
fn search_artist_request(meta: &ArtistMeta) -> VecDeque<MbParams> { fn search_artist_request(matching: ArtistMatching) -> VecDeque<MbParams> {
VecDeque::from([MbParams::search_artist(meta.clone())]) VecDeque::from([MbParams::search_artist(matching)])
} }
fn search_albums_requests( fn search_albums_requests(
artist: &ArtistId, artist_id: ArtistId,
arid: &MbArtistRef, artist_mbid: &MbArtistRef,
albums: &[Album], albums: &[Album],
) -> VecDeque<MbParams> { ) -> VecDeque<MbParams> {
let arid = arid.mbid(); let arid = artist_mbid.mbid();
albums albums
.iter() .iter()
.filter(|album| album.meta.id.mb_ref.is_none()) .filter(|album| album.meta.id.mb_ref.is_none())
.map(|album| { .map(|album| {
MbParams::search_release_group(artist.clone(), arid.clone(), album.meta.clone()) MbParams::search_release_group(artist_id, arid.clone(), album.meta.clone())
}) })
.collect() .collect()
} }
fn browse_release_group_job(mbopt: &MbRefOption<MbArtistRef>) -> VecDeque<MbParams> { fn browse_release_group_job(
artist_id: ArtistId,
mbopt: &MbRefOption<MbArtistRef>,
) -> VecDeque<MbParams> {
match mbopt { match mbopt {
MbRefOption::Some(mbref) => Self::browse_release_group_request(mbref), MbRefOption::Some(mbref) => Self::browse_release_group_request(artist_id, mbref),
_ => VecDeque::new(), _ => VecDeque::new(),
} }
} }
fn browse_release_group_request(mbref: &MbArtistRef) -> VecDeque<MbParams> { fn browse_release_group_request(
VecDeque::from([MbParams::browse_release_group(mbref.mbid().clone())]) artist_id: ArtistId,
mbref: &MbArtistRef,
) -> VecDeque<MbParams> {
VecDeque::from([MbParams::browse_release_group(
artist_id,
mbref.mbid().clone(),
)])
} }
fn submit_lookup_artist_job( fn submit_lookup_artist_job(
musicbrainz: &dyn IMbJobSender, musicbrainz: &dyn IMbJobSender,
result_sender: ResultSender, result_sender: ResultSender,
artist: &ArtistMeta, matching: ArtistMatching,
mbid: Mbid, mbid: Mbid,
) -> Result<(), DaemonError> { ) -> Result<(), DaemonError> {
let requests = VecDeque::from([MbParams::lookup_artist(artist.clone(), mbid)]); let requests = VecDeque::from([MbParams::lookup_artist(matching, mbid)]);
musicbrainz.submit_foreground_job(result_sender, requests) musicbrainz.submit_foreground_job(result_sender, requests)
} }
@ -395,16 +406,18 @@ mod tests {
let mut fetch = FetchState::search(fetch_rx); let mut fetch = FetchState::search(fetch_rx);
fetch.lookup_rx.replace(lookup_rx); fetch.lookup_rx.replace(lookup_rx);
let artist = COLLECTION[3].meta.clone(); let id = COLLECTION[3].id;
let meta = COLLECTION[3].meta.clone();
let matching = ArtistMatching::new(id, meta.name.clone());
let matches: Vec<Entity<ArtistMeta>> = vec![]; let matches: Vec<Entity<ArtistMeta>> = vec![];
let fetch_result = MbReturn::Match(EntityMatches::artist_search(artist.clone(), matches)); let fetch_result = MbReturn::Match(EntityMatches::artist_search(matching.clone(), matches));
fetch_tx.send(Ok(fetch_result.clone())).unwrap(); fetch_tx.send(Ok(fetch_result.clone())).unwrap();
assert_eq!(fetch.try_recv(), Err(TryRecvError::Empty)); assert_eq!(fetch.try_recv(), Err(TryRecvError::Empty));
let lookup = Entity::new(artist.clone()); let lookup = Entity::new(meta.clone());
let lookup_result = MbReturn::Match(EntityMatches::artist_lookup(artist.clone(), lookup)); let lookup_result = MbReturn::Match(EntityMatches::artist_lookup(matching.clone(), lookup));
lookup_tx.send(Ok(lookup_result.clone())).unwrap(); lookup_tx.send(Ok(lookup_result.clone())).unwrap();
assert_eq!(fetch.try_recv(), Ok(Ok(lookup_result))); assert_eq!(fetch.try_recv(), Ok(Ok(lookup_result)));
@ -445,7 +458,7 @@ mod tests {
fn fetch_single_album() { fn fetch_single_album() {
let mut mb_job_sender = MockIMbJobSender::new(); let mut mb_job_sender = MockIMbJobSender::new();
let artist_id = COLLECTION[1].meta.id.clone(); let artist_id = COLLECTION[1].id;
let artist_mbid: Mbid = "11111111-1111-1111-1111-111111111111".try_into().unwrap(); let artist_mbid: Mbid = "11111111-1111-1111-1111-111111111111".try_into().unwrap();
let album_meta = COLLECTION[1].albums[0].meta.clone(); let album_meta = COLLECTION[1].albums[0].meta.clone();
@ -520,7 +533,7 @@ mod tests {
fn fetch_albums() { fn fetch_albums() {
let mut mb_job_sender = MockIMbJobSender::new(); let mut mb_job_sender = MockIMbJobSender::new();
let artist_id = COLLECTION[1].meta.id.clone(); let artist_id = COLLECTION[1].id;
let artist_mbid: Mbid = "11111111-1111-1111-1111-111111111111".try_into().unwrap(); let artist_mbid: Mbid = "11111111-1111-1111-1111-111111111111".try_into().unwrap();
let album_1_meta = COLLECTION[1].albums[0].meta.clone(); let album_1_meta = COLLECTION[1].albums[0].meta.clone();
@ -566,7 +579,7 @@ mod tests {
fn lookup_album() { fn lookup_album() {
let mut mb_job_sender = MockIMbJobSender::new(); let mut mb_job_sender = MockIMbJobSender::new();
let artist_id = COLLECTION[1].meta.id.clone(); let artist_id = COLLECTION[1].id;
let album_id = COLLECTION[1].albums[0].meta.id.clone(); let album_id = COLLECTION[1].albums[0].meta.id.clone();
lookup_album_expectation(&mut mb_job_sender, &artist_id, &album_id); lookup_album_expectation(&mut mb_job_sender, &artist_id, &album_id);
@ -579,8 +592,8 @@ mod tests {
AppMachine::app_lookup_album(inner, fetch, &artist_id, &album_id, mbid()); AppMachine::app_lookup_album(inner, fetch, &artist_id, &album_id, mbid());
} }
fn search_artist_expectation(job_sender: &mut MockIMbJobSender, artist: &ArtistMeta) { fn search_artist_expectation(job_sender: &mut MockIMbJobSender, artist: ArtistMatching) {
let requests = VecDeque::from([MbParams::search_artist(artist.clone())]); let requests = VecDeque::from([MbParams::search_artist(artist)]);
job_sender job_sender
.expect_submit_background_job() .expect_submit_background_job()
.with(predicate::always(), predicate::eq(requests)) .with(predicate::always(), predicate::eq(requests))
@ -592,8 +605,10 @@ mod tests {
fn fetch_artist() { fn fetch_artist() {
let mut mb_job_sender = MockIMbJobSender::new(); let mut mb_job_sender = MockIMbJobSender::new();
let artist = COLLECTION[3].meta.clone(); let id = COLLECTION[3].id;
search_artist_expectation(&mut mb_job_sender, &artist); let name = COLLECTION[3].meta.name.clone();
let matching = ArtistMatching::new(id, name);
search_artist_expectation(&mut mb_job_sender, matching);
let music_hoard = music_hoard(COLLECTION.to_owned()); let music_hoard = music_hoard(COLLECTION.to_owned());
let inner = AppInner::new(music_hoard, mb_job_sender); let inner = AppInner::new(music_hoard, mb_job_sender);
@ -608,8 +623,8 @@ mod tests {
assert!(matches!(app, AppState::Fetch(_))); assert!(matches!(app, AppState::Fetch(_)));
} }
fn lookup_artist_expectation(job_sender: &mut MockIMbJobSender, artist: &ArtistMeta) { fn lookup_artist_expectation(job_sender: &mut MockIMbJobSender, artist: ArtistMatching) {
let requests = VecDeque::from([MbParams::lookup_artist(artist.clone(), mbid())]); let requests = VecDeque::from([MbParams::lookup_artist(artist, mbid())]);
job_sender job_sender
.expect_submit_foreground_job() .expect_submit_foreground_job()
.with(predicate::always(), predicate::eq(requests)) .with(predicate::always(), predicate::eq(requests))
@ -621,8 +636,10 @@ mod tests {
fn lookup_artist() { fn lookup_artist() {
let mut mb_job_sender = MockIMbJobSender::new(); let mut mb_job_sender = MockIMbJobSender::new();
let artist = COLLECTION[3].meta.clone(); let id = COLLECTION[3].id;
lookup_artist_expectation(&mut mb_job_sender, &artist); let name = COLLECTION[3].meta.name.clone();
let matching = ArtistMatching::new(id, name);
lookup_artist_expectation(&mut mb_job_sender, matching.clone());
let music_hoard = music_hoard(COLLECTION.to_owned()); let music_hoard = music_hoard(COLLECTION.to_owned());
let inner = AppInner::new(music_hoard, mb_job_sender); let inner = AppInner::new(music_hoard, mb_job_sender);
@ -630,7 +647,7 @@ mod tests {
let (_fetch_tx, fetch_rx) = mpsc::channel(); let (_fetch_tx, fetch_rx) = mpsc::channel();
let fetch = FetchState::search(fetch_rx); let fetch = FetchState::search(fetch_rx);
AppMachine::app_lookup_artist(inner, fetch, &artist, mbid()); AppMachine::app_lookup_artist(inner, fetch, matching, mbid());
} }
#[test] #[test]
@ -671,7 +688,9 @@ mod tests {
.expect_submit_foreground_job() .expect_submit_foreground_job()
.return_once(|_, _| Err(DaemonError::JobChannelDisconnected)); .return_once(|_, _| Err(DaemonError::JobChannelDisconnected));
let artist = COLLECTION[3].meta.clone(); let id = COLLECTION[3].id;
let name = COLLECTION[3].meta.name.clone();
let matching = ArtistMatching::new(id, name);
let music_hoard = music_hoard(COLLECTION.to_owned()); let music_hoard = music_hoard(COLLECTION.to_owned());
let inner = AppInner::new(music_hoard, mb_job_sender); let inner = AppInner::new(music_hoard, mb_job_sender);
@ -679,7 +698,7 @@ mod tests {
let (_fetch_tx, fetch_rx) = mpsc::channel(); let (_fetch_tx, fetch_rx) = mpsc::channel();
let fetch = FetchState::search(fetch_rx); let fetch = FetchState::search(fetch_rx);
let app = AppMachine::app_lookup_artist(inner, fetch, &artist, mbid()); let app = AppMachine::app_lookup_artist(inner, fetch, matching, mbid());
assert!(matches!(app, AppState::Error(_))); assert!(matches!(app, AppState::Error(_)));
} }
@ -687,10 +706,12 @@ mod tests {
fn recv_ok_match_ok() { fn recv_ok_match_ok() {
let (tx, rx) = mpsc::channel::<MbApiResult>(); let (tx, rx) = mpsc::channel::<MbApiResult>();
let artist = COLLECTION[3].meta.clone(); let id = COLLECTION[3].id;
let name = COLLECTION[3].meta.name.clone();
let matching = ArtistMatching::new(id, name);
let artist_match = Entity::with_score(COLLECTION[2].meta.clone(), 80); let artist_match = Entity::with_score(COLLECTION[2].meta.clone(), 80);
let artist_match_info = let artist_match_info =
EntityMatches::artist_search(artist.clone(), vec![artist_match.clone()]); EntityMatches::artist_search(matching.clone(), vec![artist_match.clone()]);
let fetch_result = Ok(MbReturn::Match(artist_match_info)); let fetch_result = Ok(MbReturn::Match(artist_match_info));
tx.send(fetch_result).unwrap(); tx.send(fetch_result).unwrap();
@ -706,7 +727,7 @@ mod tests {
MatchOption::CannotHaveMbid, MatchOption::CannotHaveMbid,
MatchOption::ManualInputMbid, MatchOption::ManualInputMbid,
]; ];
let expected = EntityMatches::artist_search(artist, match_options); let expected = EntityMatches::artist_search(matching, match_options);
assert_eq!(match_state.matches, &expected); assert_eq!(match_state.matches, &expected);
} }
@ -730,7 +751,7 @@ mod tests {
let (tx, rx) = mpsc::channel::<MbApiResult>(); let (tx, rx) = mpsc::channel::<MbApiResult>();
let fetch = FetchState::fetch(rx); let fetch = FetchState::fetch(rx);
let artist_id = collection[0].meta.id.clone(); let artist_id = collection[0].id;
let old_album = collection[0].albums[0].meta.clone(); let old_album = collection[0].albums[0].meta.clone();
let new_album = AlbumMeta::new(AlbumId::new("some new album")); let new_album = AlbumMeta::new(AlbumId::new("some new album"));
@ -764,7 +785,7 @@ mod tests {
let (tx, rx) = mpsc::channel::<MbApiResult>(); let (tx, rx) = mpsc::channel::<MbApiResult>();
let fetch = FetchState::fetch(rx); let fetch = FetchState::fetch(rx);
let artist_id = collection[0].meta.id.clone(); let artist_id = collection[0].id;
let old_album = collection[0].albums[0].meta.clone(); let old_album = collection[0].albums[0].meta.clone();
let new_album = AlbumMeta::new(AlbumId::new("some new album")); let new_album = AlbumMeta::new(AlbumId::new("some new album"));
@ -802,7 +823,7 @@ mod tests {
} }
fn browse_release_group_expectation(artist: &Artist) -> MockIMbJobSender { fn browse_release_group_expectation(artist: &Artist) -> MockIMbJobSender {
let requests = AppMachine::browse_release_group_job(&artist.meta.id.mb_ref); let requests = AppMachine::browse_release_group_job(artist.id, &artist.meta.info.mb_ref);
let mut mb_job_sender = MockIMbJobSender::new(); let mut mb_job_sender = MockIMbJobSender::new();
mb_job_sender mb_job_sender
.expect_submit_background_job() .expect_submit_background_job()
@ -858,8 +879,10 @@ mod tests {
let app = AppMachine::app_fetch_next(inner, fetch); let app = AppMachine::app_fetch_next(inner, fetch);
assert!(matches!(app, AppState::Fetch(_))); assert!(matches!(app, AppState::Fetch(_)));
let artist = COLLECTION[3].meta.clone(); let id = COLLECTION[3].id;
let match_info = EntityMatches::artist_search::<Entity<ArtistMeta>>(artist, vec![]); let name = COLLECTION[3].meta.name.clone();
let matching = ArtistMatching::new(id, name);
let match_info = EntityMatches::artist_search::<Entity<ArtistMeta>>(matching, vec![]);
let fetch_result = Ok(MbReturn::Match(match_info)); let fetch_result = Ok(MbReturn::Match(match_info));
tx.send(fetch_result).unwrap(); tx.send(fetch_result).unwrap();

View File

@ -2,7 +2,7 @@ use std::cmp;
use musichoard::collection::{ use musichoard::collection::{
album::{AlbumInfo, AlbumMbRef, AlbumMeta}, album::{AlbumInfo, AlbumMbRef, AlbumMeta},
artist::{ArtistInfo, ArtistMbRef, ArtistMeta}, artist::{ArtistInfo, ArtistMeta},
musicbrainz::{MbRefOption, Mbid}, musicbrainz::{MbRefOption, Mbid},
}; };
@ -13,11 +13,6 @@ use crate::tui::app::{
MatchOption, MatchStatePublic, WidgetState, MatchOption, MatchStatePublic, WidgetState,
}; };
struct ArtistInfoTuple {
mb_ref: ArtistMbRef,
info: ArtistInfo,
}
struct AlbumInfoTuple { struct AlbumInfoTuple {
mb_ref: AlbumMbRef, mb_ref: AlbumMbRef,
info: AlbumInfo, info: AlbumInfo,
@ -27,7 +22,7 @@ trait GetInfoMeta {
type InfoType; type InfoType;
} }
impl GetInfoMeta for ArtistMeta { impl GetInfoMeta for ArtistMeta {
type InfoType = ArtistInfoTuple; type InfoType = ArtistInfo;
} }
impl GetInfoMeta for AlbumMeta { impl GetInfoMeta for AlbumMeta {
type InfoType = AlbumInfoTuple; type InfoType = AlbumInfoTuple;
@ -44,20 +39,18 @@ enum InfoOption<T> {
} }
impl GetInfo for MatchOption<ArtistMeta> { impl GetInfo for MatchOption<ArtistMeta> {
type InfoType = ArtistInfoTuple; type InfoType = ArtistInfo;
fn get_info(&self) -> InfoOption<Self::InfoType> { fn get_info(&self) -> InfoOption<Self::InfoType> {
let mb_ref;
let mut info = ArtistInfo::default(); let mut info = ArtistInfo::default();
match self { match self {
MatchOption::Some(option) => { MatchOption::Some(option) => {
mb_ref = option.entity.id.mb_ref.clone();
info = option.entity.info.clone(); info = option.entity.info.clone();
} }
MatchOption::CannotHaveMbid => mb_ref = MbRefOption::CannotHaveMbid, MatchOption::CannotHaveMbid => info.mb_ref = MbRefOption::CannotHaveMbid,
MatchOption::ManualInputMbid => return InfoOption::NeedInput, MatchOption::ManualInputMbid => return InfoOption::NeedInput,
} }
InfoOption::Info(ArtistInfoTuple { mb_ref, info }) InfoOption::Info(info)
} }
} }
@ -180,11 +173,11 @@ impl AppMachine<MatchState> {
}; };
match self.state.current { match self.state.current {
EntityMatches::Artist(artist_matches) => { EntityMatches::Artist(artist_matches) => {
let matching = &artist_matches.matching; let matching = artist_matches.matching;
AppMachine::app_lookup_artist(self.inner, self.state.fetch, matching, mbid) AppMachine::app_lookup_artist(self.inner, self.state.fetch, matching, mbid)
} }
EntityMatches::Album(album_matches) => { EntityMatches::Album(album_matches) => {
let artist_id = &album_matches.artist; let artist_id = &album_matches.artist_id;
let matching = &album_matches.matching; let matching = &album_matches.matching;
AppMachine::app_lookup_album( AppMachine::app_lookup_album(
self.inner, self.inner,
@ -205,11 +198,10 @@ impl AppMachine<MatchState> {
fn select_artist( fn select_artist(
inner: &mut AppInner, inner: &mut AppInner,
matches: &ArtistMatches, matches: &ArtistMatches,
tuple: ArtistInfoTuple, info: ArtistInfo,
) -> Result<(), musichoard::Error> { ) -> Result<(), musichoard::Error> {
let mh = &mut inner.music_hoard; let mh = &mut inner.music_hoard;
mh.merge_artist_info(&matches.matching.id, tuple.info)?; mh.merge_artist_info(&matches.matching.id, info)
mh.set_artist_mb_ref(&matches.matching.id, tuple.mb_ref)
} }
fn filter_mb_ref(left: &AlbumMbRef, right: &AlbumMbRef) -> bool { fn filter_mb_ref(left: &AlbumMbRef, right: &AlbumMbRef) -> bool {
@ -223,7 +215,7 @@ impl AppMachine<MatchState> {
) -> Result<(), musichoard::Error> { ) -> Result<(), musichoard::Error> {
let coll = inner.music_hoard.get_collection(); let coll = inner.music_hoard.get_collection();
let mut clashing = vec![]; let mut clashing = vec![];
if let Some(artist) = coll.iter().find(|artist| artist.meta.id == matches.artist) { if let Some(artist) = coll.iter().find(|artist| artist.id == matches.artist_id) {
// While we expect only one, there is nothing stopping anybody from having multiple // While we expect only one, there is nothing stopping anybody from having multiple
// different albums with the same MBID. // different albums with the same MBID.
let iter = artist.albums.iter(); let iter = artist.albums.iter();
@ -237,15 +229,15 @@ impl AppMachine<MatchState> {
let coll = inner.music_hoard.get_filtered(); let coll = inner.music_hoard.get_filtered();
let selection = KeySelection::get(coll, &inner.selection); let selection = KeySelection::get(coll, &inner.selection);
inner.music_hoard.remove_album(&matches.artist, &album)?; inner.music_hoard.remove_album(&matches.artist_id, &album)?;
let coll = inner.music_hoard.get_filtered(); let coll = inner.music_hoard.get_filtered();
inner.selection.select_by_id(coll, selection); inner.selection.select_by_id(coll, selection);
} }
let mh = &mut inner.music_hoard; let mh = &mut inner.music_hoard;
mh.merge_album_info(&matches.artist, &matches.matching, tuple.info)?; mh.merge_album_info(&matches.artist_id, &matches.matching, tuple.info)?;
mh.set_album_mb_ref(&matches.artist, &matches.matching, tuple.mb_ref) mh.set_album_mb_ref(&matches.artist_id, &matches.matching, tuple.mb_ref)
} }
} }
@ -293,11 +285,11 @@ impl IAppInteractMatch for AppMachine<MatchState> {
let inner = &mut self.inner; let inner = &mut self.inner;
let result = match self.state.current { let result = match self.state.current {
EntityMatches::Artist(ref mut matches) => match matches.list.extract_info(index) { EntityMatches::Artist(ref mut matches) => match matches.list.extract_info(index) {
InfoOption::Info(tuple) => Self::select_artist(inner, matches, tuple), InfoOption::Info(info) => Self::select_artist(inner, matches, info),
InfoOption::NeedInput => return self.get_input(), InfoOption::NeedInput => return self.get_input(),
}, },
EntityMatches::Album(ref mut matches) => match matches.list.extract_info(index) { EntityMatches::Album(ref mut matches) => match matches.list.extract_info(index) {
InfoOption::Info(tuple) => Self::select_album(inner, matches, tuple), InfoOption::Info(info) => Self::select_album(inner, matches, info),
InfoOption::NeedInput => return self.get_input(), InfoOption::NeedInput => return self.get_input(),
}, },
}; };
@ -326,14 +318,14 @@ mod tests {
album::{ album::{
Album, AlbumDate, AlbumId, AlbumInfo, AlbumMeta, AlbumPrimaryType, AlbumSecondaryType, Album, AlbumDate, AlbumId, AlbumInfo, AlbumMeta, AlbumPrimaryType, AlbumSecondaryType,
}, },
artist::{Artist, ArtistId, ArtistMeta}, artist::{Artist, ArtistId, ArtistMbRef, ArtistMeta, ArtistName},
Collection, Collection,
}; };
use crate::tui::{ use crate::tui::{
app::{ app::{
machine::tests::{inner, inner_with_mb, input_event, music_hoard}, machine::tests::{inner, inner_with_mb, input_event, music_hoard},
IApp, IAppAccess, IAppInput, ArtistMatching, IApp, IAppAccess, IAppInput,
}, },
lib::{ lib::{
interface::musicbrainz::{ interface::musicbrainz::{
@ -360,30 +352,41 @@ mod tests {
"00000000-0000-0000-0000-000000000000".try_into().unwrap() "00000000-0000-0000-0000-000000000000".try_into().unwrap()
} }
fn artist_meta() -> ArtistMeta { fn artist_id() -> ArtistId {
ArtistMeta::new(ArtistId::new("Artist").with_mb_ref(ArtistMbRef::Some(mbid().into()))) ArtistId(1)
}
fn artist_name() -> ArtistName {
"Artist".into()
}
fn artist_meta<Name: Into<ArtistName>>(name: Name) -> ArtistMeta {
ArtistMeta::new(name).with_mb_ref(ArtistMbRef::Some(mbid().into()))
} }
fn artist_match() -> EntityMatches { fn artist_match() -> EntityMatches {
let mut artist = artist_meta(); let id = artist_id();
let name = artist_name();
let meta = artist_meta(name.clone());
let artist_1 = artist.clone(); let artist_1 = meta.clone();
let artist_match_1 = Entity::with_score(artist_1, 100); let artist_match_1 = Entity::with_score(artist_1, 100);
let artist_2 = artist.clone(); let artist_2 = meta.clone();
let mut artist_match_2 = Entity::with_score(artist_2, 100); let mut artist_match_2 = Entity::with_score(artist_2, 100);
artist_match_2.disambiguation = Some(String::from("some disambiguation")); artist_match_2.disambiguation = Some(String::from("some disambiguation"));
artist.clear_mb_ref();
let list = vec![artist_match_1.clone(), artist_match_2.clone()]; let list = vec![artist_match_1.clone(), artist_match_2.clone()];
EntityMatches::artist_search(artist, list) EntityMatches::artist_search(ArtistMatching::new(id, name), list)
} }
fn artist_lookup() -> EntityMatches { fn artist_lookup() -> EntityMatches {
let mut artist = artist_meta(); let id = artist_id();
artist.clear_mb_ref(); let name = artist_name();
let artist = artist_meta(name.clone());
let lookup = Entity::new(artist.clone()); let lookup = Entity::new(artist.clone());
EntityMatches::artist_lookup(artist, lookup) EntityMatches::artist_lookup(ArtistMatching::new(id, name), lookup)
} }
fn album_id() -> AlbumId { fn album_id() -> AlbumId {
@ -393,14 +396,18 @@ mod tests {
fn album_meta(id: AlbumId) -> AlbumMeta { fn album_meta(id: AlbumId) -> AlbumMeta {
AlbumMeta::new(id) AlbumMeta::new(id)
.with_date(AlbumDate::new(Some(1990), Some(5), None)) .with_date(AlbumDate::new(Some(1990), Some(5), None))
.with_info(AlbumInfo::new( .with_info(
Some(AlbumPrimaryType::Album), AlbumInfo::default()
vec![AlbumSecondaryType::Live, AlbumSecondaryType::Compilation], .with_primary_type(AlbumPrimaryType::Album)
)) .with_secondary_types(vec![
AlbumSecondaryType::Live,
AlbumSecondaryType::Compilation,
]),
)
} }
fn album_match() -> EntityMatches { fn album_match() -> EntityMatches {
let artist_id = ArtistId::new("Artist"); let artist_id = ArtistId(1);
let mut album_id = album_id(); let mut album_id = album_id();
let album_meta = album_meta(album_id.clone()); let album_meta = album_meta(album_id.clone());
@ -418,7 +425,7 @@ mod tests {
} }
fn album_lookup() -> EntityMatches { fn album_lookup() -> EntityMatches {
let artist_id = ArtistId::new("Artist"); let artist_id = ArtistId(1);
let mut album_id = album_id(); let mut album_id = album_id();
let album_meta = album_meta(album_id.clone()); let album_meta = album_meta(album_id.clone());
@ -464,7 +471,7 @@ mod tests {
let collection = vec![]; let collection = vec![];
let mut music_hoard = music_hoard(collection.clone()); let mut music_hoard = music_hoard(collection.clone());
let artist_id = ArtistId::new("Artist"); let artist_id = ArtistId(0);
match matches_info { match matches_info {
EntityMatches::Album(_) => { EntityMatches::Album(_) => {
let album_id = AlbumId::new("Album"); let album_id = AlbumId::new("Album");
@ -491,21 +498,12 @@ mod tests {
.return_once(|_, _, _| Ok(())); .return_once(|_, _, _| Ok(()));
} }
EntityMatches::Artist(_) => { EntityMatches::Artist(_) => {
let mb_ref = MbRefOption::CannotHaveMbid; let info = ArtistInfo::default().with_mb_ref(MbRefOption::CannotHaveMbid);
let info = ArtistInfo::default();
let mut seq = Sequence::new();
music_hoard music_hoard
.expect_merge_artist_info() .expect_merge_artist_info()
.with(eq(artist_id.clone()), eq(info)) .with(eq(artist_id.clone()), eq(info))
.times(1) .times(1)
.in_sequence(&mut seq)
.return_once(|_, _| Ok(()));
music_hoard
.expect_set_artist_mb_ref()
.with(eq(artist_id.clone()), eq(mb_ref))
.times(1)
.in_sequence(&mut seq)
.return_once(|_, _| Ok(())); .return_once(|_, _| Ok(()));
} }
} }
@ -585,22 +583,13 @@ mod tests {
match matches_info { match matches_info {
EntityMatches::Album(_) => panic!(), EntityMatches::Album(_) => panic!(),
EntityMatches::Artist(_) => { EntityMatches::Artist(_) => {
let mut meta = artist_meta(); let id = artist_id();
let mb_ref = meta.id.mb_ref.clone(); let meta = artist_meta(artist_name());
meta.clear_mb_ref();
let mut seq = Sequence::new();
music_hoard music_hoard
.expect_merge_artist_info() .expect_merge_artist_info()
.with(eq(meta.id.clone()), eq(meta.info)) .with(eq(id), eq(meta.info))
.times(1) .times(1)
.in_sequence(&mut seq)
.return_once(|_, _| Ok(()));
music_hoard
.expect_set_artist_mb_ref()
.with(eq(meta.id.clone()), eq(mb_ref))
.times(1)
.in_sequence(&mut seq)
.return_once(|_, _| Ok(())); .return_once(|_, _| Ok(()));
} }
} }
@ -619,7 +608,7 @@ mod tests {
let meta = album_meta(album_id.clone()); let meta = album_meta(album_id.clone());
let mb_ref = album_id.mb_ref.clone(); let mb_ref = album_id.mb_ref.clone();
album_id.clear_mb_ref(); album_id.clear_mb_ref();
let artist = matches.artist.clone(); let artist = matches.artist_id;
let mut seq = Sequence::new(); let mut seq = Sequence::new();
mh.expect_get_collection() mh.expect_get_collection()
@ -673,7 +662,7 @@ mod tests {
// matching album_id. // matching album_id.
// (1) Same artist as matches_info. // (1) Same artist as matches_info.
let mut artist = Artist::new(ArtistId::new("Artist")); let mut artist = Artist::new(1, "Artist");
// (2) An album with the same album_id as the selected one. // (2) An album with the same album_id as the selected one.
artist.albums.push(Album::new(AlbumId::new("Album"))); artist.albums.push(Album::new(AlbumId::new("Album")));
@ -841,15 +830,15 @@ mod tests {
#[test] #[test]
fn select_manual_input_artist() { fn select_manual_input_artist() {
let mut mb_job_sender = MockIMbJobSender::new(); let mut mb_job_sender = MockIMbJobSender::new();
let artist = ArtistMeta::new(ArtistId::new("Artist")); let matching = ArtistMatching::new(artist_id(), artist_name());
let requests = VecDeque::from([MbParams::lookup_artist(artist.clone(), mbid())]); let requests = VecDeque::from([MbParams::lookup_artist(matching.clone(), mbid())]);
mb_job_sender mb_job_sender
.expect_submit_foreground_job() .expect_submit_foreground_job()
.with(predicate::always(), predicate::eq(requests)) .with(predicate::always(), predicate::eq(requests))
.return_once(|_, _| Ok(())); .return_once(|_, _| Ok(()));
let matches_vec: Vec<Entity<ArtistMeta>> = vec![]; let matches_vec: Vec<Entity<ArtistMeta>> = vec![];
let artist_match = EntityMatches::artist_search(artist.clone(), matches_vec); let artist_match = EntityMatches::artist_search(matching.clone(), matches_vec);
let matches = AppMachine::match_state( let matches = AppMachine::match_state(
inner_with_mb(music_hoard(vec![]), mb_job_sender), inner_with_mb(music_hoard(vec![]), mb_job_sender),
match_state(artist_match), match_state(artist_match),
@ -869,7 +858,7 @@ mod tests {
#[test] #[test]
fn select_manual_input_album() { fn select_manual_input_album() {
let mut mb_job_sender = MockIMbJobSender::new(); let mut mb_job_sender = MockIMbJobSender::new();
let artist_id = ArtistId::new("Artist"); let artist_id = artist_id();
let album = AlbumMeta::new("Album").with_date(1990); let album = AlbumMeta::new("Album").with_date(1990);
let requests = VecDeque::from([MbParams::lookup_release_group( let requests = VecDeque::from([MbParams::lookup_release_group(
artist_id.clone(), artist_id.clone(),

View File

@ -225,7 +225,10 @@ mod tests {
}; };
use crate::tui::{ use crate::tui::{
app::{AppState, EntityMatches, IApp, IAppInput, IAppInteractBrowse, InputEvent}, app::{
AppState, ArtistMatching, EntityMatches, IApp, IAppInput, IAppInteractBrowse,
InputEvent,
},
lib::{ lib::{
interface::musicbrainz::{api::Entity, daemon::MockIMbJobSender}, interface::musicbrainz::{api::Entity, daemon::MockIMbJobSender},
MockIMusicHoard, MockIMusicHoard,
@ -519,8 +522,13 @@ mod tests {
let (_, rx) = mpsc::channel(); let (_, rx) = mpsc::channel();
let fetch = FetchState::search(rx); let fetch = FetchState::search(rx);
let artist = ArtistMeta::new(ArtistId::new("Artist"));
let info = EntityMatches::artist_lookup(artist.clone(), Entity::new(artist.clone())); let id = ArtistId(1);
let name = String::from("Artist");
let meta = ArtistMeta::new(name.clone());
let matching = ArtistMatching::new(id, name);
let info = EntityMatches::artist_lookup(matching, Entity::new(meta));
app = app =
AppMachine::match_state(app.unwrap_browse().inner, MatchState::new(info, fetch)).into(); AppMachine::match_state(app.unwrap_browse().inner, MatchState::new(info, fetch)).into();

View File

@ -159,7 +159,7 @@ impl IAppInteractSearchPrivate for AppMachine<SearchState> {
} }
fn predicate_artists(case_sens: bool, char_sens: bool, search: &str, probe: &Artist) -> bool { fn predicate_artists(case_sens: bool, char_sens: bool, search: &str, probe: &Artist) -> bool {
let name = string::normalize_string_with(&probe.meta.id.name, !case_sens, !char_sens); let name = string::normalize_string_with(&probe.meta.name, !case_sens, !char_sens);
let mut result = name.string.starts_with(search); let mut result = name.string.starts_with(search);
if let Some(ref probe_sort) = probe.meta.sort { if let Some(ref probe_sort) = probe.meta.sort {

View File

@ -7,7 +7,7 @@ pub use selection::{Category, Selection};
use musichoard::collection::{ use musichoard::collection::{
album::{AlbumId, AlbumMeta}, album::{AlbumId, AlbumMeta},
artist::{ArtistId, ArtistMeta}, artist::{ArtistId, ArtistMeta, ArtistName},
Collection, Collection,
}; };
@ -228,13 +228,28 @@ impl<T> From<Entity<T>> for MatchOption<T> {
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct ArtistMatches { pub struct ArtistMatches {
pub matching: ArtistMeta, pub matching: ArtistMatching,
pub list: Vec<MatchOption<ArtistMeta>>, pub list: Vec<MatchOption<ArtistMeta>>,
} }
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ArtistMatching {
pub id: ArtistId,
pub name: ArtistName,
}
impl ArtistMatching {
pub fn new<Name: Into<ArtistName>>(id: ArtistId, name: Name) -> Self {
ArtistMatching {
id,
name: name.into(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct AlbumMatches { pub struct AlbumMatches {
pub artist: ArtistId, pub artist_id: ArtistId,
pub matching: AlbumId, pub matching: AlbumId,
pub list: Vec<MatchOption<AlbumMeta>>, pub list: Vec<MatchOption<AlbumMeta>>,
} }
@ -246,40 +261,43 @@ pub enum EntityMatches {
} }
impl EntityMatches { impl EntityMatches {
pub fn artist_search<M: Into<MatchOption<ArtistMeta>>>( pub fn artist_search<M>(matching: ArtistMatching, list: Vec<M>) -> Self
matching: ArtistMeta, where
list: Vec<M>, M: Into<MatchOption<ArtistMeta>>,
) -> Self { {
let list = list.into_iter().map(Into::into).collect(); let list = list.into_iter().map(Into::into).collect();
EntityMatches::Artist(ArtistMatches { matching, list }) EntityMatches::Artist(ArtistMatches { matching, list })
} }
pub fn album_search<M: Into<MatchOption<AlbumMeta>>>( pub fn album_search<M: Into<MatchOption<AlbumMeta>>>(
artist: ArtistId, artist_id: ArtistId,
matching: AlbumId, matching: AlbumId,
list: Vec<M>, list: Vec<M>,
) -> Self { ) -> Self {
let list = list.into_iter().map(Into::into).collect(); let list = list.into_iter().map(Into::into).collect();
EntityMatches::Album(AlbumMatches { EntityMatches::Album(AlbumMatches {
artist, artist_id,
matching, matching,
list, list,
}) })
} }
pub fn artist_lookup<M: Into<MatchOption<ArtistMeta>>>(matching: ArtistMeta, item: M) -> Self { pub fn artist_lookup<M>(matching: ArtistMatching, item: M) -> Self
where
M: Into<MatchOption<ArtistMeta>>,
{
let list = vec![item.into()]; let list = vec![item.into()];
EntityMatches::Artist(ArtistMatches { matching, list }) EntityMatches::Artist(ArtistMatches { matching, list })
} }
pub fn album_lookup<M: Into<MatchOption<AlbumMeta>>>( pub fn album_lookup<M: Into<MatchOption<AlbumMeta>>>(
artist: ArtistId, artist_id: ArtistId,
matching: AlbumId, matching: AlbumId,
item: M, item: M,
) -> Self { ) -> Self {
let list = vec![item.into()]; let list = vec![item.into()];
EntityMatches::Album(AlbumMatches { EntityMatches::Album(AlbumMatches {
artist, artist_id,
matching, matching,
list, list,
}) })

View File

@ -1,10 +1,6 @@
use std::cmp; use std::cmp;
use musichoard::collection::{ use musichoard::collection::{album::Album, artist::Artist, track::Track};
album::Album,
artist::{Artist, ArtistId},
track::Track,
};
use crate::tui::app::{ use crate::tui::app::{
selection::{ selection::{
@ -198,7 +194,7 @@ impl ArtistSelection {
} }
pub struct KeySelectArtist { pub struct KeySelectArtist {
key: (ArtistId,), key: (String,),
album: Option<KeySelectAlbum>, album: Option<KeySelectAlbum>,
} }
@ -215,7 +211,7 @@ impl KeySelectArtist {
} }
pub fn get_sort_key(&self) -> (&str,) { pub fn get_sort_key(&self) -> (&str,) {
(&self.key.0.name,) (&self.key.0,)
} }
} }

View File

@ -5,7 +5,7 @@ use std::collections::HashMap;
use musichoard::{ use musichoard::{
collection::{ collection::{
album::{AlbumDate, AlbumId, AlbumInfo, AlbumLibId, AlbumMbRef, AlbumMeta, AlbumSeq}, album::{AlbumDate, AlbumId, AlbumInfo, AlbumLibId, AlbumMbRef, AlbumMeta, AlbumSeq},
artist::{ArtistId, ArtistInfo, ArtistMbRef, ArtistMeta}, artist::{ArtistInfo, ArtistMbRef, ArtistMeta, ArtistName},
musicbrainz::Mbid, musicbrainz::Mbid,
}, },
external::musicbrainz::{ external::musicbrainz::{
@ -55,8 +55,8 @@ impl<Http: IMusicBrainzHttp> IMusicBrainz for MusicBrainz<Http> {
Ok(from_lookup_release_group_response(mb_response)) Ok(from_lookup_release_group_response(mb_response))
} }
fn search_artist(&mut self, artist: &ArtistMeta) -> Result<Vec<Entity<ArtistMeta>>, Error> { fn search_artist(&mut self, name: &ArtistName) -> Result<Vec<Entity<ArtistMeta>>, Error> {
let query = SearchArtistRequest::new().string(&artist.id.name); let query = SearchArtistRequest::new().string(name);
let paging = PageSettings::default(); let paging = PageSettings::default();
let mb_response = self.client.search_artist(&query, &paging)?; let mb_response = self.client.search_artist(&query, &paging)?;
@ -70,19 +70,19 @@ impl<Http: IMusicBrainzHttp> IMusicBrainz for MusicBrainz<Http> {
fn search_release_group( fn search_release_group(
&mut self, &mut self,
arid: &Mbid, artist_mbid: &Mbid,
album: &AlbumMeta, meta: &AlbumMeta,
) -> Result<Vec<Entity<AlbumMeta>>, Error> { ) -> Result<Vec<Entity<AlbumMeta>>, Error> {
// Some release groups may have a promotional early release messing up the search. Searching // Some release groups may have a promotional early release messing up the search. Searching
// with just the year should be enough anyway. // with just the year should be enough anyway.
let date = AlbumDate::new(album.date.year, None, None); let date = AlbumDate::new(meta.info.date.year, None, None);
let query = SearchReleaseGroupRequest::new() let query = SearchReleaseGroupRequest::new()
.arid(arid) .arid(artist_mbid)
.and() .and()
.first_release_date(&date) .first_release_date(&date)
.and() .and()
.release_group(&album.id.title); .release_group(&meta.id.title);
let paging = PageSettings::default(); let paging = PageSettings::default();
let mb_response = self.client.search_release_group(&query, &paging)?; let mb_response = self.client.search_release_group(&query, &paging)?;
@ -96,10 +96,11 @@ impl<Http: IMusicBrainzHttp> IMusicBrainz for MusicBrainz<Http> {
fn browse_release_group( fn browse_release_group(
&mut self, &mut self,
artist: &Mbid, artist_mbid: &Mbid,
paging: &mut Option<PageSettings>, paging: &mut Option<PageSettings>,
) -> Result<Vec<Entity<AlbumMeta>>, Error> { ) -> Result<Vec<Entity<AlbumMeta>>, Error> {
let request = BrowseReleaseGroupRequest::artist(artist).filter_status_website_default(); let request =
BrowseReleaseGroupRequest::artist(artist_mbid).filter_status_website_default();
let page = paging.take().unwrap_or_default(); let page = paging.take().unwrap_or_default();
let mb_response = self.client.browse_release_group(&request, &page)?; let mb_response = self.client.browse_release_group(&request, &page)?;
@ -115,12 +116,10 @@ fn from_mb_artist_meta(meta: MbArtistMeta) -> (ArtistMeta, Option<String>) {
let sort = Some(meta.sort_name).filter(|s| s != &meta.name); let sort = Some(meta.sort_name).filter(|s| s != &meta.name);
( (
ArtistMeta { ArtistMeta {
id: ArtistId {
name: meta.name, name: meta.name,
mb_ref: ArtistMbRef::Some(meta.id.into()),
},
sort, sort,
info: ArtistInfo { info: ArtistInfo {
mb_ref: ArtistMbRef::Some(meta.id.into()),
properties: HashMap::new(), properties: HashMap::new(),
}, },
}, },
@ -135,9 +134,9 @@ fn from_mb_release_group_meta(meta: MbReleaseGroupMeta) -> AlbumMeta {
lib_id: AlbumLibId::None, lib_id: AlbumLibId::None,
mb_ref: AlbumMbRef::Some(meta.id.into()), mb_ref: AlbumMbRef::Some(meta.id.into()),
}, },
info: AlbumInfo {
date: meta.first_release_date, date: meta.first_release_date,
seq: AlbumSeq::default(), seq: AlbumSeq::default(),
info: AlbumInfo {
primary_type: meta.primary_type, primary_type: meta.primary_type,
secondary_types: meta.secondary_types.unwrap_or_default(), secondary_types: meta.secondary_types.unwrap_or_default(),
}, },

View File

@ -256,30 +256,26 @@ impl JobInstance {
MbParams::Lookup(lookup) => match lookup { MbParams::Lookup(lookup) => match lookup {
LookupParams::Artist(p) => musicbrainz LookupParams::Artist(p) => musicbrainz
.lookup_artist(&p.mbid) .lookup_artist(&p.mbid)
.map(|rv| EntityMatches::artist_lookup(p.artist.clone(), rv)), .map(|rv| EntityMatches::artist_lookup(p.matching.clone(), rv)),
LookupParams::ReleaseGroup(p) => { LookupParams::ReleaseGroup(p) => musicbrainz
musicbrainz.lookup_release_group(&p.mbid).map(|rv| { .lookup_release_group(&p.mbid)
EntityMatches::album_lookup(p.artist_id.clone(), p.album_id.clone(), rv) .map(|rv| EntityMatches::album_lookup(p.artist_id, p.id.clone(), rv)),
})
}
} }
.map(MbReturn::Match), .map(MbReturn::Match),
MbParams::Search(search) => match search { MbParams::Search(search) => match search {
SearchParams::Artist(p) => musicbrainz SearchParams::Artist(p) => musicbrainz
.search_artist(&p.artist) .search_artist(&p.matching.name)
.map(|rv| EntityMatches::artist_search(p.artist.clone(), rv)), .map(|rv| EntityMatches::artist_search(p.matching.clone(), rv)),
SearchParams::ReleaseGroup(p) => musicbrainz SearchParams::ReleaseGroup(p) => musicbrainz
.search_release_group(&p.artist_mbid, &p.album) .search_release_group(&p.artist_mbid, &p.meta)
.map(|rv| { .map(|rv| EntityMatches::album_search(p.artist_id, p.meta.id.clone(), rv)),
EntityMatches::album_search(p.artist_id.clone(), p.album.id.clone(), rv)
}),
} }
.map(MbReturn::Match), .map(MbReturn::Match),
MbParams::Browse(browse) => match browse { MbParams::Browse(browse) => match browse {
BrowseParams::ReleaseGroup(params) => { BrowseParams::ReleaseGroup(params) => {
Self::init_paging_if_none(paging); Self::init_paging_if_none(paging);
musicbrainz musicbrainz
.browse_release_group(&params.artist, paging) .browse_release_group(&params.artist_mbid, paging)
.map(|rv| EntityList::Album(rv.into_iter().map(|rg| rg.entity).collect())) .map(|rv| EntityList::Album(rv.into_iter().map(|rg| rg.entity).collect()))
} }
} }
@ -350,11 +346,12 @@ mod tests {
use mockall::{predicate, Sequence}; use mockall::{predicate, Sequence};
use musichoard::collection::{ use musichoard::collection::{
album::AlbumMeta, album::AlbumMeta,
artist::{ArtistId, ArtistMeta}, artist::{ArtistId, ArtistMeta, ArtistName},
musicbrainz::{IMusicBrainzRef, MbRefOption, Mbid}, musicbrainz::{IMusicBrainzRef, MbRefOption, Mbid},
}; };
use crate::tui::{ use crate::tui::{
app::ArtistMatching,
event::{Event, EventError, MockIFetchCompleteEventSender}, event::{Event, EventError, MockIFetchCompleteEventSender},
lib::interface::musicbrainz::api::{Entity, MockIMusicBrainz}, lib::interface::musicbrainz::api::{Entity, MockIMusicBrainz},
testmod::COLLECTION, testmod::COLLECTION,
@ -426,38 +423,43 @@ mod tests {
} }
fn lookup_artist_requests() -> VecDeque<MbParams> { fn lookup_artist_requests() -> VecDeque<MbParams> {
let artist = COLLECTION[3].meta.clone(); let id = COLLECTION[3].id;
let name = COLLECTION[3].meta.name.clone();
let matching = ArtistMatching::new(id, name);
let mbid = mbid(); let mbid = mbid();
VecDeque::from([MbParams::lookup_artist(artist, mbid)]) VecDeque::from([MbParams::lookup_artist(matching, mbid)])
} }
fn lookup_release_group_requests() -> VecDeque<MbParams> { fn lookup_release_group_requests() -> VecDeque<MbParams> {
let artist_id = COLLECTION[1].meta.id.clone(); let artist_id = COLLECTION[1].id;
let album_id = COLLECTION[1].albums[0].meta.id.clone(); let album_id = COLLECTION[1].albums[0].meta.id.clone();
let mbid = mbid(); let mbid = mbid();
VecDeque::from([MbParams::lookup_release_group(artist_id, album_id, mbid)]) VecDeque::from([MbParams::lookup_release_group(artist_id, album_id, mbid)])
} }
fn search_artist_requests() -> VecDeque<MbParams> { fn search_artist_requests() -> VecDeque<MbParams> {
let artist = COLLECTION[3].meta.clone(); let id = COLLECTION[3].id;
VecDeque::from([MbParams::search_artist(artist)]) let name = COLLECTION[3].meta.name.clone();
let matching = ArtistMatching::new(id, name);
VecDeque::from([MbParams::search_artist(matching)])
} }
fn search_artist_expectations() -> (ArtistMeta, Vec<Entity<ArtistMeta>>) { fn search_artist_expectations() -> (ArtistName, Vec<Entity<ArtistMeta>>) {
let artist = COLLECTION[3].meta.clone(); let name = COLLECTION[3].meta.name.clone();
let meta = COLLECTION[3].meta.clone();
let artist_match_1 = Entity::with_score(artist.clone(), 100); let artist_match_1 = Entity::with_score(meta.clone(), 100);
let artist_match_2 = Entity::with_score(artist.clone(), 50); let artist_match_2 = Entity::with_score(meta.clone(), 50);
let matches = vec![artist_match_1.clone(), artist_match_2.clone()]; let matches = vec![artist_match_1.clone(), artist_match_2.clone()];
(artist, matches) (name, matches)
} }
fn search_albums_requests() -> VecDeque<MbParams> { fn search_albums_requests() -> VecDeque<MbParams> {
let mbref = mb_ref_opt_as_ref(&COLLECTION[1].meta.id.mb_ref); let mbref = mb_ref_opt_as_ref(&COLLECTION[1].meta.info.mb_ref);
let arid = mb_ref_opt_unwrap(mbref).mbid().clone(); let arid = mb_ref_opt_unwrap(mbref).mbid().clone();
let artist_id = COLLECTION[1].meta.id.clone(); let artist_id = COLLECTION[1].id;
let album_1 = COLLECTION[1].albums[0].meta.clone(); let album_1 = COLLECTION[1].albums[0].meta.clone();
let album_4 = COLLECTION[1].albums[3].meta.clone(); let album_4 = COLLECTION[1].albums[3].meta.clone();
@ -468,17 +470,21 @@ mod tests {
} }
fn browse_albums_requests() -> VecDeque<MbParams> { fn browse_albums_requests() -> VecDeque<MbParams> {
let mbref = mb_ref_opt_as_ref(&COLLECTION[1].meta.id.mb_ref); let mbref = mb_ref_opt_as_ref(&COLLECTION[1].meta.info.mb_ref);
let arid = mb_ref_opt_unwrap(mbref).mbid().clone(); let arid = mb_ref_opt_unwrap(mbref).mbid().clone();
VecDeque::from([MbParams::browse_release_group(arid)]) VecDeque::from([MbParams::browse_release_group(album_artist_id(), arid)])
}
fn artist_id() -> ArtistId {
ArtistId(1)
} }
fn album_artist_id() -> ArtistId { fn album_artist_id() -> ArtistId {
COLLECTION[1].meta.id.clone() COLLECTION[1].id
} }
fn album_arid_expectation() -> Mbid { fn album_arid_expectation() -> Mbid {
let mbref = mb_ref_opt_as_ref(&COLLECTION[1].meta.id.mb_ref); let mbref = mb_ref_opt_as_ref(&COLLECTION[1].meta.info.mb_ref);
mb_ref_opt_unwrap(mbref).mbid().clone() mb_ref_opt_unwrap(mbref).mbid().clone()
} }
@ -594,8 +600,12 @@ mod tests {
fn execute_lookup_artist() { fn execute_lookup_artist() {
let mut musicbrainz = musicbrainz(); let mut musicbrainz = musicbrainz();
let mbid = mbid(); let mbid = mbid();
let artist = COLLECTION[3].meta.clone(); let id = COLLECTION[3].id;
let lookup = Entity::new(artist.clone()); let name = COLLECTION[3].meta.name.clone();
let meta = COLLECTION[3].meta.clone();
let matching = ArtistMatching::new(id, name);
let lookup = Entity::new(meta.clone());
lookup_artist_expectation(&mut musicbrainz, &mbid, &lookup); lookup_artist_expectation(&mut musicbrainz, &mbid, &lookup);
let mut event_sender = event_sender(); let mut event_sender = event_sender();
@ -622,7 +632,7 @@ mod tests {
assert_eq!( assert_eq!(
result, result,
Ok(MbReturn::Match(EntityMatches::artist_lookup( Ok(MbReturn::Match(EntityMatches::artist_lookup(
artist, lookup matching, lookup
))) )))
); );
} }
@ -681,13 +691,13 @@ mod tests {
fn search_artist_expectation( fn search_artist_expectation(
musicbrainz: &mut MockIMusicBrainz, musicbrainz: &mut MockIMusicBrainz,
artist: &ArtistMeta, name: &ArtistName,
matches: &[Entity<ArtistMeta>], matches: &[Entity<ArtistMeta>],
) { ) {
let result = Ok(matches.to_owned()); let result = Ok(matches.to_owned());
musicbrainz musicbrainz
.expect_search_artist() .expect_search_artist()
.with(predicate::eq(artist.clone())) .with(predicate::eq(name.clone()))
.times(1) .times(1)
.return_once(|_| result); .return_once(|_| result);
} }
@ -695,8 +705,9 @@ mod tests {
#[test] #[test]
fn execute_search_artist() { fn execute_search_artist() {
let mut musicbrainz = musicbrainz(); let mut musicbrainz = musicbrainz();
let (artist, matches) = search_artist_expectations(); let id = artist_id();
search_artist_expectation(&mut musicbrainz, &artist, &matches); let (name, matches) = search_artist_expectations();
search_artist_expectation(&mut musicbrainz, &name, &matches);
let mut event_sender = event_sender(); let mut event_sender = event_sender();
fetch_complete_expectation(&mut event_sender, 1); fetch_complete_expectation(&mut event_sender, 1);
@ -719,10 +730,11 @@ mod tests {
assert_eq!(result, Err(JobError::JobQueueEmpty)); assert_eq!(result, Err(JobError::JobQueueEmpty));
let result = result_receiver.try_recv().unwrap(); let result = result_receiver.try_recv().unwrap();
let matching = ArtistMatching::new(id, name);
assert_eq!( assert_eq!(
result, result,
Ok(MbReturn::Match(EntityMatches::artist_search( Ok(MbReturn::Match(EntityMatches::artist_search(
artist, matches matching, matches
))) )))
); );
} }

View File

@ -4,7 +4,11 @@
use mockall::automock; use mockall::automock;
use musichoard::{ use musichoard::{
collection::{album::AlbumMeta, artist::ArtistMeta, musicbrainz::Mbid}, collection::{
album::AlbumMeta,
artist::{ArtistMeta, ArtistName},
musicbrainz::Mbid,
},
external::musicbrainz::api::PageSettings, external::musicbrainz::api::PageSettings,
}; };
@ -12,15 +16,16 @@ use musichoard::{
pub trait IMusicBrainz { pub trait IMusicBrainz {
fn lookup_artist(&mut self, mbid: &Mbid) -> Result<Entity<ArtistMeta>, Error>; fn lookup_artist(&mut self, mbid: &Mbid) -> Result<Entity<ArtistMeta>, Error>;
fn lookup_release_group(&mut self, mbid: &Mbid) -> Result<Entity<AlbumMeta>, Error>; fn lookup_release_group(&mut self, mbid: &Mbid) -> Result<Entity<AlbumMeta>, Error>;
fn search_artist(&mut self, artist: &ArtistMeta) -> Result<Vec<Entity<ArtistMeta>>, Error>; fn search_artist(&mut self, name: &ArtistName) -> Result<Vec<Entity<ArtistMeta>>, Error>;
// TODO: AlbumMeta -> AlbumTitle
fn search_release_group( fn search_release_group(
&mut self, &mut self,
arid: &Mbid, artist_mbid: &Mbid,
album: &AlbumMeta, meta: &AlbumMeta,
) -> Result<Vec<Entity<AlbumMeta>>, Error>; ) -> Result<Vec<Entity<AlbumMeta>>, Error>;
fn browse_release_group( fn browse_release_group(
&mut self, &mut self,
artist: &Mbid, artist_mbid: &Mbid,
paging: &mut Option<PageSettings>, paging: &mut Option<PageSettings>,
) -> Result<Vec<Entity<AlbumMeta>>, Error>; ) -> Result<Vec<Entity<AlbumMeta>>, Error>;
} }

View File

@ -2,11 +2,14 @@ use std::{collections::VecDeque, fmt, sync::mpsc};
use musichoard::collection::{ use musichoard::collection::{
album::{AlbumId, AlbumMeta}, album::{AlbumId, AlbumMeta},
artist::{ArtistId, ArtistMeta}, artist::ArtistId,
musicbrainz::Mbid, musicbrainz::Mbid,
}; };
use crate::tui::{app::EntityMatches, lib::interface::musicbrainz::api::Error as MbApiError}; use crate::tui::{
app::{ArtistMatching, EntityMatches},
lib::interface::musicbrainz::api::Error as MbApiError,
};
#[cfg(test)] #[cfg(test)]
use mockall::automock; use mockall::automock;
@ -74,14 +77,14 @@ pub enum LookupParams {
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct LookupArtistParams { pub struct LookupArtistParams {
pub artist: ArtistMeta, pub matching: ArtistMatching,
pub mbid: Mbid, pub mbid: Mbid,
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct LookupReleaseGroupParams { pub struct LookupReleaseGroupParams {
pub artist_id: ArtistId, pub artist_id: ArtistId,
pub album_id: AlbumId, pub id: AlbumId,
pub mbid: Mbid, pub mbid: Mbid,
} }
@ -93,14 +96,15 @@ pub enum SearchParams {
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct SearchArtistParams { pub struct SearchArtistParams {
pub artist: ArtistMeta, pub matching: ArtistMatching,
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct SearchReleaseGroupParams { pub struct SearchReleaseGroupParams {
pub artist_id: ArtistId, pub artist_id: ArtistId,
pub artist_mbid: Mbid, pub artist_mbid: Mbid,
pub album: AlbumMeta, // TODO: probably needs AlbumId when we get there
pub meta: AlbumMeta,
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
@ -110,37 +114,39 @@ pub enum BrowseParams {
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct BrowseReleaseGroupParams { pub struct BrowseReleaseGroupParams {
pub artist: Mbid, pub artist_id: ArtistId,
pub artist_mbid: Mbid,
} }
impl MbParams { impl MbParams {
pub fn lookup_artist(artist: ArtistMeta, mbid: Mbid) -> Self { pub fn lookup_artist(matching: ArtistMatching, mbid: Mbid) -> Self {
MbParams::Lookup(LookupParams::Artist(LookupArtistParams { artist, mbid })) MbParams::Lookup(LookupParams::Artist(LookupArtistParams { matching, mbid }))
} }
pub fn lookup_release_group(artist_id: ArtistId, album_id: AlbumId, mbid: Mbid) -> Self { pub fn lookup_release_group(artist_id: ArtistId, id: AlbumId, mbid: Mbid) -> Self {
MbParams::Lookup(LookupParams::ReleaseGroup(LookupReleaseGroupParams { MbParams::Lookup(LookupParams::ReleaseGroup(LookupReleaseGroupParams {
artist_id, artist_id,
album_id, id,
mbid, mbid,
})) }))
} }
pub fn search_artist(artist: ArtistMeta) -> Self { pub fn search_artist(matching: ArtistMatching) -> Self {
MbParams::Search(SearchParams::Artist(SearchArtistParams { artist })) MbParams::Search(SearchParams::Artist(SearchArtistParams { matching }))
} }
pub fn search_release_group(artist_id: ArtistId, artist_mbid: Mbid, album: AlbumMeta) -> Self { pub fn search_release_group(artist_id: ArtistId, artist_mbid: Mbid, meta: AlbumMeta) -> Self {
MbParams::Search(SearchParams::ReleaseGroup(SearchReleaseGroupParams { MbParams::Search(SearchParams::ReleaseGroup(SearchReleaseGroupParams {
artist_id, artist_id,
artist_mbid, artist_mbid,
album, meta,
})) }))
} }
pub fn browse_release_group(artist: Mbid) -> Self { pub fn browse_release_group(artist_id: ArtistId, artist_mbid: Mbid) -> Self {
MbParams::Browse(BrowseParams::ReleaseGroup(BrowseReleaseGroupParams { MbParams::Browse(BrowseParams::ReleaseGroup(BrowseReleaseGroupParams {
artist, artist_id,
artist_mbid,
})) }))
} }
} }

View File

@ -4,7 +4,7 @@ pub mod interface;
use musichoard::{ use musichoard::{
collection::{ collection::{
album::{AlbumId, AlbumInfo, AlbumMbRef, AlbumMeta}, album::{AlbumId, AlbumInfo, AlbumMbRef, AlbumMeta},
artist::{ArtistId, ArtistInfo, ArtistMbRef}, artist::{ArtistId, ArtistInfo},
Collection, Collection,
}, },
interface::{database::IDatabase, library::ILibrary}, interface::{database::IDatabase, library::ILibrary},
@ -33,11 +33,6 @@ pub trait IMusicHoard {
album_id: &AlbumId, album_id: &AlbumId,
) -> Result<(), musichoard::Error>; ) -> Result<(), musichoard::Error>;
fn set_artist_mb_ref(
&mut self,
artist_id: &ArtistId,
mb_ref: ArtistMbRef,
) -> Result<(), musichoard::Error>;
fn merge_artist_info( fn merge_artist_info(
&mut self, &mut self,
id: &ArtistId, id: &ArtistId,
@ -91,14 +86,6 @@ impl<Database: IDatabase, Library: ILibrary> IMusicHoard for MusicHoard<Database
<Self as IMusicHoardDatabase>::remove_album(self, artist_id, album_id) <Self as IMusicHoardDatabase>::remove_album(self, artist_id, album_id)
} }
fn set_artist_mb_ref(
&mut self,
artist_id: &ArtistId,
mb_ref: ArtistMbRef,
) -> Result<(), musichoard::Error> {
<Self as IMusicHoardDatabase>::set_artist_mb_ref(self, artist_id, mb_ref)
}
fn merge_artist_info( fn merge_artist_info(
&mut self, &mut self,
id: &ArtistId, id: &ArtistId,

View File

@ -1,9 +1,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use musichoard::collection::{ use musichoard::collection::{
album::{ album::{Album, AlbumId, AlbumInfo, AlbumLibId, AlbumMbRef, AlbumMeta, AlbumPrimaryType},
Album, AlbumId, AlbumInfo, AlbumLibId, AlbumMbRef, AlbumMeta, AlbumPrimaryType, AlbumSeq,
},
artist::{Artist, ArtistId, ArtistInfo, ArtistMbRef, ArtistMeta}, artist::{Artist, ArtistId, ArtistInfo, ArtistMbRef, ArtistMeta},
musicbrainz::{MbAlbumRef, MbArtistRef}, musicbrainz::{MbAlbumRef, MbArtistRef},
track::{Track, TrackFormat, TrackId, TrackNum, TrackQuality}, track::{Track, TrackFormat, TrackId, TrackNum, TrackQuality},

View File

@ -126,7 +126,7 @@ impl<'a, 'b> ArtistState<'a, 'b> {
let list = List::new( let list = List::new(
artists artists
.iter() .iter()
.map(|a| ListItem::new(a.meta.id.name.as_str())) .map(|a| ListItem::new(a.meta.name.as_str()))
.collect::<Vec<ListItem>>(), .collect::<Vec<ListItem>>(),
); );
@ -166,7 +166,7 @@ impl<'a, 'b> AlbumState<'a, 'b> {
Ownership: {}", Ownership: {}",
album.map(|a| a.meta.id.title.as_str()).unwrap_or(""), album.map(|a| a.meta.id.title.as_str()).unwrap_or(""),
album album
.map(|a| UiDisplay::display_date(&a.meta.date, &a.meta.seq)) .map(|a| UiDisplay::display_date(&a.meta.info.date, &a.meta.info.seq))
.unwrap_or_default(), .unwrap_or_default(),
album album
.map(|a| UiDisplay::display_album_type( .map(|a| UiDisplay::display_album_type(

View File

@ -3,7 +3,7 @@ use musichoard::collection::{
AlbumDate, AlbumId, AlbumLibId, AlbumMeta, AlbumOwnership, AlbumPrimaryType, AlbumDate, AlbumId, AlbumLibId, AlbumMeta, AlbumOwnership, AlbumPrimaryType,
AlbumSecondaryType, AlbumSeq, AlbumSecondaryType, AlbumSeq,
}, },
artist::ArtistMeta, artist::{ArtistMeta, ArtistName},
musicbrainz::{IMusicBrainzRef, MbRefOption}, musicbrainz::{IMusicBrainzRef, MbRefOption},
track::{TrackFormat, TrackQuality}, track::{TrackFormat, TrackQuality},
}; };
@ -118,8 +118,8 @@ impl UiDisplay {
} }
} }
pub fn display_artist_matching(artist: &ArtistMeta) -> String { pub fn display_artist_matching(name: &ArtistName) -> String {
format!("Matching artist: {}", &artist.id.name) format!("Matching artist: {}", name)
} }
pub fn display_album_matching(album: &AlbumId) -> String { pub fn display_album_matching(album: &AlbumId) -> String {
@ -128,7 +128,7 @@ impl UiDisplay {
pub fn display_matching_info(info: &EntityMatches) -> String { pub fn display_matching_info(info: &EntityMatches) -> String {
match info { match info {
EntityMatches::Artist(m) => UiDisplay::display_artist_matching(&m.matching), EntityMatches::Artist(m) => UiDisplay::display_artist_matching(&m.matching.name),
EntityMatches::Album(m) => UiDisplay::display_album_matching(&m.matching), EntityMatches::Album(m) => UiDisplay::display_album_matching(&m.matching),
} }
} }
@ -159,7 +159,7 @@ impl UiDisplay {
fn display_option_artist(artist: &ArtistMeta, disambiguation: &Option<String>) -> String { fn display_option_artist(artist: &ArtistMeta, disambiguation: &Option<String>) -> String {
format!( format!(
"{}{}", "{}{}",
artist.id.name, artist.name,
disambiguation disambiguation
.as_ref() .as_ref()
.filter(|s| !s.is_empty()) .filter(|s| !s.is_empty())
@ -171,7 +171,7 @@ impl UiDisplay {
fn display_option_album(album: &AlbumMeta, _disambiguation: &Option<String>) -> String { fn display_option_album(album: &AlbumMeta, _disambiguation: &Option<String>) -> String {
format!( format!(
"{:010} | {} [{}]", "{:010} | {} [{}]",
UiDisplay::display_album_date(&album.date), UiDisplay::display_album_date(&album.info.date),
album.id.title, album.id.title,
UiDisplay::display_album_type(&album.info.primary_type, &album.info.secondary_types), UiDisplay::display_album_type(&album.info.primary_type, &album.info.secondary_types),
) )

View File

@ -74,9 +74,9 @@ impl<'a> ArtistOverlay<'a> {
"Artist: {}\n\n{item_indent}\ "Artist: {}\n\n{item_indent}\
MusicBrainz: {}\n{item_indent}\ MusicBrainz: {}\n{item_indent}\
Properties: {}", Properties: {}",
artist.map(|a| a.meta.id.name.as_str()).unwrap_or(""), artist.map(|a| a.meta.name.as_str()).unwrap_or(""),
artist artist
.map(|a| UiDisplay::display_mb_ref_option_as_url(&a.meta.id.mb_ref)) .map(|a| UiDisplay::display_mb_ref_option_as_url(&a.meta.info.mb_ref))
.unwrap_or_default(), .unwrap_or_default(),
Self::opt_hashmap_to_string( Self::opt_hashmap_to_string(
artist.map(|a| &a.meta.info.properties), artist.map(|a| &a.meta.info.properties),

View File

@ -1,6 +1,6 @@
use musichoard::collection::{ use musichoard::collection::{
album::{AlbumId, AlbumMeta}, album::{AlbumId, AlbumMeta},
artist::ArtistMeta, artist::{ArtistMeta, ArtistName},
}; };
use ratatui::widgets::{List, ListItem}; use ratatui::widgets::{List, ListItem};
@ -18,13 +18,13 @@ pub struct MatchOverlay<'a, 'b> {
impl<'a, 'b> MatchOverlay<'a, 'b> { impl<'a, 'b> MatchOverlay<'a, 'b> {
pub fn new(info: &'a EntityMatches, state: &'b mut WidgetState) -> Self { pub fn new(info: &'a EntityMatches, state: &'b mut WidgetState) -> Self {
match info { match info {
EntityMatches::Artist(m) => Self::artists(&m.matching, &m.list, state), EntityMatches::Artist(m) => Self::artists(&m.matching.name, &m.list, state),
EntityMatches::Album(m) => Self::albums(&m.matching, &m.list, state), EntityMatches::Album(m) => Self::albums(&m.matching, &m.list, state),
} }
} }
fn artists( fn artists(
matching: &ArtistMeta, matching: &ArtistName,
matches: &'a [MatchOption<ArtistMeta>], matches: &'a [MatchOption<ArtistMeta>],
state: &'b mut WidgetState, state: &'b mut WidgetState,
) -> Self { ) -> Self {

View File

@ -200,11 +200,11 @@ impl IUi for Ui {
mod tests { mod tests {
use musichoard::collection::{ use musichoard::collection::{
album::{AlbumDate, AlbumId, AlbumInfo, AlbumMeta, AlbumPrimaryType, AlbumSecondaryType}, album::{AlbumDate, AlbumId, AlbumInfo, AlbumMeta, AlbumPrimaryType, AlbumSecondaryType},
artist::{Artist, ArtistId, ArtistMeta}, artist::{Artist, ArtistId, ArtistMeta, ArtistName},
}; };
use crate::tui::{ use crate::tui::{
app::{AppPublic, AppPublicInner, Delta, MatchStatePublic}, app::{AppPublic, AppPublicInner, ArtistMatching, Delta, MatchStatePublic},
lib::interface::musicbrainz::api::Entity, lib::interface::musicbrainz::api::Entity,
testmod::COLLECTION, testmod::COLLECTION,
tests::terminal, tests::terminal,
@ -287,7 +287,7 @@ mod tests {
#[test] #[test]
fn empty_album() { fn empty_album() {
let mut artists: Vec<Artist> = vec![Artist::new(ArtistId::new("An artist"))]; let mut artists: Vec<Artist> = vec![Artist::new(0, "An artist")];
artists[0].albums.push(Album::new("An album")); artists[0].albums.push(Album::new("An album"));
let mut selection = Selection::new(&artists); let mut selection = Selection::new(&artists);
@ -324,33 +324,49 @@ mod tests {
draw_test_suite(artists, &mut selection); draw_test_suite(artists, &mut selection);
} }
fn artist_meta() -> ArtistMeta { fn artist_id() -> ArtistId {
ArtistMeta::new(ArtistId::new("an artist")) ArtistId(1)
}
fn artist_name() -> ArtistName {
"an artist".into()
}
fn artist_meta<Name: Into<ArtistName>>(name: Name) -> ArtistMeta {
ArtistMeta::new(name)
} }
fn artist_matches() -> EntityMatches { fn artist_matches() -> EntityMatches {
let artist = artist_meta(); let id = artist_id();
let artist_match = Entity::with_score(artist.clone(), 80); let name = artist_name();
let meta = artist_meta(name.clone());
let matching = ArtistMatching::new(id, name);
let artist_match = Entity::with_score(meta, 80);
let list = vec![artist_match.clone(), artist_match.clone()]; let list = vec![artist_match.clone(), artist_match.clone()];
let mut info = EntityMatches::artist_search(artist, list); let mut info = EntityMatches::artist_search(matching, list);
info.push_cannot_have_mbid(); info.push_cannot_have_mbid();
info.push_manual_input_mbid(); info.push_manual_input_mbid();
info info
} }
fn artist_lookup() -> EntityMatches { fn artist_lookup() -> EntityMatches {
let artist = artist_meta(); let id = artist_id();
let artist_lookup = Entity::new(artist.clone()); let name = artist_name();
let meta = artist_meta(name.clone());
let matching = ArtistMatching::new(id, name);
let mut info = EntityMatches::artist_lookup(artist, artist_lookup); let artist_lookup = Entity::new(meta.clone());
let mut info = EntityMatches::artist_lookup(matching, artist_lookup);
info.push_cannot_have_mbid(); info.push_cannot_have_mbid();
info.push_manual_input_mbid(); info.push_manual_input_mbid();
info info
} }
fn album_artist_id() -> ArtistId { fn album_artist_id() -> ArtistId {
ArtistId::new("Artist") ArtistId(1)
} }
fn album_id() -> AlbumId { fn album_id() -> AlbumId {
@ -360,10 +376,14 @@ mod tests {
fn album_meta(id: AlbumId) -> AlbumMeta { fn album_meta(id: AlbumId) -> AlbumMeta {
AlbumMeta::new(id) AlbumMeta::new(id)
.with_date(AlbumDate::new(Some(1990), Some(5), None)) .with_date(AlbumDate::new(Some(1990), Some(5), None))
.with_info(AlbumInfo::new( .with_info(
Some(AlbumPrimaryType::Album), AlbumInfo::default()
vec![AlbumSecondaryType::Live, AlbumSecondaryType::Compilation], .with_primary_type(AlbumPrimaryType::Album)
)) .with_secondary_types(vec![
AlbumSecondaryType::Live,
AlbumSecondaryType::Compilation,
]),
)
} }
fn album_matches() -> EntityMatches { fn album_matches() -> EntityMatches {

View File

@ -1,7 +1,3 @@
use std::{fs, path::PathBuf};
use once_cell::sync::Lazy;
use musichoard::{ use musichoard::{
collection::{artist::Artist, Collection}, collection::{artist::Artist, Collection},
external::database::sql::{backend::SqlDatabaseSqliteBackend, SqlDatabase}, external::database::sql::{backend::SqlDatabaseSqliteBackend, SqlDatabase},
@ -9,10 +5,7 @@ use musichoard::{
}; };
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
use crate::testlib::COLLECTION; use crate::{copy_file_into_temp, testlib::COLLECTION, COMPLETE_DB_TEST_FILE};
pub static DATABASE_TEST_FILE: Lazy<PathBuf> =
Lazy::new(|| fs::canonicalize("./tests/files/database/database.db").unwrap());
fn expected() -> Collection { fn expected() -> Collection {
let mut expected = COLLECTION.to_owned(); let mut expected = COLLECTION.to_owned();
@ -24,6 +17,16 @@ fn expected() -> Collection {
expected expected
} }
#[test]
fn reset() {
let file = NamedTempFile::new().unwrap();
let backend = SqlDatabaseSqliteBackend::new(file.path()).unwrap();
let mut database = SqlDatabase::new(backend).unwrap();
database.reset().unwrap();
}
#[test] #[test]
fn save() { fn save() {
let file = NamedTempFile::new().unwrap(); let file = NamedTempFile::new().unwrap();
@ -37,7 +40,7 @@ fn save() {
#[test] #[test]
fn load() { fn load() {
let backend = SqlDatabaseSqliteBackend::new(&*DATABASE_TEST_FILE).unwrap(); let backend = SqlDatabaseSqliteBackend::new(&*COMPLETE_DB_TEST_FILE).unwrap();
let mut database = SqlDatabase::new(backend).unwrap(); let mut database = SqlDatabase::new(backend).unwrap();
let read_data: Vec<Artist> = database.load().unwrap(); let read_data: Vec<Artist> = database.load().unwrap();
@ -61,3 +64,18 @@ fn reverse() {
let expected = expected(); let expected = expected();
assert_eq!(read_data, expected); assert_eq!(read_data, expected);
} }
#[test]
fn reverse_with_reset() {
let file = copy_file_into_temp(&*COMPLETE_DB_TEST_FILE);
let backend = SqlDatabaseSqliteBackend::new(file.path()).unwrap();
let mut database = SqlDatabase::new(backend).unwrap();
let expected: Vec<Artist> = database.load().unwrap();
database.reset().unwrap();
database.save(&expected).unwrap();
let read_data: Vec<Artist> = database.load().unwrap();
assert_eq!(read_data, expected);
}

Binary file not shown.

View File

@ -6,7 +6,7 @@ mod library;
mod testlib; mod testlib;
use std::{fs, path::PathBuf}; use std::{ffi::OsStr, fs, path::PathBuf, process::Command};
use musichoard::{ use musichoard::{
external::{ external::{
@ -15,16 +15,33 @@ use musichoard::{
}, },
IMusicHoardBase, IMusicHoardDatabase, IMusicHoardLibrary, MusicHoard, IMusicHoardBase, IMusicHoardDatabase, IMusicHoardLibrary, MusicHoard,
}; };
use once_cell::sync::Lazy;
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
use crate::testlib::COLLECTION; use crate::testlib::COLLECTION;
fn copy_file_into_temp<P: Into<PathBuf>>(path: P) -> NamedTempFile { pub static PARTIAL_DB_TEST_FILE: Lazy<PathBuf> =
Lazy::new(|| fs::canonicalize("./tests/files/database/partial.db").unwrap());
pub static COMPLETE_DB_TEST_FILE: Lazy<PathBuf> =
Lazy::new(|| fs::canonicalize("./tests/files/database/complete.db").unwrap());
pub fn copy_file_into_temp<P: Into<PathBuf>>(path: P) -> NamedTempFile {
let temp = NamedTempFile::new().unwrap(); let temp = NamedTempFile::new().unwrap();
fs::copy(path.into(), temp.path()).unwrap(); fs::copy(path.into(), temp.path()).unwrap();
temp temp
} }
pub fn sqldiff(left: &OsStr, right: &OsStr) {
let output = Command::new("sqldiff")
.arg(left)
.arg(right)
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(stdout, "");
}
#[test] #[test]
fn merge_library_then_database() { fn merge_library_then_database() {
// Acquired the lock on the beets config file. We need to own the underlying object so later we // Acquired the lock on the beets config file. We need to own the underlying object so later we
@ -37,7 +54,7 @@ fn merge_library_then_database() {
.config(Some(&*library::beets::BEETS_TEST_CONFIG_PATH)); .config(Some(&*library::beets::BEETS_TEST_CONFIG_PATH));
let library = BeetsLibrary::new(executor); let library = BeetsLibrary::new(executor);
let file = copy_file_into_temp(&*database::sql::DATABASE_TEST_FILE); let file = copy_file_into_temp(&*PARTIAL_DB_TEST_FILE);
let backend = SqlDatabaseSqliteBackend::new(file.path()).unwrap(); let backend = SqlDatabaseSqliteBackend::new(file.path()).unwrap();
let database = SqlDatabase::new(backend).unwrap(); let database = SqlDatabase::new(backend).unwrap();
@ -47,6 +64,8 @@ fn merge_library_then_database() {
music_hoard.reload_database().unwrap(); music_hoard.reload_database().unwrap();
assert_eq!(music_hoard.get_collection(), &*COLLECTION); assert_eq!(music_hoard.get_collection(), &*COLLECTION);
sqldiff(COMPLETE_DB_TEST_FILE.as_os_str(), file.path().as_os_str());
} }
#[test] #[test]
@ -61,7 +80,7 @@ fn merge_database_then_library() {
.config(Some(&*library::beets::BEETS_TEST_CONFIG_PATH)); .config(Some(&*library::beets::BEETS_TEST_CONFIG_PATH));
let library = BeetsLibrary::new(executor); let library = BeetsLibrary::new(executor);
let file = copy_file_into_temp(&*database::sql::DATABASE_TEST_FILE); let file = copy_file_into_temp(&*PARTIAL_DB_TEST_FILE);
let backend = SqlDatabaseSqliteBackend::new(file.path()).unwrap(); let backend = SqlDatabaseSqliteBackend::new(file.path()).unwrap();
let database = SqlDatabase::new(backend).unwrap(); let database = SqlDatabase::new(backend).unwrap();
@ -71,4 +90,6 @@ fn merge_database_then_library() {
music_hoard.rescan_library().unwrap(); music_hoard.rescan_library().unwrap();
assert_eq!(music_hoard.get_collection(), &*COLLECTION); assert_eq!(music_hoard.get_collection(), &*COLLECTION);
sqldiff(COMPLETE_DB_TEST_FILE.as_os_str(), file.path().as_os_str());
} }

View File

@ -4,7 +4,7 @@ use std::collections::HashMap;
use musichoard::collection::{ use musichoard::collection::{
album::{ album::{
Album, AlbumId, AlbumInfo, AlbumLibId, AlbumMbRef, AlbumMeta, AlbumPrimaryType, Album, AlbumId, AlbumInfo, AlbumLibId, AlbumMbRef, AlbumMeta, AlbumPrimaryType,
AlbumSecondaryType, AlbumSeq, AlbumSecondaryType,
}, },
artist::{Artist, ArtistId, ArtistInfo, ArtistMbRef, ArtistMeta}, artist::{Artist, ArtistId, ArtistInfo, ArtistMbRef, ArtistMeta},
musicbrainz::MbArtistRef, musicbrainz::MbArtistRef,
@ -15,15 +15,14 @@ use musichoard::collection::{
pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection { pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
vec![ vec![
Artist { Artist {
id: ArtistId(1),
meta: ArtistMeta { meta: ArtistMeta {
id: ArtistId {
name: String::from("Аркона"), name: String::from("Аркона"),
sort: Some(String::from("Arkona")),
info: ArtistInfo {
mb_ref: ArtistMbRef::Some(MbArtistRef::from_url_str( mb_ref: ArtistMbRef::Some(MbArtistRef::from_url_str(
"https://musicbrainz.org/artist/baad262d-55ef-427a-83c7-f7530964f212" "https://musicbrainz.org/artist/baad262d-55ef-427a-83c7-f7530964f212"
).unwrap()), ).unwrap()),
},
sort: Some(String::from("Arkona")),
info: ArtistInfo {
properties: HashMap::from([ properties: HashMap::from([
(String::from("MusicButler"), vec![ (String::from("MusicButler"), vec![
String::from("https://www.musicbutler.io/artist-page/283448581"), String::from("https://www.musicbutler.io/artist-page/283448581"),
@ -44,12 +43,9 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
lib_id: AlbumLibId::Value(7), lib_id: AlbumLibId::Value(7),
mb_ref: AlbumMbRef::None, mb_ref: AlbumMbRef::None,
}, },
date: 2011.into(), info: AlbumInfo::default()
seq: AlbumSeq(0), .with_date(2011)
info: AlbumInfo { .with_primary_type(AlbumPrimaryType::Album),
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
},
}, },
tracks: vec![ tracks: vec![
Track { Track {
@ -210,15 +206,14 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
}], }],
}, },
Artist { Artist {
id: ArtistId(2),
meta: ArtistMeta { meta: ArtistMeta {
id: ArtistId {
name: String::from("Eluveitie"), name: String::from("Eluveitie"),
sort: None,
info: ArtistInfo {
mb_ref: ArtistMbRef::Some(MbArtistRef::from_url_str( mb_ref: ArtistMbRef::Some(MbArtistRef::from_url_str(
"https://musicbrainz.org/artist/8000598a-5edb-401c-8e6d-36b167feaf38" "https://musicbrainz.org/artist/8000598a-5edb-401c-8e6d-36b167feaf38"
).unwrap()), ).unwrap()),
},
sort: None,
info: ArtistInfo {
properties: HashMap::from([ properties: HashMap::from([
(String::from("MusicButler"), vec![ (String::from("MusicButler"), vec![
String::from("https://www.musicbutler.io/artist-page/269358403"), String::from("https://www.musicbutler.io/artist-page/269358403"),
@ -237,12 +232,9 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
lib_id: AlbumLibId::Value(1), lib_id: AlbumLibId::Value(1),
mb_ref: AlbumMbRef::None, mb_ref: AlbumMbRef::None,
}, },
date: 2004.into(), info: AlbumInfo::default()
seq: AlbumSeq(0), .with_date(2004)
info: AlbumInfo { .with_primary_type(AlbumPrimaryType::Ep),
primary_type: Some(AlbumPrimaryType::Ep),
secondary_types: vec![],
},
}, },
tracks: vec![ tracks: vec![
Track { Track {
@ -320,12 +312,9 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
lib_id: AlbumLibId::Value(2), lib_id: AlbumLibId::Value(2),
mb_ref: AlbumMbRef::None, mb_ref: AlbumMbRef::None,
}, },
date: 2008.into(), info: AlbumInfo::default()
seq: AlbumSeq(0), .with_date(2008)
info: AlbumInfo { .with_primary_type(AlbumPrimaryType::Album),
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
},
}, },
tracks: vec![ tracks: vec![
Track { Track {
@ -465,15 +454,14 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
], ],
}, },
Artist { Artist {
id: ArtistId(3),
meta: ArtistMeta { meta: ArtistMeta {
id: ArtistId {
name: String::from("Frontside"), name: String::from("Frontside"),
sort: None,
info: ArtistInfo {
mb_ref: ArtistMbRef::Some(MbArtistRef::from_url_str( mb_ref: ArtistMbRef::Some(MbArtistRef::from_url_str(
"https://musicbrainz.org/artist/3a901353-fccd-4afd-ad01-9c03f451b490" "https://musicbrainz.org/artist/3a901353-fccd-4afd-ad01-9c03f451b490"
).unwrap()), ).unwrap()),
},
sort: None,
info: ArtistInfo {
properties: HashMap::from([ properties: HashMap::from([
(String::from("MusicButler"), vec![ (String::from("MusicButler"), vec![
String::from("https://www.musicbutler.io/artist-page/826588800"), String::from("https://www.musicbutler.io/artist-page/826588800"),
@ -491,12 +479,9 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
lib_id: AlbumLibId::Value(3), lib_id: AlbumLibId::Value(3),
mb_ref: AlbumMbRef::None, mb_ref: AlbumMbRef::None,
}, },
date: 2001.into(), info: AlbumInfo::default()
seq: AlbumSeq(0), .with_date(2001)
info: AlbumInfo { .with_primary_type(AlbumPrimaryType::Album),
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
},
}, },
tracks: vec![ tracks: vec![
Track { Track {
@ -624,15 +609,14 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
}], }],
}, },
Artist { Artist {
id: ArtistId(4),
meta: ArtistMeta { meta: ArtistMeta {
id: ArtistId {
name: String::from("Heavens Basement"), name: String::from("Heavens Basement"),
sort: Some(String::from("Heavens Basement")),
info: ArtistInfo {
mb_ref: ArtistMbRef::Some(MbArtistRef::from_url_str( mb_ref: ArtistMbRef::Some(MbArtistRef::from_url_str(
"https://musicbrainz.org/artist/c2c4d56a-d599-4a18-bd2f-ae644e2198cc" "https://musicbrainz.org/artist/c2c4d56a-d599-4a18-bd2f-ae644e2198cc"
).unwrap()), ).unwrap()),
},
sort: Some(String::from("Heavens Basement")),
info: ArtistInfo {
properties: HashMap::from([ properties: HashMap::from([
(String::from("MusicButler"), vec![ (String::from("MusicButler"), vec![
String::from("https://www.musicbutler.io/artist-page/291158685"), String::from("https://www.musicbutler.io/artist-page/291158685"),
@ -650,9 +634,7 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
lib_id: AlbumLibId::Singleton, lib_id: AlbumLibId::Singleton,
mb_ref: AlbumMbRef::None, mb_ref: AlbumMbRef::None,
}, },
date: 2011.into(), info: AlbumInfo::default().with_date(2011),
seq: AlbumSeq(0),
info: AlbumInfo::default(),
}, },
tracks: vec![ tracks: vec![
Track { Track {
@ -674,12 +656,9 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
lib_id: AlbumLibId::Value(4), lib_id: AlbumLibId::Value(4),
mb_ref: AlbumMbRef::None, mb_ref: AlbumMbRef::None,
}, },
date: 2011.into(), info: AlbumInfo::default()
seq: AlbumSeq(0), .with_date(2011)
info: AlbumInfo { .with_primary_type(AlbumPrimaryType::Album),
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
},
}, },
tracks: vec![ tracks: vec![
Track { Track {
@ -763,15 +742,14 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
}], }],
}, },
Artist { Artist {
id: ArtistId(5),
meta: ArtistMeta { meta: ArtistMeta {
id: ArtistId {
name: String::from("Metallica"), name: String::from("Metallica"),
sort: None,
info: ArtistInfo {
mb_ref: ArtistMbRef::Some(MbArtistRef::from_url_str( mb_ref: ArtistMbRef::Some(MbArtistRef::from_url_str(
"https://musicbrainz.org/artist/65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab" "https://musicbrainz.org/artist/65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab"
).unwrap()), ).unwrap()),
},
sort: None,
info: ArtistInfo {
properties: HashMap::from([ properties: HashMap::from([
(String::from("MusicButler"), vec![ (String::from("MusicButler"), vec![
String::from("https://www.musicbutler.io/artist-page/3996865"), String::from("https://www.musicbutler.io/artist-page/3996865"),
@ -790,12 +768,9 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
lib_id: AlbumLibId::Value(5), lib_id: AlbumLibId::Value(5),
mb_ref: AlbumMbRef::None, mb_ref: AlbumMbRef::None,
}, },
date: 1984.into(), info: AlbumInfo::default()
seq: AlbumSeq(0), .with_date(1984)
info: AlbumInfo { .with_primary_type(AlbumPrimaryType::Album),
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
},
}, },
tracks: vec![ tracks: vec![
Track { Track {
@ -895,12 +870,10 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
lib_id: AlbumLibId::Value(6), lib_id: AlbumLibId::Value(6),
mb_ref: AlbumMbRef::None, mb_ref: AlbumMbRef::None,
}, },
date: 1999.into(), info: AlbumInfo::default()
seq: AlbumSeq(0), .with_date(1999)
info: AlbumInfo { .with_primary_type(AlbumPrimaryType::Album)
primary_type: Some(AlbumPrimaryType::Album), .with_secondary_types(vec![AlbumSecondaryType::Live]),
secondary_types: vec![AlbumSecondaryType::Live],
},
}, },
tracks: vec![ tracks: vec![
Track { Track {