Add code for remaining urls
Some checks failed
Cargo CI / Build and Test (pull_request) Failing after 3m17s
Cargo CI / Lint (pull_request) Successful in 42s

This commit is contained in:
Wojciech Kozlowski 2024-01-08 22:14:25 +01:00
parent 3ea68d2935
commit 326b876d9c
4 changed files with 349 additions and 148 deletions

12
Cargo.lock generated
View File

@ -273,6 +273,15 @@ dependencies = [
"either", "either",
] ]
[[package]]
name = "itertools"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.6" version = "1.0.6"
@ -366,6 +375,7 @@ name = "musichoard"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"crossterm", "crossterm",
"itertools 0.12.0",
"mockall", "mockall",
"once_cell", "once_cell",
"openssh", "openssh",
@ -476,7 +486,7 @@ checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd"
dependencies = [ dependencies = [
"difflib", "difflib",
"float-cmp", "float-cmp",
"itertools", "itertools 0.10.5",
"normalize-line-endings", "normalize-line-endings",
"predicates-core", "predicates-core",
"regex", "regex",

View File

@ -7,6 +7,7 @@ edition = "2021"
[dependencies] [dependencies]
crossterm = { version = "0.26.1", optional = true} crossterm = { version = "0.26.1", optional = true}
itertools = { version = "0.12.0" }
openssh = { version = "0.9.9", features = ["native-mux"], default-features = false, optional = true} openssh = { version = "0.9.9", features = ["native-mux"], default-features = false, optional = true}
ratatui = { version = "0.20.1", optional = true} ratatui = { version = "0.20.1", optional = true}
serde = { version = "1.0.159", features = ["derive"] } serde = { version = "1.0.159", features = ["derive"] }

View File

@ -126,38 +126,24 @@ impl ArtistCommand {
clear_musicbutler_urls, clear_musicbutler_urls,
urls urls
), ),
ArtistCommand::Bandcamp(url_command) => { ArtistCommand::Bandcamp(url_command) => url_command_dispatch!(
match url_command { url_command,
UrlCommand::Add(_) => { music_hoard,
// Add URL. add_bandcamp_urls,
} remove_bandcamp_urls,
UrlCommand::Remove(_) => { set_bandcamp_urls,
// Remove URL if it exists. clear_bandcamp_urls,
} urls
UrlCommand::Set(_) => { ),
// Set the URLs regardless of previous (if any) value. ArtistCommand::Qobuz(url_command) => url_command_dispatch!(
} url_command,
UrlCommand::Clear(_) => { music_hoard,
// Remove the URLs. add_qobuz_url,
} remove_qobuz_url,
} set_qobuz_url,
} clear_qobuz_url,
ArtistCommand::Qobuz(url_command) => { url
match url_command { ),
UrlCommand::Add(_) => {
// Add URL or return error if one already existss.
}
UrlCommand::Remove(_) => {
// Remove URL if it exists.
}
UrlCommand::Set(_) => {
// Set the URL regardless of previous (if any) value.
}
UrlCommand::Clear(_) => {
// Remove the URL.
}
}
}
} }
} }
} }

View File

