diff --git a/Cargo.lock b/Cargo.lock index d4320f6..491d365 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -184,6 +184,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + [[package]] name = "fragile" version = "2.0.0" @@ -225,6 +234,16 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "instant" version = "0.1.12" @@ -356,6 +375,7 @@ dependencies = [ "structopt", "tempfile", "tokio", + "url", "uuid", ] @@ -436,6 +456,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + [[package]] name = "pin-project-lite" version = "0.2.9" @@ -801,6 +827,21 @@ dependencies = [ "syn 2.0.11", ] +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.27.0" @@ -859,12 +900,27 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + [[package]] name = "unicode-ident" version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-segmentation" version = "1.10.1" @@ -877,6 +933,18 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + [[package]] name = "uuid" version = "1.3.0" diff --git a/Cargo.toml b/Cargo.toml index 4ef252b..33dc5d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ serde = { version = "1.0.159", features = ["derive"] } serde_json = { version = "1.0.95", optional = true} structopt = { version = "0.3.26", optional = true} tokio = { version = "1.27.0", features = ["rt"], optional = true} +url = { version = "2.3.1", features = ["serde"] } uuid = { version = "1.3.0", features = ["serde"] } [dev-dependencies] diff --git a/src/lib.rs b/src/lib.rs index 4dcdda4..f88fb56 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,10 +14,133 @@ use std::{ use database::IDatabase; use library::{ILibrary, Item, Query}; use serde::{Deserialize, Serialize}; +use url::Url; use uuid::Uuid; /// [MusicBrainz Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) (MBID). -pub type Mbid = Uuid; +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +struct Mbid { + pub uuid: Uuid, + pub string: String, +} + +impl Mbid { + pub fn new>(uuid: S) -> Result { + let uuid_string: String = uuid.into(); + Ok(Mbid { + uuid: Uuid::try_parse(&uuid_string)?, + string: uuid_string, + }) + } + + pub fn as_str(&self) -> &str { + self.string.as_str() + } +} + +pub trait IUrl { + fn url(&self) -> &str; +} + +pub trait IUuid { + fn uuid(&self) -> &str; +} + +/// MusicBrainz reference. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +pub struct MusicBrainz { + url: Url, + mbid: Mbid, +} + +impl MusicBrainz { + pub fn new>(url: S) -> Result { + let url = Url::parse(url.as_ref())?; + let mbid: Mbid = match url.path_segments().and_then(|mut ps| ps.nth(2)) { + Some(segment) => Mbid::new(segment)?, + None => { + return Err(Error::UrlParseError(String::from(format!( + "invalid MusicBrainz URL: {}", + url.as_str() + )))) + } + }; + + Ok(MusicBrainz { url, mbid }) + } +} + +impl IUrl for MusicBrainz { + fn url(&self) -> &str { + self.url.as_str() + } +} + +impl IUuid for MusicBrainz { + fn uuid(&self) -> &str { + self.mbid.as_str() + } +} + +/// MusicButler reference. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +pub struct MusicButler { + url: Url, +} + +impl MusicButler { + pub fn new>(url: S) -> Result { + Ok(MusicButler { + url: Url::parse(url.as_ref())?, + }) + } +} + +impl IUrl for MusicButler { + fn url(&self) -> &str { + self.url.as_str() + } +} + +/// Bandcamp reference. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +pub struct Bandcamp { + url: Url, +} + +impl Bandcamp { + pub fn new>(url: S) -> Result { + Ok(Bandcamp { + url: Url::parse(url.as_ref())?, + }) + } +} + +impl IUrl for Bandcamp { + fn url(&self) -> &str { + self.url.as_str() + } +} + +/// Qobuz reference. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +pub struct Qobuz { + url: Url, +} + +impl Qobuz { + pub fn new>(url: S) -> Result { + Ok(Qobuz { + url: Url::parse(url.as_ref())?, + }) + } +} + +impl IUrl for Qobuz { + fn url(&self) -> &str { + self.url.as_str() + } +} /// The track file format. #[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)] @@ -107,6 +230,15 @@ pub struct ArtistId { pub name: String, } +/// The artist properties. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ArtistProperties { + pub musicbrainz: Option, + pub musicbutler: Vec, + pub bandcamp: Vec, + pub qobuz: Option, +} + /// An artist. #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] pub struct Artist { @@ -194,6 +326,8 @@ pub enum Error { LibraryError(String), /// The [`MusicHoard`] failed to read/write from/to the database. DatabaseError(String), + /// The [`MusicHoard`] failed to parse a user-provided URL. + UrlParseError(String), } impl fmt::Display for Error { @@ -203,6 +337,7 @@ impl fmt::Display for Error { Self::DatabaseError(ref s) => { write!(f, "failed to read/write from/to the database: {s}") } + Self::UrlParseError(ref s) => write!(f, "failed to parse a user-provided URL: {s}"), } } } @@ -225,6 +360,18 @@ impl From for Error { } } +impl From for Error { + fn from(err: url::ParseError) -> Error { + Error::UrlParseError(err.to_string()) + } +} + +impl From for Error { + fn from(err: uuid::Error) -> Error { + Error::UrlParseError(err.to_string()) + } +} + /// The Music Hoard. It is responsible for pulling information from both the library and the /// database, ensuring its consistent and writing back any changes. pub struct MusicHoard {