diff --git a/Cargo.lock b/Cargo.lock index f5030d8..e49a215 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -51,6 +51,18 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "anyhow" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "clap" version = "4.5.20" @@ -97,18 +109,72 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "libc" +version = "0.2.161" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + [[package]] name = "proc-macro2" version = "1.0.89" @@ -127,6 +193,44 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "serde" +version = "1.0.214" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.214" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.132" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + [[package]] name = "strsim" version = "0.11.1" @@ -144,18 +248,76 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +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 = "unicode-bidi" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" + [[package]] name = "unicode-ident" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +dependencies = [ + "getrandom", + "serde", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "windows-sys" version = "0.52.0" @@ -254,3 +416,10 @@ version = "2.0.0" [[package]] name = "xmpd-manifest" version = "2.0.0" +dependencies = [ + "anyhow", + "serde", + "serde_json", + "url", + "uuid", +] diff --git a/xmpd-manifest/Cargo.toml b/xmpd-manifest/Cargo.toml index 8d7bb42..b8df441 100644 --- a/xmpd-manifest/Cargo.toml +++ b/xmpd-manifest/Cargo.toml @@ -18,3 +18,8 @@ crate-type = ["rlib"] bench = false [dependencies] +anyhow.workspace = true +uuid.workspace = true +serde.workspace = true +serde_json.workspace = true +url.workspace = true diff --git a/xmpd-manifest/src/lib.rs b/xmpd-manifest/src/lib.rs index 9815d27..f7249a3 100644 --- a/xmpd-manifest/src/lib.rs +++ b/xmpd-manifest/src/lib.rs @@ -1,3 +1,54 @@ -pub fn test() { - println!("Hello, world!"); +use std::path::Path; + + +#[cfg(test)] +pub mod tests; +pub mod store; +pub mod song; +pub mod playlist; +pub mod query; + +pub type Result = anyhow::Result; + +pub struct Manifest { + store: Box, } + +impl Manifest { + pub fn new(p: &Path) -> Result{ + let mut store = ST::empty(); + if p.exists() { + store.load_from(p)?; + } else { + store.save_to(p)?; + } + store.save_original_path(p); + Ok(Self { + store: Box::new(store) + }) + } + pub fn store(&self) -> &ST { + self.store.as_ref() + } + pub fn store_mut(&mut self) -> &mut ST { + self.store.as_mut() + } + pub fn save(&self) -> Result<()> { + self.store().save_to(self.store().get_original_path())?; + Ok(()) + } + pub fn load(&mut self) -> Result<()> { + let p = self.store().get_original_path().to_path_buf(); + self.store_mut().load_from(&p)?; + Ok(()) + } +} + + + + + + + + + diff --git a/xmpd-manifest/src/playlist.rs b/xmpd-manifest/src/playlist.rs new file mode 100644 index 0000000..72a0127 --- /dev/null +++ b/xmpd-manifest/src/playlist.rs @@ -0,0 +1,40 @@ +use uuid::Uuid; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, PartialOrd, Default)] +pub struct Playlist { + name: String, + author: String, + songs: Vec +} + +impl Playlist { + pub fn name(&self) -> &str { + &self.name + } + pub fn author(&self) -> &str { + &self.author + } + pub fn songs(&self) -> &Vec { + &self.songs + } + pub fn songs_mut(&mut self) -> &mut Vec { + &mut self.songs + } + pub fn set_name(&mut self, v: &str) { + self.name = v.to_string(); + } + pub fn set_author(&mut self, v: &str) { + self.author = v.to_string(); + } + pub fn add_song(&mut self, v: &Uuid) { + self.songs.push(v.clone()); + } + pub fn remove_song(&mut self, v: &Uuid) { + for (i, id) in self.songs.iter().enumerate() { + if id == v { + self.songs.remove(i); + break; + } + } + } +} diff --git a/xmpd-manifest/src/query/mod.rs b/xmpd-manifest/src/query/mod.rs new file mode 100644 index 0000000..734592d --- /dev/null +++ b/xmpd-manifest/src/query/mod.rs @@ -0,0 +1,35 @@ +use std::marker::PhantomData; + +use uuid::Uuid; + + +mod playlist; +mod song; + +pub struct Query { + playlist: Option, + song: Option, + _phantom: PhantomData +} + +impl Query<()> { + pub fn song(id: Uuid) -> Query { + Query { + song: Some(id), + playlist: None, + _phantom: PhantomData + } + } + pub fn playlist(id: Uuid) -> Query { + Query { + playlist: Some(id), + song: None, + _phantom: PhantomData + } + } +} + + +pub trait QueryType {} + + diff --git a/xmpd-manifest/src/query/playlist.rs b/xmpd-manifest/src/query/playlist.rs new file mode 100644 index 0000000..d55d67f --- /dev/null +++ b/xmpd-manifest/src/query/playlist.rs @@ -0,0 +1,47 @@ +use uuid::Uuid; + +use crate::{song::Song, store, Manifest}; + +use super::{Query, QueryType}; + +pub struct QueryPlaylist; + +impl QueryType for QueryPlaylist {} + +impl Query { + pub fn id(&self) -> &Uuid { + if let Some(id) = &self.playlist { + return id; + } + unreachable!() + } + pub fn name<'a, ST: store::BaseStore + Clone>(&self, manifest: &'a Manifest) -> Option<&'a str> { + let pl = manifest.store().get_playlist(self.id())?; + Some(pl.name()) + } + pub fn set_name(&self, manifest: &mut Manifest, name: &str) -> Option<()> { + let pl = manifest.store_mut().get_playlist_mut(self.id())?; + pl.set_name(name); + Some(()) + } + pub fn author<'a, ST: store::BaseStore + Clone>(&self, manifest: &'a Manifest) -> Option<&'a str> { + let pl = manifest.store().get_playlist(self.id())?; + Some(pl.author()) + } + pub fn set_author(&self, manifest: &mut Manifest, author: &str) -> Option<()> { + let pl = manifest.store_mut().get_playlist_mut(self.id())?; + pl.set_author(author); + Some(()) + } + pub fn songs<'a, ST: store::BaseStore + Clone>(&self, manifest: &'a Manifest) -> Option<&'a Vec> { + let pl = manifest.store().get_playlist(self.id())?; + Some(pl.songs()) + } + pub fn add_song(&self, manifest: &mut Manifest, song: Song) -> Option { + let pl = manifest.store_mut().get_playlist_mut(self.id())?; + let id = Uuid::new_v4(); + pl.add_song(&id); + manifest.store_mut().get_songs_mut().insert(id, song); + Some(id) + } +} diff --git a/xmpd-manifest/src/query/song.rs b/xmpd-manifest/src/query/song.rs new file mode 100644 index 0000000..8d81fa4 --- /dev/null +++ b/xmpd-manifest/src/query/song.rs @@ -0,0 +1,68 @@ +use std::str::FromStr; + +use url::Url; +use uuid::Uuid; + +use crate::{song::SourceType, store, Manifest}; + +use super::{Query, QueryType}; + + +pub struct QuerySong; +impl QueryType for QuerySong {} + +impl Query { + pub fn id(&self) -> &Uuid { + if let Some(id) = &self.song { + return id; + } + unreachable!() + } + pub fn name<'a, ST: store::BaseStore + Clone>(&self, manifest: &'a Manifest) -> Option<&'a str> { + let pl = manifest.store().get_song(self.id())?; + Some(pl.name()) + } + pub fn set_name(&self, manifest: &mut Manifest, name: &str) -> Option<()> { + let pl = manifest.store_mut().get_song_mut(self.id())?; + pl.set_name(name); + Some(()) + } + pub fn author<'a, ST: store::BaseStore + Clone>(&self, manifest: &'a Manifest) -> Option<&'a str> { + let pl = manifest.store().get_song(self.id())?; + Some(pl.author()) + } + pub fn set_author(&self, manifest: &mut Manifest, author: &str) -> Option<()> { + let pl = manifest.store_mut().get_song_mut(self.id())?; + pl.set_author(author); + Some(()) + } + pub fn url<'a, ST: store::BaseStore + Clone>(&self, manifest: &'a Manifest) -> Option<&'a Url> { + let pl = manifest.store().get_song(self.id())?; + Some(pl.url()) + } + pub fn url_as_str<'a, ST: store::BaseStore + Clone>(&self, manifest: &'a Manifest) -> Option<&'a str> { + Some(self.url(manifest)?.as_str()) + } + pub fn set_url(&self, manifest: &mut Manifest, url: &Url) -> Option<()> { + let pl = manifest.store_mut().get_song_mut(self.id())?; + pl.set_url(url); + Some(()) + } + pub fn set_url_from_str(&self, manifest: &mut Manifest, url: &str) -> Option<()> { + let Ok(url) = Url::from_str(url) else {return None}; + self.set_url(manifest, &url); + Some(()) + } + pub fn source_type<'a, ST: store::BaseStore + Clone>(&self, manifest: &'a Manifest) -> Option<&'a SourceType> { + let pl = manifest.store().get_song(self.id())?; + Some(pl.source_type()) + } + pub fn set_source_type(&self, manifest: &mut Manifest, source_type: &SourceType) -> Option<()> { + let pl = manifest.store_mut().get_song_mut(self.id())?; + pl.set_source_type(source_type); + Some(()) + } + +} + + diff --git a/xmpd-manifest/src/song.rs b/xmpd-manifest/src/song.rs new file mode 100644 index 0000000..2564bfc --- /dev/null +++ b/xmpd-manifest/src/song.rs @@ -0,0 +1,69 @@ +use std::str::FromStr; + + + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, PartialOrd)] +pub struct Song { + name: String, + author: String, + url: url::Url, + source_type: SourceType, +} + +impl Song { + pub fn new(url: &url::Url) -> crate::Result { + Ok(Self { + name: String::default(), + author: String::default(), + source_type: SourceType::from_url(url)?, + url: url.clone() + }) + } + pub fn new_from_str(url: &str) -> crate::Result { + Self::new(&url::Url::from_str(url)?) + } + pub fn name(&self) -> &str { + &self.name + } + pub fn author(&self) -> &str { + &self.author + } + pub fn url(&self) -> &url::Url { + &self.url + } + pub fn source_type(&self) -> &SourceType { + &self.source_type + } + pub fn set_name(&mut self, v: &str) { + self.name = v.to_string(); + } + pub fn set_author(&mut self, v: &str) { + self.author = v.to_string(); + } + pub fn set_url(&mut self, v: &url::Url) { + self.url.clone_from(v); + } + pub fn set_source_type(&mut self, v: &SourceType) { + self.source_type.clone_from(v); + } +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, PartialOrd)] +pub enum SourceType { + Youtube, + Spotify, + Soundcloud, +} + +impl SourceType { + fn from_url(url: &url::Url) -> crate::Result { + match url.host_str() { + Some("youtube.com") | Some("youtu.be") => Ok(Self::Youtube), + Some("open.spotify.com") => Ok(Self::Spotify), + Some("soundcloud.com") |Some("on.soundcloud.com") => Ok(Self::Soundcloud), + Some(host) => anyhow::bail!("Unknown host {host:?}"), + None => anyhow::bail!("Unknown host: (none)"), + } + } +} + diff --git a/xmpd-manifest/src/store/json.rs b/xmpd-manifest/src/store/json.rs new file mode 100644 index 0000000..8e879bf --- /dev/null +++ b/xmpd-manifest/src/store/json.rs @@ -0,0 +1,71 @@ +use std::{collections::HashMap, path::PathBuf}; +use uuid::Uuid; +use crate::{playlist::Playlist, song::Song}; + +const DEFAULT_TEXT: &str = r#"{ + "songs": {}, + "playlists": {} +}"#; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] +pub struct JsonStore { + #[serde(skip)] + original_path: PathBuf, + songs: HashMap, + playlists: HashMap +} + +impl super::BaseStore for JsonStore { + fn get_default_file_contents() -> &'static str { + &DEFAULT_TEXT + } + fn get_file_extension() -> &'static str { + "json" + } + fn empty() -> Self where Self: Sized { + Self { + original_path: PathBuf::new(), + songs: HashMap::default(), + playlists: HashMap::default(), + } + } + fn to_bytes(&self) -> crate::Result> { + let s = serde_json::to_vec_pretty(self)?; + Ok(s) + } + fn from_bytes(s: &[u8]) -> crate::Result where Self: Sized { + let s: Self = serde_json::from_slice(s)?; + Ok(s) + } + fn get_songs(&self) -> &HashMap { + &self.songs + } + fn get_songs_mut(&mut self) -> &mut HashMap { + &mut self.songs + } + fn get_song(&self, id: &Uuid) -> Option<&Song> { + self.songs.get(id) + } + fn get_song_mut(&mut self, id: &Uuid) -> Option<&mut Song> { + self.songs.get_mut(id) + } + fn get_playlists(&self) -> &HashMap { + &self.playlists + } + fn get_playlists_mut(&mut self) -> &mut HashMap { + &mut self.playlists + } + fn get_playlist(&self, id: &Uuid) -> Option<&Playlist> { + self.playlists.get(id) + } + fn get_playlist_mut(&mut self, id: &Uuid) -> Option<&mut Playlist> { + self.playlists.get_mut(id) + } + fn save_original_path(&mut self, p: &std::path::Path) { + self.original_path = p.to_path_buf(); + } + fn get_original_path(&self) -> &std::path::Path { + &self.original_path + } +} + diff --git a/xmpd-manifest/src/store/mod.rs b/xmpd-manifest/src/store/mod.rs new file mode 100644 index 0000000..acb4392 --- /dev/null +++ b/xmpd-manifest/src/store/mod.rs @@ -0,0 +1,40 @@ +use std::{collections::HashMap, path::Path}; + +use uuid::Uuid; + +use crate::{playlist::Playlist, song::Song}; + +mod json; +pub use json::JsonStore; + +pub trait BaseStore { + fn get_songs(&self) -> &HashMap; + fn get_songs_mut(&mut self) -> &mut HashMap; + fn get_song(&self, id: &Uuid) -> Option<&Song>; + fn get_song_mut(&mut self, id: &Uuid) -> Option<&mut Song>; + + fn get_playlists(&self) -> &HashMap; + fn get_playlists_mut(&mut self) -> &mut HashMap; + fn get_playlist(&self, id: &Uuid) -> Option<&Playlist>; + fn get_playlist_mut(&mut self, id: &Uuid) -> Option<&mut Playlist>; + + fn to_bytes(&self) -> crate::Result>; + fn from_bytes(s: &[u8]) -> crate::Result where Self: Sized; + fn empty() -> Self where Self: Sized; + fn save_to(&self, p: &Path) -> crate::Result<()> { + let bin = self.to_bytes()?; + std::fs::write(p, bin)?; + Ok(()) + } + fn load_from(&mut self, p: &Path) -> crate::Result<()> where Self: Clone { + let bin = std::fs::read(p)?; + let s = Self::from_bytes(&bin)?; + self.clone_from(&s); + Ok(()) + } + fn get_default_file_contents() -> &'static str; + fn get_file_extension() -> &'static str; + fn save_original_path(&mut self, p: &Path); + fn get_original_path(&self) -> &Path; +} + diff --git a/xmpd-manifest/src/tests/mod.rs b/xmpd-manifest/src/tests/mod.rs new file mode 100644 index 0000000..d121e18 --- /dev/null +++ b/xmpd-manifest/src/tests/mod.rs @@ -0,0 +1,19 @@ + +fn manifest_creation_base() { + use crate::Manifest; + + let mut p = std::env::temp_dir(); + p.push("test_manifest"); + p.with_extension(ST::get_file_extension()); + + let manifest: Manifest = Manifest::new(&p).unwrap(); + manifest.save().unwrap(); + let content = std::fs::read_to_string(&p).unwrap(); + println!("{}", content); + assert!(content == ST::get_default_file_contents().to_string()); +} + +#[test] +pub fn manifest_creation_json() { + manifest_creation_base::(); +}