Basic manifest implementation

This commit is contained in:
2024-11-06 12:12:07 +02:00
parent d9b23d4a24
commit a00486eeaf
11 changed files with 616 additions and 2 deletions

View File

@@ -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

View File

@@ -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(())
}
}

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>();
}