diff --git a/errors/src/lib.rs b/errors/src/lib.rs index 755788d..1128ff5 100644 --- a/errors/src/lib.rs +++ b/errors/src/lib.rs @@ -16,3 +16,14 @@ pub fn error(err: E) -> Result { pub fn fatal(err: FE) -> Result { Err(err) } + +#[macro_export] +macro_rules! result { + ($x:expr) => { + match $x { + Ok(Ok(val)) => val, + Ok(Err(err)) => return Ok(Err(err.into())), + Err(err) => return Err(err), + } + }; +} diff --git a/flake.nix b/flake.nix index 618a89a..b58b894 100644 --- a/flake.nix +++ b/flake.nix @@ -26,18 +26,19 @@ name = "ld-tools-devshell"; buildInputs = [ pkgs.clang + pkgs.dbus + pkgs.entr pkgs.glib - pkgs.gst_all_1.gstreamer + pkgs.gst_all_1.gst-plugins-bad pkgs.gst_all_1.gst-plugins-base pkgs.gst_all_1.gst-plugins-good - pkgs.gst_all_1.gst-plugins-bad pkgs.gst_all_1.gst-plugins-ugly - pkgs.pipewire - pkgs.openssl - pkgs.pkg-config + pkgs.gst_all_1.gstreamer pkgs.nodejs - pkgs.entr - pkgs.dbus + pkgs.openssl + pkgs.pipewire + pkgs.pkg-config + pkgs.sqlite rust ]; LIBCLANG_PATH="${pkgs.llvmPackages.libclang.lib}/lib"; diff --git a/music-player/errors.rs b/music-player/errors.rs new file mode 100644 index 0000000..5a4e5a3 --- /dev/null +++ b/music-player/errors.rs @@ -0,0 +1,7 @@ +pub use error::{error, fatal, ok, Result}; + +pub enum FatalError { + UnexpectedError, +} + +impl error::FatalError for FatalError {} diff --git a/music-player/server/Cargo.lock b/music-player/server/Cargo.lock index 402af25..6196929 100644 --- a/music-player/server/Cargo.lock +++ b/music-player/server/Cargo.lock @@ -2,6 +2,17 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -158,6 +169,18 @@ dependencies = [ name = "errors" version = "0.1.0" +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "1.8.0" @@ -290,6 +313,18 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa" +dependencies = [ + "hashbrown", +] [[package]] name = "headers" @@ -448,6 +483,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libsqlite3-sys" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29f835d03d717946d28b1d1ed632eb6f0e24a299388ee623d0c23118d3e8a7fa" +dependencies = [ + "pkg-config", + "vcpkg", +] + [[package]] name = "lock_api" version = "0.4.9" @@ -539,9 +584,11 @@ dependencies = [ "dbus", "errors", "mpris", + "rusqlite", "serde", "thiserror", "tokio", + "url", "warp", ] @@ -712,6 +759,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "rusqlite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01e213bc3ecb39ac32e81e51ebe31fd888a940515173e3a18a35f8c6e896422a" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "rustls-pemfile" version = "0.2.1" @@ -1124,6 +1185,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.4" diff --git a/music-player/server/Cargo.toml b/music-player/server/Cargo.toml index 8e3b176..f77f620 100644 --- a/music-player/server/Cargo.toml +++ b/music-player/server/Cargo.toml @@ -9,9 +9,11 @@ edition = "2021" dbus = { version = "0.9.7" } errors = { path = "../../errors" } 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" warp = { version = "0.3" } [lib] diff --git a/music-player/server/src/audio.rs b/music-player/server/src/audio.rs index e68e7cb..380c7db 100644 --- a/music-player/server/src/audio.rs +++ b/music-player/server/src/audio.rs @@ -69,6 +69,9 @@ pub enum AudioError { #[error("Unknown problem with mpris")] MprisError(mpris::DBusError), + + #[error("url parse error {0}")] + UrlError(url::ParseError), } impl From for AudioError { @@ -83,9 +86,16 @@ impl From for AudioError { } } +impl From for AudioError { + fn from(err: url::ParseError) -> Self { + Self::UrlError(err) + } +} + #[derive(Clone, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct Track { + pub id: String, pub track_number: Option, pub name: Option, pub album: Option, @@ -95,6 +105,7 @@ pub struct Track { impl From<&mpris::Metadata> for Track { fn from(data: &mpris::Metadata) -> Self { Self { + id: data.track_id().unwrap().to_string(), track_number: data.track_number(), name: data.title().map(|s| s.to_owned()), album: data.album_name().map(|s| s.to_owned()), @@ -134,9 +145,33 @@ pub struct Capabilities { pub trait AudioPlayer { fn capabilities(&self) -> Result; fn state(&self) -> Result; + fn play(&self, trackid: url::Url) -> Result; fn play_pause(&self) -> Result; } +pub struct GStreamerPlayer { + url: url::Url, +} + +impl AudioPlayer for GStreamerPlayer { + fn capabilities(&self) -> Result { + unimplemented!() + } + + fn state(&self) -> Result { + unimplemented!() + } + + fn play(&self, trackid: url::Url) -> Result { + unimplemented!() + } + + fn play_pause(&self) -> Result { + unimplemented!() + } +} + +/* pub struct MprisDevice { device_id: String, player: Player, @@ -153,7 +188,7 @@ impl MprisDevice { }) } - pub fn run(&mut self, control_channel: Receiver) { + pub fn monitor(&mut self, control_channel: Receiver) { let (tx, rx) = channel(); { let device_id = self.device_id.clone(); @@ -226,7 +261,17 @@ impl AudioPlayer for MprisDevice { unimplemented!("AudioPlayer state") } + fn play(&self, track_id: String) -> Result { + println!("playing: {}", track_id); + self.player + .go_to(&mpris::TrackID::from(dbus::Path::from(track_id)))?; + self.player.play(); + Ok(State::Stopped) + } + fn play_pause(&self) -> Result { - unimplemented!("Audioplayer play/pause command") + self.player.play_pause()?; + Ok(State::Stopped) } } +*/ diff --git a/music-player/server/src/bin/server.rs b/music-player/server/src/bin/server.rs index 7560eaf..b93784b 100644 --- a/music-player/server/src/bin/server.rs +++ b/music-player/server/src/bin/server.rs @@ -1,10 +1,7 @@ -use dbus::ffidisp::Connection; -use serde::Serialize; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; -use std::path::PathBuf; -use warp::Filter; +use std::{io::stdin, path::PathBuf, thread, time::Duration}; +// use warp::Filter; -pub mod audio; +use music_player::core::Core; /* fn tracks() -> Vec { @@ -52,6 +49,16 @@ fn tracks() -> Vec { #[tokio::main] pub async fn main() { + match Core::new(PathBuf::from(":memory:")) { + Ok(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), + } + /* let connection = Connection::new_session().expect("to connect to dbus"); diff --git a/music-player/server/src/core.rs b/music-player/server/src/core.rs index 571d84d..680a59a 100644 --- a/music-player/server/src/core.rs +++ b/music-player/server/src/core.rs @@ -1,45 +1,110 @@ -use dbus::ffidisp::Connection; -use mpris::{Player, PlayerFinder}; +use crate::{database::Database, Error, FatalError}; +use errors::{ok, result, Result}; +use std::{ + path::PathBuf, + sync::mpsc::{channel, Receiver, RecvTimeoutError, Sender}, + thread, + thread::JoinHandle, + time::{Duration, Instant}, +}; -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -struct Device { - dbus_name: String, - name: String, +pub enum ControlMsg { + Exit, +} + +pub enum TrackMsg { + DbUpdate, +} + +pub enum PlaybackMsg { + PositionUpdate, + Playing, + Pausing, + Stopping, } pub struct Core { - conn: Connection, - player: Player, + db: Database, + track_handle: JoinHandle<()>, + track_rx: Receiver, + playback_handle: JoinHandle<()>, + playback_rx: Receiver, + control_tx: Sender, +} + +fn scan_frequency() -> Duration { + Duration::from_secs(60) +} + +pub struct FileScanner { + db: Database, + control_rx: Receiver, + tracker_tx: Sender, + next_scan: Instant, +} + +impl FileScanner { + fn new(db: Database, control_rx: Receiver, tracker_tx: Sender) -> Self { + Self { + db, + control_rx, + tracker_tx, + next_scan: Instant::now(), + } + } + + fn scan(&mut self) { + 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 { + println!("scan"); + self.next_scan = Instant::now() + scan_frequency(); + } + } + } } impl Core { - fn new(&self) -> Result { - let conn = Connection::new_session()?; - Ok(Core { - conn, - player: mpris::Player::new(conn, ":1.6".to_owned(), 1000)?, + pub fn new(db_path: PathBuf) -> Result { + let db = result!(Database::new(db_path)); + + 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(); + }); + (track_handle, track_rx) + }; + + let (playback_handle, playback_rx) = { + let (playback_tx, playback_rx) = channel(); + let playback_handle = thread::spawn(move || {}); + (playback_handle, playback_rx) + }; + + ok(Core { + db, + track_handle, + track_rx, + playback_handle, + playback_rx, + control_tx, }) } - fn list_devices(&self) -> Result, Error> { - mpris::PlayerFinder::for_connection(conn) - .find_all()? - .into_iter() - .map(|player| Device { - dbus_name: player.unique_name().to_owned(), - name: player.identity().to_owned(), - }) - .collect() - } - - fn list_tracks(&self) -> Result, Error> { - self.player - .get_track_list()? - .ids() - .into_iter() - .map(|id| id.as_str().to_owned()) - .collect() + pub fn exit(&self) { + let _ = self.control_tx.send(ControlMsg::Exit); + /* + self.track_handle.join(); + self.playback_handle.join(); + */ } } diff --git a/music-player/server/src/database.rs b/music-player/server/src/database.rs new file mode 100644 index 0000000..775369c --- /dev/null +++ b/music-player/server/src/database.rs @@ -0,0 +1,49 @@ +use crate::FatalError; +use errors::{error, ok, Result}; +use rusqlite::Connection; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum DatabaseError { + #[error("database is unreadable")] + DatabaseUnreadable, + #[error("unhandled database problem: {0}")] + UnhandledError(rusqlite::Error), +} + +pub struct ManagedConnection<'a> { + pool: &'a Database, + conn: Option, +} + +impl<'a> Drop for ManagedConnection<'a> { + fn drop(&mut self) { + self.pool.r(self.conn.take().unwrap()); + } +} + +#[derive(Clone)] +pub struct Database { + path: PathBuf, + pool: Arc>>, +} + +impl Database { + pub fn new(path: PathBuf) -> Result { + let connection = match Connection::open(path.clone()) { + Ok(connection) => connection, + Err(err) => return error(DatabaseError::UnhandledError(err)), + }; + ok(Database { + path, + pool: Arc::new(Mutex::new(vec![connection])), + }) + } + + pub fn r(&self, conn: Connection) { + let mut pool = self.pool.lock().unwrap(); + pool.push(conn); + } +} diff --git a/music-player/server/src/lib.rs b/music-player/server/src/lib.rs index fa3e4e4..e35a59f 100644 --- a/music-player/server/src/lib.rs +++ b/music-player/server/src/lib.rs @@ -1 +1,24 @@ pub mod audio; +pub mod core; +pub mod database; +use database::DatabaseError; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[error("Database error: {0}")] + DatabaseError(DatabaseError), +} + +impl From for Error { + fn from(err: DatabaseError) -> Self { + Self::DatabaseError(err) + } +} + +#[derive(Debug)] +pub enum FatalError { + UnexpectedError, +} + +impl errors::FatalError for FatalError {}