Compare commits

..

No commits in common. "268---refactor-the-idatabase-calls-to-write-directly-to-the-database" and "main" have entirely different histories.

43 changed files with 1464 additions and 1260 deletions

View File

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

View File

@ -8,7 +8,7 @@
This feature requires the `sqlite` library.
Either install system libraries with:
Either install system libraries: with
On Fedora:
``` sh
@ -17,13 +17,6 @@ sudo dnf install sqlite-devel
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
This feature requires the `openssl` system library.

View File

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

View File

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

View File

@ -105,26 +105,10 @@ 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;
impl MergeCollections {
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>) {
pub fn merge_by_name<T: Merge>(mut primary: NormalMap<T>, secondary: NormalMap<T>) -> Vec<T> {
let mut merged = vec![];
for (title, mut secondary_items) in secondary.0.into_iter() {
match primary.remove(&title) {
@ -133,17 +117,14 @@ impl MergeCollections {
// added once encountered in the wild.
assert_eq!(primary_items.len(), 1, "multiple primaries unsupported");
assert_eq!(secondary_items.len(), 1, "multiple secondaries unsupported");
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);
let mut primary_item = primary_items.pop().unwrap();
primary_item.merge_in_place(secondary_items.pop().unwrap());
merged.push(primary_item);
}
None => merged.extend(secondary_items),
}
}
(merged, primary.0.into_values().flatten().collect())
merged.extend(primary.0.into_values().flatten());
merged
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -3,28 +3,40 @@ use std::mem;
use crate::{
collection::{
album::{AlbumInfo, AlbumMbRef, AlbumMeta},
artist::ArtistInfo,
merge::{Merge, MergeCollections, NormalMap},
string,
artist::{ArtistInfo, ArtistMbRef},
merge::Merge,
},
core::{
collection::{
album::{Album, AlbumId},
album::{Album, AlbumId, AlbumSeq},
artist::{Artist, ArtistId},
Collection,
},
interface::database::IDatabase,
musichoard::{
base::IMusicHoardBasePrivate, Error, IntoId, LibArtist, MusicHoard, NoDatabase,
},
musichoard::{base::IMusicHoardBasePrivate, Error, MusicHoard, NoDatabase},
},
};
pub trait IMusicHoardDatabase {
fn merge_collections(&mut self) -> Result<Collection, 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>>(
&mut self,
artist_id: Id,
@ -32,6 +44,30 @@ pub trait IMusicHoardDatabase {
) -> 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>>(
&mut self,
artist_id: ArtistIdRef,
@ -54,7 +90,17 @@ pub trait IMusicHoardDatabase {
artist_id: ArtistIdRef,
album_id: AlbumIdRef,
) -> 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>>(
&mut self,
artist_id: ArtistIdRef,
@ -69,39 +115,80 @@ pub trait IMusicHoardDatabase {
}
impl<Database: IDatabase, Library> IMusicHoardDatabase for MusicHoard<Database, Library> {
fn merge_collections(&mut self) -> Result<Collection, Error> {
let mut database = self.database.load()?;
Self::sort_albums_and_tracks(database.iter_mut().map(|a| &mut a.albums));
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()?;
let mut database_cache = self.database.load()?;
Self::sort_albums_and_tracks(database_cache.iter_mut());
self.collection = self.merge_collections(database_cache);
self.filtered = self.filter_collection();
self.pre_commit = self.collection.clone();
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>>(
&mut self,
artist_id: Id,
@ -119,6 +206,49 @@ 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>>(
&mut self,
artist_id: ArtistIdRef,
@ -158,9 +288,12 @@ impl<Database: IDatabase, Library> IMusicHoardDatabase for MusicHoard<Database,
album_id: AlbumIdRef,
mb_ref: AlbumMbRef,
) -> Result<(), Error> {
self.update_album(artist_id.as_ref(), album_id.as_ref(), |album| {
album.meta.set_mb_ref(mb_ref)
})
self.update_album_and(
artist_id.as_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>>(
@ -168,9 +301,39 @@ impl<Database: IDatabase, Library> IMusicHoardDatabase for MusicHoard<Database,
artist_id: ArtistIdRef,
album_id: AlbumIdRef,
) -> Result<(), Error> {
self.update_album(artist_id.as_ref(), album_id.as_ref(), |album| {
album.meta.clear_mb_ref()
})
self.update_album_and(
artist_id.as_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>>(
@ -179,7 +342,7 @@ impl<Database: IDatabase, Library> IMusicHoardDatabase for MusicHoard<Database,
album_id: AlbumIdRef,
mut info: AlbumInfo,
) -> Result<(), Error> {
self.update_album_and_sort(artist_id.as_ref(), album_id.as_ref(), |album| {
self.update_album(artist_id.as_ref(), album_id.as_ref(), |album| {
mem::swap(&mut album.meta.info, &mut info);
album.meta.info.merge_in_place(info);
})
@ -190,7 +353,7 @@ impl<Database: IDatabase, Library> IMusicHoardDatabase for MusicHoard<Database,
artist_id: Id,
album_id: AlbumIdRef,
) -> Result<(), Error> {
self.update_album_and_sort(artist_id.as_ref(), album_id.as_ref(), |album| {
self.update_album(artist_id.as_ref(), album_id.as_ref(), |album| {
album.meta.info = AlbumInfo::default()
})
}
@ -202,6 +365,7 @@ pub trait IMusicHoardDatabasePrivate {
impl<Library> IMusicHoardDatabasePrivate for MusicHoard<NoDatabase, Library> {
fn commit(&mut self) -> Result<(), Error> {
self.collection = self.pre_commit.clone();
self.filtered = self.filter_collection();
Ok(())
}
@ -209,11 +373,14 @@ impl<Library> IMusicHoardDatabasePrivate for MusicHoard<NoDatabase, Library> {
impl<Database: IDatabase, Library> IMusicHoardDatabasePrivate for MusicHoard<Database, Library> {
fn commit(&mut self) -> Result<(), Error> {
if let Err(err) = self.database.save(&self.collection) {
self.reload_database()?;
if self.collection != self.pre_commit {
if let Err(err) = self.database.save(&self.pre_commit) {
self.pre_commit = self.collection.clone();
return Err(err.into());
}
self.collection = self.pre_commit.clone();
self.filtered = self.filter_collection();
}
Ok(())
}
}
@ -223,7 +390,7 @@ impl<Database: IDatabase, Library> MusicHoard<Database, Library> {
where
FnColl: FnOnce(&mut Collection),
{
fn_coll(&mut self.collection);
fn_coll(&mut self.pre_commit);
self.commit()
}
@ -237,7 +404,7 @@ impl<Database: IDatabase, Library> MusicHoard<Database, Library> {
FnArtist: FnOnce(&mut Artist),
FnColl: FnOnce(&mut Collection),
{
let artist = Self::get_artist_mut_or_err(&mut self.collection, artist_id)?;
let artist = Self::get_artist_mut_or_err(&mut self.pre_commit, artist_id)?;
fn_artist(artist);
self.update_collection(fn_coll)
}
@ -264,27 +431,13 @@ impl<Database: IDatabase, Library> MusicHoard<Database, Library> {
FnAlbum: FnOnce(&mut Album),
FnArtist: FnOnce(&mut Artist),
{
let artist = Self::get_artist_mut_or_err(&mut self.collection, artist_id)?;
let artist = Self::get_artist_mut_or_err(&mut self.pre_commit, artist_id)?;
let album = Self::get_album_mut_or_err(artist, album_id)?;
fn_album(album);
fn_artist(artist);
self.update_collection(|_| {})
}
fn 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>(
&mut self,
artist_id: &ArtistId,
@ -305,13 +458,12 @@ mod tests {
use crate::{
collection::{
album::{AlbumPrimaryType, AlbumSecondaryType},
artist::ArtistMbRef,
musicbrainz::MbArtistRef,
},
core::{
collection::artist::ArtistId,
interface::database::{self, MockIDatabase},
musichoard::base::IMusicHoardBase,
musichoard::{base::IMusicHoardBase, NoLibrary},
testmod::FULL_COLLECTION,
},
};
@ -319,13 +471,113 @@ mod tests {
use super::*;
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]
fn collection_error() {
let database = MockIDatabase::new();
let mut music_hoard = MusicHoard::database(database);
let artist_id = ArtistId(1);
let artist_id = ArtistId::new("an artist");
let actual_err = music_hoard
.merge_artist_info(&artist_id, ArtistInfo::default())
.unwrap_err();
@ -336,35 +588,77 @@ mod tests {
}
#[test]
fn set_clear_artist_info() {
fn set_clear_artist_mb_ref() {
let mut database = MockIDatabase::new();
database.expect_save().times(2).returning(|_| Ok(()));
database.expect_save().times(3).returning(|_| Ok(()));
let artist = Artist::new(1, "an artist");
let artist_2 = Artist::new(2, "another artist");
let mb_ref = ArtistMbRef::Some(MbArtistRef::from_uuid_str(MBID).unwrap());
let mut artist_id = ArtistId::new("an artist");
let artist_id_2 = ArtistId::new("another artist");
let mut music_hoard = MusicHoard::database(database);
music_hoard.collection.push(artist.clone());
music_hoard.collection.sort_unstable();
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]
fn set_clear_artist_info() {
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 = ArtistInfo::default();
assert_eq!(music_hoard.collection[0].meta.info, expected);
let mut info = ArtistInfo::default().with_mb_ref(mb_ref.clone());
let mut info = ArtistInfo::default();
info.add_to_property("property", vec!["value-1", "value-2"]);
// Setting info on an artist not in the collection is an error.
assert!(music_hoard
.merge_artist_info(&artist_2.id, info.clone())
.merge_artist_info(&artist_id_2, info.clone())
.is_err());
assert_eq!(music_hoard.collection[0].meta.info, expected);
// Setting info on an artist.
assert!(music_hoard
.merge_artist_info(&artist.id, info.clone())
.merge_artist_info(&artist_id, info.clone())
.is_ok());
expected.mb_ref = mb_ref.clone();
expected.properties.insert(
String::from("property"),
vec![String::from("value-1"), String::from("value-2")],
@ -372,16 +666,106 @@ mod tests {
assert_eq!(music_hoard.collection[0].meta.info, expected);
// Clearing info on an artist that does not exist is an error.
assert!(music_hoard.clear_artist_info(&artist_2.id).is_err());
assert!(music_hoard.clear_artist_info(&artist_id_2).is_err());
assert_eq!(music_hoard.collection[0].meta.info, expected);
// Clearing info.
assert!(music_hoard.clear_artist_info(&artist.id).is_ok());
expected.mb_ref.take();
assert!(music_hoard.clear_artist_info(&artist_id).is_ok());
expected.properties.clear();
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]
fn album_new_delete() {
let album_id = AlbumId::new("an album");
@ -390,7 +774,7 @@ mod tests {
let album_meta_2 = AlbumMeta::new(album_id_2);
let collection = FULL_COLLECTION.to_owned();
let artist_id = collection[0].id;
let artist_id = collection[0].meta.id.clone();
let mut with_album = collection.clone();
with_album[0].albums.push(Album::new(album_id));
with_album[0].albums.sort_unstable();
@ -405,7 +789,7 @@ mod tests {
.returning(|| Ok(FULL_COLLECTION.to_owned()));
database
.expect_save()
.times(3)
.times(1)
.in_sequence(&mut seq)
.with(predicate::eq(with_album.clone()))
.returning(|_| Ok(()));
@ -443,11 +827,11 @@ mod tests {
fn set_clear_album_mb_ref() {
let mut database = MockIDatabase::new();
let artist = Artist::new(1, "an artist");
let artist_id = ArtistId::new("an artist");
let mut album_id = AlbumId::new("an album");
let album_id_2 = AlbumId::new("another album");
let mut database_result = vec![artist.clone()];
let mut database_result = vec![Artist::new(artist_id.clone())];
database_result[0].albums.push(Album::new(album_id.clone()));
database
@ -463,27 +847,27 @@ mod tests {
// Seting mb_ref on an album not belonging to the artist is an error.
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());
let album = &music_hoard.collection[0].albums[0];
assert_eq!(album.meta.id.mb_ref, AlbumMbRef::None);
// Set mb_ref.
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());
let album = &music_hoard.collection[0].albums[0];
assert_eq!(album.meta.id.mb_ref, AlbumMbRef::CannotHaveMbid);
// Clearing mb_ref on an album that does not exist is an error.
assert!(music_hoard
.clear_album_mb_ref(&artist.id, &album_id_2)
.clear_album_mb_ref(&artist_id, &album_id_2)
.is_err());
// Clearing mb_ref from an album without the mb_ref set is an error. Effectively the album
// does not exist.
assert!(music_hoard
.clear_album_mb_ref(&artist.id, &album_id)
.clear_album_mb_ref(&artist_id, &album_id)
.is_err());
let album = &music_hoard.collection[0].albums[0];
assert_eq!(album.meta.id.mb_ref, AlbumMbRef::CannotHaveMbid);
@ -492,21 +876,62 @@ mod tests {
// album.
album_id.set_mb_ref(AlbumMbRef::CannotHaveMbid);
assert!(music_hoard
.clear_album_mb_ref(&artist.id, &album_id)
.clear_album_mb_ref(&artist_id, &album_id)
.is_ok());
let album = &music_hoard.collection[0].albums[0];
assert_eq!(album.meta.id.mb_ref, AlbumMbRef::None);
}
#[test]
fn set_clear_album_info() {
fn set_clear_album_seq() {
let mut database = MockIDatabase::new();
let artist = Artist::new(1, "an artist");
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.clone()];
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]
fn set_clear_album_info() {
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
@ -521,31 +946,32 @@ mod tests {
assert_eq!(meta.info.primary_type, None);
assert_eq!(meta.info.secondary_types, Vec::new());
let info = AlbumInfo::default()
.with_primary_type(AlbumPrimaryType::Album)
.with_secondary_types(vec![AlbumSecondaryType::Live]);
let info = AlbumInfo::new(
Some(AlbumPrimaryType::Album),
vec![AlbumSecondaryType::Live],
);
// Seting info on an album not belonging to the artist is an error.
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());
let meta = &music_hoard.collection[0].albums[0].meta;
assert_eq!(meta.info, AlbumInfo::default());
// Set info.
assert!(music_hoard
.merge_album_info(&artist.id, &album_id, info.clone())
.merge_album_info(&artist_id, &album_id, info.clone())
.is_ok());
let meta = &music_hoard.collection[0].albums[0].meta;
assert_eq!(meta.info, info);
// Clearing info on an album that does not exist is an error.
assert!(music_hoard
.clear_album_info(&artist.id, &album_id_2)
.clear_album_info(&artist_id, &album_id_2)
.is_err());
// 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;
assert_eq!(meta.info, AlbumInfo::default());
}
@ -592,30 +1018,17 @@ mod tests {
let database_result = Err(database::SaveError::IoError(String::from("I/O error")));
let mut seq = Sequence::new();
database
.expect_load()
.times(1)
.in_sequence(&mut seq)
.return_once(|| Ok(vec![]));
database.expect_load().return_once(|| Ok(vec![]));
database
.expect_save()
.times(1)
.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);
music_hoard.reload_database().unwrap();
let artist = Artist::new(1, "an artist");
music_hoard.collection.push(artist.clone());
let actual_err = music_hoard
.add_album(artist.id, AlbumMeta::new("an album"))
.add_artist(ArtistId::new("an artist"))
.unwrap_err();
let expected_err = Error::DatabaseError(
database::SaveError::IoError(String::from("I/O error")).to_string(),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -225,10 +225,7 @@ mod tests {
};
use crate::tui::{
app::{
AppState, ArtistMatching, EntityMatches, IApp, IAppInput, IAppInteractBrowse,
InputEvent,
},
app::{AppState, EntityMatches, IApp, IAppInput, IAppInteractBrowse, InputEvent},
lib::{
interface::musicbrainz::{api::Entity, daemon::MockIMbJobSender},
MockIMusicHoard,
@ -522,13 +519,8 @@ mod tests {
let (_, rx) = mpsc::channel();
let fetch = FetchState::search(rx);
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));
let artist = ArtistMeta::new(ArtistId::new("Artist"));
let info = EntityMatches::artist_lookup(artist.clone(), Entity::new(artist.clone()));
app =
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 {
let name = string::normalize_string_with(&probe.meta.name, !case_sens, !char_sens);
let name = string::normalize_string_with(&probe.meta.id.name, !case_sens, !char_sens);
let mut result = name.string.starts_with(search);
if let Some(ref probe_sort) = probe.meta.sort {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@ use musichoard::collection::{
AlbumDate, AlbumId, AlbumLibId, AlbumMeta, AlbumOwnership, AlbumPrimaryType,
AlbumSecondaryType, AlbumSeq,
},
artist::{ArtistMeta, ArtistName},
artist::ArtistMeta,
musicbrainz::{IMusicBrainzRef, MbRefOption},
track::{TrackFormat, TrackQuality},
};
@ -118,8 +118,8 @@ impl UiDisplay {
}
}
pub fn display_artist_matching(name: &ArtistName) -> String {
format!("Matching artist: {}", name)
pub fn display_artist_matching(artist: &ArtistMeta) -> String {
format!("Matching artist: {}", &artist.id.name)
}
pub fn display_album_matching(album: &AlbumId) -> String {
@ -128,7 +128,7 @@ impl UiDisplay {
pub fn display_matching_info(info: &EntityMatches) -> String {
match info {
EntityMatches::Artist(m) => UiDisplay::display_artist_matching(&m.matching.name),
EntityMatches::Artist(m) => UiDisplay::display_artist_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 {
format!(
"{}{}",
artist.name,
artist.id.name,
disambiguation
.as_ref()
.filter(|s| !s.is_empty())
@ -171,7 +171,7 @@ impl UiDisplay {
fn display_option_album(album: &AlbumMeta, _disambiguation: &Option<String>) -> String {
format!(
"{:010} | {} [{}]",
UiDisplay::display_album_date(&album.info.date),
UiDisplay::display_album_date(&album.date),
album.id.title,
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}\
MusicBrainz: {}\n{item_indent}\
Properties: {}",
artist.map(|a| a.meta.name.as_str()).unwrap_or(""),
artist.map(|a| a.meta.id.name.as_str()).unwrap_or(""),
artist
.map(|a| UiDisplay::display_mb_ref_option_as_url(&a.meta.info.mb_ref))
.map(|a| UiDisplay::display_mb_ref_option_as_url(&a.meta.id.mb_ref))
.unwrap_or_default(),
Self::opt_hashmap_to_string(
artist.map(|a| &a.meta.info.properties),

View File

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

View File

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

View File

@ -1,3 +1,7 @@
use std::{fs, path::PathBuf};
use once_cell::sync::Lazy;
use musichoard::{
collection::{artist::Artist, Collection},
external::database::sql::{backend::SqlDatabaseSqliteBackend, SqlDatabase},
@ -5,7 +9,10 @@ use musichoard::{
};
use tempfile::NamedTempFile;
use crate::{copy_file_into_temp, testlib::COLLECTION, COMPLETE_DB_TEST_FILE};
use crate::testlib::COLLECTION;
pub static DATABASE_TEST_FILE: Lazy<PathBuf> =
Lazy::new(|| fs::canonicalize("./tests/files/database/database.db").unwrap());
fn expected() -> Collection {
let mut expected = COLLECTION.to_owned();
@ -17,16 +24,6 @@ fn expected() -> Collection {
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]
fn save() {
let file = NamedTempFile::new().unwrap();
@ -40,7 +37,7 @@ fn save() {
#[test]
fn load() {
let backend = SqlDatabaseSqliteBackend::new(&*COMPLETE_DB_TEST_FILE).unwrap();
let backend = SqlDatabaseSqliteBackend::new(&*DATABASE_TEST_FILE).unwrap();
let mut database = SqlDatabase::new(backend).unwrap();
let read_data: Vec<Artist> = database.load().unwrap();
@ -64,18 +61,3 @@ fn reverse() {
let expected = 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;
use std::{ffi::OsStr, fs, path::PathBuf, process::Command};
use std::{fs, path::PathBuf};
use musichoard::{
external::{
@ -15,33 +15,16 @@ use musichoard::{
},
IMusicHoardBase, IMusicHoardDatabase, IMusicHoardLibrary, MusicHoard,
};
use once_cell::sync::Lazy;
use tempfile::NamedTempFile;
use crate::testlib::COLLECTION;
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 {
fn copy_file_into_temp<P: Into<PathBuf>>(path: P) -> NamedTempFile {
let temp = NamedTempFile::new().unwrap();
fs::copy(path.into(), temp.path()).unwrap();
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]
fn merge_library_then_database() {
// Acquired the lock on the beets config file. We need to own the underlying object so later we
@ -54,7 +37,7 @@ fn merge_library_then_database() {
.config(Some(&*library::beets::BEETS_TEST_CONFIG_PATH));
let library = BeetsLibrary::new(executor);
let file = copy_file_into_temp(&*PARTIAL_DB_TEST_FILE);
let file = copy_file_into_temp(&*database::sql::DATABASE_TEST_FILE);
let backend = SqlDatabaseSqliteBackend::new(file.path()).unwrap();
let database = SqlDatabase::new(backend).unwrap();
@ -64,8 +47,6 @@ fn merge_library_then_database() {
music_hoard.reload_database().unwrap();
assert_eq!(music_hoard.get_collection(), &*COLLECTION);
sqldiff(COMPLETE_DB_TEST_FILE.as_os_str(), file.path().as_os_str());
}
#[test]
@ -80,7 +61,7 @@ fn merge_database_then_library() {
.config(Some(&*library::beets::BEETS_TEST_CONFIG_PATH));
let library = BeetsLibrary::new(executor);
let file = copy_file_into_temp(&*PARTIAL_DB_TEST_FILE);
let file = copy_file_into_temp(&*database::sql::DATABASE_TEST_FILE);
let backend = SqlDatabaseSqliteBackend::new(file.path()).unwrap();
let database = SqlDatabase::new(backend).unwrap();
@ -90,6 +71,4 @@ fn merge_database_then_library() {
music_hoard.rescan_library().unwrap();
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::{
album::{
Album, AlbumId, AlbumInfo, AlbumLibId, AlbumMbRef, AlbumMeta, AlbumPrimaryType,
AlbumSecondaryType,
AlbumSecondaryType, AlbumSeq,
},
artist::{Artist, ArtistId, ArtistInfo, ArtistMbRef, ArtistMeta},
musicbrainz::MbArtistRef,
@ -15,14 +15,15 @@ use musichoard::collection::{
pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
vec![
Artist {
id: ArtistId(1),
meta: ArtistMeta {
id: ArtistId {
name: String::from("Аркона"),
sort: Some(String::from("Arkona")),
info: ArtistInfo {
mb_ref: ArtistMbRef::Some(MbArtistRef::from_url_str(
"https://musicbrainz.org/artist/baad262d-55ef-427a-83c7-f7530964f212"
).unwrap()),
},
sort: Some(String::from("Arkona")),
info: ArtistInfo {
properties: HashMap::from([
(String::from("MusicButler"), vec![
String::from("https://www.musicbutler.io/artist-page/283448581"),
@ -43,9 +44,12 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
lib_id: AlbumLibId::Value(7),
mb_ref: AlbumMbRef::None,
},
info: AlbumInfo::default()
.with_date(2011)
.with_primary_type(AlbumPrimaryType::Album),
date: 2011.into(),
seq: AlbumSeq(0),
info: AlbumInfo {
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
},
},
tracks: vec![
Track {
@ -206,14 +210,15 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
}],
},
Artist {
id: ArtistId(2),
meta: ArtistMeta {
id: ArtistId {
name: String::from("Eluveitie"),
sort: None,
info: ArtistInfo {
mb_ref: ArtistMbRef::Some(MbArtistRef::from_url_str(
"https://musicbrainz.org/artist/8000598a-5edb-401c-8e6d-36b167feaf38"
).unwrap()),
},
sort: None,
info: ArtistInfo {
properties: HashMap::from([
(String::from("MusicButler"), vec![
String::from("https://www.musicbutler.io/artist-page/269358403"),
@ -232,9 +237,12 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
lib_id: AlbumLibId::Value(1),
mb_ref: AlbumMbRef::None,
},
info: AlbumInfo::default()
.with_date(2004)
.with_primary_type(AlbumPrimaryType::Ep),
date: 2004.into(),
seq: AlbumSeq(0),
info: AlbumInfo {
primary_type: Some(AlbumPrimaryType::Ep),
secondary_types: vec![],
},
},
tracks: vec![
Track {
@ -312,9 +320,12 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
lib_id: AlbumLibId::Value(2),
mb_ref: AlbumMbRef::None,
},
info: AlbumInfo::default()
.with_date(2008)
.with_primary_type(AlbumPrimaryType::Album),
date: 2008.into(),
seq: AlbumSeq(0),
info: AlbumInfo {
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
},
},
tracks: vec![
Track {
@ -454,14 +465,15 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
],
},
Artist {
id: ArtistId(3),
meta: ArtistMeta {
id: ArtistId {
name: String::from("Frontside"),
sort: None,
info: ArtistInfo {
mb_ref: ArtistMbRef::Some(MbArtistRef::from_url_str(
"https://musicbrainz.org/artist/3a901353-fccd-4afd-ad01-9c03f451b490"
).unwrap()),
},
sort: None,
info: ArtistInfo {
properties: HashMap::from([
(String::from("MusicButler"), vec![
String::from("https://www.musicbutler.io/artist-page/826588800"),
@ -479,9 +491,12 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
lib_id: AlbumLibId::Value(3),
mb_ref: AlbumMbRef::None,
},
info: AlbumInfo::default()
.with_date(2001)
.with_primary_type(AlbumPrimaryType::Album),
date: 2001.into(),
seq: AlbumSeq(0),
info: AlbumInfo {
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
},
},
tracks: vec![
Track {
@ -609,14 +624,15 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
}],
},
Artist {
id: ArtistId(4),
meta: ArtistMeta {
id: ArtistId {
name: String::from("Heavens Basement"),
sort: Some(String::from("Heavens Basement")),
info: ArtistInfo {
mb_ref: ArtistMbRef::Some(MbArtistRef::from_url_str(
"https://musicbrainz.org/artist/c2c4d56a-d599-4a18-bd2f-ae644e2198cc"
).unwrap()),
},
sort: Some(String::from("Heavens Basement")),
info: ArtistInfo {
properties: HashMap::from([
(String::from("MusicButler"), vec![
String::from("https://www.musicbutler.io/artist-page/291158685"),
@ -634,7 +650,9 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
lib_id: AlbumLibId::Singleton,
mb_ref: AlbumMbRef::None,
},
info: AlbumInfo::default().with_date(2011),
date: 2011.into(),
seq: AlbumSeq(0),
info: AlbumInfo::default(),
},
tracks: vec![
Track {
@ -656,9 +674,12 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
lib_id: AlbumLibId::Value(4),
mb_ref: AlbumMbRef::None,
},
info: AlbumInfo::default()
.with_date(2011)
.with_primary_type(AlbumPrimaryType::Album),
date: 2011.into(),
seq: AlbumSeq(0),
info: AlbumInfo {
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
},
},
tracks: vec![
Track {
@ -742,14 +763,15 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
}],
},
Artist {
id: ArtistId(5),
meta: ArtistMeta {
id: ArtistId {
name: String::from("Metallica"),
sort: None,
info: ArtistInfo {
mb_ref: ArtistMbRef::Some(MbArtistRef::from_url_str(
"https://musicbrainz.org/artist/65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab"
).unwrap()),
},
sort: None,
info: ArtistInfo {
properties: HashMap::from([
(String::from("MusicButler"), vec![
String::from("https://www.musicbutler.io/artist-page/3996865"),
@ -768,9 +790,12 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
lib_id: AlbumLibId::Value(5),
mb_ref: AlbumMbRef::None,
},
info: AlbumInfo::default()
.with_date(1984)
.with_primary_type(AlbumPrimaryType::Album),
date: 1984.into(),
seq: AlbumSeq(0),
info: AlbumInfo {
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
},
},
tracks: vec![
Track {
@ -870,10 +895,12 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
lib_id: AlbumLibId::Value(6),
mb_ref: AlbumMbRef::None,
},
info: AlbumInfo::default()
.with_date(1999)
.with_primary_type(AlbumPrimaryType::Album)
.with_secondary_types(vec![AlbumSecondaryType::Live]),
date: 1999.into(),
seq: AlbumSeq(0),
info: AlbumInfo {
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![AlbumSecondaryType::Live],
},
},
tracks: vec![
Track {