@ -6,12 +6,13 @@ pub mod library;
use std::{ use std::{
cmp::Ordering, cmp::Ordering,
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
fmt, fmt::{self, Debug},
iter::Peekable, iter::Peekable,
mem, mem,
}; };
use database::IDatabase; use database::IDatabase;
use itertools::Itertools;
use library::{ILibrary, Item, Query}; use library::{ILibrary, Item, Query};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url; use url::Url;
@ -79,6 +80,20 @@ impl MusicBrainz {
} }
} }
impl TryFrom<&str> for MusicBrainz {
type Error = Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
MusicBrainz::new(value)
}
}
impl AsRef<str> for MusicBrainz {
fn as_ref(&self) -> &str {
self.0.as_str()
}
}
impl IUrl for MusicBrainz { impl IUrl for MusicBrainz {
fn url(&self) -> &str { fn url(&self) -> &str {
self.0.as_str() self.0.as_str()
@ -119,6 +134,20 @@ impl MusicButler {
} }
} }
impl TryFrom<&str> for MusicButler {
type Error = Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
MusicButler::new(value)
}
}
impl AsRef<str> for MusicButler {
fn as_ref(&self) -> &str {
self.0.as_str()
}
}
impl IUrl for MusicButler { impl IUrl for MusicButler {
fn url(&self) -> &str { fn url(&self) -> &str {
self.0.as_str() self.0.as_str()
@ -152,6 +181,20 @@ impl Bandcamp {
} }
} }
impl TryFrom<&str> for Bandcamp {
type Error = Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Bandcamp::new(value)
}
}
impl AsRef<str> for Bandcamp {
fn as_ref(&self) -> &str {
self.0.as_str()
}
}
impl IUrl for Bandcamp { impl IUrl for Bandcamp {
fn url(&self) -> &str { fn url(&self) -> &str {
self.0.as_str() self.0.as_str()
@ -185,6 +228,20 @@ impl Qobuz {
} }
} }
impl TryFrom<&str> for Qobuz {
type Error = Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Qobuz::new(value)
}
}
impl AsRef<str> for Qobuz {
fn as_ref(&self) -> &str {
self.0.as_str()
}
}
impl IUrl for Qobuz { impl IUrl for Qobuz {
fn url(&self) -> &str { fn url(&self) -> &str {
self.0.as_str() self.0.as_str()
@ -325,6 +382,46 @@ pub struct Artist {
pub albums: Vec<Album>, pub albums: Vec<Album>,
} }
macro_rules! artist_unique_url_dispatch {
($add:ident, $remove:ident, $set:ident, $clear:ident, $label:literal, $field:ident) => {
fn $add<S: AsRef<str>>(&mut self, url: S) -> Result<(), Error> {
Self::add_unique_url(&self.id, $label, &mut self.properties.$field, url)
}
fn $remove<S: AsRef<str>>(&mut self, url: S) -> Result<(), Error> {
Self::remove_unique_url(&self.id, $label, &mut self.properties.$field, url)
}
fn $set<S: AsRef<str>>(&mut self, url: S) -> Result<(), Error> {
Self::set_unique_url(&mut self.properties.$field, url)
}
fn $clear(&mut self) {
Self::clear_unique_url(&mut self.properties.$field);
}
};
}
macro_rules! artist_multi_url_dispatch {
($add:ident, $remove:ident, $set:ident, $clear:ident, $label:literal, $field:ident) => {
fn $add<S: AsRef<str>>(&mut self, urls: Vec<S>) -> Result<(), Error> {
Self::add_multi_urls(&self.id, $label, &mut self.properties.$field, urls)
}
fn $remove<S: AsRef<str>>(&mut self, urls: Vec<S>) -> Result<(), Error> {
Self::remove_multi_urls(&self.id, $label, &mut self.properties.$field, urls)
}
fn $set<S: AsRef<str>>(&mut self, urls: Vec<S>) -> Result<(), Error> {
Self::set_multi_urls(&mut self.properties.$field, urls)
}
fn $clear(&mut self) {
Self::clear_multi_urls(&mut self.properties.$field);
}
};
}
impl Artist { impl Artist {
pub fn new(id: ArtistId) -> Self { pub fn new(id: ArtistId) -> Self {
Artist { Artist {
@ -334,106 +431,189 @@ impl Artist {
} }
} }
fn add_musicbrainz_url<S: AsRef<str>>(&mut self, url: S) -> Result<(), Error> { fn add_unique_url<
if self.properties.musicbrainz.is_some() { S: AsRef<str>,
return Err(Error::CollectionError(format!( T: for<'a> TryFrom<&'a str, Error = Error> + Eq + AsRef<str>,
"artist '{}' already has a MusicBrainz URL", >(
self.id artist_id: &ArtistId,
))); label: &'static str,
container: &mut Option<T>,
url: S,
) -> Result<(), Error> {
let url: T = url.as_ref().try_into()?;
if let Some(current) = container {
if current == &url {
return Err(Error::CollectionError(format!(
"artist '{}' already has this {} URL: {}",
artist_id,
label,
current.as_ref()
)));
} else {
return Err(Error::CollectionError(format!(
"artist '{}' already has a different {} URL: {}",
artist_id,
label,
current.as_ref()
)));
}
} }
self.properties.musicbrainz = Some(MusicBrainz::new(url)?); _ = container.insert(url);
Ok(()) Ok(())
} }
fn remove_musicbrainz_url<S: AsRef<str>>(&mut self, url: S) -> Result<(), Error> { fn remove_unique_url<
if self.properties.musicbrainz.is_none() { S: AsRef<str>,
T: for<'a> TryFrom<&'a str, Error = Error> + Eq + AsRef<str>,
>(
artist_id: &ArtistId,
label: &'static str,
container: &mut Option<T>,
url: S,
) -> Result<(), Error> {
if container.is_none() {
return Err(Error::CollectionError(format!( return Err(Error::CollectionError(format!(
"artist '{}' does not have a MusicBrainz URL", "artist '{}' does not have a {} URL",
self.id artist_id, label
))); )));
} }
if self.properties.musicbrainz.as_ref().unwrap().0.as_str() == url.as_ref() { let url: T = url.as_ref().try_into()?;
self.properties.musicbrainz = None; if container.as_ref().unwrap() == &url {
_ = container.take();
Ok(()) Ok(())
} else { } else {
Err(Error::CollectionError(format!( Err(Error::CollectionError(format!(
"artist '{}' does not have this MusicBrainz URL {}", "artist '{}' does not have this {} URL: {}",
self.id, artist_id,
label,
url.as_ref(), url.as_ref(),
))) )))
} }
} }
fn set_musicbrainz_url<S: AsRef<str>>(&mut self, url: S) -> Result<(), Error> { fn set_unique_url<S: AsRef<str>, T: for<'a> TryFrom<&'a str, Error = Error>>(
self.properties.musicbrainz = Some(MusicBrainz::new(url)?); container: &mut Option<T>,
url: S,
) -> Result<(), Error> {
_ = container.insert(url.as_ref().try_into()?);
Ok(()) Ok(())
} }
fn clear_musicbrainz_url(&mut self) -> Result<(), Error> { fn clear_unique_url<T>(container: &mut Option<T>) {
self.properties.musicbrainz = None; _ = container.take();
Ok(())
} }
fn add_musicbutler_urls<S: AsRef<str>>(&mut self, urls: Vec<S>) -> Result<(), Error> { fn add_multi_urls<
S: AsRef<str>,
T: for<'a> TryFrom<&'a str, Error = Error> + Eq + AsRef<str>,
>(
artist_id: &ArtistId,
label: &'static str,
container: &mut Vec<T>,
urls: Vec<S>,
) -> Result<(), Error> {
// Convert into URLs first to facilitate later comparison. // Convert into URLs first to facilitate later comparison.
let urls: Result<Vec<MusicButler>, Error> = urls.iter().map(MusicButler::new).collect(); let urls: Result<Vec<T>, Error> = urls.iter().map(|url| url.as_ref().try_into()).collect();
let mut urls = urls?; let mut urls = urls?;
// Do not check and insert. First check if any of the provided URLs already exist so that // Do not check and insert. First check if any of the provided URLs already exist so that
// the vector remains unchanged in case of failure. // the vector remains unchanged in case of failure.
let overlap: Vec<&MusicButler> = urls let overlap: Vec<&T> = urls.iter().filter(|url| container.contains(url)).collect();
.iter()
.filter(|url| self.properties.musicbutler.contains(url))
.collect();
if !overlap.is_empty() { if !overlap.is_empty() {
return Err(Error::CollectionError(format!( return Err(Error::CollectionError(format!(
"artist '{}' already has these MusicButler URL(s): {:?}", "artist '{}' already has these {} URL(s): {}",
self.id, overlap artist_id,
label,
overlap.iter().map(|url| url.as_ref()).format(", ")
))); )));
} }
self.properties.musicbutler.append(&mut urls); container.append(&mut urls);
Ok(()) Ok(())
} }
fn remove_musicbutler_urls<S: AsRef<str>>(&mut self, urls: Vec<S>) -> Result<(), Error> { fn remove_multi_urls<
S: AsRef<str>,
T: for<'a> TryFrom<&'a str, Error = Error> + Eq + AsRef<str>,
>(
artist_id: &ArtistId,
label: &'static str,
container: &mut Vec<T>,
urls: Vec<S>,
) -> Result<(), Error> {
// Convert into URLs first to facilitate later comparison. // Convert into URLs first to facilitate later comparison.
let urls: Result<Vec<MusicButler>, Error> = urls.iter().map(MusicButler::new).collect(); let urls: Result<Vec<T>, Error> = urls.iter().map(|url| url.as_ref().try_into()).collect();
let urls = urls?; let urls = urls?;
// Do not check and insert. First check if any of the provided URLs already exist so that // Do not check and insert. First check if any of the provided URLs already exist so that
// the vector remains unchanged in case of failure. // the vector remains unchanged in case of failure.
let difference: Vec<&MusicButler> = urls let difference: Vec<&T> = urls.iter().filter(|url| !container.contains(url)).collect();
.iter()
.filter(|url| !self.properties.musicbutler.contains(url))
.collect();
if !difference.is_empty() { if !difference.is_empty() {
return Err(Error::CollectionError(format!( return Err(Error::CollectionError(format!(
"artist '{}' does not have these MusicButler URL(s): {:?}", "artist '{}' does not have these {} URL(s): {}",
self.id, difference artist_id,
label,
difference.iter().map(|url| url.as_ref()).format(", ")
))); )));
} }
let musicbutler = mem::take(&mut self.properties.musicbutler); container.retain(|url| !urls.contains(url));
self.properties.musicbutler = musicbutler
.into_iter()
.filter(|url| !urls.contains(url))
.collect();
Ok(()) Ok(())
} }
fn set_musicbutler_urls<S: AsRef<str>>(&mut self, urls: Vec<S>) -> Result<(), Error> { fn set_multi_urls<S: AsRef<str>, T: for<'a> TryFrom<&'a str, Error = Error>>(
let urls: Result<Vec<MusicButler>, Error> = urls.iter().map(MusicButler::new).collect(); container: &mut Vec<T>,
self.properties.musicbutler = urls?; urls: Vec<S>,
) -> Result<(), Error> {
let urls: Result<Vec<T>, Error> = urls.iter().map(|url| url.as_ref().try_into()).collect();
let mut urls = urls?;
container.clear();
container.append(&mut urls);
Ok(()) Ok(())
} }
fn clear_musicbutler_urls(&mut self) -> Result<(), Error> { fn clear_multi_urls<T>(container: &mut Vec<T>) {
self.properties.musicbutler.clear(); container.clear();
Ok(())
} }
artist_unique_url_dispatch!(
add_musicbrainz_url,
remove_musicbrainz_url,
set_musicbrainz_url,
clear_musicbrainz_url,
"MusicBrainz",
musicbrainz
);
artist_multi_url_dispatch!(
add_musicbutler_urls,
remove_musicbutler_urls,
set_musicbutler_urls,
clear_musicbutler_urls,
"MusicButler",
musicbutler
);
artist_multi_url_dispatch!(
add_bandcamp_urls,
remove_bandcamp_urls,
set_bandcamp_urls,
clear_bandcamp_urls,
"Bandcamp",
bandcamp
);
artist_unique_url_dispatch!(
add_qobuz_url,
remove_qobuz_url,
set_qobuz_url,
clear_qobuz_url,
"Qobuz",
qobuz
);
} }
impl PartialOrd for Artist { impl PartialOrd for Artist {
@ -597,6 +777,72 @@ pub struct MusicHoard<LIB, DB> {
collection: Collection, collection: Collection,
} }
macro_rules! music_hoard_unique_url_dispatch {
($add:ident, $remove:ident, $set:ident, $clear:ident) => {
pub fn $add<ID: AsRef<ArtistId>, S: AsRef<str>>(
&mut self,
artist_id: ID,
url: S,
) -> Result<(), Error> {
self.get_artist_or_err(artist_id.as_ref())?.$add(url)
}
pub fn $remove<ID: AsRef<ArtistId>, S: AsRef<str>>(
&mut self,
artist_id: ID,
url: S,
) -> Result<(), Error> {
self.get_artist_or_err(artist_id.as_ref())?.$remove(url)
}
pub fn $set<ID: AsRef<ArtistId>, S: AsRef<str>>(
&mut self,
artist_id: ID,
url: S,
) -> Result<(), Error> {
self.get_artist_or_err(artist_id.as_ref())?.$set(url)
}
pub fn $clear<ID: AsRef<ArtistId>>(&mut self, artist_id: ID) -> Result<(), Error> {
self.get_artist_or_err(artist_id.as_ref())?.$clear();
Ok(())
}
};
}
macro_rules! music_hoard_multi_url_dispatch {
($add:ident, $remove:ident, $set:ident, $clear:ident) => {
pub fn $add<ID: AsRef<ArtistId>, S: AsRef<str>>(
&mut self,
artist_id: ID,
urls: Vec<S>,
) -> Result<(), Error> {
self.get_artist_or_err(artist_id.as_ref())?.$add(urls)
}
pub fn $remove<ID: AsRef<ArtistId>, S: AsRef<str>>(
&mut self,
artist_id: ID,
urls: Vec<S>,
) -> Result<(), Error> {
self.get_artist_or_err(artist_id.as_ref())?.$remove(urls)
}
pub fn $set<ID: AsRef<ArtistId>, S: AsRef<str>>(
&mut self,
artist_id: ID,
urls: Vec<S>,
) -> Result<(), Error> {
self.get_artist_or_err(artist_id.as_ref())?.$set(urls)
}
pub fn $clear<ID: AsRef<ArtistId>>(&mut self, artist_id: ID) -> Result<(), Error> {
self.get_artist_or_err(artist_id.as_ref())?.$clear();
Ok(())
}
};
}
impl<LIB: ILibrary, DB: IDatabase> MusicHoard<LIB, DB> { impl<LIB: ILibrary, DB: IDatabase> MusicHoard<LIB, DB> {
/// Create a new [`MusicHoard`] with the provided [`ILibrary`] and [`IDatabase`]. /// Create a new [`MusicHoard`] with the provided [`ILibrary`] and [`IDatabase`].
pub fn new(library: Option<LIB>, database: Option<DB>) -> Self { pub fn new(library: Option<LIB>, database: Option<DB>) -> Self {
@ -687,75 +933,33 @@ impl<LIB: ILibrary, DB: IDatabase> MusicHoard<LIB, DB> {
} }
} }
pub fn add_musicbrainz_url<ID: AsRef<ArtistId>, S: AsRef<str>>( music_hoard_unique_url_dispatch!(
&mut self, add_musicbrainz_url,
artist_id: ID, remove_musicbrainz_url,
url: S, set_musicbrainz_url,
) -> Result<(), Error> { clear_musicbrainz_url
self.get_artist_or_err(artist_id.as_ref())? );
.add_musicbrainz_url(url)
}
pub fn remove_musicbrainz_url<ID: AsRef<ArtistId>, S: AsRef<str>>( music_hoard_multi_url_dispatch!(
&mut self, add_musicbutler_urls,
artist_id: ID, remove_musicbutler_urls,
url: S, set_musicbutler_urls,
) -> Result<(), Error> { clear_musicbutler_urls
self.get_artist_or_err(artist_id.as_ref())? );
.remove_musicbrainz_url(url)
}
pub fn set_musicbrainz_url<ID: AsRef<ArtistId>, S: AsRef<str>>( music_hoard_multi_url_dispatch!(
&mut self, add_bandcamp_urls,
artist_id: ID, remove_bandcamp_urls,
url: S, set_bandcamp_urls,
) -> Result<(), Error> { clear_bandcamp_urls
self.get_artist_or_err(artist_id.as_ref())? );
.set_musicbrainz_url(url)
}
pub fn clear_musicbrainz_url<ID: AsRef<ArtistId>>( music_hoard_unique_url_dispatch!(
&mut self, add_qobuz_url,
artist_id: ID, remove_qobuz_url,
) -> Result<(), Error> { set_qobuz_url,
self.get_artist_or_err(artist_id.as_ref())? clear_qobuz_url
.clear_musicbrainz_url() );
}
pub fn add_musicbutler_urls<ID: AsRef<ArtistId>, S: AsRef<str>>(
&mut self,
artist_id: ID,
urls: Vec<S>,
) -> Result<(), Error> {
self.get_artist_or_err(artist_id.as_ref())?
.add_musicbutler_urls(urls)
}
pub fn remove_musicbutler_urls<ID: AsRef<ArtistId>, S: AsRef<str>>(
&mut self,
artist_id: ID,
urls: Vec<S>,
) -> Result<(), Error> {
self.get_artist_or_err(artist_id.as_ref())?
.remove_musicbutler_urls(urls)
}
pub fn set_musicbutler_urls<ID: AsRef<ArtistId>, S: AsRef<str>>(
&mut self,
artist_id: ID,
urls: Vec<S>,
) -> Result<(), Error> {
self.get_artist_or_err(artist_id.as_ref())?
.set_musicbutler_urls(urls)
}
pub fn clear_musicbutler_urls<ID: AsRef<ArtistId>>(
&mut self,
artist_id: ID,
) -> Result<(), Error> {
self.get_artist_or_err(artist_id.as_ref())?
.clear_musicbutler_urls()
}
fn sort(collection: &mut [Artist]) { fn sort(collection: &mut [Artist]) {
collection.sort_unstable(); collection.sort_unstable();