Add method to manually add artist metadata #85

Merged
wojtek merged 16 commits from 55---add-method-to-manually-add-artist-metadata into main 2024-01-10 22:33:58 +01:00
3 changed files with 346 additions and 7 deletions
Showing only changes of commit 73c90a7c11 - Show all commits

View File

@ -23,14 +23,19 @@ tempfile = "3.5.0"
[features] [features]
default = ["database-json", "library-beets"] default = ["database-json", "library-beets"]
bin = ["structopt"]
database-json = ["serde_json"] database-json = ["serde_json"]
library-beets = [] library-beets = []
ssh-library = ["openssh", "tokio"] ssh-library = ["openssh", "tokio"]
tui = ["structopt", "crossterm", "ratatui"] tui = ["crossterm", "ratatui"]
[[bin]] [[bin]]
name = "musichoard" name = "musichoard"
required-features = ["database-json", "library-beets", "ssh-library", "tui"] required-features = ["bin", "database-json", "library-beets", "ssh-library", "tui"]
[[bin]]
name = "mh-edit"
required-features = ["bin", "database-json"]
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true

181
src/bin/mh-edit.rs Normal file
View File

@ -0,0 +1,181 @@
use std::fs::OpenOptions;
use std::path::PathBuf;
use structopt::StructOpt;
use musichoard::{
database::{
json::{backend::JsonDatabaseFileBackend, JsonDatabase},
IDatabase,
},
library::NoLibrary,
Artist, ArtistId, ArtistProperties, MusicHoard,
};
#[derive(StructOpt, Debug)]
struct Opt {
#[structopt(subcommand)]
category: Category,
#[structopt(
long = "database",
name = "database file path",
default_value = "database.json"
)]
database_file_path: PathBuf,
}
#[derive(StructOpt, Debug)]
enum Category {
Artist(ArtistCommand),
}
#[derive(StructOpt, Debug)]
enum ArtistCommand {
New(ArtistValue),
Delete(ArtistValue),
#[structopt(name = "musicbrainz")]
MusicBrainz(UrlCommand<SingleUrlValue>),
#[structopt(name = "musicbutler")]
MusicButler(UrlCommand<MultiUrlValue>),
Bandcamp(UrlCommand<MultiUrlValue>),
Qobuz(UrlCommand<SingleUrlValue>),
}
#[derive(StructOpt, Debug)]
struct ArtistValue {
artist: String,
}
#[derive(StructOpt, Debug)]
enum UrlCommand<T: StructOpt> {
Add(T),
Remove(T),
Set(T),
Clear(ArtistValue),
}
#[derive(StructOpt, Debug)]
struct SingleUrlValue {
artist: String,
url: String,
}
#[derive(StructOpt, Debug)]
struct MultiUrlValue {
artist: String,
urls: Vec<String>,
}
fn main() {
let opt = Opt::from_args();
let lib: Option<NoLibrary> = None;
let db = Some(JsonDatabase::new(JsonDatabaseFileBackend::new(
&opt.database_file_path,
)));
let mut music_hoard = MusicHoard::new(lib, db);
music_hoard
.load_from_database()
.expect("failed to load database");
match opt.category {
Category::Artist(artist_command) => {
match artist_command {
ArtistCommand::New(artist_value) => {
music_hoard
.new_artist(ArtistId::new(artist_value.artist))
.expect("failed to add new artist");
}
ArtistCommand::Delete(artist_value) => {
music_hoard
.delete_artist(ArtistId::new(artist_value.artist))
.expect("failed to delete artist");
}
ArtistCommand::MusicBrainz(url_command) => match url_command {
UrlCommand::Add(single_url_value) => {
music_hoard
.add_musicbrainz_url(
ArtistId::new(single_url_value.artist),
single_url_value.url,
)
.expect("failed to add MusicBrainz URL");
}
UrlCommand::Remove(single_url_value) => {
music_hoard
.remove_musicbrainz_url(
ArtistId::new(single_url_value.artist),
single_url_value.url,
)
.expect("failed to remove MusicBrainz URL");
}
UrlCommand::Set(single_url_value) => {
music_hoard
.set_musicbrainz_url(
ArtistId::new(single_url_value.artist),
single_url_value.url,
)
.expect("failed to set MusicBrainz URL");
}
UrlCommand::Clear(artist_value) => {
music_hoard
.clear_musicbrainz_url(ArtistId::new(artist_value.artist))
.expect("failed to clear MusicBrainz URL");
}
},
ArtistCommand::MusicButler(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::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.
}
}
}
}
}
}
music_hoard
.save_to_database()
.expect("failed to save database");
}

View File

