From c76fccb42ed8320fe3623727d32317743c907dff Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Sat, 11 Feb 2023 12:30:31 -0500 Subject: [PATCH] Set up the app structure and the file scanner --- music-player/errors.rs | 4 +- music-player/server/Cargo.lock | 20 ++++-- music-player/server/Cargo.toml | 5 +- music-player/server/src/audio.rs | 41 ++++++++++-- music-player/server/src/bin/server.rs | 13 ++-- music-player/server/src/core.rs | 93 +++++++++++++++++++++++---- music-player/server/src/database.rs | 67 +++++++++++++++++-- music-player/server/src/lib.rs | 5 +- 8 files changed, 209 insertions(+), 39 deletions(-) diff --git a/music-player/errors.rs b/music-player/errors.rs index 5a4e5a3..5080d42 100644 --- a/music-player/errors.rs +++ b/music-player/errors.rs @@ -1,7 +1,7 @@ -pub use error::{error, fatal, ok, Result}; +pub use flow::error; pub enum FatalError { UnexpectedError, } -impl error::FatalError for FatalError {} +impl flow::FatalError for FatalError {} diff --git a/music-player/server/Cargo.lock b/music-player/server/Cargo.lock index 6196929..278a186 100644 --- a/music-player/server/Cargo.lock +++ b/music-player/server/Cargo.lock @@ -165,10 +165,6 @@ dependencies = [ "syn 1.0.107", ] -[[package]] -name = "errors" -version = "0.1.0" - [[package]] name = "fallible-iterator" version = "0.2.0" @@ -190,6 +186,10 @@ dependencies = [ "instant", ] +[[package]] +name = "flow" +version = "0.1.0" + [[package]] name = "fnv" version = "1.0.7" @@ -582,13 +582,14 @@ name = "music-player" version = "0.1.0" dependencies = [ "dbus", - "errors", + "flow", "mpris", "rusqlite", "serde", "thiserror", "tokio", "url", + "uuid", "warp", ] @@ -1185,6 +1186,15 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "uuid" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79" +dependencies = [ + "getrandom", +] + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/music-player/server/Cargo.toml b/music-player/server/Cargo.toml index f77f620..e526c24 100644 --- a/music-player/server/Cargo.toml +++ b/music-player/server/Cargo.toml @@ -7,13 +7,14 @@ edition = "2021" [dependencies] dbus = { version = "0.9.7" } -errors = { path = "../../errors" } +flow = { path = "../../flow" } mpris = { version = "2.0" } rusqlite = { version = "0.28" } serde = { version = "1.0", features = ["derive"] } thiserror = { version = "1.0" } tokio = { version = "1.24", features = ["full"] } -url = "2.3.1" +url = { version = "2.3" } +uuid = { version = "1", features = ["v4"] } warp = { version = "0.3" } [lib] diff --git a/music-player/server/src/audio.rs b/music-player/server/src/audio.rs index 380c7db..8071628 100644 --- a/music-player/server/src/audio.rs +++ b/music-player/server/src/audio.rs @@ -92,20 +92,50 @@ impl From for AudioError { } } -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Track { - pub id: String, +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize)] +pub struct TrackId(String); + +impl Default for TrackId { + fn default() -> Self { + Self(uuid::Uuid::new_v4().as_hyphenated().to_string()) + } +} + +impl From for TrackId { + fn from(id: String) -> Self { + Self(id) + } +} + +impl AsRef for TrackId { + fn as_ref<'a>(&'a self) -> &'a String { + &self.0 + } +} + +#[derive(Clone, Debug)] +pub struct TrackInfo { pub track_number: Option, pub name: Option, pub album: Option, pub artist: Option, } +#[derive(Clone, Debug, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Track { + pub id: TrackId, + pub track_number: Option, + pub name: Option, + pub album: Option, + pub artist: Option, +} + +/* impl From<&mpris::Metadata> for Track { fn from(data: &mpris::Metadata) -> Self { Self { - id: data.track_id().unwrap().to_string(), + id: data.track_id().unwrap(), track_number: data.track_number(), name: data.title().map(|s| s.to_owned()), album: data.album_name().map(|s| s.to_owned()), @@ -119,6 +149,7 @@ impl From for Track { Self::from(&data) } } +*/ #[derive(Clone, Debug, Serialize)] #[serde(rename_all = "camelCase")] diff --git a/music-player/server/src/bin/server.rs b/music-player/server/src/bin/server.rs index b93784b..519ac16 100644 --- a/music-player/server/src/bin/server.rs +++ b/music-player/server/src/bin/server.rs @@ -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 music_player::core::Core; +use music_player::{core::Core, database::MemoryIndex}; /* fn tracks() -> Vec { @@ -49,14 +50,14 @@ fn tracks() -> Vec { #[tokio::main] pub async fn main() { - match Core::new(PathBuf::from(":memory:")) { - Ok(Ok(core)) => { + match Core::new(Arc::new(MemoryIndex::new())) { + Flow::Ok(core) => { let mut buf = String::new(); let _ = stdin().read_line(&mut buf).unwrap(); core.exit(); } - Ok(Err(err)) => println!("non-fatal error: {:?}", err), - Err(err) => println!("fatal error: {:?}", err), + Flow::Err(err) => println!("non-fatal error: {:?}", err), + Flow::Fatal(err) => println!("fatal error: {:?}", err), } /* diff --git a/music-player/server/src/core.rs b/music-player/server/src/core.rs index 680a59a..d85cdc7 100644 --- a/music-player/server/src/core.rs +++ b/music-player/server/src/core.rs @@ -1,13 +1,33 @@ -use crate::{database::Database, Error, FatalError}; -use errors::{ok, result, Result}; +use crate::{ + database::{Database, MemoryIndex, MusicIndex}, + Error, FatalError, +}; +use flow::{error, fatal, ok, return_error, return_fatal, Flow}; use std::{ - path::PathBuf, - sync::mpsc::{channel, Receiver, RecvTimeoutError, Sender}, + path::{Path, PathBuf}, + sync::{ + mpsc::{channel, Receiver, RecvTimeoutError, Sender}, + Arc, + }, thread, thread::JoinHandle, 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 for ScannerError { + fn from(err: std::io::Error) -> Self { + Self::IO(err) + } +} + pub enum ControlMsg { Exit, } @@ -24,7 +44,7 @@ pub enum PlaybackMsg { } pub struct Core { - db: Database, + db: Arc, track_handle: JoinHandle<()>, track_rx: Receiver, playback_handle: JoinHandle<()>, @@ -37,19 +57,26 @@ fn scan_frequency() -> Duration { } pub struct FileScanner { - db: Database, + db: Arc, control_rx: Receiver, tracker_tx: Sender, next_scan: Instant, + music_directories: Vec, } impl FileScanner { - fn new(db: Database, control_rx: Receiver, tracker_tx: Sender) -> Self { + fn new( + db: Arc, + roots: Vec, + control_rx: Receiver, + tracker_tx: Sender, + ) -> Self { Self { db, control_rx, tracker_tx, next_scan: Instant::now(), + music_directories: roots, } } @@ -61,24 +88,66 @@ impl FileScanner { Err(RecvTimeoutError::Disconnected) => return, } 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(); } } } + + fn scan_dir(&self, mut paths: Vec) -> 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, + 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 { - pub fn new(db_path: PathBuf) -> Result { - let db = result!(Database::new(db_path)); - + pub fn new(db: Arc) -> Flow { let (control_tx, control_rx) = channel::(); let (track_handle, track_rx) = { let (track_tx, track_rx) = channel(); let db = db.clone(); 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) }; diff --git a/music-player/server/src/database.rs b/music-player/server/src/database.rs index 775369c..7ee198c 100644 --- a/music-player/server/src/database.rs +++ b/music-player/server/src/database.rs @@ -1,8 +1,14 @@ -use crate::FatalError; -use errors::{error, ok, Result}; +use crate::{ + audio::{Track, TrackId, TrackInfo}, + FatalError, +}; +use flow::{error, ok, Flow}; use rusqlite::Connection; -use std::path::PathBuf; -use std::sync::{Arc, Mutex}; +use std::collections::HashMap; +use std::{ + path::PathBuf, + sync::{Arc, Mutex, RwLock}, +}; use thiserror::Error; #[derive(Debug, Error)] @@ -13,6 +19,57 @@ pub enum DatabaseError { UnhandledError(rusqlite::Error), } +pub trait MusicIndex: Sync + Send { + fn add_track(&mut self, track: &TrackInfo) -> Flow; + fn remove_track(&mut self, id: &TrackId) -> Flow<(), FatalError, DatabaseError>; + fn get_track_info(&self, id: &TrackId) -> Flow, FatalError, DatabaseError>; +} + +pub struct MemoryIndex { + tracks: RwLock>, +} + +impl MemoryIndex { + pub fn new() -> MemoryIndex { + MemoryIndex { + tracks: RwLock::new(HashMap::default()), + } + } +} + +impl MusicIndex for MemoryIndex { + fn add_track(&mut self, info: &TrackInfo) -> Flow { + 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, FatalError, DatabaseError> { + let track = { + let tracks = self.tracks.read().unwrap(); + tracks.get(&id).cloned() + }; + ok(track) + } +} + pub struct ManagedConnection<'a> { pool: &'a Database, conn: Option, @@ -31,7 +88,7 @@ pub struct Database { } impl Database { - pub fn new(path: PathBuf) -> Result { + pub fn new(path: PathBuf) -> Flow { let connection = match Connection::open(path.clone()) { Ok(connection) => connection, Err(err) => return error(DatabaseError::UnhandledError(err)), diff --git a/music-player/server/src/lib.rs b/music-player/server/src/lib.rs index e35a59f..53ec710 100644 --- a/music-player/server/src/lib.rs +++ b/music-player/server/src/lib.rs @@ -16,9 +16,10 @@ impl From for Error { } } -#[derive(Debug)] +#[derive(Debug, Error)] pub enum FatalError { + #[error("Unexpected error")] UnexpectedError, } -impl errors::FatalError for FatalError {} +impl flow::FatalError for FatalError {}