From 58b9d10441ae7d39d4155f3ce4f52de79666001d Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Sat, 25 Feb 2023 21:22:47 -0500 Subject: [PATCH] Scan the filesystem --- music-player/Makefile | 9 ++ music-player/client/index.html | 1 + music-player/client/src/main.ts | 23 +++- music-player/client/styles.css | 9 +- music-player/server/src/bin/server.rs | 80 +++++-------- music-player/server/src/core.rs | 8 +- music-player/server/src/music_scanner.rs | 146 +++++++++++++---------- 7 files changed, 150 insertions(+), 126 deletions(-) create mode 100644 music-player/Makefile diff --git a/music-player/Makefile b/music-player/Makefile new file mode 100644 index 0000000..af820bf --- /dev/null +++ b/music-player/Makefile @@ -0,0 +1,9 @@ + +server-dev: + cd server && cargo watch -x run + +server-test: + cd server && cargo watch -x test + +client-dev: + cd client && npm run watch diff --git a/music-player/client/index.html b/music-player/client/index.html index 0068341..0f7cea5 100644 --- a/music-player/client/index.html +++ b/music-player/client/index.html @@ -24,6 +24,7 @@ + diff --git a/music-player/client/src/main.ts b/music-player/client/src/main.ts index faf4d1c..ddea3c6 100644 --- a/music-player/client/src/main.ts +++ b/music-player/client/src/main.ts @@ -19,25 +19,40 @@ const getTracks = () => fetch("/api/v1/tracks").then((r) => r.json()); const formatTrack = (track: TrackInfo) => { let row = document.createElement("tr"); + row.classList.add("track-list__row"); + + let track_id = document.createElement("td"); + track_id.appendChild(document.createTextNode(track.id)); + track_id.classList.add("track-list__cell"); let track_number = document.createElement("td"); track_number.appendChild( - document.createTextNode(track.track_number.toString()) + document.createTextNode(track.track_number?.toString() || "") ); + track_number.classList.add("track-list__cell"); let name = document.createElement("td"); - name.appendChild(document.createTextNode(track.name)); + name.appendChild(document.createTextNode(track.name || "")); + name.classList.add("track-list__cell"); let album = document.createElement("td"); - album.appendChild(document.createTextNode(track.album)); + album.appendChild(document.createTextNode(track.album || "")); + album.classList.add("track-list__cell"); let artist = document.createElement("td"); - artist.appendChild(document.createTextNode(track.artist)); + artist.appendChild(document.createTextNode(track.artist || "")); + artist.classList.add("track-list__cell"); + let length = document.createElement("td"); + artist.appendChild(document.createTextNode("")); + length.classList.add("track-list__cell"); + + row.appendChild(track_id); row.appendChild(track_number); row.appendChild(name); row.appendChild(artist); row.appendChild(album); + row.appendChild(length); return row; }; diff --git a/music-player/client/styles.css b/music-player/client/styles.css index d28cbe6..420ac1d 100644 --- a/music-player/client/styles.css +++ b/music-player/client/styles.css @@ -25,15 +25,18 @@ body { list-style: none; } -.track-list__track-row { +.track-list__row { background-color: rgb(10, 10, 10); } -.track-list__track-row:nth-child(even) { +.track-list__row:nth-child(even) { background-color: rgb(255, 255, 255); } -.track-list__track-row:nth-child(odd) { +.track-list__row:nth-child(odd) { background-color: rgb(200, 200, 200); } +.track-list__cell { + padding: 8px; +} diff --git a/music-player/server/src/bin/server.rs b/music-player/server/src/bin/server.rs index 17e43d7..434deea 100644 --- a/music-player/server/src/bin/server.rs +++ b/music-player/server/src/bin/server.rs @@ -12,55 +12,16 @@ use warp::{Filter, Reply}; use music_player::{ audio::{TrackId, TrackInfo}, core::Core, - database::MemoryIndex, + database::{MemoryIndex, MusicIndex}, + music_scanner::FileScanner, }; -fn tracks() -> Vec { - vec![ - TrackInfo { - track_number: Some(1), - name: Some("Underground".to_owned()), - album: Some("Artemis".to_owned()), - artist: Some("Lindsey Stirling".to_owned()), - id: TrackId::from( - "/mnt/music/Lindsey Stirling/Artemis/01 - Underground.ogg".to_owned(), - ), - }, - TrackInfo { - track_number: Some(2), - name: Some("Artemis".to_owned()), - album: Some("Artemis".to_owned()), - artist: Some("Lindsey Stirling".to_owned()), - id: TrackId::from("/mnt/music/Lindsey Stirling/Artemis/02 - Artemis.ogg".to_owned()), - }, - TrackInfo { - track_number: Some(3), - name: Some("Til the Light Goes Out".to_owned()), - album: Some("Artemis".to_owned()), - artist: Some("Lindsey Stirling".to_owned()), - id: TrackId::from( - "/mnt/music/Lindsey Stirling/Artemis/03 - Til the Light Goes Out.ogg".to_owned(), - ), - }, - TrackInfo { - track_number: Some(4), - name: Some("Between Twilight".to_owned()), - album: Some("Artemis".to_owned()), - artist: Some("Lindsey Stirling".to_owned()), - id: TrackId::from( - "/mnt/music/Lindsey Stirling/Artemis/04 - Between Twilight.ogg".to_owned(), - ), - }, - TrackInfo { - track_number: Some(5), - name: Some("Foreverglow".to_owned()), - album: Some("Artemis".to_owned()), - artist: Some("Lindsey Stirling".to_owned()), - id: TrackId::from( - "/mnt/music/Lindsey Stirling/Artemis/05 - Foreverglow.ogg".to_owned(), - ), - }, - ] +fn tracks(index: &Arc) -> Vec { + match index.list_tracks() { + Flow::Ok(tracks) => tracks, + Flow::Err(err) => panic!("error: {}", err), + Flow::Fatal(err) => panic!("fatal: {}", err), + } } enum Bundle { @@ -91,10 +52,21 @@ pub async fn main() { let bundle_root = std::env::var("BUNDLE_ROOT") .map(|b| PathBuf::from(b)) .unwrap(); + let music_root = std::env::var("MUSIC_ROOT") + .map(|b| PathBuf::from(b)) + .unwrap(); - println!("config: {:?} {:?}", dev, bundle_root); + let index = Arc::new(MemoryIndex::new()); + let scanner = FileScanner::new(vec![music_root.clone()]); + let core = match Core::new(index.clone(), scanner) { + Flow::Ok(core) => core, + Flow::Err(error) => panic!("error: {}", error), + Flow::Fatal(error) => panic!("fatal: {}", error), + }; - let index = warp::path!().and(warp::get()).map({ + println!("config: {:?} {:?} {:?}", dev, bundle_root, music_root); + + let root = warp::path!().and(warp::get()).map({ let bundle_root = bundle_root.clone(); move || { warp::http::Response::builder() @@ -128,9 +100,11 @@ pub async fn main() { }); */ - let track_list = warp::path!("api" / "v1" / "tracks") - .and(warp::get()) - .map(|| warp::reply::json(&tracks())); + let track_list = warp::path!("api" / "v1" / "tracks").and(warp::get()).map({ + let index = index.clone(); + move || warp::reply::json(&tracks(&index)) + }); + /* let tracks_for_artist = warp::path!("api" / "v1" / "artist" / String) .and(warp::get()) @@ -157,7 +131,7 @@ pub async fn main() { .or(queue) .or(playing_status); */ - let routes = index.or(app).or(styles).or(track_list); + let routes = root.or(app).or(styles).or(track_list); let server = warp::serve(routes); server .run(SocketAddr::new( diff --git a/music-player/server/src/core.rs b/music-player/server/src/core.rs index 69215c6..5126a2a 100644 --- a/music-player/server/src/core.rs +++ b/music-player/server/src/core.rs @@ -47,19 +47,21 @@ impl Core { scanner: impl MusicScanner + 'static, ) -> Flow { let (control_tx, control_rx) = channel::(); + let db = db; let (_track_handle, _track_rx) = { let (track_tx, track_rx) = channel(); let db = db.clone(); let track_handle = thread::spawn(move || { - println!("tracker thread started"); let mut next_scan = Instant::now(); loop { if Instant::now() >= next_scan { let _ = track_tx.send(TrackMsg::UpdateInProgress); for track in scanner.scan() { - println!("scanning {:?}", track); - db.add_track(track); + match track { + Ok(track) => db.add_track(track), + Err(_) => ok(()), + }; } let _ = track_tx.send(TrackMsg::UpdateComplete); next_scan = Instant::now() + scan_frequency(); diff --git a/music-player/server/src/music_scanner.rs b/music-player/server/src/music_scanner.rs index 57d071e..19d464b 100644 --- a/music-player/server/src/music_scanner.rs +++ b/music-player/server/src/music_scanner.rs @@ -1,11 +1,11 @@ use crate::{ - audio::TrackInfo, + audio::{TrackId, TrackInfo}, core::{ControlMsg, TrackMsg}, database::MusicIndex, FatalError, }; -use flow::{ok, return_error, return_fatal, Flow}; use std::{ + fs::{DirEntry, ReadDir}, path::PathBuf, sync::{ mpsc::{Receiver, RecvTimeoutError, Sender}, @@ -19,6 +19,8 @@ use thiserror::Error; pub enum ScannerError { #[error("Cannot scan {0}")] CannotScan(PathBuf), + #[error("Not found {0}")] + NotFound(PathBuf), #[error("IO error {0}")] IO(std::io::Error), } @@ -30,83 +32,101 @@ impl From for ScannerError { } pub trait MusicScanner: Send { - fn scan<'a>(&'a self) -> Box + 'a>; + fn scan<'a>(&'a self) -> Box> + 'a>; } -/* pub struct FileScanner { - db: Arc, - tracker_tx: Sender, - music_directories: Vec, + roots: Vec, } impl FileScanner { - fn new(db: Arc, roots: Vec, tracker_tx: Sender) -> Self { - Self { - db, - tracker_tx, - music_directories: roots, - } + pub fn new(roots: Vec) -> Self { + Self { roots } } +} - 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(()) +pub struct FileIterator { + dirs: Vec, + file_iter: Option, +} + +impl FileIterator { + fn scan_file(&self, path: PathBuf) -> Result { + Ok(TrackInfo { + id: TrackId::from(path.to_str().unwrap().to_owned()), + album: None, + artist: None, + name: None, + track_number: None, + }) } +} - 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>(()) - })); - () +enum EntryInfo { + Dir(PathBuf), + File(PathBuf), +} + +impl Iterator for FileIterator { + type Item = Result; + + fn next(&mut self) -> Option { + fn process_entry(entry: DirEntry) -> Result { + entry + .metadata() + .map_err(ScannerError::from) + .map(|metadata| { + if metadata.is_dir() { + EntryInfo::Dir(entry.path()) + } else { + EntryInfo::File(entry.path()) + } + }) + } + + let next_file = match &mut self.file_iter { + Some(iter) => iter.next(), + None => None, + }; + + match next_file { + Some(Ok(entry)) => match process_entry(entry) { + Ok(EntryInfo::Dir(path)) => { + self.dirs.push(path); + self.next() } - Err(err) => { - println!("scan_dir could not read path: ({:?})", err); - } - } + Ok(EntryInfo::File(path)) => Some(self.scan_file(path)), + Err(err) => Some(Err(err)), + }, + Some(Err(err)) => Some(Err(ScannerError::from(err))), + None => match self.dirs.pop() { + Some(dir) => match dir.read_dir() { + Ok(entry) => { + self.file_iter = Some(entry); + self.next() + } + Err(err) => { + if err.kind() == std::io::ErrorKind::NotFound { + Some(Err(ScannerError::NotFound(dir))) + } else { + Some(Err(ScannerError::from(err))) + } + } + }, + None => None, + }, } - ok(()) - } - - fn scan_file(&self, path: PathBuf) -> Flow<(), FatalError, ScannerError> { - ok(()) } } impl MusicScanner for FileScanner { - fn scan<'a>(&'a self) -> Box<&'a dyn Iterator> { - unimplemented!() - /* - loop { - match self.control_rx.recv_timeout(Duration::from_millis(100)) { - Ok(ControlMsg::Exit) => return, - Err(RecvTimeoutError::Timeout) => (), - Err(RecvTimeoutError::Disconnected) => return, - } - if Instant::now() >= self.next_scan { - for root in self.music_directories.iter() { - self.scan_dir(vec![root.clone()]); - } - self.next_scan = Instant::now() + scan_frequency(); - } - } - */ + fn scan<'a>(&'a self) -> Box> + 'a> { + Box::new(FileIterator { + dirs: self.roots.clone(), + file_iter: None, + }) } } -*/ #[cfg(test)] pub mod factories { @@ -162,8 +182,8 @@ pub mod factories { } impl MusicScanner for MockScanner { - fn scan<'a>(&'a self) -> Box + 'a> { - Box::new(self.data.iter().map(|t| t.clone())) + fn scan<'a>(&'a self) -> Box> + 'a> { + Box::new(self.data.iter().map(|t| Ok(t.clone()))) } } }
id Track # Title Artist