@ -279,6 +279,18 @@ pub struct ArtistId {
pub name: String, pub name: String,
} }
impl ArtistId {
pub fn new<S: Into<String>>(name: S) -> ArtistId {
ArtistId { name: name.into() }
}
}
impl fmt::Display for ArtistId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.name)
}
}
/// The artist properties. /// The artist properties.
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] #[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
pub struct ArtistProperties { pub struct ArtistProperties {
@ -307,6 +319,58 @@ pub struct Artist {
pub albums: Vec<Album>, pub albums: Vec<Album>,
} }
impl Artist {
pub fn new(id: ArtistId) -> Self {
Artist {
id,
properties: ArtistProperties::default(),
albums: vec![],
}
}
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
)));
}
self.properties.musicbrainz = Some(MusicBrainz::new(url)?);
Ok(())
}
fn remove_musicbrainz_url<S: AsRef<str>>(&mut self, url: S) -> Result<(), Error> {
if self.properties.musicbrainz.is_none() {
return Err(Error::CollectionError(format!(
"artist '{}' does not have a MusicBrainz URL",
self.id
)));
}
if self.properties.musicbrainz.as_ref().unwrap().0.as_str() == url.as_ref() {
self.properties.musicbrainz = None;
Ok(())
} else {
Err(Error::CollectionError(format!(
"artist '{}' does not have this MusicBrainz URL {}",
self.id,
url.as_ref(),
)))
}
}
fn set_musicbrainz_url<S: AsRef<str>>(&mut self, url: S) -> Result<(), Error> {
self.properties.musicbrainz = Some(MusicBrainz::new(url)?);
Ok(())
}
fn clear_musicbrainz_url(&mut self) -> Result<(), Error> {
self.properties.musicbrainz = None;
Ok(())
}
}
impl PartialOrd for Artist { impl PartialOrd for Artist {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other)) Some(self.cmp(other))
@ -398,6 +462,8 @@ where
/// Error type for `musichoard`. /// Error type for `musichoard`.
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum Error { pub enum Error {
/// The [`MusicHoard`] is not able to read/write its in-memory collection.
CollectionError(String),
/// The [`MusicHoard`] failed to read/write from/to the library. /// The [`MusicHoard`] failed to read/write from/to the library.
LibraryError(String), LibraryError(String),
/// The [`MusicHoard`] failed to read/write from/to the database. /// The [`MusicHoard`] failed to read/write from/to the database.
@ -411,6 +477,7 @@ pub enum Error {
impl fmt::Display for Error { impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self { match *self {
Self::CollectionError(ref s) => write!(f, "failed to read/write the collection: {s}"),
Self::LibraryError(ref s) => write!(f, "failed to read/write from/to the library: {s}"), Self::LibraryError(ref s) => write!(f, "failed to read/write from/to the library: {s}"),
Self::DatabaseError(ref s) => { Self::DatabaseError(ref s) => {
write!(f, "failed to read/write from/to the database: {s}") write!(f, "failed to read/write from/to the database: {s}")
@ -572,11 +639,7 @@ impl<LIB: ILibrary, DB: IDatabase> MusicHoard<LIB, DB> {
.unwrap() .unwrap()
} else { } else {
album_ids.insert(artist_id.clone(), HashSet::<AlbumId>::new()); album_ids.insert(artist_id.clone(), HashSet::<AlbumId>::new());
artists.push(Artist { artists.push(Artist::new(artist_id.clone()));
id: artist_id.clone(),
properties: ArtistProperties::default(),
albums: vec![],
});
artists.last_mut().unwrap() artists.last_mut().unwrap()
}; };
@ -605,6 +668,96 @@ impl<LIB: ILibrary, DB: IDatabase> MusicHoard<LIB, DB> {
artists artists
} }
pub fn new_artist(&mut self, artist_id: ArtistId) -> Result<(), Error> {
// We want to return an error if the artist already exists so we first do a check.
let artists: &Vec<Artist> = &self.collection;
if let Some(ref a) = artists.iter().find(|a| a.id == artist_id) {
return Err(Error::CollectionError(format!(
"artist '{}' is already in the collection",
a.id
)));
}
let new_artist = vec![Artist::new(artist_id)];
let collection = mem::take(&mut self.collection);
self.collection = Self::merge(collection, new_artist);
Ok(())
}
pub fn delete_artist(&mut self, artist_id: ArtistId) -> Result<(), Error> {
let index_opt = self.collection.iter().position(|a| a.id == artist_id);
match index_opt {
Some(index) => {
self.collection.remove(index);
Ok(())
}
None => Err(Error::CollectionError(format!(
"artist '{}' is not in the collection",
artist_id
))),
}
}
pub fn add_musicbrainz_url<S: AsRef<str>>(
&mut self,
artist_id: ArtistId,
url: S,
) -> Result<(), Error> {
let mut artist_opt = self.collection.iter_mut().find(|a| a.id == artist_id);
match artist_opt {
Some(ref mut artist) => artist.add_musicbrainz_url(url),
None => Err(Error::CollectionError(format!(
"artist '{}' is not in the collection",
artist_id
))),
}
}
pub fn remove_musicbrainz_url<S: AsRef<str>>(
&mut self,
artist_id: ArtistId,
url: S,
) -> Result<(), Error> {
let mut artist_opt = self.collection.iter_mut().find(|a| a.id == artist_id);
match artist_opt {
Some(ref mut artist) => artist.remove_musicbrainz_url(url),
None => Err(Error::CollectionError(format!(
"artist '{}' is not in the collection",
artist_id
))),
}
}
pub fn set_musicbrainz_url<S: AsRef<str>>(
&mut self,
artist_id: ArtistId,
url: S,
) -> Result<(), Error> {
let mut artist_opt = self.collection.iter_mut().find(|a| a.id == artist_id);
match artist_opt {
Some(ref mut artist) => artist.set_musicbrainz_url(url),
None => Err(Error::CollectionError(format!(
"artist '{}' is not in the collection",
artist_id
))),
}
}
pub fn clear_musicbrainz_url(&mut self, artist_id: ArtistId) -> Result<(), Error> {
let mut artist_opt = self.collection.iter_mut().find(|a| a.id == artist_id);
match artist_opt {
Some(ref mut artist) => artist.clear_musicbrainz_url(),
None => Err(Error::CollectionError(format!(
"artist '{}' is not in the collection",
artist_id
))),
}
}
} }
#[cfg(test)] #[cfg(test)]