Compare commits

..

12 Commits

Author SHA1 Message Date
38a2e1f33c Merge figured out
Some checks failed
Cargo CI / Build and Test (pull_request) Failing after 1m20s
Cargo CI / Lint (pull_request) Failing after 37s
2025-01-19 20:43:16 +01:00
3b0fa28dfc WIP: use unique artist id 2025-01-19 18:46:14 +01:00
3901c161bd Move artist mbref back to info 2025-01-18 11:30:41 +01:00
fdd6d13146 Correct album id-info split
Some checks failed
Cargo CI / Build and Test (pull_request) Failing after 1m43s
Cargo CI / Lint (pull_request) Successful in 1m17s
2025-01-18 10:14:18 +01:00
7934d7eccf Correct artist id-info split 2025-01-18 09:46:28 +01:00
8a9030bf61 Tests 2025-01-15 21:42:28 +01:00
aea710eb32 Update tables without dropping
Some checks failed
Cargo CI / Build and Test (pull_request) Successful in 2m53s
Cargo CI / Lint (pull_request) Failing after 1m16s
2025-01-14 21:49:05 +01:00
64f3e2d8b7 Convenience method 2025-01-13 19:22:38 +01:00
6a80c05909 Correctly sort
All checks were successful
Cargo CI / Build and Test (pull_request) Successful in 2m18s
Cargo CI / Lint (pull_request) Successful in 1m17s
2025-01-12 22:56:26 +01:00
c2973680e8 Remove unused methods
All checks were successful
Cargo CI / Build and Test (pull_request) Successful in 2m19s
Cargo CI / Lint (pull_request) Successful in 1m18s
2025-01-12 22:40:59 +01:00
a8cd1d341d Remove pre_commit
All checks were successful
Cargo CI / Build and Test (pull_request) Successful in 2m21s
Cargo CI / Lint (pull_request) Successful in 1m17s
2025-01-12 22:04:47 +01:00
16dc2917f0 Update default database path
All checks were successful
Cargo CI / Build and Test (pull_request) Successful in 2m26s
Cargo CI / Lint (pull_request) Successful in 1m19s
2025-01-12 21:38:30 +01:00
43 changed files with 1258 additions and 1462 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
- run: cargo test --no-default-features --all-targets --no-fail-fast -- --include-ignored
- run: cargo build --all-targets
- run: cargo test --all-targets --no-fail-fast
- run: cargo test --all-targets --no-fail-fast -- --include-ignored
- run: cargo build --all-features --all-targets
- run: cargo test --all-features --all-targets --no-fail-fast
- run: cargo test --all-features --all-targets --no-fail-fast -- --include-ignored
- 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,6 +17,13 @@ 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::{Merge, MergeSorted},
merge::{IntoId, Merge, MergeSorted, WithId},
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,6 +93,12 @@ 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 {
@ -181,7 +187,7 @@ impl Album {
}
pub fn with_date<Date: Into<AlbumDate>>(mut self, date: Date) -> Self {
self.meta.date = date.into();
self.meta.info.date = date.into();
self
}
@ -202,6 +208,23 @@ impl Ord for Album {
}
}
impl WithId for Album {
type Id = AlbumId;
fn id(&self) -> &Self::Id {
&self.meta.id
}
}
impl IntoId for Album {
type Id = AlbumId;
type IdSelf = Album;
fn into_id(self, _: &Self::Id) -> Self::IdSelf {
self
}
}
impl Merge for Album {
fn merge_in_place(&mut self, other: Self) {
self.meta.merge_in_place(other.meta);
@ -214,14 +237,12 @@ 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.date = date.into();
self.info.date = date.into();
self
}
@ -232,8 +253,8 @@ impl AlbumMeta {
pub fn get_sort_key(&self) -> (&AlbumDate, &AlbumSeq, &str, &Option<AlbumPrimaryType>) {
(
&self.date,
&self.seq,
&self.info.date,
&self.info.seq,
&self.id.title,
&self.info.primary_type,
)
@ -247,6 +268,36 @@ 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;
}
@ -256,18 +307,6 @@ impl AlbumMeta {
}
}
impl AlbumInfo {
pub fn new(
primary_type: Option<AlbumPrimaryType>,
secondary_types: Vec<AlbumSecondaryType>,
) -> Self {
AlbumInfo {
primary_type,
secondary_types,
}
}
}
impl PartialOrd for AlbumMeta {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
@ -284,16 +323,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;
@ -385,21 +424,21 @@ mod tests {
fn set_clear_seq() {
let mut album = Album::new("An album");
assert_eq!(album.meta.seq, AlbumSeq(0));
assert_eq!(album.meta.info.seq, AlbumSeq(0));
// Setting a seq on an album.
album.meta.set_seq(AlbumSeq(6));
assert_eq!(album.meta.seq, AlbumSeq(6));
assert_eq!(album.meta.info.seq, AlbumSeq(6));
album.meta.set_seq(AlbumSeq(6));
assert_eq!(album.meta.seq, AlbumSeq(6));
assert_eq!(album.meta.info.seq, AlbumSeq(6));
album.meta.set_seq(AlbumSeq(8));
assert_eq!(album.meta.seq, AlbumSeq(8));
assert_eq!(album.meta.info.seq, AlbumSeq(8));
// Clearing seq.
album.meta.clear_seq();
assert_eq!(album.meta.seq, AlbumSeq(0));
assert_eq!(album.meta.info.seq, AlbumSeq(0));
}
#[test]
@ -439,7 +478,7 @@ mod tests {
#[test]
fn merge_album_dates() {
let meta = AlbumMeta::new(AlbumId::new("An album"));
let meta = AlbumInfo::default();
// No merge if years are different.
let left = meta.clone().with_date((2000, 1, 6));

View File

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

View File

@ -105,10 +105,26 @@ impl<T> NormalMap<T> {
}
}
pub trait WithId {
type Id;
fn id(&self) -> &Self::Id;
}
pub trait IntoId {
type Id;
type IdSelf;
fn into_id(self, id: &Self::Id) -> Self::IdSelf;
}
pub struct MergeCollections;
impl MergeCollections {
pub fn merge_by_name<T: Merge>(mut primary: NormalMap<T>, secondary: NormalMap<T>) -> Vec<T> {
pub fn merge_by_name<Id, T1: Merge + WithId<Id = Id>, T2: IntoId<Id = Id, IdSelf = T1>>(
mut primary: NormalMap<T2>,
secondary: NormalMap<T1>,
) -> (Vec<T1>, Vec<T2>) {
let mut merged = vec![];
for (title, mut secondary_items) in secondary.0.into_iter() {
match primary.remove(&title) {
@ -117,14 +133,17 @@ 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 mut primary_item = primary_items.pop().unwrap();
primary_item.merge_in_place(secondary_items.pop().unwrap());
let secondary_item = secondary_items.pop().unwrap();
let id = secondary_item.id();
let mut primary_item = primary_items.pop().unwrap().into_id(id);
primary_item.merge_in_place(secondary_item);
merged.push(primary_item);
}
None => merged.extend(secondary_items),
}
}
merged.extend(primary.0.into_values().flatten());
merged
(merged, primary.0.into_values().flatten().collect())
}
}

View File

@ -45,6 +45,12 @@ 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,22 +5,32 @@ use std::fmt;
#[cfg(test)]
use mockall::automock;
use crate::core::collection::{self, Collection};
use crate::{collection::artist::{ArtistId, ArtistMeta}, core::collection::{self, Collection}};
/// Trait for interacting with the database.
#[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![])
}
@ -28,6 +38,10 @@ 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.
@ -98,6 +112,12 @@ 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,8 +2,7 @@ use crate::core::{
collection::{
album::{Album, AlbumId},
artist::{Artist, ArtistId},
merge::{MergeCollections, NormalMap},
string, Collection,
Collection,
},
musichoard::{filter::CollectionFilter, Error, MusicHoard},
};
@ -30,14 +29,11 @@ impl<Database, Library> IMusicHoardBase for MusicHoard<Database, Library> {
}
pub trait IMusicHoardBasePrivate {
fn sort_artists(collection: &mut [Artist]);
fn sort_albums_and_tracks<'a, C: Iterator<Item = &'a mut Artist>>(collection: C);
fn sort_albums_and_tracks<'a, COL: Iterator<Item = &'a mut Vec<Album>>>(collection: COL);
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,
@ -56,37 +52,15 @@ pub trait IMusicHoardBasePrivate {
}
impl<Database, Library> IMusicHoardBasePrivate for MusicHoard<Database, Library> {
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() {
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() {
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()
@ -101,19 +75,18 @@ impl<Database, Library> IMusicHoardBasePrivate for MusicHoard<Database, Library>
return None;
}
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)
Some(Artist {
id: artist.id,
meta: artist.meta.clone(),
albums,
})
}
fn get_artist_mut<'a>(
collection: &'a mut Collection,
artist_id: &ArtistId,
) -> Option<&'a mut Artist> {
collection.iter_mut().find(|a| &a.meta.id == artist_id)
collection.iter_mut().find(|a| &a.id == artist_id)
}
fn get_artist_mut_or_err<'a>(
@ -148,180 +121,224 @@ impl<Database, Library> IMusicHoardBasePrivate for MusicHoard<Database, Library>
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use crate::{
collection::{album::AlbumPrimaryType, artist::ArtistMeta},
core::testmod::FULL_COLLECTION,
collection::{
album::AlbumPrimaryType,
artist::{ArtistMeta, ArtistName},
},
core::{musichoard::LibArtist, 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.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.info.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.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.info.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 = Collection::new();
let mut right = Collection::new();
// let mut left = HashMap::<String, LibArtist>::new();
// let mut right = Collection::new();
let artist = Artist::new(ArtistId::new("Artist"));
// let name = ArtistName::new("Artist");
left.push(artist.clone());
right.push(artist.clone());
right.push(artist.clone());
// 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()));
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.push(artist.clone());
left.push(artist.clone());
right.push(artist.clone());
// 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());
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 = Collection::new();
let mut right = Collection::new();
// let mut left = HashMap::<String, LibArtist>::new();
// let mut right = Collection::new();
left.push(Artist::new(ArtistId::new("ArtistName Name")));
// let left_name = "ArtistName Name";
// left.insert(
// String::from(left_name),
// LibArtist {
// meta: ArtistMeta::new(left_name.into()),
// albums: vec![],
// },
// );
right.push(Artist::new(ArtistId::new("arTist—naMe 'name")));
right.push(Artist::new(ArtistId::new("ArtistName “Name”")));
// right.push(Artist::new(1, "arTist—naMe 'name"));
// right.push(Artist::new(2, "ArtistName “Name”"));
// 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();
// // 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();
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 {
meta: ArtistMeta::new(ArtistId::new("Artist")),
id: 0.into(),
meta: ArtistMeta::new("Artist"),
albums: vec![
Album::new(AlbumId::new("Album 1")),
Album::new(AlbumId::new("Album 2")),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -34,6 +34,7 @@ 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>,
@ -117,13 +118,12 @@ 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,7 +4,12 @@ use serde::Serialize;
use crate::{
collection::musicbrainz::{MbRefOption, Mbid},
core::collection::{album::Album, artist::Artist, musicbrainz::IMusicBrainzRef, Collection},
core::collection::{
album::Album,
artist::{Artist, ArtistMeta},
musicbrainz::IMusicBrainzRef,
Collection,
},
external::database::serde::common::{
MbRefOptionDef, SerdeAlbumDate, SerdeAlbumLibId, SerdeAlbumPrimaryType,
SerdeAlbumSecondaryType,
@ -73,12 +78,20 @@ 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: &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(),
name: &meta.name,
mb_ref: (&meta.info.mb_ref).into(),
sort: &meta.sort,
properties: &meta.info.properties,
albums: vec![],
}
}
}
@ -89,8 +102,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.date.into(),
seq: album.meta.seq.0,
date: album.meta.info.date.into(),
seq: album.meta.info.seq.0,
primary_type: album.meta.info.primary_type.map(Into::into),
secondary_types: album
.meta

View File

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

View File

@ -9,7 +9,10 @@ use mockall::automock;
use crate::{
core::{
collection::Collection,
collection::{
artist::{ArtistId, ArtistMeta},
Collection,
},
interface::database::{IDatabase, LoadError, SaveError},
},
external::database::serde::{
@ -60,7 +63,11 @@ 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<(), Error>;
fn insert_artist<'a>(&self, artist: &SerializeArtist<'a>) -> Result<i64, Error>;
/// Update an artist in the artist table.
#[allow(clippy::needless_lifetimes)] // Conflicts with automock.
fn update_artist<'a>(&self, oid: i64, artist: &SerializeArtist<'a>) -> Result<(), Error>;
/// Get all artists from the artist table.
fn select_all_artists(&self) -> Result<Vec<DeserializeArtist>, Error>;
@ -152,6 +159,15 @@ impl<SDB: for<'conn> ISqlDatabaseBackend<'conn>> SqlDatabase<SDB> {
}
impl<SDB: for<'conn> ISqlDatabaseBackend<'conn>> IDatabase for SqlDatabase<SDB> {
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()?;
@ -178,7 +194,6 @@ 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 {
@ -196,6 +211,16 @@ 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)]
@ -203,7 +228,7 @@ pub mod testmod;
#[cfg(test)]
mod tests {
use std::collections::VecDeque;
use std::{collections::VecDeque, ops::AddAssign};
use mockall::{predicate, Sequence};
@ -300,21 +325,33 @@ 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();
then1!(tx, seq, expect_insert_artist)
rowid.add_assign(1);
then!(tx, seq, expect_insert_artist)
.return_once(move |_| Ok(rowid))
.withf(move |a| a == &Into::<SerializeArtist>::into(&ac));
for album in artist.albums.iter() {
let (nc, ac) = (artist.meta.id.name.clone(), album.clone());
let (nc, ac) = (artist.meta.name.clone(), album.clone());
then2!(tx, seq, expect_insert_album)
.withf(move |n, a| n == nc && a == &Into::<SerializeAlbum>::into(&ac));
}
@ -337,9 +374,9 @@ mod tests {
then!(tx, seq, expect_select_all_artists).return_once(|| Ok(de_artists));
for artist in artists.iter() {
let de_albums = DATABASE_SQL_ALBUMS.get(&artist.meta.id.name).unwrap();
let de_albums = DATABASE_SQL_ALBUMS.get(&artist.meta.name).unwrap();
then!(tx, seq, expect_select_artist_albums)
.with(predicate::eq(artist.meta.id.name.clone()))
.with(predicate::eq(artist.meta.name.clone()))
.return_once(|_| Ok(de_albums.to_owned()));
}
@ -396,7 +433,6 @@ 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))
@ -426,7 +462,6 @@ 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,6 +20,7 @@ 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(),
@ -42,6 +43,7 @@ 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(),
@ -64,6 +66,7 @@ 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")),
@ -71,6 +74,7 @@ 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.json"
default_value = "database.db"
)]
database_file_path: PathBuf,

View File

@ -2,15 +2,14 @@ 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"),
@ -33,12 +32,10 @@ macro_rules! full_collection {
"https://musicbrainz.org/release-group/00000000-0000-0000-0000-000000000000"
).unwrap()),
},
date: 1998.into(),
seq: AlbumSeq(1),
info: AlbumInfo {
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
},
info: AlbumInfo::default()
.with_date(1998)
.with_seq(1)
.with_primary_type(AlbumPrimaryType::Album),
},
tracks: vec![
Track {
@ -97,12 +94,10 @@ macro_rules! full_collection {
lib_id: AlbumLibId::Value(2),
mb_ref: AlbumMbRef::None,
},
date: (2015, 4).into(),
seq: AlbumSeq(1),
info: AlbumInfo {
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
},
info: AlbumInfo::default()
.with_date((2015, 4))
.with_seq(1)
.with_primary_type(AlbumPrimaryType::Album),
},
tracks: vec![
Track {
@ -132,15 +127,14 @@ 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"),
@ -165,12 +159,10 @@ macro_rules! full_collection {
lib_id: AlbumLibId::Value(3),
mb_ref: AlbumMbRef::None,
},
date: (2003, 6, 6).into(),
seq: AlbumSeq(1),
info: AlbumInfo {
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
},
info: AlbumInfo::default()
.with_date((2003, 6, 6))
.with_seq(1)
.with_primary_type(AlbumPrimaryType::Album),
},
tracks: vec![
Track {
@ -209,12 +201,10 @@ macro_rules! full_collection {
"https://musicbrainz.org/release-group/11111111-1111-1111-1111-111111111111"
).unwrap()),
},
date: 2008.into(),
seq: AlbumSeq(3),
info: AlbumInfo {
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
},
info: AlbumInfo::default()
.with_date(2008)
.with_seq(3)
.with_primary_type(AlbumPrimaryType::Album),
},
tracks: vec![
Track {
@ -253,12 +243,10 @@ macro_rules! full_collection {
"https://musicbrainz.org/release-group/11111111-1111-1111-1111-111111111112"
).unwrap()),
},
date: 2009.into(),
seq: AlbumSeq(2),
info: AlbumInfo {
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
},
info: AlbumInfo::default()
.with_date(2009)
.with_seq(2)
.with_primary_type(AlbumPrimaryType::Album),
},
tracks: vec![
Track {
@ -295,12 +283,10 @@ macro_rules! full_collection {
lib_id: AlbumLibId::Value(6),
mb_ref: AlbumMbRef::None,
},
date: 2015.into(),
seq: AlbumSeq(4),
info: AlbumInfo {
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
},
info: AlbumInfo::default()
.with_date(2015)
.with_seq(4)
.with_primary_type(AlbumPrimaryType::Album),
},
tracks: vec![
Track {
@ -333,13 +319,12 @@ 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(),
},
},
@ -351,12 +336,9 @@ macro_rules! full_collection {
lib_id: AlbumLibId::Value(7),
mb_ref: AlbumMbRef::None,
},
date: 1985.into(),
seq: AlbumSeq(0),
info: AlbumInfo {
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
},
info: AlbumInfo::default()
.with_date(1985)
.with_primary_type(AlbumPrimaryType::Album),
},
tracks: vec![
Track {
@ -393,12 +375,9 @@ macro_rules! full_collection {
lib_id: AlbumLibId::Value(8),
mb_ref: AlbumMbRef::None,
},
date: 2018.into(),
seq: AlbumSeq(0),
info: AlbumInfo {
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
},
info: AlbumInfo::default()
.with_date(2018)
.with_primary_type(AlbumPrimaryType::Album),
},
tracks: vec![
Track {
@ -431,13 +410,12 @@ 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(),
},
},
@ -449,12 +427,9 @@ macro_rules! full_collection {
lib_id: AlbumLibId::Value(9),
mb_ref: AlbumMbRef::None,
},
date: 1995.into(),
seq: AlbumSeq(0),
info: AlbumInfo {
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
},
info: AlbumInfo::default()
.with_date(1995)
.with_primary_type(AlbumPrimaryType::Album),
},
tracks: vec![
Track {
@ -491,12 +466,9 @@ macro_rules! full_collection {
lib_id: AlbumLibId::Value(10),
mb_ref: AlbumMbRef::None,
},
date: 2028.into(),
seq: AlbumSeq(0),
info: AlbumInfo {
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
},
info: AlbumInfo::default()
.with_date(2028)
.with_primary_type(AlbumPrimaryType::Album),
},
tracks: vec![
Track {

View File

@ -3,13 +3,12 @@ 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(),
},
},
@ -21,9 +20,7 @@ macro_rules! library_collection {
lib_id: AlbumLibId::Value(1),
mb_ref: AlbumMbRef::None,
},
date: 1998.into(),
seq: AlbumSeq(0),
info: AlbumInfo::default(),
info: AlbumInfo::default().with_date(1998),
},
tracks: vec![
Track {
@ -82,9 +79,7 @@ macro_rules! library_collection {
lib_id: AlbumLibId::Value(2),
mb_ref: AlbumMbRef::None,
},
date: (2015, 4).into(),
seq: AlbumSeq(0),
info: AlbumInfo::default(),
info: AlbumInfo::default().with_date((2015, 4)),
},
tracks: vec![
Track {
@ -114,13 +109,12 @@ 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(),
},
},
@ -132,9 +126,7 @@ macro_rules! library_collection {
lib_id: AlbumLibId::Value(3),
mb_ref: AlbumMbRef::None,
},
date: (2003, 6, 6).into(),
seq: AlbumSeq(0),
info: AlbumInfo::default(),
info: AlbumInfo::default().with_date((2003, 6, 6)),
},
tracks: vec![
Track {
@ -171,9 +163,7 @@ macro_rules! library_collection {
lib_id: AlbumLibId::Value(4),
mb_ref: AlbumMbRef::None,
},
date: 2008.into(),
seq: AlbumSeq(0),
info: AlbumInfo::default(),
info: AlbumInfo::default().with_date(2008),
},
tracks: vec![
Track {
@ -210,9 +200,7 @@ macro_rules! library_collection {
lib_id: AlbumLibId::Value(5),
mb_ref: AlbumMbRef::None,
},
date: 2009.into(),
seq: AlbumSeq(0),
info: AlbumInfo::default(),
info: AlbumInfo::default().with_date(2009),
},
tracks: vec![
Track {
@ -249,9 +237,7 @@ macro_rules! library_collection {
lib_id: AlbumLibId::Value(6),
mb_ref: AlbumMbRef::None,
},
date: 2015.into(),
seq: AlbumSeq(0),
info: AlbumInfo::default(),
info: AlbumInfo::default().with_date(2015),
},
tracks: vec![
Track {
@ -284,13 +270,12 @@ 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(),
},
},
@ -302,9 +287,7 @@ macro_rules! library_collection {
lib_id: AlbumLibId::Value(7),
mb_ref: AlbumMbRef::None,
},
date: 1985.into(),
seq: AlbumSeq(0),
info: AlbumInfo::default(),
info: AlbumInfo::default().with_date(1985),
},
tracks: vec![
Track {
@ -341,9 +324,7 @@ macro_rules! library_collection {
lib_id: AlbumLibId::Value(8),
mb_ref: AlbumMbRef::None,
},
date: 2018.into(),
seq: AlbumSeq(0),
info: AlbumInfo::default(),
info: AlbumInfo::default().with_date(2018),
},
tracks: vec![
Track {
@ -376,13 +357,12 @@ 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(),
},
},
@ -394,9 +374,7 @@ macro_rules! library_collection {
lib_id: AlbumLibId::Value(9),
mb_ref: AlbumMbRef::None,
},
date: 1995.into(),
seq: AlbumSeq(0),
info: AlbumInfo::default(),
info: AlbumInfo::default().with_date(1995),
},
tracks: vec![
Track {
@ -433,9 +411,7 @@ macro_rules! library_collection {
lib_id: AlbumLibId::Value(10),
mb_ref: AlbumMbRef::None,
},
date: 2028.into(),
seq: AlbumSeq(0),
info: AlbumInfo::default(),
info: AlbumInfo::default().with_date(2028),
},
tracks: vec![
Track {

View File

@ -6,7 +6,7 @@ use std::{
use musichoard::collection::{
album::{Album, AlbumId, AlbumMeta},
artist::{Artist, ArtistId, ArtistMeta},
artist::{Artist, ArtistId},
musicbrainz::{IMusicBrainzRef, MbArtistRef, MbRefOption, Mbid},
};
@ -14,7 +14,7 @@ use crate::tui::{
app::{
machine::{match_state::MatchState, App, AppInner, AppMachine},
selection::KeySelection,
AppPublicState, AppState, Category, IAppEventFetch, IAppInteractFetch,
AppPublicState, AppState, ArtistMatching, 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.meta.id.mb_ref);
requests = Self::browse_release_group_job(artist.id, &artist.meta.info.mb_ref);
} else {
fetch = FetchState::search(rx);
}
SubmitJob { fetch, requests }
}
_ => {
let arid = match artist.meta.id.mb_ref {
let arid = match artist.meta.info.mb_ref {
MbRefOption::Some(ref mbref) => mbref,
_ => return Err("cannot fetch album: artist has no MBID"),
};
@ -130,10 +130,9 @@ 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),
}
}
};
@ -191,9 +190,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.meta.id.clone();
let artist_id = artist.id.clone();
let coll = inner.music_hoard.get_collection();
let artist = coll.iter().find(|a| a.meta.id == artist_id).unwrap();
let artist = coll.iter().find(|a| a.id == artist_id).unwrap();
for new in Self::new_albums(fetch_albums, &artist.albums).into_iter() {
inner.music_hoard.add_album(&artist_id, new)?;
@ -223,11 +222,11 @@ impl AppMachine<FetchState> {
pub fn app_lookup_artist(
inner: AppInner,
fetch: FetchState,
artist: &ArtistMeta,
matching: ArtistMatching,
mbid: Mbid,
) -> App {
let f = Self::submit_lookup_artist_job;
Self::app_lookup(f, inner, fetch, artist, mbid)
Self::app_lookup(f, inner, fetch, matching, mbid)
}
pub fn app_lookup_album(
@ -243,18 +242,18 @@ impl AppMachine<FetchState> {
Self::app_lookup(f, inner, fetch, album_id, mbid)
}
fn app_lookup<F, Meta>(
fn app_lookup<F, Matching>(
submit: F,
inner: AppInner,
mut fetch: FetchState,
meta: Meta,
matching: Matching,
mbid: Mbid,
) -> App
where
F: FnOnce(&dyn IMbJobSender, ResultSender, Meta, Mbid) -> Result<(), DaemonError>,
F: FnOnce(&dyn IMbJobSender, ResultSender, Matching, Mbid) -> Result<(), DaemonError>,
{
let (lookup_tx, lookup_rx) = mpsc::channel::<MbApiResult>();
if let Err(err) = submit(&*inner.musicbrainz, lookup_tx, meta, mbid) {
if let Err(err) = submit(&*inner.musicbrainz, lookup_tx, matching, mbid) {
return AppMachine::error_state(inner, err.to_string()).into();
}
fetch.lookup_rx.replace(lookup_rx);
@ -262,60 +261,72 @@ impl AppMachine<FetchState> {
}
fn search_artist_job(artist: &Artist) -> VecDeque<MbParams> {
match artist.meta.id.mb_ref {
match artist.meta.info.mb_ref {
MbRefOption::Some(ref arid) => {
Self::search_albums_requests(&artist.meta.id, arid, &artist.albums)
Self::search_albums_requests(artist.id, arid, &artist.albums)
}
MbRefOption::CannotHaveMbid => VecDeque::new(),
MbRefOption::None => Self::search_artist_request(&artist.meta),
MbRefOption::None => Self::search_artist_request(ArtistMatching::new(
artist.id,
artist.meta.name.clone(),
)),
}
}
fn search_release_group_job(
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(meta: &ArtistMeta) -> VecDeque<MbParams> {
VecDeque::from([MbParams::search_artist(meta.clone())])
fn search_artist_request(matching: ArtistMatching) -> VecDeque<MbParams> {
VecDeque::from([MbParams::search_artist(matching)])
}
fn search_albums_requests(
artist: &ArtistId,
arid: &MbArtistRef,
artist_id: ArtistId,
artist_mbid: &MbArtistRef,
albums: &[Album],
) -> VecDeque<MbParams> {
let arid = arid.mbid();
let arid = artist_mbid.mbid();
albums
.iter()
.filter(|album| album.meta.id.mb_ref.is_none())
.map(|album| {
MbParams::search_release_group(artist.clone(), arid.clone(), album.meta.clone())
MbParams::search_release_group(artist_id, arid.clone(), album.meta.clone())
})
.collect()
}
fn browse_release_group_job(mbopt: &MbRefOption<MbArtistRef>) -> VecDeque<MbParams> {
fn browse_release_group_job(
artist_id: ArtistId,
mbopt: &MbRefOption<MbArtistRef>,
) -> VecDeque<MbParams> {
match mbopt {
MbRefOption::Some(mbref) => Self::browse_release_group_request(mbref),
MbRefOption::Some(mbref) => Self::browse_release_group_request(artist_id, mbref),
_ => VecDeque::new(),
}
}
fn browse_release_group_request(mbref: &MbArtistRef) -> VecDeque<MbParams> {
VecDeque::from([MbParams::browse_release_group(mbref.mbid().clone())])
fn browse_release_group_request(
artist_id: ArtistId,
mbref: &MbArtistRef,
) -> VecDeque<MbParams> {
VecDeque::from([MbParams::browse_release_group(
artist_id,
mbref.mbid().clone(),
)])
}
fn submit_lookup_artist_job(
musicbrainz: &dyn IMbJobSender,
result_sender: ResultSender,
artist: &ArtistMeta,
matching: ArtistMatching,
mbid: Mbid,
) -> Result<(), DaemonError> {
let requests = VecDeque::from([MbParams::lookup_artist(artist.clone(), mbid)]);
let requests = VecDeque::from([MbParams::lookup_artist(matching, mbid)]);
musicbrainz.submit_foreground_job(result_sender, requests)
}
@ -395,16 +406,18 @@ mod tests {
let mut fetch = FetchState::search(fetch_rx);
fetch.lookup_rx.replace(lookup_rx);
let artist = COLLECTION[3].meta.clone();
let id = COLLECTION[3].id;
let meta = COLLECTION[3].meta.clone();
let matching = ArtistMatching::new(id, meta.name.clone());
let matches: Vec<Entity<ArtistMeta>> = vec![];
let fetch_result = MbReturn::Match(EntityMatches::artist_search(artist.clone(), matches));
let fetch_result = MbReturn::Match(EntityMatches::artist_search(matching.clone(), matches));
fetch_tx.send(Ok(fetch_result.clone())).unwrap();
assert_eq!(fetch.try_recv(), Err(TryRecvError::Empty));
let lookup = Entity::new(artist.clone());
let lookup_result = MbReturn::Match(EntityMatches::artist_lookup(artist.clone(), lookup));
let lookup = Entity::new(meta.clone());
let lookup_result = MbReturn::Match(EntityMatches::artist_lookup(matching.clone(), lookup));
lookup_tx.send(Ok(lookup_result.clone())).unwrap();
assert_eq!(fetch.try_recv(), Ok(Ok(lookup_result)));
@ -445,7 +458,7 @@ mod tests {
fn fetch_single_album() {
let mut mb_job_sender = MockIMbJobSender::new();
let artist_id = COLLECTION[1].meta.id.clone();
let artist_id = COLLECTION[1].id;
let artist_mbid: Mbid = "11111111-1111-1111-1111-111111111111".try_into().unwrap();
let album_meta = COLLECTION[1].albums[0].meta.clone();
@ -520,7 +533,7 @@ mod tests {
fn fetch_albums() {
let mut mb_job_sender = MockIMbJobSender::new();
let artist_id = COLLECTION[1].meta.id.clone();
let artist_id = COLLECTION[1].id;
let artist_mbid: Mbid = "11111111-1111-1111-1111-111111111111".try_into().unwrap();
let album_1_meta = COLLECTION[1].albums[0].meta.clone();
@ -566,7 +579,7 @@ mod tests {
fn lookup_album() {
let mut mb_job_sender = MockIMbJobSender::new();
let artist_id = COLLECTION[1].meta.id.clone();
let artist_id = COLLECTION[1].id;
let album_id = COLLECTION[1].albums[0].meta.id.clone();
lookup_album_expectation(&mut mb_job_sender, &artist_id, &album_id);
@ -579,8 +592,8 @@ mod tests {
AppMachine::app_lookup_album(inner, fetch, &artist_id, &album_id, mbid());
}
fn search_artist_expectation(job_sender: &mut MockIMbJobSender, artist: &ArtistMeta) {
let requests = VecDeque::from([MbParams::search_artist(artist.clone())]);
fn search_artist_expectation(job_sender: &mut MockIMbJobSender, artist: ArtistMatching) {
let requests = VecDeque::from([MbParams::search_artist(artist)]);
job_sender
.expect_submit_background_job()
.with(predicate::always(), predicate::eq(requests))
@ -592,8 +605,10 @@ mod tests {
fn fetch_artist() {
let mut mb_job_sender = MockIMbJobSender::new();
let artist = COLLECTION[3].meta.clone();
search_artist_expectation(&mut mb_job_sender, &artist);
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 music_hoard = music_hoard(COLLECTION.to_owned());
let inner = AppInner::new(music_hoard, mb_job_sender);
@ -608,8 +623,8 @@ mod tests {
assert!(matches!(app, AppState::Fetch(_)));
}
fn lookup_artist_expectation(job_sender: &mut MockIMbJobSender, artist: &ArtistMeta) {
let requests = VecDeque::from([MbParams::lookup_artist(artist.clone(), mbid())]);
fn lookup_artist_expectation(job_sender: &mut MockIMbJobSender, artist: ArtistMatching) {
let requests = VecDeque::from([MbParams::lookup_artist(artist, mbid())]);
job_sender
.expect_submit_foreground_job()
.with(predicate::always(), predicate::eq(requests))
@ -621,8 +636,10 @@ mod tests {
fn lookup_artist() {
let mut mb_job_sender = MockIMbJobSender::new();
let artist = COLLECTION[3].meta.clone();
lookup_artist_expectation(&mut mb_job_sender, &artist);
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 music_hoard = music_hoard(COLLECTION.to_owned());
let inner = AppInner::new(music_hoard, mb_job_sender);
@ -630,7 +647,7 @@ mod tests {
let (_fetch_tx, fetch_rx) = mpsc::channel();
let fetch = FetchState::search(fetch_rx);
AppMachine::app_lookup_artist(inner, fetch, &artist, mbid());
AppMachine::app_lookup_artist(inner, fetch, matching, mbid());
}
#[test]
@ -671,7 +688,9 @@ mod tests {
.expect_submit_foreground_job()
.return_once(|_, _| Err(DaemonError::JobChannelDisconnected));
let artist = COLLECTION[3].meta.clone();
let id = COLLECTION[3].id;
let name = COLLECTION[3].meta.name.clone();
let matching = ArtistMatching::new(id, name);
let music_hoard = music_hoard(COLLECTION.to_owned());
let inner = AppInner::new(music_hoard, mb_job_sender);
@ -679,7 +698,7 @@ mod tests {
let (_fetch_tx, fetch_rx) = mpsc::channel();
let fetch = FetchState::search(fetch_rx);
let app = AppMachine::app_lookup_artist(inner, fetch, &artist, mbid());
let app = AppMachine::app_lookup_artist(inner, fetch, matching, mbid());
assert!(matches!(app, AppState::Error(_)));
}
@ -687,10 +706,12 @@ mod tests {
fn recv_ok_match_ok() {
let (tx, rx) = mpsc::channel::<MbApiResult>();
let artist = COLLECTION[3].meta.clone();
let id = COLLECTION[3].id;
let name = COLLECTION[3].meta.name.clone();
let matching = ArtistMatching::new(id, name);
let artist_match = Entity::with_score(COLLECTION[2].meta.clone(), 80);
let artist_match_info =
EntityMatches::artist_search(artist.clone(), vec![artist_match.clone()]);
EntityMatches::artist_search(matching.clone(), vec![artist_match.clone()]);
let fetch_result = Ok(MbReturn::Match(artist_match_info));
tx.send(fetch_result).unwrap();
@ -706,7 +727,7 @@ mod tests {
MatchOption::CannotHaveMbid,
MatchOption::ManualInputMbid,
];
let expected = EntityMatches::artist_search(artist, match_options);
let expected = EntityMatches::artist_search(matching, match_options);
assert_eq!(match_state.matches, &expected);
}
@ -730,7 +751,7 @@ mod tests {
let (tx, rx) = mpsc::channel::<MbApiResult>();
let fetch = FetchState::fetch(rx);
let artist_id = collection[0].meta.id.clone();
let artist_id = collection[0].id;
let old_album = collection[0].albums[0].meta.clone();
let new_album = AlbumMeta::new(AlbumId::new("some new album"));
@ -764,7 +785,7 @@ mod tests {
let (tx, rx) = mpsc::channel::<MbApiResult>();
let fetch = FetchState::fetch(rx);
let artist_id = collection[0].meta.id.clone();
let artist_id = collection[0].id;
let old_album = collection[0].albums[0].meta.clone();
let new_album = AlbumMeta::new(AlbumId::new("some new album"));
@ -802,7 +823,7 @@ mod tests {
}
fn browse_release_group_expectation(artist: &Artist) -> MockIMbJobSender {
let requests = AppMachine::browse_release_group_job(&artist.meta.id.mb_ref);
let requests = AppMachine::browse_release_group_job(artist.id, &artist.meta.info.mb_ref);
let mut mb_job_sender = MockIMbJobSender::new();
mb_job_sender
.expect_submit_background_job()
@ -858,8 +879,10 @@ mod tests {
let app = AppMachine::app_fetch_next(inner, fetch);
assert!(matches!(app, AppState::Fetch(_)));
let artist = COLLECTION[3].meta.clone();
let match_info = EntityMatches::artist_search::<Entity<ArtistMeta>>(artist, vec![]);
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 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, ArtistMbRef, ArtistMeta},
artist::{ArtistInfo, ArtistMeta},
musicbrainz::{MbRefOption, Mbid},
};
@ -13,11 +13,6 @@ use crate::tui::app::{
MatchOption, MatchStatePublic, WidgetState,
};
struct ArtistInfoTuple {
mb_ref: ArtistMbRef,
info: ArtistInfo,
}
struct AlbumInfoTuple {
mb_ref: AlbumMbRef,
info: AlbumInfo,
@ -27,7 +22,7 @@ trait GetInfoMeta {
type InfoType;
}
impl GetInfoMeta for ArtistMeta {
type InfoType = ArtistInfoTuple;
type InfoType = ArtistInfo;
}
impl GetInfoMeta for AlbumMeta {
type InfoType = AlbumInfoTuple;
@ -44,20 +39,18 @@ enum InfoOption<T> {
}
impl GetInfo for MatchOption<ArtistMeta> {
type InfoType = ArtistInfoTuple;
type InfoType = ArtistInfo;
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 => mb_ref = MbRefOption::CannotHaveMbid,
MatchOption::CannotHaveMbid => info.mb_ref = MbRefOption::CannotHaveMbid,
MatchOption::ManualInputMbid => return InfoOption::NeedInput,
}
InfoOption::Info(ArtistInfoTuple { mb_ref, info })
InfoOption::Info(info)
}
}
@ -180,11 +173,11 @@ impl AppMachine<MatchState> {
};
match self.state.current {
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;
let artist_id = &album_matches.artist_id;
let matching = &album_matches.matching;
AppMachine::app_lookup_album(
self.inner,
@ -205,11 +198,10 @@ impl AppMachine<MatchState> {
fn select_artist(
inner: &mut AppInner,
matches: &ArtistMatches,
tuple: ArtistInfoTuple,
info: ArtistInfo,
) -> Result<(), musichoard::Error> {
let mh = &mut inner.music_hoard;
mh.merge_artist_info(&matches.matching.id, tuple.info)?;
mh.set_artist_mb_ref(&matches.matching.id, tuple.mb_ref)
mh.merge_artist_info(&matches.matching.id, info)
}
fn filter_mb_ref(left: &AlbumMbRef, right: &AlbumMbRef) -> bool {
@ -223,7 +215,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.meta.id == matches.artist) {
if let Some(artist) = coll.iter().find(|artist| artist.id == matches.artist_id) {
// While we expect only one, there is nothing stopping anybody from having multiple
// different albums with the same MBID.
let iter = artist.albums.iter();
@ -237,15 +229,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, &album)?;
inner.music_hoard.remove_album(&matches.artist_id, &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, &matches.matching, tuple.info)?;
mh.set_album_mb_ref(&matches.artist, &matches.matching, tuple.mb_ref)
mh.merge_album_info(&matches.artist_id, &matches.matching, tuple.info)?;
mh.set_album_mb_ref(&matches.artist_id, &matches.matching, tuple.mb_ref)
}
}
@ -293,11 +285,11 @@ impl IAppInteractMatch for AppMachine<MatchState> {
let inner = &mut self.inner;
let result = match self.state.current {
EntityMatches::Artist(ref mut matches) => match matches.list.extract_info(index) {
InfoOption::Info(tuple) => Self::select_artist(inner, matches, tuple),
InfoOption::Info(info) => Self::select_artist(inner, matches, info),
InfoOption::NeedInput => return self.get_input(),
},
EntityMatches::Album(ref mut matches) => match matches.list.extract_info(index) {
InfoOption::Info(tuple) => Self::select_album(inner, matches, tuple),
InfoOption::Info(info) => Self::select_album(inner, matches, info),
InfoOption::NeedInput => return self.get_input(),
},
};
@ -326,14 +318,14 @@ mod tests {
album::{
Album, AlbumDate, AlbumId, AlbumInfo, AlbumMeta, AlbumPrimaryType, AlbumSecondaryType,
},
artist::{Artist, ArtistId, ArtistMeta},
artist::{Artist, ArtistId, ArtistMbRef, ArtistMeta, ArtistName},
Collection,
};
use crate::tui::{
app::{
machine::tests::{inner, inner_with_mb, input_event, music_hoard},
IApp, IAppAccess, IAppInput,
ArtistMatching, IApp, IAppAccess, IAppInput,
},
lib::{
interface::musicbrainz::{
@ -360,30 +352,41 @@ mod tests {
"00000000-0000-0000-0000-000000000000".try_into().unwrap()
}
fn artist_meta() -> ArtistMeta {
ArtistMeta::new(ArtistId::new("Artist").with_mb_ref(ArtistMbRef::Some(mbid().into())))
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_match() -> EntityMatches {
let mut artist = artist_meta();
let id = artist_id();
let name = artist_name();
let meta = artist_meta(name.clone());
let artist_1 = artist.clone();
let artist_1 = meta.clone();
let artist_match_1 = Entity::with_score(artist_1, 100);
let artist_2 = artist.clone();
let artist_2 = meta.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(artist, list)
EntityMatches::artist_search(ArtistMatching::new(id, name), list)
}
fn artist_lookup() -> EntityMatches {
let mut artist = artist_meta();
artist.clear_mb_ref();
let id = artist_id();
let name = artist_name();
let artist = artist_meta(name.clone());
let lookup = Entity::new(artist.clone());
EntityMatches::artist_lookup(artist, lookup)
EntityMatches::artist_lookup(ArtistMatching::new(id, name), lookup)
}
fn album_id() -> AlbumId {
@ -393,14 +396,18 @@ mod tests {
fn album_meta(id: AlbumId) -> AlbumMeta {
AlbumMeta::new(id)
.with_date(AlbumDate::new(Some(1990), Some(5), None))
.with_info(AlbumInfo::new(
Some(AlbumPrimaryType::Album),
vec![AlbumSecondaryType::Live, AlbumSecondaryType::Compilation],
))
.with_info(
AlbumInfo::default()
.with_primary_type(AlbumPrimaryType::Album)
.with_secondary_types(vec![
AlbumSecondaryType::Live,
AlbumSecondaryType::Compilation,
]),
)
}
fn album_match() -> EntityMatches {
let artist_id = ArtistId::new("Artist");
let artist_id = ArtistId(1);
let mut album_id = album_id();
let album_meta = album_meta(album_id.clone());
@ -418,7 +425,7 @@ mod tests {
}
fn album_lookup() -> EntityMatches {
let artist_id = ArtistId::new("Artist");
let artist_id = ArtistId(1);
let mut album_id = album_id();
let album_meta = album_meta(album_id.clone());
@ -464,7 +471,7 @@ mod tests {
let collection = vec![];
let mut music_hoard = music_hoard(collection.clone());
let artist_id = ArtistId::new("Artist");
let artist_id = ArtistId(0);
match matches_info {
EntityMatches::Album(_) => {
let album_id = AlbumId::new("Album");
@ -491,21 +498,12 @@ mod tests {
.return_once(|_, _, _| Ok(()));
}
EntityMatches::Artist(_) => {
let mb_ref = MbRefOption::CannotHaveMbid;
let info = ArtistInfo::default();
let info = ArtistInfo::default().with_mb_ref(MbRefOption::CannotHaveMbid);
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(()));
}
}
@ -585,22 +583,13 @@ mod tests {
match matches_info {
EntityMatches::Album(_) => panic!(),
EntityMatches::Artist(_) => {
let mut meta = artist_meta();
let mb_ref = meta.id.mb_ref.clone();
meta.clear_mb_ref();
let id = artist_id();
let meta = artist_meta(artist_name());
let mut seq = Sequence::new();
music_hoard
.expect_merge_artist_info()
.with(eq(meta.id.clone()), eq(meta.info))
.with(eq(id), 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(()));
}
}
@ -619,7 +608,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.clone();
let artist = matches.artist_id;
let mut seq = Sequence::new();
mh.expect_get_collection()
@ -673,7 +662,7 @@ mod tests {
// matching album_id.
// (1) Same artist as matches_info.
let mut artist = Artist::new(ArtistId::new("Artist"));
let mut artist = Artist::new(1, "Artist");
// (2) An album with the same album_id as the selected one.
artist.albums.push(Album::new(AlbumId::new("Album")));
@ -841,15 +830,15 @@ mod tests {
#[test]
fn select_manual_input_artist() {
let mut mb_job_sender = MockIMbJobSender::new();
let artist = ArtistMeta::new(ArtistId::new("Artist"));
let requests = VecDeque::from([MbParams::lookup_artist(artist.clone(), mbid())]);
let matching = ArtistMatching::new(artist_id(), artist_name());
let requests = VecDeque::from([MbParams::lookup_artist(matching.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(artist.clone(), matches_vec);
let artist_match = EntityMatches::artist_search(matching.clone(), matches_vec);
let matches = AppMachine::match_state(
inner_with_mb(music_hoard(vec![]), mb_job_sender),
match_state(artist_match),
@ -869,7 +858,7 @@ mod tests {
#[test]
fn select_manual_input_album() {
let mut mb_job_sender = MockIMbJobSender::new();
let artist_id = ArtistId::new("Artist");
let artist_id = artist_id();
let album = AlbumMeta::new("Album").with_date(1990);
let requests = VecDeque::from([MbParams::lookup_release_group(
artist_id.clone(),

View File

@ -225,7 +225,10 @@ mod tests {
};
use crate::tui::{
app::{AppState, EntityMatches, IApp, IAppInput, IAppInteractBrowse, InputEvent},
app::{
AppState, ArtistMatching, EntityMatches, IApp, IAppInput, IAppInteractBrowse,
InputEvent,
},
lib::{
interface::musicbrainz::{api::Entity, daemon::MockIMbJobSender},
MockIMusicHoard,
@ -519,8 +522,13 @@ mod tests {
let (_, rx) = mpsc::channel();
let fetch = FetchState::search(rx);
let artist = ArtistMeta::new(ArtistId::new("Artist"));
let info = EntityMatches::artist_lookup(artist.clone(), Entity::new(artist.clone()));
let id = ArtistId(1);
let name = String::from("Artist");
let meta = ArtistMeta::new(name.clone());
let matching = ArtistMatching::new(id, name);
let info = EntityMatches::artist_lookup(matching, Entity::new(meta));
app =
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.id.name, !case_sens, !char_sens);
let name = string::normalize_string_with(&probe.meta.name, !case_sens, !char_sens);
let mut result = name.string.starts_with(search);
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},
artist::{ArtistId, ArtistMeta, ArtistName},
Collection,
};
@ -228,13 +228,28 @@ impl<T> From<Entity<T>> for MatchOption<T> {
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ArtistMatches {
pub matching: ArtistMeta,
pub matching: ArtistMatching,
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: ArtistId,
pub artist_id: ArtistId,
pub matching: AlbumId,
pub list: Vec<MatchOption<AlbumMeta>>,
}
@ -246,40 +261,43 @@ pub enum EntityMatches {
}
impl EntityMatches {
pub fn artist_search<M: Into<MatchOption<ArtistMeta>>>(
matching: ArtistMeta,
list: Vec<M>,
) -> Self {
pub fn artist_search<M>(matching: ArtistMatching, list: Vec<M>) -> Self
where
M: Into<MatchOption<ArtistMeta>>,
{
let list = list.into_iter().map(Into::into).collect();
EntityMatches::Artist(ArtistMatches { matching, list })
}
pub fn album_search<M: Into<MatchOption<AlbumMeta>>>(
artist: ArtistId,
artist_id: ArtistId,
matching: AlbumId,
list: Vec<M>,
) -> Self {
let list = list.into_iter().map(Into::into).collect();
EntityMatches::Album(AlbumMatches {
artist,
artist_id,
matching,
list,
})
}
pub fn artist_lookup<M: Into<MatchOption<ArtistMeta>>>(matching: ArtistMeta, item: M) -> Self {
pub fn artist_lookup<M>(matching: ArtistMatching, item: M) -> Self
where
M: Into<MatchOption<ArtistMeta>>,
{
let list = vec![item.into()];
EntityMatches::Artist(ArtistMatches { matching, list })
}
pub fn album_lookup<M: Into<MatchOption<AlbumMeta>>>(
artist: ArtistId,
artist_id: ArtistId,
matching: AlbumId,
item: M,
) -> Self {
let list = vec![item.into()];
EntityMatches::Album(AlbumMatches {
artist,
artist_id,
matching,
list,
})

View File

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

View File

@ -5,7 +5,7 @@ use std::collections::HashMap;
use musichoard::{
collection::{
album::{AlbumDate, AlbumId, AlbumInfo, AlbumLibId, AlbumMbRef, AlbumMeta, AlbumSeq},
artist::{ArtistId, ArtistInfo, ArtistMbRef, ArtistMeta},
artist::{ArtistInfo, ArtistMbRef, ArtistMeta, ArtistName},
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, artist: &ArtistMeta) -> Result<Vec<Entity<ArtistMeta>>, Error> {
let query = SearchArtistRequest::new().string(&artist.id.name);
fn search_artist(&mut self, name: &ArtistName) -> Result<Vec<Entity<ArtistMeta>>, Error> {
let query = SearchArtistRequest::new().string(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,
arid: &Mbid,
album: &AlbumMeta,
artist_mbid: &Mbid,
meta: &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(album.date.year, None, None);
let date = AlbumDate::new(meta.info.date.year, None, None);
let query = SearchReleaseGroupRequest::new()
.arid(arid)
.arid(artist_mbid)
.and()
.first_release_date(&date)
.and()
.release_group(&album.id.title);
.release_group(&meta.id.title);
let paging = PageSettings::default();
let mb_response = self.client.search_release_group(&query, &paging)?;
@ -96,10 +96,11 @@ impl<Http: IMusicBrainzHttp> IMusicBrainz for MusicBrainz<Http> {
fn browse_release_group(
&mut self,
artist: &Mbid,
artist_mbid: &Mbid,
paging: &mut Option<PageSettings>,
) -> Result<Vec<Entity<AlbumMeta>>, Error> {
let request = BrowseReleaseGroupRequest::artist(artist).filter_status_website_default();
let request =
BrowseReleaseGroupRequest::artist(artist_mbid).filter_status_website_default();
let page = paging.take().unwrap_or_default();
let mb_response = self.client.browse_release_group(&request, &page)?;
@ -115,12 +116,10 @@ fn from_mb_artist_meta(meta: MbArtistMeta) -> (ArtistMeta, Option<String>) {
let sort = Some(meta.sort_name).filter(|s| s != &meta.name);
(
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(),
},
},
@ -135,9 +134,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,30 +256,26 @@ impl JobInstance {
MbParams::Lookup(lookup) => match lookup {
LookupParams::Artist(p) => musicbrainz
.lookup_artist(&p.mbid)
.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(|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(MbReturn::Match),
MbParams::Search(search) => match search {
SearchParams::Artist(p) => musicbrainz
.search_artist(&p.artist)
.map(|rv| EntityMatches::artist_search(p.artist.clone(), rv)),
.search_artist(&p.matching.name)
.map(|rv| EntityMatches::artist_search(p.matching.clone(), rv)),
SearchParams::ReleaseGroup(p) => musicbrainz
.search_release_group(&p.artist_mbid, &p.album)
.map(|rv| {
EntityMatches::album_search(p.artist_id.clone(), p.album.id.clone(), rv)
}),
.search_release_group(&p.artist_mbid, &p.meta)
.map(|rv| EntityMatches::album_search(p.artist_id, p.meta.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, paging)
.browse_release_group(&params.artist_mbid, paging)
.map(|rv| EntityList::Album(rv.into_iter().map(|rg| rg.entity).collect()))
}
}
@ -350,11 +346,12 @@ mod tests {
use mockall::{predicate, Sequence};
use musichoard::collection::{
album::AlbumMeta,
artist::{ArtistId, ArtistMeta},
artist::{ArtistId, ArtistMeta, ArtistName},
musicbrainz::{IMusicBrainzRef, MbRefOption, Mbid},
};
use crate::tui::{
app::ArtistMatching,
event::{Event, EventError, MockIFetchCompleteEventSender},
lib::interface::musicbrainz::api::{Entity, MockIMusicBrainz},
testmod::COLLECTION,
@ -426,38 +423,43 @@ mod tests {
}
fn lookup_artist_requests() -> VecDeque<MbParams> {
let artist = COLLECTION[3].meta.clone();
let id = COLLECTION[3].id;
let name = COLLECTION[3].meta.name.clone();
let matching = ArtistMatching::new(id, name);
let mbid = mbid();
VecDeque::from([MbParams::lookup_artist(artist, mbid)])
VecDeque::from([MbParams::lookup_artist(matching, mbid)])
}
fn lookup_release_group_requests() -> VecDeque<MbParams> {
let artist_id = COLLECTION[1].meta.id.clone();
let artist_id = COLLECTION[1].id;
let album_id = COLLECTION[1].albums[0].meta.id.clone();
let mbid = mbid();
VecDeque::from([MbParams::lookup_release_group(artist_id, album_id, mbid)])
}
fn search_artist_requests() -> VecDeque<MbParams> {
let artist = COLLECTION[3].meta.clone();
VecDeque::from([MbParams::search_artist(artist)])
let id = COLLECTION[3].id;
let name = COLLECTION[3].meta.name.clone();
let matching = ArtistMatching::new(id, name);
VecDeque::from([MbParams::search_artist(matching)])
}
fn search_artist_expectations() -> (ArtistMeta, Vec<Entity<ArtistMeta>>) {
let artist = COLLECTION[3].meta.clone();
fn search_artist_expectations() -> (ArtistName, Vec<Entity<ArtistMeta>>) {
let name = COLLECTION[3].meta.name.clone();
let meta = COLLECTION[3].meta.clone();
let artist_match_1 = Entity::with_score(artist.clone(), 100);
let artist_match_2 = Entity::with_score(artist.clone(), 50);
let artist_match_1 = Entity::with_score(meta.clone(), 100);
let artist_match_2 = Entity::with_score(meta.clone(), 50);
let matches = vec![artist_match_1.clone(), artist_match_2.clone()];
(artist, matches)
(name, matches)
}
fn search_albums_requests() -> VecDeque<MbParams> {
let mbref = mb_ref_opt_as_ref(&COLLECTION[1].meta.id.mb_ref);
let mbref = mb_ref_opt_as_ref(&COLLECTION[1].meta.info.mb_ref);
let arid = mb_ref_opt_unwrap(mbref).mbid().clone();
let artist_id = COLLECTION[1].meta.id.clone();
let artist_id = COLLECTION[1].id;
let album_1 = COLLECTION[1].albums[0].meta.clone();
let album_4 = COLLECTION[1].albums[3].meta.clone();
@ -468,17 +470,21 @@ mod tests {
}
fn browse_albums_requests() -> VecDeque<MbParams> {
let mbref = mb_ref_opt_as_ref(&COLLECTION[1].meta.id.mb_ref);
let mbref = mb_ref_opt_as_ref(&COLLECTION[1].meta.info.mb_ref);
let arid = mb_ref_opt_unwrap(mbref).mbid().clone();
VecDeque::from([MbParams::browse_release_group(arid)])
VecDeque::from([MbParams::browse_release_group(album_artist_id(), arid)])
}
fn artist_id() -> ArtistId {
ArtistId(1)
}
fn album_artist_id() -> ArtistId {
COLLECTION[1].meta.id.clone()
COLLECTION[1].id
}
fn album_arid_expectation() -> Mbid {
let mbref = mb_ref_opt_as_ref(&COLLECTION[1].meta.id.mb_ref);
let mbref = mb_ref_opt_as_ref(&COLLECTION[1].meta.info.mb_ref);
mb_ref_opt_unwrap(mbref).mbid().clone()
}
@ -594,8 +600,12 @@ mod tests {
fn execute_lookup_artist() {
let mut musicbrainz = musicbrainz();
let mbid = mbid();
let artist = COLLECTION[3].meta.clone();
let lookup = Entity::new(artist.clone());
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());
lookup_artist_expectation(&mut musicbrainz, &mbid, &lookup);
let mut event_sender = event_sender();
@ -622,7 +632,7 @@ mod tests {
assert_eq!(
result,
Ok(MbReturn::Match(EntityMatches::artist_lookup(
artist, lookup
matching, lookup
)))
);
}
@ -681,13 +691,13 @@ mod tests {
fn search_artist_expectation(
musicbrainz: &mut MockIMusicBrainz,
artist: &ArtistMeta,
name: &ArtistName,
matches: &[Entity<ArtistMeta>],
) {
let result = Ok(matches.to_owned());
musicbrainz
.expect_search_artist()
.with(predicate::eq(artist.clone()))
.with(predicate::eq(name.clone()))
.times(1)
.return_once(|_| result);
}
@ -695,8 +705,9 @@ mod tests {
#[test]
fn execute_search_artist() {
let mut musicbrainz = musicbrainz();
let (artist, matches) = search_artist_expectations();
search_artist_expectation(&mut musicbrainz, &artist, &matches);
let id = artist_id();
let (name, matches) = search_artist_expectations();
search_artist_expectation(&mut musicbrainz, &name, &matches);
let mut event_sender = event_sender();
fetch_complete_expectation(&mut event_sender, 1);
@ -719,10 +730,11 @@ 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(
artist, matches
matching, matches
)))
);
}

View File

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

View File

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

View File

@ -4,7 +4,7 @@ pub mod interface;
use musichoard::{
collection::{
album::{AlbumId, AlbumInfo, AlbumMbRef, AlbumMeta},
artist::{ArtistId, ArtistInfo, ArtistMbRef},
artist::{ArtistId, ArtistInfo},
Collection,
},
interface::{database::IDatabase, library::ILibrary},
@ -33,11 +33,6 @@ 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,
@ -91,14 +86,6 @@ 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,9 +1,7 @@
use std::collections::HashMap;
use musichoard::collection::{
album::{
Album, AlbumId, AlbumInfo, AlbumLibId, AlbumMbRef, AlbumMeta, AlbumPrimaryType, AlbumSeq,
},
album::{Album, AlbumId, AlbumInfo, AlbumLibId, AlbumMbRef, AlbumMeta, AlbumPrimaryType},
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.id.name.as_str()))
.map(|a| ListItem::new(a.meta.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.date, &a.meta.seq))
.map(|a| UiDisplay::display_date(&a.meta.info.date, &a.meta.info.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,
artist::{ArtistMeta, ArtistName},
musicbrainz::{IMusicBrainzRef, MbRefOption},
track::{TrackFormat, TrackQuality},
};
@ -118,8 +118,8 @@ impl UiDisplay {
}
}
pub fn display_artist_matching(artist: &ArtistMeta) -> String {
format!("Matching artist: {}", &artist.id.name)
pub fn display_artist_matching(name: &ArtistName) -> String {
format!("Matching artist: {}", 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),
EntityMatches::Artist(m) => UiDisplay::display_artist_matching(&m.matching.name),
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.id.name,
artist.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.date),
UiDisplay::display_album_date(&album.info.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.id.name.as_str()).unwrap_or(""),
artist.map(|a| a.meta.name.as_str()).unwrap_or(""),
artist
.map(|a| UiDisplay::display_mb_ref_option_as_url(&a.meta.id.mb_ref))
.map(|a| UiDisplay::display_mb_ref_option_as_url(&a.meta.info.mb_ref))
.unwrap_or_default(),
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,
artist::{ArtistMeta, ArtistName},
};
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, &m.list, state),
EntityMatches::Artist(m) => Self::artists(&m.matching.name, &m.list, state),
EntityMatches::Album(m) => Self::albums(&m.matching, &m.list, state),
}
}
fn artists(
matching: &ArtistMeta,
matching: &ArtistName,
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},
artist::{Artist, ArtistId, ArtistMeta, ArtistName},
};
use crate::tui::{
app::{AppPublic, AppPublicInner, Delta, MatchStatePublic},
app::{AppPublic, AppPublicInner, ArtistMatching, 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(ArtistId::new("An artist"))];
let mut artists: Vec<Artist> = vec![Artist::new(0, "An artist")];
artists[0].albums.push(Album::new("An album"));
let mut selection = Selection::new(&artists);
@ -324,33 +324,49 @@ mod tests {
draw_test_suite(artists, &mut selection);
}
fn artist_meta() -> ArtistMeta {
ArtistMeta::new(ArtistId::new("an artist"))
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_matches() -> EntityMatches {
let artist = artist_meta();
let artist_match = Entity::with_score(artist.clone(), 80);
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 list = vec![artist_match.clone(), artist_match.clone()];
let mut info = EntityMatches::artist_search(artist, list);
let mut info = EntityMatches::artist_search(matching, list);
info.push_cannot_have_mbid();
info.push_manual_input_mbid();
info
}
fn artist_lookup() -> EntityMatches {
let artist = artist_meta();
let artist_lookup = Entity::new(artist.clone());
let id = artist_id();
let name = artist_name();
let meta = artist_meta(name.clone());
let matching = ArtistMatching::new(id, name);
let mut info = EntityMatches::artist_lookup(artist, artist_lookup);
let artist_lookup = Entity::new(meta.clone());
let mut info = EntityMatches::artist_lookup(matching, artist_lookup);
info.push_cannot_have_mbid();
info.push_manual_input_mbid();
info
}
fn album_artist_id() -> ArtistId {
ArtistId::new("Artist")
ArtistId(1)
}
fn album_id() -> AlbumId {
@ -360,10 +376,14 @@ mod tests {
fn album_meta(id: AlbumId) -> AlbumMeta {
AlbumMeta::new(id)
.with_date(AlbumDate::new(Some(1990), Some(5), None))
.with_info(AlbumInfo::new(
Some(AlbumPrimaryType::Album),
vec![AlbumSecondaryType::Live, AlbumSecondaryType::Compilation],
))
.with_info(
AlbumInfo::default()
.with_primary_type(AlbumPrimaryType::Album)
.with_secondary_types(vec![
AlbumSecondaryType::Live,
AlbumSecondaryType::Compilation,
]),
)
}
fn album_matches() -> EntityMatches {

View File

@ -1,7 +1,3 @@
use std::{fs, path::PathBuf};
use once_cell::sync::Lazy;
use musichoard::{
collection::{artist::Artist, Collection},
external::database::sql::{backend::SqlDatabaseSqliteBackend, SqlDatabase},
@ -9,10 +5,7 @@ use musichoard::{
};
use tempfile::NamedTempFile;
use crate::testlib::COLLECTION;
pub static DATABASE_TEST_FILE: Lazy<PathBuf> =
Lazy::new(|| fs::canonicalize("./tests/files/database/database.db").unwrap());
use crate::{copy_file_into_temp, testlib::COLLECTION, COMPLETE_DB_TEST_FILE};
fn expected() -> Collection {
let mut expected = COLLECTION.to_owned();
@ -24,6 +17,16 @@ 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();
@ -37,7 +40,7 @@ fn save() {
#[test]
fn load() {
let backend = SqlDatabaseSqliteBackend::new(&*DATABASE_TEST_FILE).unwrap();
let backend = SqlDatabaseSqliteBackend::new(&*COMPLETE_DB_TEST_FILE).unwrap();
let mut database = SqlDatabase::new(backend).unwrap();
let read_data: Vec<Artist> = database.load().unwrap();
@ -61,3 +64,18 @@ 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::{fs, path::PathBuf};
use std::{ffi::OsStr, fs, path::PathBuf, process::Command};
use musichoard::{
external::{
@ -15,16 +15,33 @@ use musichoard::{
},
IMusicHoardBase, IMusicHoardDatabase, IMusicHoardLibrary, MusicHoard,
};
use once_cell::sync::Lazy;
use tempfile::NamedTempFile;
use crate::testlib::COLLECTION;
fn copy_file_into_temp<P: Into<PathBuf>>(path: P) -> NamedTempFile {
pub static PARTIAL_DB_TEST_FILE: Lazy<PathBuf> =
Lazy::new(|| fs::canonicalize("./tests/files/database/partial.db").unwrap());
pub static COMPLETE_DB_TEST_FILE: Lazy<PathBuf> =
Lazy::new(|| fs::canonicalize("./tests/files/database/complete.db").unwrap());
pub fn copy_file_into_temp<P: Into<PathBuf>>(path: P) -> NamedTempFile {
let temp = NamedTempFile::new().unwrap();
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
@ -37,7 +54,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(&*database::sql::DATABASE_TEST_FILE);
let file = copy_file_into_temp(&*PARTIAL_DB_TEST_FILE);
let backend = SqlDatabaseSqliteBackend::new(file.path()).unwrap();
let database = SqlDatabase::new(backend).unwrap();
@ -47,6 +64,8 @@ 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]
@ -61,7 +80,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(&*database::sql::DATABASE_TEST_FILE);
let file = copy_file_into_temp(&*PARTIAL_DB_TEST_FILE);
let backend = SqlDatabaseSqliteBackend::new(file.path()).unwrap();
let database = SqlDatabase::new(backend).unwrap();
@ -71,4 +90,6 @@ 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, AlbumSeq,
AlbumSecondaryType,
},
artist::{Artist, ArtistId, ArtistInfo, ArtistMbRef, ArtistMeta},
musicbrainz::MbArtistRef,
@ -15,15 +15,14 @@ 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"),
@ -44,12 +43,9 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
lib_id: AlbumLibId::Value(7),
mb_ref: AlbumMbRef::None,
},
date: 2011.into(),
seq: AlbumSeq(0),
info: AlbumInfo {
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
},
info: AlbumInfo::default()
.with_date(2011)
.with_primary_type(AlbumPrimaryType::Album),
},
tracks: vec![
Track {
@ -210,15 +206,14 @@ 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"),
@ -237,12 +232,9 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
lib_id: AlbumLibId::Value(1),
mb_ref: AlbumMbRef::None,
},
date: 2004.into(),
seq: AlbumSeq(0),
info: AlbumInfo {
primary_type: Some(AlbumPrimaryType::Ep),
secondary_types: vec![],
},
info: AlbumInfo::default()
.with_date(2004)
.with_primary_type(AlbumPrimaryType::Ep),
},
tracks: vec![
Track {
@ -320,12 +312,9 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
lib_id: AlbumLibId::Value(2),
mb_ref: AlbumMbRef::None,
},
date: 2008.into(),
seq: AlbumSeq(0),
info: AlbumInfo {
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
},
info: AlbumInfo::default()
.with_date(2008)
.with_primary_type(AlbumPrimaryType::Album),
},
tracks: vec![
Track {
@ -465,15 +454,14 @@ 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"),
@ -491,12 +479,9 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
lib_id: AlbumLibId::Value(3),
mb_ref: AlbumMbRef::None,
},
date: 2001.into(),
seq: AlbumSeq(0),
info: AlbumInfo {
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
},
info: AlbumInfo::default()
.with_date(2001)
.with_primary_type(AlbumPrimaryType::Album),
},
tracks: vec![
Track {
@ -624,15 +609,14 @@ 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"),
@ -650,9 +634,7 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
lib_id: AlbumLibId::Singleton,
mb_ref: AlbumMbRef::None,
},
date: 2011.into(),
seq: AlbumSeq(0),
info: AlbumInfo::default(),
info: AlbumInfo::default().with_date(2011),
},
tracks: vec![
Track {
@ -674,12 +656,9 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
lib_id: AlbumLibId::Value(4),
mb_ref: AlbumMbRef::None,
},
date: 2011.into(),
seq: AlbumSeq(0),
info: AlbumInfo {
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
},
info: AlbumInfo::default()
.with_date(2011)
.with_primary_type(AlbumPrimaryType::Album),
},
tracks: vec![
Track {
@ -763,15 +742,14 @@ 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"),
@ -790,12 +768,9 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
lib_id: AlbumLibId::Value(5),
mb_ref: AlbumMbRef::None,
},
date: 1984.into(),
seq: AlbumSeq(0),
info: AlbumInfo {
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
},
info: AlbumInfo::default()
.with_date(1984)
.with_primary_type(AlbumPrimaryType::Album),
},
tracks: vec![
Track {
@ -895,12 +870,10 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
lib_id: AlbumLibId::Value(6),
mb_ref: AlbumMbRef::None,
},
date: 1999.into(),
seq: AlbumSeq(0),
info: AlbumInfo {
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![AlbumSecondaryType::Live],
},
info: AlbumInfo::default()
.with_date(1999)
.with_primary_type(AlbumPrimaryType::Album)
.with_secondary_types(vec![AlbumSecondaryType::Live]),
},
tracks: vec![
Track {