Basic manifest implementation
This commit is contained in:
@@ -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<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(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
40
xmpd-manifest/src/playlist.rs
Normal file
40
xmpd-manifest/src/playlist.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
35
xmpd-manifest/src/query/mod.rs
Normal file
35
xmpd-manifest/src/query/mod.rs
Normal 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 {}
|
||||
|
||||
|
||||
47
xmpd-manifest/src/query/playlist.rs
Normal file
47
xmpd-manifest/src/query/playlist.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
68
xmpd-manifest/src/query/song.rs
Normal file
68
xmpd-manifest/src/query/song.rs
Normal 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
69
xmpd-manifest/src/song.rs
Normal 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)"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
71
xmpd-manifest/src/store/json.rs
Normal file
71
xmpd-manifest/src/store/json.rs
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
40
xmpd-manifest/src/store/mod.rs
Normal file
40
xmpd-manifest/src/store/mod.rs
Normal 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;
|
||||
}
|
||||
|
||||
19
xmpd-manifest/src/tests/mod.rs
Normal file
19
xmpd-manifest/src/tests/mod.rs
Normal 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>();
|
||||
}
|
||||
Reference in New Issue
Block a user