Basic manifest implementation

This commit is contained in:
Gvidas Juknevičius 2024-11-06 12:12:07 +02:00
parent d9b23d4a24
commit a00486eeaf
Signed by: MCorange
GPG Key ID: 12B1346D720B7FBB
11 changed files with 616 additions and 2 deletions

169
Cargo.lock generated
View File

@ -51,6 +51,18 @@ dependencies = [
"windows-sys", "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]] [[package]]
name = "clap" name = "clap"
version = "4.5.20" version = "4.5.20"
@ -97,18 +109,72 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" 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]] [[package]]
name = "heck" name = "heck"
version = "0.5.0" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 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]] [[package]]
name = "is_terminal_polyfill" name = "is_terminal_polyfill"
version = "1.70.1" version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 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]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.89" version = "1.0.89"
@ -127,6 +193,44 @@ dependencies = [
"proc-macro2", "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]] [[package]]
name = "strsim" name = "strsim"
version = "0.11.1" version = "0.11.1"
@ -144,18 +248,76 @@ dependencies = [
"unicode-ident", "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]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.13" version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" 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]] [[package]]
name = "utf8parse" name = "utf8parse"
version = "0.2.2" version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 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]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.52.0" version = "0.52.0"
@ -254,3 +416,10 @@ version = "2.0.0"
[[package]] [[package]]
name = "xmpd-manifest" name = "xmpd-manifest"
version = "2.0.0" version = "2.0.0"
dependencies = [
"anyhow",
"serde",
"serde_json",
"url",
"uuid",
]

View File

@ -18,3 +18,8 @@ crate-type = ["rlib"]
bench = false bench = false
[dependencies] [dependencies]
anyhow.workspace = true
uuid.workspace = true
serde.workspace = true
serde_json.workspace = true
url.workspace = true

View File

@ -1,3 +1,54 @@
pub fn test() { use std::path::Path;
println!("Hello, world!");
#[cfg(test)]
pub mod tests;
pub mod store;
pub mod song;
pub mod playlist;
pub mod query;
pub type Result<T> = anyhow::Result<T>;
pub struct Manifest<ST: store::BaseStore> {
store: Box<ST>,
} }
impl<ST: store::BaseStore + Clone> Manifest<ST> {
pub fn new(p: &Path) -> Result<Self>{
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(())
}
}

View File

@ -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<Uuid>
}
impl Playlist {
pub fn name(&self) -> &str {
&self.name
}
pub fn author(&self) -> &str {
&self.author
}
pub fn songs(&self) -> &Vec<Uuid> {
&self.songs
}
pub fn songs_mut(&mut self) -> &mut Vec<Uuid> {
&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;
}
}
}
}

View File

@ -0,0 +1,35 @@
use std::marker::PhantomData;
use uuid::Uuid;
mod playlist;
mod song;
pub struct Query<QT: ?Sized> {
playlist: Option<Uuid>,
song: Option<Uuid>,
_phantom: PhantomData<QT>
}
impl Query<()> {
pub fn song(id: Uuid) -> Query<song::QuerySong> {
Query {
song: Some(id),
playlist: None,
_phantom: PhantomData
}
}
pub fn playlist(id: Uuid) -> Query<playlist::QueryPlaylist> {
Query {
playlist: Some(id),
song: None,
_phantom: PhantomData
}
}
}
pub trait QueryType {}

View File

@ -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<QueryPlaylist> {
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<ST>) -> Option<&'a str> {
let pl = manifest.store().get_playlist(self.id())?;
Some(pl.name())
}
pub fn set_name<ST: store::BaseStore + Clone>(&self, manifest: &mut Manifest<ST>, 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<ST>) -> Option<&'a str> {
let pl = manifest.store().get_playlist(self.id())?;
Some(pl.author())
}
pub fn set_author<ST: store::BaseStore + Clone>(&self, manifest: &mut Manifest<ST>, 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<ST>) -> Option<&'a Vec<Uuid>> {
let pl = manifest.store().get_playlist(self.id())?;
Some(pl.songs())
}
pub fn add_song<ST: store::BaseStore + Clone>(&self, manifest: &mut Manifest<ST>, song: Song) -> Option<Uuid> {
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)
}
}

View File

@ -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<QuerySong> {
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<ST>) -> Option<&'a str> {
let pl = manifest.store().get_song(self.id())?;
Some(pl.name())
}
pub fn set_name<ST: store::BaseStore + Clone>(&self, manifest: &mut Manifest<ST>, 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<ST>) -> Option<&'a str> {
let pl = manifest.store().get_song(self.id())?;
Some(pl.author())
}
pub fn set_author<ST: store::BaseStore + Clone>(&self, manifest: &mut Manifest<ST>, 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<ST>) -> 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<ST>) -> Option<&'a str> {
Some(self.url(manifest)?.as_str())
}
pub fn set_url<ST: store::BaseStore + Clone>(&self, manifest: &mut Manifest<ST>, url: &Url) -> Option<()> {
let pl = manifest.store_mut().get_song_mut(self.id())?;
pl.set_url(url);
Some(())
}
pub fn set_url_from_str<ST: store::BaseStore + Clone>(&self, manifest: &mut Manifest<ST>, 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<ST>) -> Option<&'a SourceType> {
let pl = manifest.store().get_song(self.id())?;
Some(pl.source_type())
}
pub fn set_source_type<ST: store::BaseStore + Clone>(&self, manifest: &mut Manifest<ST>, source_type: &SourceType) -> Option<()> {
let pl = manifest.store_mut().get_song_mut(self.id())?;
pl.set_source_type(source_type);
Some(())
}
}

69
xmpd-manifest/src/song.rs Normal file
View File

@ -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<Self> {
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> {
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<Self> {
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)"),
}
}
}

View File

@ -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<Uuid, Song>,
playlists: HashMap<Uuid, Playlist>
}
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<Vec<u8>> {
let s = serde_json::to_vec_pretty(self)?;
Ok(s)
}
fn from_bytes(s: &[u8]) -> crate::Result<Self> where Self: Sized {
let s: Self = serde_json::from_slice(s)?;
Ok(s)
}
fn get_songs(&self) -> &HashMap<Uuid, Song> {
&self.songs
}
fn get_songs_mut(&mut self) -> &mut HashMap<Uuid, Song> {
&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<Uuid, Playlist> {
&self.playlists
}
fn get_playlists_mut(&mut self) -> &mut HashMap<Uuid, Playlist> {
&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
}
}

View File

@ -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<Uuid, Song>;
fn get_songs_mut(&mut self) -> &mut HashMap<Uuid, Song>;
fn get_song(&self, id: &Uuid) -> Option<&Song>;
fn get_song_mut(&mut self, id: &Uuid) -> Option<&mut Song>;
fn get_playlists(&self) -> &HashMap<Uuid, Playlist>;
fn get_playlists_mut(&mut self) -> &mut HashMap<Uuid, Playlist>;
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<Vec<u8>>;
fn from_bytes(s: &[u8]) -> crate::Result<Self> 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;
}

View File

@ -0,0 +1,19 @@
fn manifest_creation_base<ST: crate::store::BaseStore + Clone>() {
use crate::Manifest;
let mut p = std::env::temp_dir();
p.push("test_manifest");
p.with_extension(ST::get_file_extension());
let manifest: Manifest<ST> = 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::<crate::store::JsonStore>();
}