Enable changs via command
This commit is contained in:
parent
0fee810040
commit
adddc5ba2f
@ -3,7 +3,7 @@ use std::path::PathBuf;
|
|||||||
use structopt::{clap::AppSettings, StructOpt};
|
use structopt::{clap::AppSettings, StructOpt};
|
||||||
|
|
||||||
use musichoard::{
|
use musichoard::{
|
||||||
collection::artist::ArtistId,
|
collection::{album::AlbumId, artist::ArtistId},
|
||||||
database::json::{backend::JsonDatabaseFileBackend, JsonDatabase},
|
database::json::{backend::JsonDatabaseFileBackend, JsonDatabase},
|
||||||
MusicHoard, MusicHoardBuilder, NoLibrary,
|
MusicHoard, MusicHoardBuilder, NoLibrary,
|
||||||
};
|
};
|
||||||
@ -22,56 +22,54 @@ struct Opt {
|
|||||||
database_file_path: PathBuf,
|
database_file_path: PathBuf,
|
||||||
|
|
||||||
#[structopt(subcommand)]
|
#[structopt(subcommand)]
|
||||||
category: Category,
|
command: Command,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(StructOpt, Debug)]
|
#[derive(StructOpt, Debug)]
|
||||||
enum Category {
|
enum Command {
|
||||||
#[structopt(about = "Edit artist information")]
|
#[structopt(about = "Modify artist information")]
|
||||||
Artist(ArtistCommand),
|
Artist(ArtistOpt),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Category {
|
#[derive(StructOpt, Debug)]
|
||||||
fn handle(self, music_hoard: &mut MH) {
|
struct ArtistOpt {
|
||||||
match self {
|
// For some reason, not specyfing the artist name with the `long` name makes StructOpt failed
|
||||||
Category::Artist(artist_command) => artist_command.handle(music_hoard),
|
// for inexplicable reason. For example, it won't recognise `Abadde` or `Abadden` as a name and
|
||||||
}
|
// will insteady try to process it as a command.
|
||||||
}
|
#[structopt(long, help = "The name of the artist")]
|
||||||
|
name: String,
|
||||||
|
|
||||||
|
#[structopt(subcommand)]
|
||||||
|
command: ArtistCommand,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(StructOpt, Debug)]
|
#[derive(StructOpt, Debug)]
|
||||||
enum ArtistCommand {
|
enum ArtistCommand {
|
||||||
#[structopt(about = "Add a new artist to the collection")]
|
#[structopt(about = "Add a new artist to the collection")]
|
||||||
Add(ArtistValue),
|
Add,
|
||||||
#[structopt(about = "Remove an artist from the collection")]
|
#[structopt(about = "Remove an artist from the collection")]
|
||||||
Remove(ArtistValue),
|
Remove,
|
||||||
#[structopt(about = "Edit the artist's sort name")]
|
#[structopt(about = "Edit the artist's sort name")]
|
||||||
Sort(SortCommand),
|
Sort(SortCommand),
|
||||||
#[structopt(name = "musicbrainz", about = "Edit the MusicBrainz URL of an artist")]
|
#[structopt(name = "musicbrainz", about = "Edit the MusicBrainz URL of an artist")]
|
||||||
MusicBrainz(MusicBrainzCommand),
|
MusicBrainz(MusicBrainzCommand),
|
||||||
#[structopt(name = "property", about = "Edit a property of an artist")]
|
#[structopt(about = "Edit a property of an artist")]
|
||||||
Property(PropertyCommand),
|
Property(PropertyCommand),
|
||||||
|
#[structopt(about = "Modify the artist's album information")]
|
||||||
|
Album(AlbumOpt),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(StructOpt, Debug)]
|
#[derive(StructOpt, Debug)]
|
||||||
enum SortCommand {
|
enum SortCommand {
|
||||||
#[structopt(about = "Set the provided name as the artist's sort name")]
|
#[structopt(about = "Set the provided name as the artist's sort name")]
|
||||||
Set(ArtistSortValue),
|
Set(SortValue),
|
||||||
#[structopt(about = "Clear the artist's sort name")]
|
#[structopt(about = "Clear the artist's sort name")]
|
||||||
Clear(ArtistValue),
|
Clear,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(StructOpt, Debug)]
|
#[derive(StructOpt, Debug)]
|
||||||
struct ArtistValue {
|
struct SortValue {
|
||||||
#[structopt(help = "The name of the artist")]
|
#[structopt(help = "The sort name")]
|
||||||
artist: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(StructOpt, Debug)]
|
|
||||||
struct ArtistSortValue {
|
|
||||||
#[structopt(help = "The name of the artist")]
|
|
||||||
artist: String,
|
|
||||||
#[structopt(help = "The sort name of the artist")]
|
|
||||||
sort: String,
|
sort: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,13 +78,11 @@ enum MusicBrainzCommand {
|
|||||||
#[structopt(about = "Set the MusicBrainz URL overwriting any existing value")]
|
#[structopt(about = "Set the MusicBrainz URL overwriting any existing value")]
|
||||||
Set(MusicBrainzValue),
|
Set(MusicBrainzValue),
|
||||||
#[structopt(about = "Clear the MusicBrainz URL)")]
|
#[structopt(about = "Clear the MusicBrainz URL)")]
|
||||||
Clear(ArtistValue),
|
Clear,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(StructOpt, Debug)]
|
#[derive(StructOpt, Debug)]
|
||||||
struct MusicBrainzValue {
|
struct MusicBrainzValue {
|
||||||
#[structopt(help = "The name of the artist")]
|
|
||||||
artist: String,
|
|
||||||
#[structopt(help = "The MusicBrainz URL")]
|
#[structopt(help = "The MusicBrainz URL")]
|
||||||
url: String,
|
url: String,
|
||||||
}
|
}
|
||||||
@ -105,8 +101,6 @@ enum PropertyCommand {
|
|||||||
|
|
||||||
#[derive(StructOpt, Debug)]
|
#[derive(StructOpt, Debug)]
|
||||||
struct PropertyValue {
|
struct PropertyValue {
|
||||||
#[structopt(help = "The name of the artist")]
|
|
||||||
artist: String,
|
|
||||||
#[structopt(help = "The name of the property")]
|
#[structopt(help = "The name of the property")]
|
||||||
property: String,
|
property: String,
|
||||||
#[structopt(help = "The list of values")]
|
#[structopt(help = "The list of values")]
|
||||||
@ -115,101 +109,176 @@ struct PropertyValue {
|
|||||||
|
|
||||||
#[derive(StructOpt, Debug)]
|
#[derive(StructOpt, Debug)]
|
||||||
struct PropertyName {
|
struct PropertyName {
|
||||||
#[structopt(help = "The name of the artist")]
|
|
||||||
artist: String,
|
|
||||||
#[structopt(help = "The name of the property")]
|
#[structopt(help = "The name of the property")]
|
||||||
property: String,
|
property: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ArtistCommand {
|
#[derive(StructOpt, Debug)]
|
||||||
|
struct AlbumOpt {
|
||||||
|
// Using `long` for consistency with `ArtistOpt`.
|
||||||
|
#[structopt(long, help = "The title of the album")]
|
||||||
|
title: String,
|
||||||
|
|
||||||
|
#[structopt(subcommand)]
|
||||||
|
command: AlbumCommand,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(StructOpt, Debug)]
|
||||||
|
enum AlbumCommand {
|
||||||
|
#[structopt(about = "Edit the album's sequence value")]
|
||||||
|
Seq(AlbumSeqCommand),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(StructOpt, Debug)]
|
||||||
|
enum AlbumSeqCommand {
|
||||||
|
#[structopt(about = "Set the sequence value overwriting any existing value")]
|
||||||
|
Set(AlbumSeqValue),
|
||||||
|
#[structopt(about = "Clear the sequence value")]
|
||||||
|
Clear,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(StructOpt, Debug)]
|
||||||
|
struct AlbumSeqValue {
|
||||||
|
#[structopt(help = "The new sequence value")]
|
||||||
|
value: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Command {
|
||||||
fn handle(self, music_hoard: &mut MH) {
|
fn handle(self, music_hoard: &mut MH) {
|
||||||
match self {
|
match self {
|
||||||
ArtistCommand::Add(artist_value) => {
|
Command::Artist(artist_opt) => artist_opt.handle(music_hoard),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ArtistOpt {
|
||||||
|
fn handle(self, music_hoard: &mut MH) {
|
||||||
|
self.command.handle(music_hoard, &self.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ArtistCommand {
|
||||||
|
fn handle(self, music_hoard: &mut MH, artist_name: &str) {
|
||||||
|
match self {
|
||||||
|
ArtistCommand::Add => {
|
||||||
music_hoard
|
music_hoard
|
||||||
.add_artist(ArtistId::new(artist_value.artist))
|
.add_artist(ArtistId::new(artist_name))
|
||||||
.expect("failed to add artist");
|
.expect("failed to add artist");
|
||||||
}
|
}
|
||||||
ArtistCommand::Remove(artist_value) => {
|
ArtistCommand::Remove => {
|
||||||
music_hoard
|
music_hoard
|
||||||
.remove_artist(ArtistId::new(artist_value.artist))
|
.remove_artist(ArtistId::new(artist_name))
|
||||||
.expect("failed to remove artist");
|
.expect("failed to remove artist");
|
||||||
}
|
}
|
||||||
ArtistCommand::Sort(sort_command) => {
|
ArtistCommand::Sort(sort_command) => {
|
||||||
sort_command.handle(music_hoard);
|
sort_command.handle(music_hoard, artist_name);
|
||||||
}
|
}
|
||||||
ArtistCommand::MusicBrainz(musicbrainz_command) => {
|
ArtistCommand::MusicBrainz(musicbrainz_command) => {
|
||||||
musicbrainz_command.handle(music_hoard)
|
musicbrainz_command.handle(music_hoard, artist_name)
|
||||||
}
|
}
|
||||||
ArtistCommand::Property(property_command) => {
|
ArtistCommand::Property(property_command) => {
|
||||||
property_command.handle(music_hoard);
|
property_command.handle(music_hoard, artist_name);
|
||||||
|
}
|
||||||
|
ArtistCommand::Album(album_opt) => {
|
||||||
|
album_opt.handle(music_hoard, artist_name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SortCommand {
|
impl SortCommand {
|
||||||
fn handle(self, music_hoard: &mut MH) {
|
fn handle(self, music_hoard: &mut MH, artist_name: &str) {
|
||||||
match self {
|
match self {
|
||||||
SortCommand::Set(artist_sort_value) => music_hoard
|
SortCommand::Set(artist_sort_value) => music_hoard
|
||||||
.set_artist_sort(
|
.set_artist_sort(
|
||||||
ArtistId::new(artist_sort_value.artist),
|
ArtistId::new(artist_name),
|
||||||
ArtistId::new(artist_sort_value.sort),
|
ArtistId::new(artist_sort_value.sort),
|
||||||
)
|
)
|
||||||
.expect("faild to set artist sort name"),
|
.expect("faild to set artist sort name"),
|
||||||
SortCommand::Clear(artist_value) => music_hoard
|
SortCommand::Clear => music_hoard
|
||||||
.clear_artist_sort(ArtistId::new(artist_value.artist))
|
.clear_artist_sort(ArtistId::new(artist_name))
|
||||||
.expect("failed to clear artist sort name"),
|
.expect("failed to clear artist sort name"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MusicBrainzCommand {
|
impl MusicBrainzCommand {
|
||||||
fn handle(self, music_hoard: &mut MH) {
|
fn handle(self, music_hoard: &mut MH, artist_name: &str) {
|
||||||
match self {
|
match self {
|
||||||
MusicBrainzCommand::Set(musicbrainz_value) => music_hoard
|
MusicBrainzCommand::Set(musicbrainz_value) => music_hoard
|
||||||
.set_musicbrainz_url(
|
.set_artist_musicbrainz(ArtistId::new(artist_name), musicbrainz_value.url)
|
||||||
ArtistId::new(musicbrainz_value.artist),
|
|
||||||
musicbrainz_value.url,
|
|
||||||
)
|
|
||||||
.expect("failed to set MusicBrainz URL"),
|
.expect("failed to set MusicBrainz URL"),
|
||||||
MusicBrainzCommand::Clear(artist_value) => music_hoard
|
MusicBrainzCommand::Clear => music_hoard
|
||||||
.clear_musicbrainz_url(ArtistId::new(artist_value.artist))
|
.clear_artist_musicbrainz(ArtistId::new(artist_name))
|
||||||
.expect("failed to clear MusicBrainz URL"),
|
.expect("failed to clear MusicBrainz URL"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PropertyCommand {
|
impl PropertyCommand {
|
||||||
fn handle(self, music_hoard: &mut MH) {
|
fn handle(self, music_hoard: &mut MH, artist_name: &str) {
|
||||||
match self {
|
match self {
|
||||||
PropertyCommand::Add(property_value) => music_hoard
|
PropertyCommand::Add(property_value) => music_hoard
|
||||||
.add_to_property(
|
.add_to_artist_property(
|
||||||
ArtistId::new(property_value.artist),
|
ArtistId::new(artist_name),
|
||||||
property_value.property,
|
property_value.property,
|
||||||
property_value.values,
|
property_value.values,
|
||||||
)
|
)
|
||||||
.expect("failed to add values to property"),
|
.expect("failed to add values to property"),
|
||||||
PropertyCommand::Remove(property_value) => music_hoard
|
PropertyCommand::Remove(property_value) => music_hoard
|
||||||
.remove_from_property(
|
.remove_from_artist_property(
|
||||||
ArtistId::new(property_value.artist),
|
ArtistId::new(artist_name),
|
||||||
property_value.property,
|
property_value.property,
|
||||||
property_value.values,
|
property_value.values,
|
||||||
)
|
)
|
||||||
.expect("failed to remove values from property"),
|
.expect("failed to remove values from property"),
|
||||||
PropertyCommand::Set(property_value) => music_hoard
|
PropertyCommand::Set(property_value) => music_hoard
|
||||||
.set_property(
|
.set_artist_property(
|
||||||
ArtistId::new(property_value.artist),
|
ArtistId::new(artist_name),
|
||||||
property_value.property,
|
property_value.property,
|
||||||
property_value.values,
|
property_value.values,
|
||||||
)
|
)
|
||||||
.expect("failed to set property"),
|
.expect("failed to set property"),
|
||||||
PropertyCommand::Clear(property_name) => music_hoard
|
PropertyCommand::Clear(property_name) => music_hoard
|
||||||
.clear_property(ArtistId::new(property_name.artist), property_name.property)
|
.clear_artist_property(ArtistId::new(artist_name), property_name.property)
|
||||||
.expect("failed to clear property"),
|
.expect("failed to clear property"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AlbumOpt {
|
||||||
|
fn handle(self, music_hoard: &mut MH, artist_name: &str) {
|
||||||
|
self.command.handle(music_hoard, artist_name, &self.title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AlbumCommand {
|
||||||
|
fn handle(self, music_hoard: &mut MH, artist_name: &str, album_name: &str) {
|
||||||
|
match self {
|
||||||
|
AlbumCommand::Seq(seq_command) => {
|
||||||
|
seq_command.handle(music_hoard, artist_name, album_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AlbumSeqCommand {
|
||||||
|
fn handle(self, music_hoard: &mut MH, artist_name: &str, album_name: &str) {
|
||||||
|
match self {
|
||||||
|
AlbumSeqCommand::Set(seq_value) => music_hoard
|
||||||
|
.set_album_seq(
|
||||||
|
ArtistId::new(artist_name),
|
||||||
|
AlbumId::new(album_name),
|
||||||
|
seq_value.value,
|
||||||
|
)
|
||||||
|
.expect("failed to set sequence value"),
|
||||||
|
AlbumSeqCommand::Clear => music_hoard
|
||||||
|
.clear_album_seq(ArtistId::new(artist_name), AlbumId::new(album_name))
|
||||||
|
.expect("failed to clear sequence value"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let opt = Opt::from_args();
|
let opt = Opt::from_args();
|
||||||
|
|
||||||
@ -219,5 +288,5 @@ fn main() {
|
|||||||
.set_database(db)
|
.set_database(db)
|
||||||
.build()
|
.build()
|
||||||
.expect("failed to initialise MusicHoard");
|
.expect("failed to initialise MusicHoard");
|
||||||
opt.category.handle(&mut music_hoard);
|
opt.command.handle(&mut music_hoard);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
use std::mem;
|
use std::{
|
||||||
|
fmt::{self, Display},
|
||||||
|
mem,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::core::collection::{
|
use crate::core::collection::{
|
||||||
merge::{Merge, MergeSorted, WithId},
|
merge::{Merge, MergeSorted, WithId},
|
||||||
@ -30,25 +33,15 @@ pub struct AlbumId {
|
|||||||
|
|
||||||
// There are crates for handling dates, but we don't need much complexity beyond year-month-day.
|
// There are crates for handling dates, but we don't need much complexity beyond year-month-day.
|
||||||
/// The album's release date.
|
/// The album's release date.
|
||||||
#[derive(Clone, Debug, PartialEq, PartialOrd, Ord, Eq, Hash)]
|
#[derive(Clone, Debug, Default, PartialEq, PartialOrd, Ord, Eq, Hash)]
|
||||||
pub struct AlbumDate {
|
pub struct AlbumDate {
|
||||||
pub year: u32,
|
pub year: u32,
|
||||||
pub month: AlbumMonth,
|
pub month: AlbumMonth,
|
||||||
pub day: u8,
|
pub day: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AlbumDate {
|
|
||||||
fn default() -> Self {
|
|
||||||
AlbumDate {
|
|
||||||
year: 0,
|
|
||||||
month: AlbumMonth::None,
|
|
||||||
day: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The album's sequence to determine order when two or more albums have the same release date.
|
/// The album's sequence to determine order when two or more albums have the same release date.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Ord, Eq, Hash)]
|
#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd, Ord, Eq, Hash)]
|
||||||
pub struct AlbumSeq(pub u8);
|
pub struct AlbumSeq(pub u8);
|
||||||
|
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
@ -69,6 +62,12 @@ pub enum AlbumMonth {
|
|||||||
December = 12,
|
December = 12,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for AlbumMonth {
|
||||||
|
fn default() -> Self {
|
||||||
|
AlbumMonth::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<u8> for AlbumMonth {
|
impl From<u8> for AlbumMonth {
|
||||||
fn from(value: u8) -> Self {
|
fn from(value: u8) -> Self {
|
||||||
match value {
|
match value {
|
||||||
@ -93,6 +92,14 @@ impl Album {
|
|||||||
pub fn get_sort_key(&self) -> (&AlbumDate, &AlbumSeq, &AlbumId) {
|
pub fn get_sort_key(&self) -> (&AlbumDate, &AlbumSeq, &AlbumId) {
|
||||||
(&self.date, &self.seq, &self.id)
|
(&self.date, &self.seq, &self.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_seq(&mut self, seq: AlbumSeq) {
|
||||||
|
self.seq = seq;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_seq(&mut self) {
|
||||||
|
self.seq = AlbumSeq::default();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialOrd for Album {
|
impl PartialOrd for Album {
|
||||||
@ -116,6 +123,24 @@ impl Merge for Album {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AsRef<AlbumId> for AlbumId {
|
||||||
|
fn as_ref(&self) -> &AlbumId {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AlbumId {
|
||||||
|
pub fn new<S: Into<String>>(name: S) -> AlbumId {
|
||||||
|
AlbumId { title: name.into() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for AlbumId {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::core::testmod::FULL_COLLECTION;
|
use crate::core::testmod::FULL_COLLECTION;
|
||||||
|
@ -171,6 +171,22 @@ impl<LIB, DB> MusicHoard<LIB, DB> {
|
|||||||
Error::CollectionError(format!("artist '{}' is not in the collection", artist_id))
|
Error::CollectionError(format!("artist '{}' is not in the collection", artist_id))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_album_mut<'a>(artist: &'a mut Artist, album_id: &AlbumId) -> Option<&'a mut Album> {
|
||||||
|
artist.albums.iter_mut().find(|a| &a.id == album_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_album_mut_or_err<'a>(
|
||||||
|
artist: &'a mut Artist,
|
||||||
|
album_id: &AlbumId,
|
||||||
|
) -> Result<&'a mut Album, Error> {
|
||||||
|
Self::get_album_mut(artist, album_id).ok_or_else(|| {
|
||||||
|
Error::CollectionError(format!(
|
||||||
|
"album '{}' does not belong to the artist",
|
||||||
|
album_id
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<LIB: ILibrary> MusicHoard<LIB, NoDatabase> {
|
impl<LIB: ILibrary> MusicHoard<LIB, NoDatabase> {
|
||||||
@ -243,38 +259,62 @@ impl<LIB, DB: IDatabase> MusicHoard<LIB, DB> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_collection<F>(&mut self, func: F) -> Result<(), Error>
|
fn update_collection<FN>(&mut self, func: FN) -> Result<(), Error>
|
||||||
where
|
where
|
||||||
F: FnOnce(&mut Collection),
|
FN: FnOnce(&mut Collection),
|
||||||
{
|
{
|
||||||
func(&mut self.pre_commit);
|
func(&mut self.pre_commit);
|
||||||
self.commit()
|
self.commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_artist_and<ID: AsRef<ArtistId>, F1, F2>(
|
fn update_artist_and<ID: AsRef<ArtistId>, FNARTIST, FNCOLL>(
|
||||||
&mut self,
|
&mut self,
|
||||||
artist_id: ID,
|
artist_id: ID,
|
||||||
f1: F1,
|
fn_artist: FNARTIST,
|
||||||
f2: F2,
|
fn_collection: FNCOLL,
|
||||||
) -> Result<(), Error>
|
) -> Result<(), Error>
|
||||||
where
|
where
|
||||||
F1: FnOnce(&mut Artist),
|
FNARTIST: FnOnce(&mut Artist),
|
||||||
F2: FnOnce(&mut Collection),
|
FNCOLL: FnOnce(&mut Collection),
|
||||||
{
|
{
|
||||||
f1(Self::get_artist_mut_or_err(
|
fn_artist(Self::get_artist_mut_or_err(
|
||||||
&mut self.pre_commit,
|
&mut self.pre_commit,
|
||||||
artist_id.as_ref(),
|
artist_id.as_ref(),
|
||||||
)?);
|
)?);
|
||||||
self.update_collection(f2)
|
self.update_collection(fn_collection)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_artist<ID: AsRef<ArtistId>, F>(&mut self, artist_id: ID, func: F) -> Result<(), Error>
|
fn update_artist<ID: AsRef<ArtistId>, FN>(
|
||||||
|
&mut self,
|
||||||
|
artist_id: ID,
|
||||||
|
func: FN,
|
||||||
|
) -> Result<(), Error>
|
||||||
where
|
where
|
||||||
F: FnOnce(&mut Artist),
|
FN: FnOnce(&mut Artist),
|
||||||
{
|
{
|
||||||
self.update_artist_and(artist_id, func, |_| {})
|
self.update_artist_and(artist_id, func, |_| {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_album_and<ARTIST: AsRef<ArtistId>, ALBUM: AsRef<AlbumId>, FNALBUM, FNARTIST, FNCOLL>(
|
||||||
|
&mut self,
|
||||||
|
artist_id: ARTIST,
|
||||||
|
album_id: ALBUM,
|
||||||
|
fn_album: FNALBUM,
|
||||||
|
fn_artist: FNARTIST,
|
||||||
|
fn_collection: FNCOLL,
|
||||||
|
) -> Result<(), Error>
|
||||||
|
where
|
||||||
|
FNALBUM: FnOnce(&mut Album),
|
||||||
|
FNARTIST: FnOnce(&mut Artist),
|
||||||
|
FNCOLL: FnOnce(&mut Collection),
|
||||||
|
{
|
||||||
|
let artist = Self::get_artist_mut_or_err(&mut self.pre_commit, artist_id.as_ref())?;
|
||||||
|
let album = Self::get_album_mut_or_err(artist, album_id.as_ref())?;
|
||||||
|
fn_album(album);
|
||||||
|
fn_artist(artist);
|
||||||
|
self.update_collection(fn_collection)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn add_artist<ID: Into<ArtistId>>(&mut self, artist_id: ID) -> Result<(), Error> {
|
pub fn add_artist<ID: Into<ArtistId>>(&mut self, artist_id: ID) -> Result<(), Error> {
|
||||||
let artist_id: ArtistId = artist_id.into();
|
let artist_id: ArtistId = artist_id.into();
|
||||||
|
|
||||||
@ -315,7 +355,7 @@ impl<LIB, DB: IDatabase> MusicHoard<LIB, DB> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_musicbrainz_url<ID: AsRef<ArtistId>, S: AsRef<str>>(
|
pub fn set_artist_musicbrainz<ID: AsRef<ArtistId>, S: AsRef<str>>(
|
||||||
&mut self,
|
&mut self,
|
||||||
artist_id: ID,
|
artist_id: ID,
|
||||||
url: S,
|
url: S,
|
||||||
@ -324,14 +364,14 @@ impl<LIB, DB: IDatabase> MusicHoard<LIB, DB> {
|
|||||||
self.update_artist(artist_id, |artist| artist.set_musicbrainz_url(url))
|
self.update_artist(artist_id, |artist| artist.set_musicbrainz_url(url))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear_musicbrainz_url<ID: AsRef<ArtistId>>(
|
pub fn clear_artist_musicbrainz<ID: AsRef<ArtistId>>(
|
||||||
&mut self,
|
&mut self,
|
||||||
artist_id: ID,
|
artist_id: ID,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
self.update_artist(artist_id, |artist| artist.clear_musicbrainz_url())
|
self.update_artist(artist_id, |artist| artist.clear_musicbrainz_url())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_to_property<ID: AsRef<ArtistId>, S: AsRef<str> + Into<String>>(
|
pub fn add_to_artist_property<ID: AsRef<ArtistId>, S: AsRef<str> + Into<String>>(
|
||||||
&mut self,
|
&mut self,
|
||||||
artist_id: ID,
|
artist_id: ID,
|
||||||
property: S,
|
property: S,
|
||||||
@ -340,7 +380,7 @@ impl<LIB, DB: IDatabase> MusicHoard<LIB, DB> {
|
|||||||
self.update_artist(artist_id, |artist| artist.add_to_property(property, values))
|
self.update_artist(artist_id, |artist| artist.add_to_property(property, values))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_from_property<ID: AsRef<ArtistId>, S: AsRef<str>>(
|
pub fn remove_from_artist_property<ID: AsRef<ArtistId>, S: AsRef<str>>(
|
||||||
&mut self,
|
&mut self,
|
||||||
artist_id: ID,
|
artist_id: ID,
|
||||||
property: S,
|
property: S,
|
||||||
@ -351,7 +391,7 @@ impl<LIB, DB: IDatabase> MusicHoard<LIB, DB> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_property<ID: AsRef<ArtistId>, S: AsRef<str> + Into<String>>(
|
pub fn set_artist_property<ID: AsRef<ArtistId>, S: AsRef<str> + Into<String>>(
|
||||||
&mut self,
|
&mut self,
|
||||||
artist_id: ID,
|
artist_id: ID,
|
||||||
property: S,
|
property: S,
|
||||||
@ -360,13 +400,42 @@ impl<LIB, DB: IDatabase> MusicHoard<LIB, DB> {
|
|||||||
self.update_artist(artist_id, |artist| artist.set_property(property, values))
|
self.update_artist(artist_id, |artist| artist.set_property(property, values))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear_property<ID: AsRef<ArtistId>, S: AsRef<str>>(
|
pub fn clear_artist_property<ID: AsRef<ArtistId>, S: AsRef<str>>(
|
||||||
&mut self,
|
&mut self,
|
||||||
artist_id: ID,
|
artist_id: ID,
|
||||||
property: S,
|
property: S,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
self.update_artist(artist_id, |artist| artist.clear_property(property))
|
self.update_artist(artist_id, |artist| artist.clear_property(property))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_album_seq<ARTIST: AsRef<ArtistId>, ALBUM: AsRef<AlbumId>>(
|
||||||
|
&mut self,
|
||||||
|
artist_id: ARTIST,
|
||||||
|
album_id: ALBUM,
|
||||||
|
seq: u8,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
self.update_album_and(
|
||||||
|
artist_id,
|
||||||
|
album_id,
|
||||||
|
|album| album.set_seq(AlbumSeq(seq)),
|
||||||
|
|artist| artist.albums.sort_unstable(),
|
||||||
|
|_| {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_album_seq<ARTIST: AsRef<ArtistId>, ALBUM: AsRef<AlbumId>>(
|
||||||
|
&mut self,
|
||||||
|
artist_id: ARTIST,
|
||||||
|
album_id: ALBUM,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
self.update_album_and(
|
||||||
|
artist_id,
|
||||||
|
album_id,
|
||||||
|
|album| album.clear_seq(),
|
||||||
|
|artist| artist.albums.sort_unstable(),
|
||||||
|
|_| {},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<LIB: ILibrary, DB: IDatabase> MusicHoard<LIB, DB> {
|
impl<LIB: ILibrary, DB: IDatabase> MusicHoard<LIB, DB> {
|
||||||
@ -518,7 +587,7 @@ mod tests {
|
|||||||
assert!(music_hoard.add_artist(artist_id.clone()).is_ok());
|
assert!(music_hoard.add_artist(artist_id.clone()).is_ok());
|
||||||
|
|
||||||
let actual_err = music_hoard
|
let actual_err = music_hoard
|
||||||
.set_musicbrainz_url(&artist_id, MUSICBUTLER)
|
.set_artist_musicbrainz(&artist_id, MUSICBUTLER)
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
let expected_err = Error::CollectionError(format!(
|
let expected_err = Error::CollectionError(format!(
|
||||||
"an error occurred when processing a URL: invalid MusicBrainz URL: {MUSICBUTLER}"
|
"an error occurred when processing a URL: invalid MusicBrainz URL: {MUSICBUTLER}"
|
||||||
@ -544,23 +613,23 @@ mod tests {
|
|||||||
|
|
||||||
// Setting a URL on an artist not in the collection is an error.
|
// Setting a URL on an artist not in the collection is an error.
|
||||||
assert!(music_hoard
|
assert!(music_hoard
|
||||||
.set_musicbrainz_url(&artist_id_2, MUSICBRAINZ)
|
.set_artist_musicbrainz(&artist_id_2, MUSICBRAINZ)
|
||||||
.is_err());
|
.is_err());
|
||||||
assert_eq!(music_hoard.collection[0].musicbrainz, expected);
|
assert_eq!(music_hoard.collection[0].musicbrainz, expected);
|
||||||
|
|
||||||
// Setting a URL on an artist.
|
// Setting a URL on an artist.
|
||||||
assert!(music_hoard
|
assert!(music_hoard
|
||||||
.set_musicbrainz_url(&artist_id, MUSICBRAINZ)
|
.set_artist_musicbrainz(&artist_id, MUSICBRAINZ)
|
||||||
.is_ok());
|
.is_ok());
|
||||||
_ = expected.insert(MusicBrainz::new(MUSICBRAINZ).unwrap());
|
_ = expected.insert(MusicBrainz::new(MUSICBRAINZ).unwrap());
|
||||||
assert_eq!(music_hoard.collection[0].musicbrainz, expected);
|
assert_eq!(music_hoard.collection[0].musicbrainz, expected);
|
||||||
|
|
||||||
// Clearing URLs on an artist that does not exist is an error.
|
// Clearing URLs on an artist that does not exist is an error.
|
||||||
assert!(music_hoard.clear_musicbrainz_url(&artist_id_2).is_err());
|
assert!(music_hoard.clear_artist_musicbrainz(&artist_id_2).is_err());
|
||||||
assert_eq!(music_hoard.collection[0].musicbrainz, expected);
|
assert_eq!(music_hoard.collection[0].musicbrainz, expected);
|
||||||
|
|
||||||
// Clearing URLs.
|
// Clearing URLs.
|
||||||
assert!(music_hoard.clear_musicbrainz_url(&artist_id).is_ok());
|
assert!(music_hoard.clear_artist_musicbrainz(&artist_id).is_ok());
|
||||||
_ = expected.take();
|
_ = expected.take();
|
||||||
assert_eq!(music_hoard.collection[0].musicbrainz, expected);
|
assert_eq!(music_hoard.collection[0].musicbrainz, expected);
|
||||||
}
|
}
|
||||||
@ -582,13 +651,13 @@ mod tests {
|
|||||||
|
|
||||||
// Adding URLs to an artist not in the collection is an error.
|
// Adding URLs to an artist not in the collection is an error.
|
||||||
assert!(music_hoard
|
assert!(music_hoard
|
||||||
.add_to_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
|
.add_to_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
|
||||||
.is_err());
|
.is_err());
|
||||||
assert!(music_hoard.collection[0].properties.is_empty());
|
assert!(music_hoard.collection[0].properties.is_empty());
|
||||||
|
|
||||||
// Adding mutliple URLs without clashes.
|
// Adding mutliple URLs without clashes.
|
||||||
assert!(music_hoard
|
assert!(music_hoard
|
||||||
.add_to_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2])
|
.add_to_artist_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2])
|
||||||
.is_ok());
|
.is_ok());
|
||||||
expected.push(MUSICBUTLER.to_owned());
|
expected.push(MUSICBUTLER.to_owned());
|
||||||
expected.push(MUSICBUTLER_2.to_owned());
|
expected.push(MUSICBUTLER_2.to_owned());
|
||||||
@ -599,7 +668,7 @@ mod tests {
|
|||||||
|
|
||||||
// Removing URLs from an artist not in the collection is an error.
|
// Removing URLs from an artist not in the collection is an error.
|
||||||
assert!(music_hoard
|
assert!(music_hoard
|
||||||
.remove_from_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
|
.remove_from_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
|
||||||
.is_err());
|
.is_err());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
music_hoard.collection[0].properties.get("MusicButler"),
|
music_hoard.collection[0].properties.get("MusicButler"),
|
||||||
@ -608,7 +677,11 @@ mod tests {
|
|||||||
|
|
||||||
// Removing multiple URLs without clashes.
|
// Removing multiple URLs without clashes.
|
||||||
assert!(music_hoard
|
assert!(music_hoard
|
||||||
.remove_from_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2])
|
.remove_from_artist_property(
|
||||||
|
&artist_id,
|
||||||
|
"MusicButler",
|
||||||
|
vec![MUSICBUTLER, MUSICBUTLER_2]
|
||||||
|
)
|
||||||
.is_ok());
|
.is_ok());
|
||||||
expected.clear();
|
expected.clear();
|
||||||
assert!(music_hoard.collection[0].properties.is_empty());
|
assert!(music_hoard.collection[0].properties.is_empty());
|
||||||
@ -631,13 +704,13 @@ mod tests {
|
|||||||
|
|
||||||
// Seting URL on an artist not in the collection is an error.
|
// Seting URL on an artist not in the collection is an error.
|
||||||
assert!(music_hoard
|
assert!(music_hoard
|
||||||
.set_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
|
.set_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
|
||||||
.is_err());
|
.is_err());
|
||||||
assert!(music_hoard.collection[0].properties.is_empty());
|
assert!(music_hoard.collection[0].properties.is_empty());
|
||||||
|
|
||||||
// Set URLs.
|
// Set URLs.
|
||||||
assert!(music_hoard
|
assert!(music_hoard
|
||||||
.set_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2])
|
.set_artist_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2])
|
||||||
.is_ok());
|
.is_ok());
|
||||||
expected.clear();
|
expected.clear();
|
||||||
expected.push(MUSICBUTLER.to_owned());
|
expected.push(MUSICBUTLER.to_owned());
|
||||||
@ -649,12 +722,12 @@ mod tests {
|
|||||||
|
|
||||||
// Clearing URLs on an artist that does not exist is an error.
|
// Clearing URLs on an artist that does not exist is an error.
|
||||||
assert!(music_hoard
|
assert!(music_hoard
|
||||||
.clear_property(&artist_id_2, "MusicButler")
|
.clear_artist_property(&artist_id_2, "MusicButler")
|
||||||
.is_err());
|
.is_err());
|
||||||
|
|
||||||
// Clear URLs.
|
// Clear URLs.
|
||||||
assert!(music_hoard
|
assert!(music_hoard
|
||||||
.clear_property(&artist_id, "MusicButler")
|
.clear_artist_property(&artist_id, "MusicButler")
|
||||||
.is_ok());
|
.is_ok());
|
||||||
expected.clear();
|
expected.clear();
|
||||||
assert!(music_hoard.collection[0].properties.is_empty());
|
assert!(music_hoard.collection[0].properties.is_empty());
|
||||||
|
@ -75,7 +75,12 @@ fn with<LIB: ILibrary, DB: IDatabase>(builder: MusicHoardBuilder<LIB, DB>) {
|
|||||||
let ui = Ui;
|
let ui = Ui;
|
||||||
|
|
||||||
// Run the TUI application.
|
// Run the TUI application.
|
||||||
Tui::run(terminal, app, ui, handler, listener).expect("failed to run tui");
|
let result = Tui::run(terminal, app, ui, handler, listener);
|
||||||
|
if let Err(tui::Error::ListenerPanic(err)) = result {
|
||||||
|
std::panic::resume_unwind(err);
|
||||||
|
} else {
|
||||||
|
result.expect("failed to run tui")
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn with_database<LIB: ILibrary>(db_opt: DbOpt, builder: MusicHoardBuilder<LIB, NoDatabase>) {
|
fn with_database<LIB: ILibrary>(db_opt: DbOpt, builder: MusicHoardBuilder<LIB, NoDatabase>) {
|
||||||
|
@ -11,12 +11,16 @@ pub use handler::EventHandler;
|
|||||||
pub use listener::EventListener;
|
pub use listener::EventListener;
|
||||||
pub use ui::Ui;
|
pub use ui::Ui;
|
||||||
|
|
||||||
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
|
use crossterm::{
|
||||||
use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen};
|
event::{DisableMouseCapture, EnableMouseCapture},
|
||||||
use ratatui::backend::Backend;
|
terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
use ratatui::Terminal;
|
};
|
||||||
use std::io;
|
use ratatui::{backend::Backend, Terminal};
|
||||||
use std::marker::PhantomData;
|
use std::{
|
||||||
|
marker::PhantomData,
|
||||||
|
ptr,
|
||||||
|
{any::Any, io},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::tui::{
|
use crate::tui::{
|
||||||
app::{IAppAccess, IAppInteract},
|
app::{IAppAccess, IAppInteract},
|
||||||
@ -26,11 +30,32 @@ use crate::tui::{
|
|||||||
ui::IUi,
|
ui::IUi,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
Io(String),
|
Io(String),
|
||||||
Event(String),
|
Event(String),
|
||||||
ListenerPanic,
|
ListenerPanic(Box<dyn Any + Send>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for Error {}
|
||||||
|
|
||||||
|
impl PartialEq for Error {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
match self {
|
||||||
|
Error::Io(this) => match other {
|
||||||
|
Error::Io(other) => this == other,
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
Error::Event(this) => match other {
|
||||||
|
Error::Event(other) => this == other,
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
Error::ListenerPanic(this) => match other {
|
||||||
|
Error::ListenerPanic(other) => ptr::eq(this.as_ref(), other.as_ref()),
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<io::Error> for Error {
|
impl From<io::Error> for Error {
|
||||||
@ -114,7 +139,8 @@ impl<B: Backend, UI: IUi, APP: IAppInteract + IAppAccess> Tui<B, UI, APP> {
|
|||||||
// Calling std::panic::resume_unwind(err) as recommended by the Rust docs
|
// Calling std::panic::resume_unwind(err) as recommended by the Rust docs
|
||||||
// will not produce an error message. The panic error message is printed at
|
// will not produce an error message. The panic error message is printed at
|
||||||
// the location of the panic which at the time is hidden by the TUI.
|
// the location of the panic which at the time is hidden by the TUI.
|
||||||
Err(_) => return Err(Error::ListenerPanic),
|
// Therefore, propagate the error for the caller to resume unwinding.
|
||||||
|
Err(panic) => return Err(Error::ListenerPanic(panic)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -301,14 +327,14 @@ mod tests {
|
|||||||
|
|
||||||
let result = Tui::main(terminal, app, ui, handler, listener);
|
let result = Tui::main(terminal, app, ui, handler, listener);
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
assert_eq!(result.unwrap_err(), Error::ListenerPanic);
|
assert!(matches!(result.unwrap_err(), Error::ListenerPanic(_)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn errors() {
|
fn errors() {
|
||||||
let io_err: Error = io::Error::new(io::ErrorKind::Interrupted, "error").into();
|
let io_err: Error = io::Error::new(io::ErrorKind::Interrupted, "error").into();
|
||||||
let event_err: Error = EventError::Recv.into();
|
let event_err: Error = EventError::Recv.into();
|
||||||
let listener_err = Error::ListenerPanic;
|
let listener_err = Error::ListenerPanic(Box::new("hello"));
|
||||||
|
|
||||||
assert!(!format!("{:?}", io_err).is_empty());
|
assert!(!format!("{:?}", io_err).is_empty());
|
||||||
assert!(!format!("{:?}", event_err).is_empty());
|
assert!(!format!("{:?}", event_err).is_empty());
|
||||||
|
Loading…
Reference in New Issue
Block a user