mirror of
https://github.com/Dummi26/musicdb.git
synced 2025-12-14 11:56:16 +01:00
init
This commit is contained in:
2
musicdb-lib/.gitignore
vendored
Executable file
2
musicdb-lib/.gitignore
vendored
Executable file
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
/Cargo.lock
|
||||
12
musicdb-lib/Cargo.toml
Executable file
12
musicdb-lib/Cargo.toml
Executable file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "musicdb-lib"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
awedio = "0.2.0"
|
||||
base64 = "0.21.2"
|
||||
rc-u8-reader = "2.0.16"
|
||||
tokio = "1.29.1"
|
||||
43
musicdb-lib/src/data/album.rs
Executable file
43
musicdb-lib/src/data/album.rs
Executable file
@@ -0,0 +1,43 @@
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use crate::load::ToFromBytes;
|
||||
|
||||
use super::{AlbumId, ArtistId, CoverId, GeneralData, SongId};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Album {
|
||||
pub id: AlbumId,
|
||||
pub name: String,
|
||||
pub artist: Option<ArtistId>,
|
||||
pub cover: Option<CoverId>,
|
||||
pub songs: Vec<SongId>,
|
||||
pub general: GeneralData,
|
||||
}
|
||||
|
||||
impl ToFromBytes for Album {
|
||||
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
|
||||
where
|
||||
T: Write,
|
||||
{
|
||||
self.id.to_bytes(s)?;
|
||||
self.name.to_bytes(s)?;
|
||||
self.artist.to_bytes(s)?;
|
||||
self.songs.to_bytes(s)?;
|
||||
self.cover.to_bytes(s)?;
|
||||
self.general.to_bytes(s)?;
|
||||
Ok(())
|
||||
}
|
||||
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
|
||||
where
|
||||
T: Read,
|
||||
{
|
||||
Ok(Self {
|
||||
id: ToFromBytes::from_bytes(s)?,
|
||||
name: ToFromBytes::from_bytes(s)?,
|
||||
artist: ToFromBytes::from_bytes(s)?,
|
||||
songs: ToFromBytes::from_bytes(s)?,
|
||||
cover: ToFromBytes::from_bytes(s)?,
|
||||
general: ToFromBytes::from_bytes(s)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
43
musicdb-lib/src/data/artist.rs
Executable file
43
musicdb-lib/src/data/artist.rs
Executable file
@@ -0,0 +1,43 @@
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use crate::load::ToFromBytes;
|
||||
|
||||
use super::{AlbumId, ArtistId, CoverId, GeneralData, SongId};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Artist {
|
||||
pub id: ArtistId,
|
||||
pub name: String,
|
||||
pub cover: Option<CoverId>,
|
||||
pub albums: Vec<AlbumId>,
|
||||
pub singles: Vec<SongId>,
|
||||
pub general: GeneralData,
|
||||
}
|
||||
|
||||
impl ToFromBytes for Artist {
|
||||
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
|
||||
where
|
||||
T: Write,
|
||||
{
|
||||
self.id.to_bytes(s)?;
|
||||
self.name.to_bytes(s)?;
|
||||
self.albums.to_bytes(s)?;
|
||||
self.singles.to_bytes(s)?;
|
||||
self.cover.to_bytes(s)?;
|
||||
self.general.to_bytes(s)?;
|
||||
Ok(())
|
||||
}
|
||||
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
|
||||
where
|
||||
T: Read,
|
||||
{
|
||||
Ok(Self {
|
||||
id: ToFromBytes::from_bytes(s)?,
|
||||
name: ToFromBytes::from_bytes(s)?,
|
||||
albums: ToFromBytes::from_bytes(s)?,
|
||||
singles: ToFromBytes::from_bytes(s)?,
|
||||
cover: ToFromBytes::from_bytes(s)?,
|
||||
general: ToFromBytes::from_bytes(s)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
377
musicdb-lib/src/data/database.rs
Executable file
377
musicdb-lib/src/data/database.rs
Executable file
@@ -0,0 +1,377 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::{self, File},
|
||||
io::{BufReader, Write},
|
||||
path::PathBuf,
|
||||
sync::{mpsc, Arc},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use crate::{load::ToFromBytes, server::Command};
|
||||
|
||||
use super::{
|
||||
album::Album,
|
||||
artist::Artist,
|
||||
queue::{Queue, QueueContent},
|
||||
song::Song,
|
||||
AlbumId, ArtistId, CoverId, DatabaseLocation, SongId,
|
||||
};
|
||||
|
||||
pub struct Database {
|
||||
db_file: PathBuf,
|
||||
pub lib_directory: PathBuf,
|
||||
artists: HashMap<ArtistId, Artist>,
|
||||
albums: HashMap<AlbumId, Album>,
|
||||
songs: HashMap<SongId, Song>,
|
||||
covers: HashMap<CoverId, DatabaseLocation>,
|
||||
// TODO! make sure this works out for the server AND clients
|
||||
// cover_cache: HashMap<CoverId, Vec<u8>>,
|
||||
db_data_file_change_first: Option<Instant>,
|
||||
db_data_file_change_last: Option<Instant>,
|
||||
pub queue: Queue,
|
||||
pub update_endpoints: Vec<UpdateEndpoint>,
|
||||
pub playing: bool,
|
||||
pub command_sender: Option<mpsc::Sender<Command>>,
|
||||
}
|
||||
pub enum UpdateEndpoint {
|
||||
Bytes(Box<dyn Write + Sync + Send>),
|
||||
CmdChannel(mpsc::Sender<Arc<Command>>),
|
||||
CmdChannelTokio(tokio::sync::mpsc::UnboundedSender<Arc<Command>>),
|
||||
Custom(Box<dyn FnMut(&Command) + Send>),
|
||||
}
|
||||
|
||||
impl Database {
|
||||
fn panic(&self, msg: &str) -> ! {
|
||||
// custom panic handler
|
||||
// make a backup
|
||||
// exit
|
||||
panic!("DatabasePanic: {msg}");
|
||||
}
|
||||
pub fn get_path(&self, location: &DatabaseLocation) -> PathBuf {
|
||||
self.lib_directory.join(&location.rel_path)
|
||||
}
|
||||
pub fn get_song(&self, song: &SongId) -> Option<&Song> {
|
||||
self.songs.get(song)
|
||||
}
|
||||
pub fn get_song_mut(&mut self, song: &SongId) -> Option<&mut Song> {
|
||||
self.songs.get_mut(song)
|
||||
}
|
||||
/// adds a song to the database.
|
||||
/// ignores song.id and just assigns a new id, which it then returns.
|
||||
/// this function also adds a reference to the new song to the album (or artist.singles, if no album)
|
||||
pub fn add_song_new(&mut self, song: Song) -> SongId {
|
||||
let album = song.album.clone();
|
||||
let artist = song.artist.clone();
|
||||
let id = self.add_song_new_nomagic(song);
|
||||
if let Some(Some(album)) = album.map(|v| self.albums.get_mut(&v)) {
|
||||
album.songs.push(id);
|
||||
} else {
|
||||
if let Some(Some(artist)) = artist.map(|v| self.artists.get_mut(&v)) {
|
||||
artist.singles.push(id);
|
||||
}
|
||||
}
|
||||
id
|
||||
}
|
||||
pub fn add_song_new_nomagic(&mut self, mut song: Song) -> SongId {
|
||||
for key in 0.. {
|
||||
if !self.songs.contains_key(&key) {
|
||||
song.id = key;
|
||||
self.songs.insert(key, song);
|
||||
return key;
|
||||
}
|
||||
}
|
||||
self.panic("database.songs all keys used - no more capacity for new songs!");
|
||||
}
|
||||
/// adds an artist to the database.
|
||||
/// ignores artist.id and just assigns a new id, which it then returns.
|
||||
/// this function does nothing special.
|
||||
pub fn add_artist_new(&mut self, artist: Artist) -> ArtistId {
|
||||
let id = self.add_artist_new_nomagic(artist);
|
||||
id
|
||||
}
|
||||
fn add_artist_new_nomagic(&mut self, mut artist: Artist) -> ArtistId {
|
||||
for key in 0.. {
|
||||
if !self.artists.contains_key(&key) {
|
||||
artist.id = key;
|
||||
self.artists.insert(key, artist);
|
||||
return key;
|
||||
}
|
||||
}
|
||||
self.panic("database.artists all keys used - no more capacity for new artists!");
|
||||
}
|
||||
/// adds an album to the database.
|
||||
/// ignores album.id and just assigns a new id, which it then returns.
|
||||
/// this function also adds a reference to the new album to the artist
|
||||
pub fn add_album_new(&mut self, album: Album) -> AlbumId {
|
||||
let artist = album.artist.clone();
|
||||
let id = self.add_album_new_nomagic(album);
|
||||
if let Some(Some(artist)) = artist.map(|v| self.artists.get_mut(&v)) {
|
||||
artist.albums.push(id);
|
||||
}
|
||||
id
|
||||
}
|
||||
fn add_album_new_nomagic(&mut self, mut album: Album) -> AlbumId {
|
||||
for key in 0.. {
|
||||
if !self.albums.contains_key(&key) {
|
||||
album.id = key;
|
||||
self.albums.insert(key, album);
|
||||
return key;
|
||||
}
|
||||
}
|
||||
self.panic("database.artists all keys used - no more capacity for new artists!");
|
||||
}
|
||||
/// updates an existing song in the database with the new value.
|
||||
/// uses song.id to find the correct song.
|
||||
/// if the id doesn't exist in the db, Err(()) is returned.
|
||||
/// Otherwise Some(old_data) is returned.
|
||||
pub fn update_song(&mut self, song: Song) -> Result<Song, ()> {
|
||||
if let Some(prev_song) = self.songs.get_mut(&song.id) {
|
||||
Ok(std::mem::replace(prev_song, song))
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
pub fn update_album(&mut self, album: Album) -> Result<Album, ()> {
|
||||
if let Some(prev_album) = self.albums.get_mut(&album.id) {
|
||||
Ok(std::mem::replace(prev_album, album))
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
pub fn update_artist(&mut self, artist: Artist) -> Result<Artist, ()> {
|
||||
if let Some(prev_artist) = self.artists.get_mut(&artist.id) {
|
||||
Ok(std::mem::replace(prev_artist, artist))
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
/// [NOT RECOMMENDED - use add_song_new or update_song instead!] inserts the song into the database.
|
||||
/// uses song.id. If another song with that ID exists, it is replaced and Some(other_song) is returned.
|
||||
/// If no other song exists, the song will be added to the database with the given ID and None is returned.
|
||||
pub fn update_or_add_song(&mut self, song: Song) -> Option<Song> {
|
||||
self.songs.insert(song.id, song)
|
||||
}
|
||||
|
||||
pub fn init_connection<T: Write>(&self, con: &mut T) -> Result<(), std::io::Error> {
|
||||
// TODO! this is slow because it clones everything - there has to be a better way...
|
||||
Command::SyncDatabase(
|
||||
self.artists().iter().map(|v| v.1.clone()).collect(),
|
||||
self.albums().iter().map(|v| v.1.clone()).collect(),
|
||||
self.songs().iter().map(|v| v.1.clone()).collect(),
|
||||
)
|
||||
.to_bytes(con)?;
|
||||
Command::QueueUpdate(vec![], self.queue.clone()).to_bytes(con)?;
|
||||
if self.playing {
|
||||
Command::Resume.to_bytes(con)?;
|
||||
}
|
||||
// since this is so easy to check for, it comes last.
|
||||
// this allows clients to find out when init_connection is done.
|
||||
Command::SetLibraryDirectory(self.lib_directory.clone()).to_bytes(con)?;
|
||||
// is initialized now - client can receive updates after this point.
|
||||
// NOTE: Don't write to connection anymore - the db will dispatch updates on its own.
|
||||
// we just need to handle commands (receive from the connection).
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn apply_command(&mut self, command: Command) {
|
||||
// since db.update_endpoints is empty for clients, this won't cause unwanted back and forth
|
||||
self.broadcast_update(&command);
|
||||
match command {
|
||||
Command::Resume => self.playing = true,
|
||||
Command::Pause => self.playing = false,
|
||||
Command::Stop => self.playing = false,
|
||||
Command::NextSong => {
|
||||
self.queue.advance_index();
|
||||
}
|
||||
Command::Save => {
|
||||
if let Err(e) = self.save_database(None) {
|
||||
eprintln!("Couldn't save: {e}");
|
||||
}
|
||||
}
|
||||
Command::SyncDatabase(a, b, c) => self.sync(a, b, c),
|
||||
Command::QueueUpdate(index, new_data) => {
|
||||
if let Some(v) = self.queue.get_item_at_index_mut(&index, 0) {
|
||||
*v = new_data;
|
||||
}
|
||||
}
|
||||
Command::QueueAdd(mut index, new_data) => {
|
||||
if let Some(v) = self.queue.get_item_at_index_mut(&index, 0) {
|
||||
v.add_to_end(new_data);
|
||||
}
|
||||
}
|
||||
Command::QueueInsert(mut index, pos, new_data) => {
|
||||
if let Some(v) = self.queue.get_item_at_index_mut(&index, 0) {
|
||||
v.insert(new_data, pos);
|
||||
}
|
||||
}
|
||||
Command::QueueRemove(index) => {
|
||||
self.queue.remove_by_index(&index, 0);
|
||||
}
|
||||
Command::QueueGoto(index) => self.queue.set_index(&index, 0),
|
||||
Command::AddSong(song) => {
|
||||
self.add_song_new(song);
|
||||
}
|
||||
Command::AddAlbum(album) => {
|
||||
self.add_album_new(album);
|
||||
}
|
||||
Command::AddArtist(artist) => {
|
||||
self.add_artist_new(artist);
|
||||
}
|
||||
Command::ModifySong(song) => {
|
||||
_ = self.update_song(song);
|
||||
}
|
||||
Command::ModifyAlbum(album) => {
|
||||
_ = self.update_album(album);
|
||||
}
|
||||
Command::ModifyArtist(artist) => {
|
||||
_ = self.update_artist(artist);
|
||||
}
|
||||
Command::SetLibraryDirectory(new_dir) => {
|
||||
self.lib_directory = new_dir;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// file saving/loading
|
||||
|
||||
impl Database {
|
||||
/// Database is also used for clients, to keep things consistent.
|
||||
/// A client database doesn't need any storage paths and won't perform autosaves.
|
||||
pub fn new_clientside() -> Self {
|
||||
Self {
|
||||
db_file: PathBuf::new(),
|
||||
lib_directory: PathBuf::new(),
|
||||
artists: HashMap::new(),
|
||||
albums: HashMap::new(),
|
||||
songs: HashMap::new(),
|
||||
covers: HashMap::new(),
|
||||
db_data_file_change_first: None,
|
||||
db_data_file_change_last: None,
|
||||
queue: QueueContent::Folder(0, vec![], String::new()).into(),
|
||||
update_endpoints: vec![],
|
||||
playing: false,
|
||||
command_sender: None,
|
||||
}
|
||||
}
|
||||
pub fn new_empty(path: PathBuf, lib_dir: PathBuf) -> Self {
|
||||
Self {
|
||||
db_file: path,
|
||||
lib_directory: lib_dir,
|
||||
artists: HashMap::new(),
|
||||
albums: HashMap::new(),
|
||||
songs: HashMap::new(),
|
||||
covers: HashMap::new(),
|
||||
db_data_file_change_first: None,
|
||||
db_data_file_change_last: None,
|
||||
queue: QueueContent::Folder(0, vec![], String::new()).into(),
|
||||
update_endpoints: vec![],
|
||||
playing: false,
|
||||
command_sender: None,
|
||||
}
|
||||
}
|
||||
pub fn load_database(path: PathBuf) -> Result<Self, std::io::Error> {
|
||||
let mut file = BufReader::new(File::open(&path)?);
|
||||
eprintln!("[info] loading library from {file:?}");
|
||||
let lib_directory = ToFromBytes::from_bytes(&mut file)?;
|
||||
eprintln!("[info] library directory is {lib_directory:?}");
|
||||
Ok(Self {
|
||||
db_file: path,
|
||||
lib_directory,
|
||||
artists: ToFromBytes::from_bytes(&mut file)?,
|
||||
albums: ToFromBytes::from_bytes(&mut file)?,
|
||||
songs: ToFromBytes::from_bytes(&mut file)?,
|
||||
covers: ToFromBytes::from_bytes(&mut file)?,
|
||||
db_data_file_change_first: None,
|
||||
db_data_file_change_last: None,
|
||||
queue: QueueContent::Folder(0, vec![], String::new()).into(),
|
||||
update_endpoints: vec![],
|
||||
playing: false,
|
||||
command_sender: None,
|
||||
})
|
||||
}
|
||||
pub fn save_database(&self, path: Option<PathBuf>) -> Result<PathBuf, std::io::Error> {
|
||||
let path = if let Some(p) = path {
|
||||
p
|
||||
} else {
|
||||
self.db_file.clone()
|
||||
};
|
||||
// if no path is set (client mode), do nothing
|
||||
if path.as_os_str().is_empty() {
|
||||
return Ok(path);
|
||||
}
|
||||
eprintln!("[info] saving db to {path:?}.");
|
||||
let mut file = fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.create(true)
|
||||
.open(&path)?;
|
||||
self.lib_directory.to_bytes(&mut file)?;
|
||||
self.artists.to_bytes(&mut file)?;
|
||||
self.albums.to_bytes(&mut file)?;
|
||||
self.songs.to_bytes(&mut file)?;
|
||||
self.covers.to_bytes(&mut file)?;
|
||||
Ok(path)
|
||||
}
|
||||
pub fn broadcast_update(&mut self, update: &Command) {
|
||||
let mut remove = vec![];
|
||||
let mut bytes = None;
|
||||
let mut arc = None;
|
||||
for (i, udep) in self.update_endpoints.iter_mut().enumerate() {
|
||||
match udep {
|
||||
UpdateEndpoint::Bytes(writer) => {
|
||||
if bytes.is_none() {
|
||||
bytes = Some(update.to_bytes_vec());
|
||||
}
|
||||
if writer.write_all(bytes.as_ref().unwrap()).is_err() {
|
||||
remove.push(i);
|
||||
}
|
||||
}
|
||||
UpdateEndpoint::CmdChannel(sender) => {
|
||||
if arc.is_none() {
|
||||
arc = Some(Arc::new(update.clone()));
|
||||
}
|
||||
if sender.send(arc.clone().unwrap()).is_err() {
|
||||
remove.push(i);
|
||||
}
|
||||
}
|
||||
UpdateEndpoint::CmdChannelTokio(sender) => {
|
||||
if arc.is_none() {
|
||||
arc = Some(Arc::new(update.clone()));
|
||||
}
|
||||
if sender.send(arc.clone().unwrap()).is_err() {
|
||||
remove.push(i);
|
||||
}
|
||||
}
|
||||
UpdateEndpoint::Custom(func) => func(update),
|
||||
}
|
||||
}
|
||||
if !remove.is_empty() {
|
||||
eprintln!(
|
||||
"[info] closing {} connections, {} are still active",
|
||||
remove.len(),
|
||||
self.update_endpoints.len() - remove.len()
|
||||
);
|
||||
for i in remove.into_iter().rev() {
|
||||
self.update_endpoints.remove(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn sync(&mut self, artists: Vec<Artist>, albums: Vec<Album>, songs: Vec<Song>) {
|
||||
self.artists = artists.iter().map(|v| (v.id, v.clone())).collect();
|
||||
self.albums = albums.iter().map(|v| (v.id, v.clone())).collect();
|
||||
self.songs = songs.iter().map(|v| (v.id, v.clone())).collect();
|
||||
}
|
||||
}
|
||||
|
||||
impl Database {
|
||||
pub fn songs(&self) -> &HashMap<SongId, Song> {
|
||||
&self.songs
|
||||
}
|
||||
pub fn albums(&self) -> &HashMap<AlbumId, Album> {
|
||||
&self.albums
|
||||
}
|
||||
pub fn artists(&self) -> &HashMap<ArtistId, Artist> {
|
||||
&self.artists
|
||||
}
|
||||
}
|
||||
73
musicdb-lib/src/data/mod.rs
Executable file
73
musicdb-lib/src/data/mod.rs
Executable file
@@ -0,0 +1,73 @@
|
||||
use std::{
|
||||
io::{Read, Write},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use crate::load::ToFromBytes;
|
||||
|
||||
pub mod album;
|
||||
pub mod artist;
|
||||
pub mod database;
|
||||
pub mod queue;
|
||||
pub mod song;
|
||||
|
||||
pub type SongId = u64;
|
||||
pub type AlbumId = u64;
|
||||
pub type ArtistId = u64;
|
||||
pub type CoverId = u64;
|
||||
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct GeneralData {
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DatabaseLocation {
|
||||
pub rel_path: PathBuf,
|
||||
}
|
||||
|
||||
impl ToFromBytes for DatabaseLocation {
|
||||
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
|
||||
where
|
||||
T: Write,
|
||||
{
|
||||
self.rel_path.to_bytes(s)
|
||||
}
|
||||
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
|
||||
where
|
||||
T: Read,
|
||||
{
|
||||
Ok(Self {
|
||||
rel_path: ToFromBytes::from_bytes(s)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<P> From<P> for DatabaseLocation
|
||||
where
|
||||
P: Into<PathBuf>,
|
||||
{
|
||||
fn from(value: P) -> Self {
|
||||
Self {
|
||||
rel_path: value.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToFromBytes for GeneralData {
|
||||
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
|
||||
where
|
||||
T: Write,
|
||||
{
|
||||
self.tags.to_bytes(s)?;
|
||||
Ok(())
|
||||
}
|
||||
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
|
||||
where
|
||||
T: Read,
|
||||
{
|
||||
Ok(Self {
|
||||
tags: ToFromBytes::from_bytes(s)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
286
musicdb-lib/src/data/queue.rs
Executable file
286
musicdb-lib/src/data/queue.rs
Executable file
@@ -0,0 +1,286 @@
|
||||
use crate::load::ToFromBytes;
|
||||
|
||||
use super::SongId;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Queue {
|
||||
enabled: bool,
|
||||
content: QueueContent,
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum QueueContent {
|
||||
Song(SongId),
|
||||
Folder(usize, Vec<Queue>, String),
|
||||
}
|
||||
|
||||
impl Queue {
|
||||
pub fn enabled(&self) -> bool {
|
||||
self.enabled
|
||||
}
|
||||
pub fn content(&self) -> &QueueContent {
|
||||
&self.content
|
||||
}
|
||||
|
||||
pub fn add_to_end(&mut self, v: Self) -> bool {
|
||||
match &mut self.content {
|
||||
QueueContent::Song(_) => false,
|
||||
QueueContent::Folder(_, vec, _) => {
|
||||
vec.push(v);
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn insert(&mut self, v: Self, index: usize) -> bool {
|
||||
match &mut self.content {
|
||||
QueueContent::Song(_) => false,
|
||||
QueueContent::Folder(_, vec, _) => {
|
||||
if index <= vec.len() {
|
||||
vec.insert(index, v);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
if !self.enabled {
|
||||
return 0;
|
||||
}
|
||||
match &self.content {
|
||||
QueueContent::Song(_) => 1,
|
||||
QueueContent::Folder(_, v, _) => v.iter().map(|v| v.len()).sum(),
|
||||
}
|
||||
}
|
||||
|
||||
/// recursively descends the queue until the current active element is found, then returns it.
|
||||
pub fn get_current(&self) -> Option<&Self> {
|
||||
match &self.content {
|
||||
QueueContent::Folder(i, v, _) => {
|
||||
let i = *i;
|
||||
if let Some(v) = v.get(i) {
|
||||
v.get_current()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
QueueContent::Song(_) => Some(self),
|
||||
}
|
||||
}
|
||||
pub fn get_current_song(&self) -> Option<&SongId> {
|
||||
if let QueueContent::Song(id) = self.get_current()?.content() {
|
||||
Some(id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
pub fn get_next_song(&self) -> Option<&SongId> {
|
||||
if let QueueContent::Song(id) = self.get_next()?.content() {
|
||||
Some(id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
pub fn get_next(&self) -> Option<&Self> {
|
||||
match &self.content {
|
||||
QueueContent::Folder(i, vec, _) => {
|
||||
let i = *i;
|
||||
if let Some(v) = vec.get(i) {
|
||||
if let Some(v) = v.get_next() {
|
||||
Some(v)
|
||||
} else {
|
||||
if let Some(v) = vec.get(i + 1) {
|
||||
v.get_current()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
QueueContent::Song(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advance_index(&mut self) -> bool {
|
||||
match &mut self.content {
|
||||
QueueContent::Song(_) => false,
|
||||
QueueContent::Folder(index, contents, _) => {
|
||||
if let Some(c) = contents.get_mut(*index) {
|
||||
// inner value could advance index, do nothing.
|
||||
if c.advance_index() {
|
||||
true
|
||||
} else {
|
||||
loop {
|
||||
if *index + 1 < contents.len() {
|
||||
// can advance
|
||||
*index += 1;
|
||||
if contents[*index].enabled {
|
||||
break true;
|
||||
}
|
||||
} else {
|
||||
// can't advance: index would be out of bounds
|
||||
*index = 0;
|
||||
break false;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
*index = 0;
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_index(&mut self, index: &Vec<usize>, depth: usize) {
|
||||
let i = index.get(depth).map(|v| *v).unwrap_or(0);
|
||||
match &mut self.content {
|
||||
QueueContent::Song(_) => {}
|
||||
QueueContent::Folder(idx, contents, _) => {
|
||||
*idx = i;
|
||||
for (i2, c) in contents.iter_mut().enumerate() {
|
||||
if i2 != i {
|
||||
c.set_index(&vec![], 0)
|
||||
}
|
||||
}
|
||||
if let Some(c) = contents.get_mut(i) {
|
||||
c.set_index(index, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_item_at_index(&self, index: &Vec<usize>, depth: usize) -> Option<&Self> {
|
||||
if let Some(i) = index.get(depth) {
|
||||
match &self.content {
|
||||
QueueContent::Song(_) => None,
|
||||
QueueContent::Folder(_, v, _) => {
|
||||
if let Some(v) = v.get(*i) {
|
||||
v.get_item_at_index(index, depth + 1)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Some(self)
|
||||
}
|
||||
}
|
||||
pub fn get_item_at_index_mut(&mut self, index: &Vec<usize>, depth: usize) -> Option<&mut Self> {
|
||||
if let Some(i) = index.get(depth) {
|
||||
match &mut self.content {
|
||||
QueueContent::Song(_) => None,
|
||||
QueueContent::Folder(_, v, _) => {
|
||||
if let Some(v) = v.get_mut(*i) {
|
||||
v.get_item_at_index_mut(index, depth + 1)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Some(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_by_index(&mut self, index: &Vec<usize>, depth: usize) -> Option<Self> {
|
||||
if let Some(i) = index.get(depth) {
|
||||
match &mut self.content {
|
||||
QueueContent::Song(_) => None,
|
||||
QueueContent::Folder(ci, v, _) => {
|
||||
if depth + 1 < index.len() {
|
||||
if let Some(v) = v.get_mut(*i) {
|
||||
v.remove_by_index(index, depth + 1)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
if *i < v.len() {
|
||||
// if current playback is past this point,
|
||||
// reduce the index by 1 so that it still points to the same element
|
||||
if *ci > *i {
|
||||
*ci -= 1;
|
||||
}
|
||||
Some(v.remove(*i))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<QueueContent> for Queue {
|
||||
fn from(value: QueueContent) -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
content: value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToFromBytes for Queue {
|
||||
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
|
||||
where
|
||||
T: std::io::Write,
|
||||
{
|
||||
s.write_all(&[if self.enabled { 0b11111111 } else { 0b00000000 }])?;
|
||||
self.content.to_bytes(s)?;
|
||||
Ok(())
|
||||
}
|
||||
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
|
||||
where
|
||||
T: std::io::Read,
|
||||
{
|
||||
let mut enabled = [0];
|
||||
s.read_exact(&mut enabled)?;
|
||||
Ok(Self {
|
||||
enabled: enabled[0].count_ones() >= 4,
|
||||
content: ToFromBytes::from_bytes(s)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToFromBytes for QueueContent {
|
||||
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
|
||||
where
|
||||
T: std::io::Write,
|
||||
{
|
||||
match self {
|
||||
Self::Song(id) => {
|
||||
s.write_all(&[0b11111111])?;
|
||||
id.to_bytes(s)?;
|
||||
}
|
||||
Self::Folder(index, contents, name) => {
|
||||
s.write_all(&[0b00000000])?;
|
||||
index.to_bytes(s)?;
|
||||
contents.to_bytes(s)?;
|
||||
name.to_bytes(s)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
|
||||
where
|
||||
T: std::io::Read,
|
||||
{
|
||||
let mut switch_on = [0];
|
||||
s.read_exact(&mut switch_on)?;
|
||||
Ok(if switch_on[0].count_ones() > 4 {
|
||||
Self::Song(ToFromBytes::from_bytes(s)?)
|
||||
} else {
|
||||
Self::Folder(
|
||||
ToFromBytes::from_bytes(s)?,
|
||||
ToFromBytes::from_bytes(s)?,
|
||||
ToFromBytes::from_bytes(s)?,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
166
musicdb-lib/src/data/song.rs
Executable file
166
musicdb-lib/src/data/song.rs
Executable file
@@ -0,0 +1,166 @@
|
||||
use std::{
|
||||
fmt::Display,
|
||||
io::{Read, Write},
|
||||
path::Path,
|
||||
sync::{Arc, Mutex},
|
||||
thread::JoinHandle,
|
||||
};
|
||||
|
||||
use crate::load::ToFromBytes;
|
||||
|
||||
use super::{
|
||||
database::Database, AlbumId, ArtistId, CoverId, DatabaseLocation, GeneralData, SongId,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Song {
|
||||
pub id: SongId,
|
||||
pub location: DatabaseLocation,
|
||||
pub title: String,
|
||||
pub album: Option<AlbumId>,
|
||||
pub artist: Option<ArtistId>,
|
||||
pub more_artists: Vec<ArtistId>,
|
||||
pub cover: Option<CoverId>,
|
||||
pub general: GeneralData,
|
||||
/// None => No cached data
|
||||
/// Some(Err) => No cached data yet, but a thread is working on loading it.
|
||||
/// Some(Ok(data)) => Cached data is available.
|
||||
pub cached_data: Arc<Mutex<Option<Result<Arc<Vec<u8>>, JoinHandle<Option<Arc<Vec<u8>>>>>>>>,
|
||||
}
|
||||
impl Song {
|
||||
pub fn new(
|
||||
location: DatabaseLocation,
|
||||
title: String,
|
||||
album: Option<AlbumId>,
|
||||
artist: Option<ArtistId>,
|
||||
more_artists: Vec<ArtistId>,
|
||||
cover: Option<CoverId>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: 0,
|
||||
location,
|
||||
title,
|
||||
album,
|
||||
artist,
|
||||
more_artists,
|
||||
cover,
|
||||
general: GeneralData::default(),
|
||||
cached_data: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
pub fn uncache_data(&self) {
|
||||
*self.cached_data.lock().unwrap() = None;
|
||||
}
|
||||
/// If no data is cached yet and no caching thread is running, starts a thread to cache the data.
|
||||
pub fn cache_data_start_thread(&self, db: &Database) -> bool {
|
||||
let mut cd = self.cached_data.lock().unwrap();
|
||||
let start_thread = match cd.as_ref() {
|
||||
None => true,
|
||||
Some(Err(_)) | Some(Ok(_)) => false,
|
||||
};
|
||||
if start_thread {
|
||||
let path = db.get_path(&self.location);
|
||||
*cd = Some(Err(std::thread::spawn(move || {
|
||||
eprintln!("[info] thread started");
|
||||
let data = Self::load_data(&path)?;
|
||||
eprintln!("[info] thread stopping after loading {path:?}");
|
||||
Some(Arc::new(data))
|
||||
})));
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
/// Gets the cached data, if available.
|
||||
/// If a thread is running to load the data, it is not awaited.
|
||||
/// This function doesn't block.
|
||||
pub fn cached_data(&self) -> Option<Arc<Vec<u8>>> {
|
||||
if let Some(Ok(v)) = self.cached_data.lock().unwrap().as_ref() {
|
||||
Some(Arc::clone(v))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
/// Gets the cached data, if available.
|
||||
/// If a thread is running to load the data, it *is* awaited.
|
||||
/// This function will block until the data is loaded.
|
||||
/// If it still returns none, some error must have occured.
|
||||
pub fn cached_data_now(&self, db: &Database) -> Option<Arc<Vec<u8>>> {
|
||||
let mut cd = self.cached_data.lock().unwrap();
|
||||
*cd = match cd.take() {
|
||||
None => {
|
||||
if let Some(v) = Self::load_data(db.get_path(&self.location)) {
|
||||
Some(Ok(Arc::new(v)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Some(Err(t)) => match t.join() {
|
||||
Err(_e) => None,
|
||||
Ok(Some(v)) => Some(Ok(v)),
|
||||
Ok(None) => None,
|
||||
},
|
||||
Some(Ok(v)) => Some(Ok(v)),
|
||||
};
|
||||
drop(cd);
|
||||
self.cached_data()
|
||||
}
|
||||
fn load_data<P: AsRef<Path>>(path: P) -> Option<Vec<u8>> {
|
||||
eprintln!("[info] loading song from {:?}", path.as_ref());
|
||||
match std::fs::read(&path) {
|
||||
Ok(v) => {
|
||||
eprintln!("[info] loaded song from {:?}", path.as_ref());
|
||||
Some(v)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[info] error loading {:?}: {e:?}", path.as_ref());
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Display for Song {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.title)?;
|
||||
match (self.artist, self.album) {
|
||||
(Some(artist), Some(album)) => write!(f, " (by {artist} on {album})")?,
|
||||
(None, Some(album)) => write!(f, " (on {album})")?,
|
||||
(Some(artist), None) => write!(f, " (by {artist})")?,
|
||||
(None, None) => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ToFromBytes for Song {
|
||||
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
|
||||
where
|
||||
T: Write,
|
||||
{
|
||||
self.id.to_bytes(s)?;
|
||||
self.location.to_bytes(s)?;
|
||||
self.title.to_bytes(s)?;
|
||||
self.album.to_bytes(s)?;
|
||||
self.artist.to_bytes(s)?;
|
||||
self.more_artists.to_bytes(s)?;
|
||||
self.cover.to_bytes(s)?;
|
||||
self.general.to_bytes(s)?;
|
||||
Ok(())
|
||||
}
|
||||
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
|
||||
where
|
||||
T: Read,
|
||||
{
|
||||
Ok(Self {
|
||||
id: ToFromBytes::from_bytes(s)?,
|
||||
location: ToFromBytes::from_bytes(s)?,
|
||||
title: ToFromBytes::from_bytes(s)?,
|
||||
album: ToFromBytes::from_bytes(s)?,
|
||||
artist: ToFromBytes::from_bytes(s)?,
|
||||
more_artists: ToFromBytes::from_bytes(s)?,
|
||||
cover: ToFromBytes::from_bytes(s)?,
|
||||
general: ToFromBytes::from_bytes(s)?,
|
||||
cached_data: Arc::new(Mutex::new(None)),
|
||||
})
|
||||
}
|
||||
}
|
||||
4
musicdb-lib/src/lib.rs
Executable file
4
musicdb-lib/src/lib.rs
Executable file
@@ -0,0 +1,4 @@
|
||||
pub mod data;
|
||||
pub mod load;
|
||||
pub mod player;
|
||||
pub mod server;
|
||||
330
musicdb-lib/src/load/mod.rs
Executable file
330
musicdb-lib/src/load/mod.rs
Executable file
@@ -0,0 +1,330 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
io::{Read, Write},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
pub trait ToFromBytes: Sized {
|
||||
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
|
||||
where
|
||||
T: Write;
|
||||
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
|
||||
where
|
||||
T: Read;
|
||||
fn to_bytes_vec(&self) -> Vec<u8> {
|
||||
let mut b = Vec::new();
|
||||
_ = self.to_bytes(&mut b);
|
||||
b
|
||||
}
|
||||
}
|
||||
|
||||
// impl ToFromBytes
|
||||
|
||||
// common types (String, Vec, ...)
|
||||
|
||||
impl ToFromBytes for String {
|
||||
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
|
||||
where
|
||||
T: Write,
|
||||
{
|
||||
self.len().to_bytes(s)?;
|
||||
s.write_all(self.as_bytes())
|
||||
}
|
||||
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
|
||||
where
|
||||
T: Read,
|
||||
{
|
||||
let len = ToFromBytes::from_bytes(s)?;
|
||||
let mut buf = vec![0; len];
|
||||
s.read_exact(&mut buf)?;
|
||||
Ok(String::from_utf8_lossy(&buf).into_owned())
|
||||
}
|
||||
}
|
||||
impl ToFromBytes for PathBuf {
|
||||
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
|
||||
where
|
||||
T: Write,
|
||||
{
|
||||
self.to_string_lossy().into_owned().to_bytes(s)
|
||||
}
|
||||
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
|
||||
where
|
||||
T: Read,
|
||||
{
|
||||
Ok(String::from_bytes(s)?.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> ToFromBytes for Vec<C>
|
||||
where
|
||||
C: ToFromBytes,
|
||||
{
|
||||
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
|
||||
where
|
||||
T: Write,
|
||||
{
|
||||
self.len().to_bytes(s)?;
|
||||
for elem in self {
|
||||
elem.to_bytes(s)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
|
||||
where
|
||||
T: Read,
|
||||
{
|
||||
let len = ToFromBytes::from_bytes(s)?;
|
||||
let mut buf = Vec::with_capacity(len);
|
||||
for _ in 0..len {
|
||||
buf.push(ToFromBytes::from_bytes(s)?);
|
||||
}
|
||||
Ok(buf)
|
||||
}
|
||||
}
|
||||
impl<A> ToFromBytes for Option<A>
|
||||
where
|
||||
A: ToFromBytes,
|
||||
{
|
||||
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
|
||||
where
|
||||
T: Write,
|
||||
{
|
||||
match self {
|
||||
None => s.write_all(&[0b11001100]),
|
||||
Some(v) => {
|
||||
s.write_all(&[0b00111010])?;
|
||||
v.to_bytes(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
|
||||
where
|
||||
T: Read,
|
||||
{
|
||||
let mut b = [0u8];
|
||||
s.read_exact(&mut b)?;
|
||||
match b[0] {
|
||||
0b00111010 => Ok(Some(ToFromBytes::from_bytes(s)?)),
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<K, V> ToFromBytes for HashMap<K, V>
|
||||
where
|
||||
K: ToFromBytes + std::cmp::Eq + std::hash::Hash,
|
||||
V: ToFromBytes,
|
||||
{
|
||||
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
|
||||
where
|
||||
T: Write,
|
||||
{
|
||||
self.len().to_bytes(s)?;
|
||||
for (key, val) in self.iter() {
|
||||
key.to_bytes(s)?;
|
||||
val.to_bytes(s)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
|
||||
where
|
||||
T: Read,
|
||||
{
|
||||
let len = ToFromBytes::from_bytes(s)?;
|
||||
let mut o = Self::with_capacity(len);
|
||||
for _ in 0..len {
|
||||
o.insert(ToFromBytes::from_bytes(s)?, ToFromBytes::from_bytes(s)?);
|
||||
}
|
||||
Ok(o)
|
||||
}
|
||||
}
|
||||
|
||||
// - for (i/u)(size/8/16/32/64/128)
|
||||
|
||||
impl ToFromBytes for usize {
|
||||
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
|
||||
where
|
||||
T: Write,
|
||||
{
|
||||
(*self as u64).to_bytes(s)
|
||||
}
|
||||
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
|
||||
where
|
||||
T: Read,
|
||||
{
|
||||
Ok(u64::from_bytes(s)? as _)
|
||||
}
|
||||
}
|
||||
impl ToFromBytes for isize {
|
||||
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
|
||||
where
|
||||
T: Write,
|
||||
{
|
||||
(*self as i64).to_bytes(s)
|
||||
}
|
||||
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
|
||||
where
|
||||
T: Read,
|
||||
{
|
||||
Ok(i64::from_bytes(s)? as _)
|
||||
}
|
||||
}
|
||||
impl ToFromBytes for u8 {
|
||||
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
|
||||
where
|
||||
T: Write,
|
||||
{
|
||||
s.write_all(&[*self])
|
||||
}
|
||||
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
|
||||
where
|
||||
T: Read,
|
||||
{
|
||||
let mut b = [0; 1];
|
||||
s.read_exact(&mut b)?;
|
||||
Ok(b[0])
|
||||
}
|
||||
}
|
||||
impl ToFromBytes for i8 {
|
||||
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
|
||||
where
|
||||
T: Write,
|
||||
{
|
||||
s.write_all(&self.to_be_bytes())
|
||||
}
|
||||
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
|
||||
where
|
||||
T: Read,
|
||||
{
|
||||
let mut b = [0; 1];
|
||||
s.read_exact(&mut b)?;
|
||||
Ok(Self::from_be_bytes(b))
|
||||
}
|
||||
}
|
||||
impl ToFromBytes for u16 {
|
||||
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
|
||||
where
|
||||
T: Write,
|
||||
{
|
||||
s.write_all(&self.to_be_bytes())
|
||||
}
|
||||
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
|
||||
where
|
||||
T: Read,
|
||||
{
|
||||
let mut b = [0; 2];
|
||||
s.read_exact(&mut b)?;
|
||||
Ok(Self::from_be_bytes(b))
|
||||
}
|
||||
}
|
||||
impl ToFromBytes for i16 {
|
||||
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
|
||||
where
|
||||
T: Write,
|
||||
{
|
||||
s.write_all(&self.to_be_bytes())
|
||||
}
|
||||
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
|
||||
where
|
||||
T: Read,
|
||||
{
|
||||
let mut b = [0; 2];
|
||||
s.read_exact(&mut b)?;
|
||||
Ok(Self::from_be_bytes(b))
|
||||
}
|
||||
}
|
||||
impl ToFromBytes for u32 {
|
||||
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
|
||||
where
|
||||
T: Write,
|
||||
{
|
||||
s.write_all(&self.to_be_bytes())
|
||||
}
|
||||
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
|
||||
where
|
||||
T: Read,
|
||||
{
|
||||
let mut b = [0; 4];
|
||||
s.read_exact(&mut b)?;
|
||||
Ok(Self::from_be_bytes(b))
|
||||
}
|
||||
}
|
||||
impl ToFromBytes for i32 {
|
||||
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
|
||||
where
|
||||
T: Write,
|
||||
{
|
||||
s.write_all(&self.to_be_bytes())
|
||||
}
|
||||
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
|
||||
where
|
||||
T: Read,
|
||||
{
|
||||
let mut b = [0; 4];
|
||||
s.read_exact(&mut b)?;
|
||||
Ok(Self::from_be_bytes(b))
|
||||
}
|
||||
}
|
||||
impl ToFromBytes for u64 {
|
||||
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
|
||||
where
|
||||
T: Write,
|
||||
{
|
||||
s.write_all(&self.to_be_bytes())
|
||||
}
|
||||
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
|
||||
where
|
||||
T: Read,
|
||||
{
|
||||
let mut b = [0; 8];
|
||||
s.read_exact(&mut b)?;
|
||||
Ok(Self::from_be_bytes(b))
|
||||
}
|
||||
}
|
||||
impl ToFromBytes for i64 {
|
||||
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
|
||||
where
|
||||
T: Write,
|
||||
{
|
||||
s.write_all(&self.to_be_bytes())
|
||||
}
|
||||
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
|
||||
where
|
||||
T: Read,
|
||||
{
|
||||
let mut b = [0; 8];
|
||||
s.read_exact(&mut b)?;
|
||||
Ok(Self::from_be_bytes(b))
|
||||
}
|
||||
}
|
||||
impl ToFromBytes for u128 {
|
||||
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
|
||||
where
|
||||
T: Write,
|
||||
{
|
||||
s.write_all(&self.to_be_bytes())
|
||||
}
|
||||
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
|
||||
where
|
||||
T: Read,
|
||||
{
|
||||
let mut b = [0; 16];
|
||||
s.read_exact(&mut b)?;
|
||||
Ok(Self::from_be_bytes(b))
|
||||
}
|
||||
}
|
||||
impl ToFromBytes for i128 {
|
||||
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
|
||||
where
|
||||
T: Write,
|
||||
{
|
||||
s.write_all(&self.to_be_bytes())
|
||||
}
|
||||
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
|
||||
where
|
||||
T: Read,
|
||||
{
|
||||
let mut b = [0; 16];
|
||||
s.read_exact(&mut b)?;
|
||||
Ok(Self::from_be_bytes(b))
|
||||
}
|
||||
}
|
||||
160
musicdb-lib/src/player/mod.rs
Executable file
160
musicdb-lib/src/player/mod.rs
Executable file
@@ -0,0 +1,160 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use awedio::{
|
||||
backends::CpalBackend,
|
||||
manager::Manager,
|
||||
sounds::wrappers::{AsyncCompletionNotifier, Controller, Pausable},
|
||||
Sound,
|
||||
};
|
||||
use rc_u8_reader::ArcU8Reader;
|
||||
|
||||
use crate::{
|
||||
data::{database::Database, SongId},
|
||||
server::Command,
|
||||
};
|
||||
|
||||
pub struct Player {
|
||||
/// can be unused, but must be present otherwise audio playback breaks
|
||||
#[allow(unused)]
|
||||
backend: CpalBackend,
|
||||
source: Option<(
|
||||
Controller<AsyncCompletionNotifier<Pausable<Box<dyn Sound>>>>,
|
||||
tokio::sync::oneshot::Receiver<()>,
|
||||
)>,
|
||||
manager: Manager,
|
||||
current_song_id: SongOpt,
|
||||
}
|
||||
|
||||
pub enum SongOpt {
|
||||
None,
|
||||
Some(SongId),
|
||||
/// Will be set to Some or None once handeled
|
||||
New(Option<SongId>),
|
||||
}
|
||||
|
||||
impl Player {
|
||||
pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let (manager, backend) = awedio::start()?;
|
||||
Ok(Self {
|
||||
manager,
|
||||
backend,
|
||||
source: None,
|
||||
current_song_id: SongOpt::None,
|
||||
})
|
||||
}
|
||||
pub fn handle_command(&mut self, command: &Command) {
|
||||
match command {
|
||||
Command::Resume => self.resume(),
|
||||
Command::Pause => self.pause(),
|
||||
Command::Stop => self.stop(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
pub fn pause(&mut self) {
|
||||
if let Some((source, _notif)) = &mut self.source {
|
||||
source.set_paused(true);
|
||||
}
|
||||
}
|
||||
pub fn resume(&mut self) {
|
||||
if let Some((source, _notif)) = &mut self.source {
|
||||
source.set_paused(false);
|
||||
} else if let SongOpt::Some(id) = &self.current_song_id {
|
||||
// there is no source to resume playback on, but there is a current song
|
||||
self.current_song_id = SongOpt::New(Some(*id));
|
||||
}
|
||||
}
|
||||
pub fn stop(&mut self) {
|
||||
if let Some((source, _notif)) = &mut self.source {
|
||||
source.set_paused(true);
|
||||
}
|
||||
self.current_song_id = SongOpt::New(None);
|
||||
}
|
||||
pub fn update(&mut self, db: &mut Database) {
|
||||
if db.playing && self.source.is_none() {
|
||||
if let Some(song) = db.queue.get_current_song() {
|
||||
// db playing, but no source - initialize a source (via SongOpt::New)
|
||||
self.current_song_id = SongOpt::New(Some(*song));
|
||||
} else {
|
||||
// db.playing, but no song in queue...
|
||||
}
|
||||
} else if let Some((_source, notif)) = &mut self.source {
|
||||
if let Ok(()) = notif.try_recv() {
|
||||
// song has finished playing
|
||||
db.apply_command(Command::NextSong);
|
||||
self.current_song_id = SongOpt::New(db.queue.get_current_song().cloned());
|
||||
}
|
||||
}
|
||||
|
||||
// check the queue's current index
|
||||
if let SongOpt::None = self.current_song_id {
|
||||
if let Some(id) = db.queue.get_current_song() {
|
||||
self.current_song_id = SongOpt::New(Some(*id));
|
||||
}
|
||||
} else if let SongOpt::Some(l_id) = &self.current_song_id {
|
||||
if let Some(id) = db.queue.get_current_song() {
|
||||
if *id != *l_id {
|
||||
self.current_song_id = SongOpt::New(Some(*id));
|
||||
}
|
||||
} else {
|
||||
self.current_song_id = SongOpt::New(None);
|
||||
}
|
||||
}
|
||||
|
||||
// new current song
|
||||
if let SongOpt::New(song_opt) = &self.current_song_id {
|
||||
// stop playback
|
||||
eprintln!("[play] stopping playback");
|
||||
self.manager.clear();
|
||||
if let Some(song_id) = song_opt {
|
||||
if db.playing {
|
||||
// start playback again
|
||||
if let Some(song) = db.get_song(song_id) {
|
||||
eprintln!("[play] starting playback...");
|
||||
// add our song
|
||||
let ext = match &song.location.rel_path.extension() {
|
||||
Some(s) => s.to_str().unwrap_or(""),
|
||||
None => "",
|
||||
};
|
||||
let (sound, notif) = Self::sound_from_bytes(
|
||||
ext,
|
||||
song.cached_data_now(db).expect("no cached data"),
|
||||
)
|
||||
.unwrap()
|
||||
.pausable()
|
||||
.with_async_completion_notifier();
|
||||
// add it
|
||||
let (sound, controller) = sound.controllable();
|
||||
self.source = Some((controller, notif));
|
||||
// and play it
|
||||
self.manager.play(Box::new(sound));
|
||||
eprintln!("[play] started playback");
|
||||
} else {
|
||||
panic!("invalid song ID: current_song_id not found in DB!");
|
||||
}
|
||||
}
|
||||
self.current_song_id = SongOpt::Some(*song_id);
|
||||
} else {
|
||||
self.current_song_id = SongOpt::None;
|
||||
}
|
||||
if let Some(Some(song)) = db.queue.get_next_song().map(|v| db.get_song(v)) {
|
||||
song.cache_data_start_thread(&db);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// partly identical to awedio/src/sounds/open_file.rs open_file_with_reader(), which is a private function I can't access
|
||||
fn sound_from_bytes(
|
||||
extension: &str,
|
||||
bytes: Arc<Vec<u8>>,
|
||||
) -> Result<Box<dyn Sound>, std::io::Error> {
|
||||
let reader = ArcU8Reader::new(bytes);
|
||||
Ok(match extension {
|
||||
"wav" => Box::new(
|
||||
awedio::sounds::decoders::WavDecoder::new(reader)
|
||||
.map_err(|_e| std::io::Error::from(std::io::ErrorKind::InvalidData))?,
|
||||
),
|
||||
"mp3" => Box::new(awedio::sounds::decoders::Mp3Decoder::new(reader)),
|
||||
_ => return Err(std::io::Error::from(std::io::ErrorKind::Unsupported)),
|
||||
})
|
||||
}
|
||||
}
|
||||
265
musicdb-lib/src/server/mod.rs
Executable file
265
musicdb-lib/src/server/mod.rs
Executable file
@@ -0,0 +1,265 @@
|
||||
use std::{
|
||||
eprintln,
|
||||
io::Write,
|
||||
net::{SocketAddr, TcpListener},
|
||||
path::PathBuf,
|
||||
sync::{mpsc, Arc, Mutex},
|
||||
thread::{self, JoinHandle},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
data::{
|
||||
album::Album,
|
||||
artist::Artist,
|
||||
database::{Database, UpdateEndpoint},
|
||||
queue::Queue,
|
||||
song::Song,
|
||||
},
|
||||
load::ToFromBytes,
|
||||
player::Player,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Command {
|
||||
Resume,
|
||||
Pause,
|
||||
Stop,
|
||||
Save,
|
||||
NextSong,
|
||||
SyncDatabase(Vec<Artist>, Vec<Album>, Vec<Song>),
|
||||
QueueUpdate(Vec<usize>, Queue),
|
||||
QueueAdd(Vec<usize>, Queue),
|
||||
QueueInsert(Vec<usize>, usize, Queue),
|
||||
QueueRemove(Vec<usize>),
|
||||
QueueGoto(Vec<usize>),
|
||||
/// .id field is ignored!
|
||||
AddSong(Song),
|
||||
/// .id field is ignored!
|
||||
AddAlbum(Album),
|
||||
/// .id field is ignored!
|
||||
AddArtist(Artist),
|
||||
ModifySong(Song),
|
||||
ModifyAlbum(Album),
|
||||
ModifyArtist(Artist),
|
||||
SetLibraryDirectory(PathBuf),
|
||||
}
|
||||
impl Command {
|
||||
pub fn send_to_server(self, db: &Database) -> Result<(), Self> {
|
||||
if let Some(sender) = &db.command_sender {
|
||||
sender.send(self).unwrap();
|
||||
Ok(())
|
||||
} else {
|
||||
Err(self)
|
||||
}
|
||||
}
|
||||
pub fn send_to_server_or_apply(self, db: &mut Database) {
|
||||
if let Some(sender) = &db.command_sender {
|
||||
sender.send(self).unwrap();
|
||||
} else {
|
||||
db.apply_command(self);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// starts handling database.command_sender events and optionally spawns a tcp server.
|
||||
/// this function creates a new command_sender.
|
||||
/// if you wish to implement your own server, set db.command_sender to None,
|
||||
/// start a new thread running this function,
|
||||
/// wait for db.command_sender to be Some,
|
||||
/// then start your server.
|
||||
/// for tcp-like protocols, you only need to
|
||||
/// a) sync and register new connections using db.init_connection and db.update_endpoints.push
|
||||
/// b) handle the decoding of messages using Command::from_bytes(), then send them to the db using db.command_sender.
|
||||
/// for other protocols (like http + sse)
|
||||
/// a) initialize new connections using db.init_connection() to synchronize the new client
|
||||
/// b) handle the decoding of messages using Command::from_bytes()
|
||||
/// c) re-encode all received messages using Command::to_bytes_vec(), send them to the db, and send them to all your clients.
|
||||
pub fn run_server(
|
||||
database: Arc<Mutex<Database>>,
|
||||
addr_tcp: Option<SocketAddr>,
|
||||
sender_sender: Option<tokio::sync::mpsc::Sender<mpsc::Sender<Command>>>,
|
||||
) {
|
||||
let mut player = Player::new().unwrap();
|
||||
let (command_sender, command_receiver) = mpsc::channel();
|
||||
if let Some(s) = sender_sender {
|
||||
s.blocking_send(command_sender.clone()).unwrap();
|
||||
}
|
||||
database.lock().unwrap().command_sender = Some(command_sender.clone());
|
||||
if let Some(addr) = addr_tcp {
|
||||
match TcpListener::bind(addr) {
|
||||
Ok(v) => {
|
||||
let command_sender = command_sender.clone();
|
||||
let db = Arc::clone(&database);
|
||||
thread::spawn(move || loop {
|
||||
if let Ok((mut connection, con_addr)) = v.accept() {
|
||||
eprintln!("[info] TCP connection accepted from {con_addr}.");
|
||||
let command_sender = command_sender.clone();
|
||||
let db = Arc::clone(&db);
|
||||
thread::spawn(move || {
|
||||
// sync database
|
||||
let mut db = db.lock().unwrap();
|
||||
db.init_connection(&mut connection)?;
|
||||
db.update_endpoints.push(UpdateEndpoint::Bytes(Box::new(
|
||||
connection.try_clone().unwrap(),
|
||||
)));
|
||||
drop(db);
|
||||
loop {
|
||||
if let Ok(command) = Command::from_bytes(&mut connection) {
|
||||
command_sender.send(command).unwrap();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok::<(), std::io::Error>(())
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[WARN] Couldn't start TCP listener: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
let dur = Duration::from_secs_f32(0.1);
|
||||
loop {
|
||||
player.update(&mut database.lock().unwrap());
|
||||
if let Ok(command) = command_receiver.recv_timeout(dur) {
|
||||
player.handle_command(&command);
|
||||
database.lock().unwrap().apply_command(command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Connection: Sized + Send + 'static {
|
||||
type SendError: Send;
|
||||
fn send_command(&mut self, command: Command) -> Result<(), Self::SendError>;
|
||||
fn receive_updates(&mut self) -> Result<Vec<Command>, Self::SendError>;
|
||||
fn receive_update_blocking(&mut self) -> Result<Command, Self::SendError>;
|
||||
fn move_to_thread<F: FnMut(&mut Self, Command) -> bool + Send + 'static>(
|
||||
mut self,
|
||||
mut handler: F,
|
||||
) -> JoinHandle<Result<Self, Self::SendError>> {
|
||||
std::thread::spawn(move || loop {
|
||||
let update = self.receive_update_blocking()?;
|
||||
if handler(&mut self, update) {
|
||||
return Ok(self);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToFromBytes for Command {
|
||||
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
|
||||
where
|
||||
T: Write,
|
||||
{
|
||||
match self {
|
||||
Self::Resume => s.write_all(&[0b11000000])?,
|
||||
Self::Pause => s.write_all(&[0b00110000])?,
|
||||
Self::Stop => s.write_all(&[0b11110000])?,
|
||||
Self::Save => s.write_all(&[0b11110011])?,
|
||||
Self::NextSong => s.write_all(&[0b11110010])?,
|
||||
Self::SyncDatabase(a, b, c) => {
|
||||
s.write_all(&[0b01011000])?;
|
||||
a.to_bytes(s)?;
|
||||
b.to_bytes(s)?;
|
||||
c.to_bytes(s)?;
|
||||
}
|
||||
Self::QueueUpdate(index, new_data) => {
|
||||
s.write_all(&[0b00011100])?;
|
||||
index.to_bytes(s)?;
|
||||
new_data.to_bytes(s)?;
|
||||
}
|
||||
Self::QueueAdd(index, new_data) => {
|
||||
s.write_all(&[0b00011010])?;
|
||||
index.to_bytes(s)?;
|
||||
new_data.to_bytes(s)?;
|
||||
}
|
||||
Self::QueueInsert(index, pos, new_data) => {
|
||||
s.write_all(&[0b00011110])?;
|
||||
index.to_bytes(s)?;
|
||||
pos.to_bytes(s)?;
|
||||
new_data.to_bytes(s)?;
|
||||
}
|
||||
Self::QueueRemove(index) => {
|
||||
s.write_all(&[0b00011001])?;
|
||||
index.to_bytes(s)?;
|
||||
}
|
||||
Self::QueueGoto(index) => {
|
||||
s.write_all(&[0b00011011])?;
|
||||
index.to_bytes(s)?;
|
||||
}
|
||||
Self::AddSong(song) => {
|
||||
s.write_all(&[0b01010000])?;
|
||||
song.to_bytes(s)?;
|
||||
}
|
||||
Self::AddAlbum(album) => {
|
||||
s.write_all(&[0b01010011])?;
|
||||
album.to_bytes(s)?;
|
||||
}
|
||||
Self::AddArtist(artist) => {
|
||||
s.write_all(&[0b01011100])?;
|
||||
artist.to_bytes(s)?;
|
||||
}
|
||||
Self::ModifySong(song) => {
|
||||
s.write_all(&[0b10010000])?;
|
||||
song.to_bytes(s)?;
|
||||
}
|
||||
Self::ModifyAlbum(album) => {
|
||||
s.write_all(&[0b10010011])?;
|
||||
album.to_bytes(s)?;
|
||||
}
|
||||
Self::ModifyArtist(artist) => {
|
||||
s.write_all(&[0b10011100])?;
|
||||
artist.to_bytes(s)?;
|
||||
}
|
||||
Self::SetLibraryDirectory(path) => {
|
||||
s.write_all(&[0b00110001])?;
|
||||
path.to_bytes(s)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
|
||||
where
|
||||
T: std::io::Read,
|
||||
{
|
||||
let mut kind = [0];
|
||||
s.read_exact(&mut kind)?;
|
||||
Ok(match kind[0] {
|
||||
0b11000000 => Self::Resume,
|
||||
0b00110000 => Self::Pause,
|
||||
0b11110000 => Self::Stop,
|
||||
0b11110011 => Self::Save,
|
||||
0b11110010 => Self::NextSong,
|
||||
0b01011000 => Self::SyncDatabase(
|
||||
ToFromBytes::from_bytes(s)?,
|
||||
ToFromBytes::from_bytes(s)?,
|
||||
ToFromBytes::from_bytes(s)?,
|
||||
),
|
||||
0b00011100 => {
|
||||
Self::QueueUpdate(ToFromBytes::from_bytes(s)?, ToFromBytes::from_bytes(s)?)
|
||||
}
|
||||
0b00011010 => Self::QueueAdd(ToFromBytes::from_bytes(s)?, ToFromBytes::from_bytes(s)?),
|
||||
0b00011110 => Self::QueueInsert(
|
||||
ToFromBytes::from_bytes(s)?,
|
||||
ToFromBytes::from_bytes(s)?,
|
||||
ToFromBytes::from_bytes(s)?,
|
||||
),
|
||||
0b00011001 => Self::QueueRemove(ToFromBytes::from_bytes(s)?),
|
||||
0b00011011 => Self::QueueGoto(ToFromBytes::from_bytes(s)?),
|
||||
0b01010000 => Self::AddSong(ToFromBytes::from_bytes(s)?),
|
||||
0b01010011 => Self::AddAlbum(ToFromBytes::from_bytes(s)?),
|
||||
0b01011100 => Self::AddArtist(ToFromBytes::from_bytes(s)?),
|
||||
0b10010000 => Self::AddSong(ToFromBytes::from_bytes(s)?),
|
||||
0b10010011 => Self::AddAlbum(ToFromBytes::from_bytes(s)?),
|
||||
0b10011100 => Self::AddArtist(ToFromBytes::from_bytes(s)?),
|
||||
0b00110001 => Self::SetLibraryDirectory(ToFromBytes::from_bytes(s)?),
|
||||
_ => {
|
||||
eprintln!("unexpected byte when reading command; stopping playback.");
|
||||
Self::Stop
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
34
musicdb-lib/src/test.rs
Executable file
34
musicdb-lib/src/test.rs
Executable file
@@ -0,0 +1,34 @@
|
||||
#![cfg(test)]
|
||||
use std::{assert_eq, path::PathBuf};
|
||||
|
||||
use crate::load::ToFromBytes;
|
||||
|
||||
#[test]
|
||||
fn string() {
|
||||
for v in ["dskjh2d89dnas2d90", "aosu 89d 89a 89", "a/b/c/12"] {
|
||||
let v = v.to_owned();
|
||||
assert_eq!(v, String::from_bytes(&mut &v.to_bytes_vec()[..]).unwrap());
|
||||
let v = PathBuf::from(v);
|
||||
assert_eq!(v, PathBuf::from_bytes(&mut &v.to_bytes_vec()[..]).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vec() {
|
||||
for v in [vec!["asdad".to_owned(), "dsnakf".to_owned()], vec![]] {
|
||||
assert_eq!(
|
||||
v,
|
||||
Vec::<String>::from_bytes(&mut &v.to_bytes_vec()[..]).unwrap()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn option() {
|
||||
for v in [None, Some("value".to_owned())] {
|
||||
assert_eq!(
|
||||
v,
|
||||
Option::<String>::from_bytes(&mut &v.to_bytes_vec()[..]).unwrap()
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user