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

View File

@ -7,6 +7,7 @@ edition = "2021"
[dependencies]
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}
ratatui = { version = "0.20.1", optional = true}
serde = { version = "1.0.159", features = ["derive"] }

View File

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

View File

@ -6,12 +6,13 @@ pub mod library;
use std::{
cmp::Ordering,
collections::{HashMap, HashSet},
fmt,
fmt::{self, Debug},
iter::Peekable,
mem,
};
use database::IDatabase;
use itertools::Itertools;
use library::{ILibrary, Item, Query};
use serde::{Deserialize, Serialize};
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 {
fn url(&self) -> &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 {
fn url(&self) -> &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 {
fn url(&self) -> &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 {
fn url(&self) -> &str {
self.0.as_str()
@ -325,6 +382,46 @@ pub struct Artist {
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 {
pub fn new(id: ArtistId) -> Self {
Artist {
@ -334,106 +431,189 @@ impl Artist {
}
}
fn add_musicbrainz_url<S: AsRef<str>>(&mut self, url: S) -> Result<(), Error> {
if self.properties.musicbrainz.is_some() {
return Err(Error::CollectionError(format!(
"artist '{}' already has a MusicBrainz URL",
self.id
)));
fn add_unique_url<
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> {
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(())
}
fn remove_musicbrainz_url<S: AsRef<str>>(&mut self, url: S) -> Result<(), Error> {
if self.properties.musicbrainz.is_none() {
fn remove_unique_url<
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!(
"artist '{}' does not have a MusicBrainz URL",
self.id
"artist '{}' does not have a {} URL",
artist_id, label
)));
}
if self.properties.musicbrainz.as_ref().unwrap().0.as_str() == url.as_ref() {
self.properties.musicbrainz = None;
let url: T = url.as_ref().try_into()?;
if container.as_ref().unwrap() == &url {
_ = container.take();
Ok(())
} else {
Err(Error::CollectionError(format!(
"artist '{}' does not have this MusicBrainz URL {}",
self.id,
"artist '{}' does not have this {} URL: {}",
artist_id,
label,
url.as_ref(),
)))
}
}
fn set_musicbrainz_url<S: AsRef<str>>(&mut self, url: S) -> Result<(), Error> {
self.properties.musicbrainz = Some(MusicBrainz::new(url)?);
fn set_unique_url<S: AsRef<str>, T: for<'a> TryFrom<&'a str, Error = Error>>(
container: &mut Option<T>,
url: S,
) -> Result<(), Error> {
_ = container.insert(url.as_ref().try_into()?);
Ok(())
}
fn clear_musicbrainz_url(&mut self) -> Result<(), Error> {
self.properties.musicbrainz = None;
Ok(())
fn clear_unique_url<T>(container: &mut Option<T>) {
_ = container.take();
}
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.
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?;
// 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.
let overlap: Vec<&MusicButler> = urls
.iter()
.filter(|url| self.properties.musicbutler.contains(url))
.collect();
let overlap: Vec<&T> = urls.iter().filter(|url| container.contains(url)).collect();
if !overlap.is_empty() {
return Err(Error::CollectionError(format!(
"artist '{}' already has these MusicButler URL(s): {:?}",
self.id, overlap
"artist '{}' already has these {} URL(s): {}",
artist_id,
label,
overlap.iter().map(|url| url.as_ref()).format(", ")
)));
}
self.properties.musicbutler.append(&mut urls);
container.append(&mut urls);
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.
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?;
// 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.
let difference: Vec<&MusicButler> = urls
.iter()
.filter(|url| !self.properties.musicbutler.contains(url))
.collect();
let difference: Vec<&T> = urls.iter().filter(|url| !container.contains(url)).collect();
if !difference.is_empty() {
return Err(Error::CollectionError(format!(
"artist '{}' does not have these MusicButler URL(s): {:?}",
self.id, difference
"artist '{}' does not have these {} URL(s): {}",
artist_id,
label,
difference.iter().map(|url| url.as_ref()).format(", ")
)));
}
let musicbutler = mem::take(&mut self.properties.musicbutler);
self.properties.musicbutler = musicbutler
.into_iter()
.filter(|url| !urls.contains(url))
.collect();
container.retain(|url| !urls.contains(url));
Ok(())
}
fn set_musicbutler_urls<S: AsRef<str>>(&mut self, urls: Vec<S>) -> Result<(), Error> {
let urls: Result<Vec<MusicButler>, Error> = urls.iter().map(MusicButler::new).collect();
self.properties.musicbutler = urls?;
fn set_multi_urls<S: AsRef<str>, T: for<'a> TryFrom<&'a str, Error = Error>>(
container: &mut Vec<T>,
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(())
}
fn clear_musicbutler_urls(&mut self) -> Result<(), Error> {
self.properties.musicbutler.clear();
Ok(())
fn clear_multi_urls<T>(container: &mut Vec<T>) {
container.clear();
}
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 {
@ -597,6 +777,72 @@ pub struct MusicHoard<LIB, DB> {
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> {
/// Create a new [`MusicHoard`] with the provided [`ILibrary`] and [`IDatabase`].
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>>(
&mut self,
artist_id: ID,
url: S,
) -> Result<(), Error> {
self.get_artist_or_err(artist_id.as_ref())?
.add_musicbrainz_url(url)
}
music_hoard_unique_url_dispatch!(
add_musicbrainz_url,
remove_musicbrainz_url,
set_musicbrainz_url,
clear_musicbrainz_url
);
pub fn remove_musicbrainz_url<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_musicbrainz_url(url)
}
music_hoard_multi_url_dispatch!(
add_musicbutler_urls,
remove_musicbutler_urls,
set_musicbutler_urls,
clear_musicbutler_urls
);
pub fn set_musicbrainz_url<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_musicbrainz_url(url)
}
music_hoard_multi_url_dispatch!(
add_bandcamp_urls,
remove_bandcamp_urls,
set_bandcamp_urls,
clear_bandcamp_urls
);
pub fn clear_musicbrainz_url<ID: AsRef<ArtistId>>(
&mut self,
artist_id: ID,
) -> Result<(), Error> {
self.get_artist_or_err(artist_id.as_ref())?
.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()
}
music_hoard_unique_url_dispatch!(
add_qobuz_url,
remove_qobuz_url,
set_qobuz_url,
clear_qobuz_url
);
fn sort(collection: &mut [Artist]) {
collection.sort_unstable();