diff --git a/Cargo.toml b/Cargo.toml index 33dc5d7..395c326 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,14 +23,19 @@ tempfile = "3.5.0" [features] default = ["database-json", "library-beets"] +bin = ["structopt"] database-json = ["serde_json"] library-beets = [] ssh-library = ["openssh", "tokio"] -tui = ["structopt", "crossterm", "ratatui"] +tui = ["crossterm", "ratatui"] [[bin]] 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] all-features = true diff --git a/src/bin/mh-edit.rs b/src/bin/mh-edit.rs new file mode 100644 index 0000000..58493c5 --- /dev/null +++ b/src/bin/mh-edit.rs @@ -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), + #[structopt(name = "musicbutler")] + MusicButler(UrlCommand), + Bandcamp(UrlCommand), + Qobuz(UrlCommand), +} + +#[derive(StructOpt, Debug)] +struct ArtistValue { + artist: String, +} + +#[derive(StructOpt, Debug)] +enum UrlCommand { + 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, +} + +fn main() { + let opt = Opt::from_args(); + + let lib: Option = 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"); +} diff --git a/src/lib.rs b/src/lib.rs index 93fe3c6..ec1ae65 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -279,6 +279,18 @@ pub struct ArtistId { pub name: String, } +impl ArtistId { + pub fn new>(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. #[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] pub struct ArtistProperties { @@ -307,6 +319,58 @@ pub struct Artist { pub albums: Vec, } +impl Artist { + pub fn new(id: ArtistId) -> Self { + Artist { + id, + properties: ArtistProperties::default(), + albums: vec![], + } + } + + fn add_musicbrainz_url>(&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>(&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>(&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 { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) @@ -398,6 +462,8 @@ where /// Error type for `musichoard`. #[derive(Debug, PartialEq, Eq)] 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. LibraryError(String), /// The [`MusicHoard`] failed to read/write from/to the database. @@ -411,6 +477,7 @@ pub enum Error { impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 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::DatabaseError(ref s) => { write!(f, "failed to read/write from/to the database: {s}") @@ -572,11 +639,7 @@ impl MusicHoard { .unwrap() } else { album_ids.insert(artist_id.clone(), HashSet::::new()); - artists.push(Artist { - id: artist_id.clone(), - properties: ArtistProperties::default(), - albums: vec![], - }); + artists.push(Artist::new(artist_id.clone())); artists.last_mut().unwrap() }; @@ -605,6 +668,96 @@ impl MusicHoard { 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 = &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>( + &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>( + &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>( + &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)]