Frist draft of mh-edit binary
This commit is contained in:
parent
1bc612dc45
commit
73c90a7c11
@ -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
181
src/bin/mh-edit.rs
Normal 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");
|
||||||
|
}
|
163
src/lib.rs
163
src/lib.rs
@ -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)]
|
||||||
|
Loading…
Reference in New Issue
Block a user