Start building a music player server #17
|
@ -1,7 +1,7 @@
|
||||||
pub use error::{error, fatal, ok, Result};
|
pub use flow::error;
|
||||||
|
|
||||||
pub enum FatalError {
|
pub enum FatalError {
|
||||||
UnexpectedError,
|
UnexpectedError,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl error::FatalError for FatalError {}
|
impl flow::FatalError for FatalError {}
|
||||||
|
|
|
@ -165,10 +165,6 @@ dependencies = [
|
||||||
"syn 1.0.107",
|
"syn 1.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "errors"
|
|
||||||
version = "0.1.0"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fallible-iterator"
|
name = "fallible-iterator"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
@ -190,6 +186,10 @@ dependencies = [
|
||||||
"instant",
|
"instant",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flow"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
|
@ -582,13 +582,14 @@ name = "music-player"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dbus",
|
"dbus",
|
||||||
"errors",
|
"flow",
|
||||||
"mpris",
|
"mpris",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"url",
|
"url",
|
||||||
|
"uuid",
|
||||||
"warp",
|
"warp",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1185,6 +1186,15 @@ version = "0.7.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uuid"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vcpkg"
|
name = "vcpkg"
|
||||||
version = "0.2.15"
|
version = "0.2.15"
|
||||||
|
|
|
@ -7,13 +7,14 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
dbus = { version = "0.9.7" }
|
dbus = { version = "0.9.7" }
|
||||||
errors = { path = "../../errors" }
|
flow = { path = "../../flow" }
|
||||||
mpris = { version = "2.0" }
|
mpris = { version = "2.0" }
|
||||||
rusqlite = { version = "0.28" }
|
rusqlite = { version = "0.28" }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
thiserror = { version = "1.0" }
|
thiserror = { version = "1.0" }
|
||||||
tokio = { version = "1.24", features = ["full"] }
|
tokio = { version = "1.24", features = ["full"] }
|
||||||
url = "2.3.1"
|
url = { version = "2.3" }
|
||||||
|
uuid = { version = "1", features = ["v4"] }
|
||||||
warp = { version = "0.3" }
|
warp = { version = "0.3" }
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
|
|
|
@ -92,20 +92,50 @@ impl From<url::ParseError> for AudioError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
pub struct TrackId(String);
|
||||||
pub struct Track {
|
|
||||||
pub id: String,
|
impl Default for TrackId {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(uuid::Uuid::new_v4().as_hyphenated().to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for TrackId {
|
||||||
|
fn from(id: String) -> Self {
|
||||||
|
Self(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<String> for TrackId {
|
||||||
|
fn as_ref<'a>(&'a self) -> &'a String {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct TrackInfo {
|
||||||
pub track_number: Option<i32>,
|
pub track_number: Option<i32>,
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub album: Option<String>,
|
pub album: Option<String>,
|
||||||
pub artist: Option<String>,
|
pub artist: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Track {
|
||||||
|
pub id: TrackId,
|
||||||
|
pub track_number: Option<i32>,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub album: Option<String>,
|
||||||
|
pub artist: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
impl From<&mpris::Metadata> for Track {
|
impl From<&mpris::Metadata> for Track {
|
||||||
fn from(data: &mpris::Metadata) -> Self {
|
fn from(data: &mpris::Metadata) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: data.track_id().unwrap().to_string(),
|
id: data.track_id().unwrap(),
|
||||||
track_number: data.track_number(),
|
track_number: data.track_number(),
|
||||||
name: data.title().map(|s| s.to_owned()),
|
name: data.title().map(|s| s.to_owned()),
|
||||||
album: data.album_name().map(|s| s.to_owned()),
|
album: data.album_name().map(|s| s.to_owned()),
|
||||||
|
@ -119,6 +149,7 @@ impl From<mpris::Metadata> for Track {
|
||||||
Self::from(&data)
|
Self::from(&data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
use std::{io::stdin, path::PathBuf, thread, time::Duration};
|
use flow::Flow;
|
||||||
|
use std::{io::stdin, path::PathBuf, sync::Arc, thread, time::Duration};
|
||||||
// use warp::Filter;
|
// use warp::Filter;
|
||||||
|
|
||||||
use music_player::core::Core;
|
use music_player::{core::Core, database::MemoryIndex};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
fn tracks() -> Vec<Track> {
|
fn tracks() -> Vec<Track> {
|
||||||
|
@ -49,14 +50,14 @@ fn tracks() -> Vec<Track> {
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
pub async fn main() {
|
pub async fn main() {
|
||||||
match Core::new(PathBuf::from(":memory:")) {
|
match Core::new(Arc::new(MemoryIndex::new())) {
|
||||||
Ok(Ok(core)) => {
|
Flow::Ok(core) => {
|
||||||
let mut buf = String::new();
|
let mut buf = String::new();
|
||||||
let _ = stdin().read_line(&mut buf).unwrap();
|
let _ = stdin().read_line(&mut buf).unwrap();
|
||||||
core.exit();
|
core.exit();
|
||||||
}
|
}
|
||||||
Ok(Err(err)) => println!("non-fatal error: {:?}", err),
|
Flow::Err(err) => println!("non-fatal error: {:?}", err),
|
||||||
Err(err) => println!("fatal error: {:?}", err),
|
Flow::Fatal(err) => println!("fatal error: {:?}", err),
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -1,13 +1,33 @@
|
||||||
use crate::{database::Database, Error, FatalError};
|
use crate::{
|
||||||
use errors::{ok, result, Result};
|
database::{Database, MemoryIndex, MusicIndex},
|
||||||
|
Error, FatalError,
|
||||||
|
};
|
||||||
|
use flow::{error, fatal, ok, return_error, return_fatal, Flow};
|
||||||
use std::{
|
use std::{
|
||||||
path::PathBuf,
|
path::{Path, PathBuf},
|
||||||
sync::mpsc::{channel, Receiver, RecvTimeoutError, Sender},
|
sync::{
|
||||||
|
mpsc::{channel, Receiver, RecvTimeoutError, Sender},
|
||||||
|
Arc,
|
||||||
|
},
|
||||||
thread,
|
thread,
|
||||||
thread::JoinHandle,
|
thread::JoinHandle,
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ScannerError {
|
||||||
|
#[error("Cannot scan {0}")]
|
||||||
|
CannotScan(PathBuf),
|
||||||
|
#[error("IO error {0}")]
|
||||||
|
IO(std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for ScannerError {
|
||||||
|
fn from(err: std::io::Error) -> Self {
|
||||||
|
Self::IO(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub enum ControlMsg {
|
pub enum ControlMsg {
|
||||||
Exit,
|
Exit,
|
||||||
}
|
}
|
||||||
|
@ -24,7 +44,7 @@ pub enum PlaybackMsg {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Core {
|
pub struct Core {
|
||||||
db: Database,
|
db: Arc<dyn MusicIndex>,
|
||||||
track_handle: JoinHandle<()>,
|
track_handle: JoinHandle<()>,
|
||||||
track_rx: Receiver<TrackMsg>,
|
track_rx: Receiver<TrackMsg>,
|
||||||
playback_handle: JoinHandle<()>,
|
playback_handle: JoinHandle<()>,
|
||||||
|
@ -37,19 +57,26 @@ fn scan_frequency() -> Duration {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct FileScanner {
|
pub struct FileScanner {
|
||||||
db: Database,
|
db: Arc<dyn MusicIndex>,
|
||||||
control_rx: Receiver<ControlMsg>,
|
control_rx: Receiver<ControlMsg>,
|
||||||
tracker_tx: Sender<TrackMsg>,
|
tracker_tx: Sender<TrackMsg>,
|
||||||
next_scan: Instant,
|
next_scan: Instant,
|
||||||
|
music_directories: Vec<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileScanner {
|
impl FileScanner {
|
||||||
fn new(db: Database, control_rx: Receiver<ControlMsg>, tracker_tx: Sender<TrackMsg>) -> Self {
|
fn new(
|
||||||
|
db: Arc<dyn MusicIndex>,
|
||||||
|
roots: Vec<PathBuf>,
|
||||||
|
control_rx: Receiver<ControlMsg>,
|
||||||
|
tracker_tx: Sender<TrackMsg>,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
db,
|
db,
|
||||||
control_rx,
|
control_rx,
|
||||||
tracker_tx,
|
tracker_tx,
|
||||||
next_scan: Instant::now(),
|
next_scan: Instant::now(),
|
||||||
|
music_directories: roots,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,24 +88,66 @@ impl FileScanner {
|
||||||
Err(RecvTimeoutError::Disconnected) => return,
|
Err(RecvTimeoutError::Disconnected) => return,
|
||||||
}
|
}
|
||||||
if Instant::now() >= self.next_scan {
|
if Instant::now() >= self.next_scan {
|
||||||
println!("scan");
|
for root in self.music_directories.iter() {
|
||||||
|
self.scan_dir(vec![root.clone()]);
|
||||||
|
}
|
||||||
self.next_scan = Instant::now() + scan_frequency();
|
self.next_scan = Instant::now() + scan_frequency();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn scan_dir(&self, mut paths: Vec<PathBuf>) -> Flow<(), FatalError, ScannerError> {
|
||||||
|
while let Some(dir) = paths.pop() {
|
||||||
|
println!("scanning {:?}", dir);
|
||||||
|
return_error!(self.scan_dir_(&mut paths, dir));
|
||||||
|
}
|
||||||
|
ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scan_dir_(
|
||||||
|
&self,
|
||||||
|
paths: &mut Vec<PathBuf>,
|
||||||
|
dir: PathBuf,
|
||||||
|
) -> Flow<(), FatalError, ScannerError> {
|
||||||
|
let dir_iter = return_error!(Flow::from(dir.read_dir().map_err(ScannerError::from)));
|
||||||
|
for entry in dir_iter {
|
||||||
|
match entry {
|
||||||
|
Ok(entry) if entry.path().is_dir() => paths.push(entry.path()),
|
||||||
|
Ok(entry) => {
|
||||||
|
let _ = return_fatal!(self.scan_file(entry.path()).or_else(|err| {
|
||||||
|
println!("scan_file failed: {:?}", err);
|
||||||
|
ok::<(), FatalError, ScannerError>(())
|
||||||
|
}));
|
||||||
|
()
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
println!("scan_dir could not read path: ({:?})", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scan_file(&self, path: PathBuf) -> Flow<(), FatalError, ScannerError> {
|
||||||
|
ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Core {
|
impl Core {
|
||||||
pub fn new(db_path: PathBuf) -> Result<Core, FatalError, Error> {
|
pub fn new(db: Arc<dyn MusicIndex>) -> Flow<Core, FatalError, Error> {
|
||||||
let db = result!(Database::new(db_path));
|
|
||||||
|
|
||||||
let (control_tx, control_rx) = channel::<ControlMsg>();
|
let (control_tx, control_rx) = channel::<ControlMsg>();
|
||||||
|
|
||||||
let (track_handle, track_rx) = {
|
let (track_handle, track_rx) = {
|
||||||
let (track_tx, track_rx) = channel();
|
let (track_tx, track_rx) = channel();
|
||||||
let db = db.clone();
|
let db = db.clone();
|
||||||
let track_handle = thread::spawn(move || {
|
let track_handle = thread::spawn(move || {
|
||||||
FileScanner::new(db, control_rx, track_tx).scan();
|
FileScanner::new(
|
||||||
|
db,
|
||||||
|
vec![PathBuf::from("/home/savanni/Music/")],
|
||||||
|
control_rx,
|
||||||
|
track_tx,
|
||||||
|
)
|
||||||
|
.scan();
|
||||||
});
|
});
|
||||||
(track_handle, track_rx)
|
(track_handle, track_rx)
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
use crate::FatalError;
|
use crate::{
|
||||||
use errors::{error, ok, Result};
|
audio::{Track, TrackId, TrackInfo},
|
||||||
|
FatalError,
|
||||||
|
};
|
||||||
|
use flow::{error, ok, Flow};
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
use std::path::PathBuf;
|
use std::collections::HashMap;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::{
|
||||||
|
path::PathBuf,
|
||||||
|
sync::{Arc, Mutex, RwLock},
|
||||||
|
};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
|
@ -13,6 +19,57 @@ pub enum DatabaseError {
|
||||||
UnhandledError(rusqlite::Error),
|
UnhandledError(rusqlite::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait MusicIndex: Sync + Send {
|
||||||
|
fn add_track(&mut self, track: &TrackInfo) -> Flow<Track, FatalError, DatabaseError>;
|
||||||
|
fn remove_track(&mut self, id: &TrackId) -> Flow<(), FatalError, DatabaseError>;
|
||||||
|
fn get_track_info(&self, id: &TrackId) -> Flow<Option<Track>, FatalError, DatabaseError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MemoryIndex {
|
||||||
|
tracks: RwLock<HashMap<TrackId, Track>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MemoryIndex {
|
||||||
|
pub fn new() -> MemoryIndex {
|
||||||
|
MemoryIndex {
|
||||||
|
tracks: RwLock::new(HashMap::default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MusicIndex for MemoryIndex {
|
||||||
|
fn add_track(&mut self, info: &TrackInfo) -> Flow<Track, FatalError, DatabaseError> {
|
||||||
|
let id = TrackId::default();
|
||||||
|
let track = Track {
|
||||||
|
id: id.clone(),
|
||||||
|
track_number: info.track_number,
|
||||||
|
name: info.name.clone(),
|
||||||
|
album: info.album.clone(),
|
||||||
|
artist: info.artist.clone(),
|
||||||
|
};
|
||||||
|
let mut tracks = self.tracks.write().unwrap();
|
||||||
|
tracks.insert(id, track.clone());
|
||||||
|
ok(track)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_track(&mut self, id: &TrackId) -> Flow<(), FatalError, DatabaseError> {
|
||||||
|
let mut tracks = self.tracks.write().unwrap();
|
||||||
|
tracks.remove(&id);
|
||||||
|
ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_track_info<'a>(
|
||||||
|
&'a self,
|
||||||
|
id: &TrackId,
|
||||||
|
) -> Flow<Option<Track>, FatalError, DatabaseError> {
|
||||||
|
let track = {
|
||||||
|
let tracks = self.tracks.read().unwrap();
|
||||||
|
tracks.get(&id).cloned()
|
||||||
|
};
|
||||||
|
ok(track)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct ManagedConnection<'a> {
|
pub struct ManagedConnection<'a> {
|
||||||
pool: &'a Database,
|
pool: &'a Database,
|
||||||
conn: Option<Connection>,
|
conn: Option<Connection>,
|
||||||
|
@ -31,7 +88,7 @@ pub struct Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Database {
|
impl Database {
|
||||||
pub fn new(path: PathBuf) -> Result<Database, FatalError, DatabaseError> {
|
pub fn new(path: PathBuf) -> Flow<Database, FatalError, DatabaseError> {
|
||||||
let connection = match Connection::open(path.clone()) {
|
let connection = match Connection::open(path.clone()) {
|
||||||
Ok(connection) => connection,
|
Ok(connection) => connection,
|
||||||
Err(err) => return error(DatabaseError::UnhandledError(err)),
|
Err(err) => return error(DatabaseError::UnhandledError(err)),
|
||||||
|
|
|
@ -16,9 +16,10 @@ impl From<DatabaseError> for Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Error)]
|
||||||
pub enum FatalError {
|
pub enum FatalError {
|
||||||
|
#[error("Unexpected error")]
|
||||||
UnexpectedError,
|
UnexpectedError,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl errors::FatalError for FatalError {}
|
impl flow::FatalError for FatalError {}
|
||||||
|
|
Loading…
Reference in New Issue