Add a license, build a tester for the media player

This commit is contained in:
Savanni D'Gerinel 2023-02-02 21:43:50 -05:00
parent 522f7eb65b
commit ae22d2191c
12 changed files with 1454 additions and 319 deletions

7
errors/Cargo.lock generated Normal file
View File

@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "errors"
version = "0.1.0"

8
errors/Cargo.toml Normal file
View File

@ -0,0 +1,8 @@
[package]
name = "errors"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

18
errors/src/lib.rs Normal file
View File

@ -0,0 +1,18 @@
use std::error::Error;
use std::result;
pub trait FatalError {}
pub type Result<A, FE, E> = result::Result<result::Result<A, E>, FE>;
pub fn ok<A, FE: FatalError, E: Error>(val: A) -> Result<A, FE, E> {
Ok(Ok(val))
}
pub fn error<A, FE: FatalError, E: Error>(err: E) -> Result<A, FE, E> {
Ok(Err(err))
}
pub fn fatal<A, FE: FatalError, E: Error>(err: FE) -> Result<A, FE, E> {
Err(err)
}

View File

@ -17,11 +17,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1674242456,
"narHash": "sha256-yBy7rCH7EiBe9+CHZm9YB5ii5GRa+MOxeW0oDEBO8SE=",
"lastModified": 1675918889,
"narHash": "sha256-hy7re4F9AEQqwZxubct7jBRos6md26bmxnCjxf5utJA=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "cdead16a444a3e5de7bc9b0af8e198b11bb01804",
"rev": "49efda9011e8cdcd6c1aad30384cb1dc230c82fe",
"type": "github"
},
"original": {
@ -52,7 +52,7 @@
"nixpkgs": "nixpkgs_2"
},
"locked": {
"narHash": "sha256-GWnDfrKbQPb+skMMesm1WVu7a3y3GTDpYpfOv3yF4gc=",
"narHash": "sha256-0eO3S+2gLODqDoloufeC99PfQ5mthuN9JADzqFXid1Y=",
"type": "tarball",
"url": "https://github.com/oxalica/rust-overlay/archive/master.tar.gz"
},
@ -70,11 +70,11 @@
},
"unstable": {
"locked": {
"lastModified": 1674211260,
"narHash": "sha256-xU6Rv9sgnwaWK7tgCPadV6HhI2Y/fl4lKxJoG2+m9qs=",
"lastModified": 1675942811,
"narHash": "sha256-/v4Z9mJmADTpXrdIlAjFa1e+gkpIIROR670UVDQFwIw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "5ed481943351e9fd354aeb557679624224de38d5",
"rev": "724bfc0892363087709bd3a5a1666296759154b1",
"type": "github"
},
"original": {

View File

@ -26,6 +26,7 @@
name = "ld-tools-devshell";
buildInputs = [
pkgs.clang
pkgs.dbus
pkgs.entr
pkgs.glade
pkgs.gtk4

File diff suppressed because it is too large Load Diff

View File

@ -6,4 +6,12 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
pipewire = { version = "0.5" }
dbus = { version = "0.9.7" }
errors = { path = "../../errors" }
mpris = { version = "2.0" }
serde = { version = "1.0", features = ["derive"] }
thiserror = { version = "1.0" }
tokio = { version = "1.24", features = ["full"] }
warp = { version = "0.3" }
[lib]

View File

@ -0,0 +1,232 @@
/*
Copyright 2023, Savanni D'Gerinel <savanni@luminescent-dreams.com>
This file is part of the Luminescent Dreams Tools.
Luminescent Dreams Tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
Luminescent Dreams Tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with Lumeto. If not, see <https://www.gnu.org/licenses/>.
*/
use dbus::ffidisp::Connection;
use mpris::{FindingError, PlaybackStatus, Player, PlayerFinder, ProgressTick};
use serde::Serialize;
use std::{
path::PathBuf,
sync::mpsc::{channel, Receiver, Sender, TryRecvError},
thread,
time::Duration,
};
use thiserror::Error;
pub enum Message {
Quit,
}
#[derive(Clone, Debug)]
pub enum Event {
Paused(Track, Duration),
Playing(Track, Duration),
Stopped,
Position(Track, Duration),
}
#[derive(Debug)]
pub struct DeviceInformation {
pub address: String,
pub name: String,
}
pub fn list_devices(conn: Connection) -> Result<Vec<DeviceInformation>, FindingError> {
Ok(PlayerFinder::for_connection(conn)
.find_all()?
.into_iter()
.map(|player| DeviceInformation {
address: player.unique_name().to_owned(),
name: player.identity().to_owned(),
})
.collect())
}
#[derive(Debug, Error)]
pub enum AudioError {
#[error("DBus device was not found")]
DeviceNotFound,
#[error("DBus connection lost or otherwise failed")]
ConnectionLost,
#[error("Specified media cannot be found")]
MediaNotFound,
#[error("Play was ordered, but nothing is in the queue")]
NothingInQueue,
#[error("Unknown dbus error")]
DbusError(dbus::Error),
#[error("Unknown problem with mpris")]
MprisError(mpris::DBusError),
}
impl From<dbus::Error> for AudioError {
fn from(err: dbus::Error) -> Self {
Self::DbusError(err)
}
}
impl From<mpris::DBusError> for AudioError {
fn from(err: mpris::DBusError) -> Self {
Self::MprisError(err)
}
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Track {
pub track_number: Option<i32>,
pub name: Option<String>,
pub album: Option<String>,
pub artist: Option<String>,
}
impl From<&mpris::Metadata> for Track {
fn from(data: &mpris::Metadata) -> Self {
Self {
track_number: data.track_number(),
name: data.title().map(|s| s.to_owned()),
album: data.album_name().map(|s| s.to_owned()),
artist: None,
}
}
}
impl From<mpris::Metadata> for Track {
fn from(data: mpris::Metadata) -> Self {
Self::from(&data)
}
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum State {
Playing(Track),
Paused(Track),
Stopped,
}
pub struct CurrentlyPlaying {
track: Track,
position: Duration,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Capabilities {
pub can_control: bool,
pub can_pause: bool,
pub can_play: bool,
pub supports_track_lists: bool,
}
pub trait AudioPlayer {
fn capabilities(&self) -> Result<Capabilities, AudioError>;
fn state(&self) -> Result<State, AudioError>;
fn play_pause(&self) -> Result<State, AudioError>;
}
pub struct MprisDevice {
device_id: String,
player: Player,
state: State,
}
impl MprisDevice {
pub fn new(device_id: String) -> Result<MprisDevice, AudioError> {
let connection = Connection::new_session()?;
Ok(MprisDevice {
device_id: device_id.clone(),
player: mpris::Player::new(connection, device_id, 1000)?,
state: State::Stopped,
})
}
pub fn run(&mut self, control_channel: Receiver<Message>) {
let (tx, rx) = channel();
{
let device_id = self.device_id.clone();
let tx = tx.clone();
thread::spawn(move || {
MprisDevice::new(device_id)
.expect("connect to bus")
.monitor_progress(tx);
});
};
loop {
match control_channel.try_recv() {
Ok(Message::Quit) => return,
Err(TryRecvError::Empty) => {}
Err(TryRecvError::Disconnected) => return,
}
let event = rx.recv().expect("receive should never fail");
println!("event received: {:?}", event);
}
}
pub fn monitor_progress(&self, tx: Sender<Event>) {
let mut tracker = self
.player
.track_progress(1000)
.expect("can get an event stream");
loop {
let ProgressTick { progress, .. } = tracker.tick();
match progress.playback_status() {
PlaybackStatus::Playing => {
tx.send(Event::Playing(
Track::from(progress.metadata()),
progress.position(),
))
.expect("send to succeed");
}
PlaybackStatus::Paused => {
tx.send(Event::Paused(
Track::from(progress.metadata()),
progress.position(),
))
.expect("send to succeed");
}
PlaybackStatus::Stopped => {
tx.send(Event::Stopped).expect("send to succeed");
}
}
}
}
}
impl AudioPlayer for MprisDevice {
fn capabilities(&self) -> Result<Capabilities, AudioError> {
Ok(Capabilities {
can_control: self.player.can_control()?,
can_pause: self.player.can_pause()?,
can_play: self.player.can_play()?,
supports_track_lists: self.player.supports_track_lists(),
})
}
fn state(&self) -> Result<State, AudioError> {
println!(
"supports track lists: {:?}",
self.player.supports_track_lists()
);
let metadata = self.player.get_metadata()?;
println!("{:?}", metadata);
unimplemented!("AudioPlayer state")
}
fn play_pause(&self) -> Result<State, AudioError> {
unimplemented!("Audioplayer play/pause command")
}
}

View File

@ -0,0 +1,52 @@
/*
Copyright 2023, Savanni D'Gerinel <savanni@luminescent-dreams.com>
This file is part of the Luminescent Dreams Tools.
Luminescent Dreams Tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
Luminescent Dreams Tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with Lumeto. If not, see <https://www.gnu.org/licenses/>.
*/
use dbus::ffidisp::Connection;
use music_player::audio::{list_devices, Message, MprisDevice};
use std::{io, thread};
fn main() {
for device in list_devices(Connection::new_session().expect("connection to dbus session"))
.expect("to list devices")
{
println!("Device: [{}] {}", device.address, device.name);
}
/*
let progress_thread = thread::spawn(|| MprisDevice::monitor_progress(":1.1611".to_owned()));
let event_thread = thread::spawn(|| MprisDevice::scan_event_stream(":1.1611".to_owned()));
*/
let (tx, rx) = std::sync::mpsc::channel();
let app_thread = thread::spawn(move || {
let mut device = MprisDevice::new(":1.1611".to_owned()).expect("to get a device");
device.run(rx);
});
let stdin = io::stdin();
let mut input = String::new();
let _ = stdin.read_line(&mut input);
let _ = tx.send(Message::Quit);
let _ = app_thread.join();
/*
println!(
"{:?}",
device.capabilities().expect("can retrieve capabilities")
);
// println!("{:?}", device.state().expect("play-pause to succeed"));
let _ = progress_thread.join();
let _ = event_thread.join();
*/
}

View File

@ -0,0 +1,47 @@
use dbus::ffidisp::Connection;
use mpris::{Player, PlayerFinder};
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct Device {
dbus_name: String,
name: String,
}
pub struct Core {
conn: Connection,
player: Player,
}
impl Core {
fn new(&self) -> Result<Core, Error> {
let conn = Connection::new_session()?;
Ok(Core {
conn,
player: mpris::Player::new(conn, ":1.6".to_owned(), 1000)?,
})
}
fn list_devices(&self) -> Result<Vec<Device>, 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<Vec<String>, Error> {
self.player
.get_track_list()?
.ids()
.into_iter()
.map(|id| id.as_str().to_owned())
.collect()
}
}
#[cfg(test)]
mod test {}

View File

@ -0,0 +1 @@
pub mod audio;

View File

@ -1,30 +1,106 @@
use pipewire::{
channel,
keys::{MEDIA_CATEGORY, MEDIA_ROLE, MEDIA_TYPE},
properties,
stream::Stream,
Context, Loop, MainLoop,
};
use std::time::Duration;
use dbus::ffidisp::Connection;
use serde::Serialize;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::path::PathBuf;
use warp::Filter;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mainloop = MainLoop::new()?;
let context = Context::new(&mainloop)?;
let core = context.connect(None)?;
let registry = core.get_registry()?;
pub mod audio;
let stream = Stream::new(
&core,
"audio-src",
properties! {
MEDIA_TYPE => "Audio",
MEDIA_CATEGORY => "Playback",
MEDIA_ROLE => "Music",
/*
fn tracks() -> Vec<Track> {
vec![
Track {
track_number: Some(1),
name: Some("Underground".to_owned()),
album: Some("Artemis".to_owned()),
artist: Some("Lindsey Stirling".to_owned()),
path: PathBuf::from("/mnt/music/Lindsey Stirling/Artemis/01 - Underground.ogg"),
},
)
.expect("to get a stream");
mainloop.run();
Ok(())
Track {
track_number: Some(2),
name: Some("Artemis".to_owned()),
album: Some("Artemis".to_owned()),
artist: Some("Lindsey Stirling".to_owned()),
path: PathBuf::from("/mnt/music/Lindsey Stirling/Artemis/02 - Artemis.ogg"),
},
Track {
track_number: Some(3),
name: Some("Til the Light Goes Out".to_owned()),
album: Some("Artemis".to_owned()),
artist: Some("Lindsey Stirling".to_owned()),
path: PathBuf::from(
"/mnt/music/Lindsey Stirling/Artemis/03 - Til the Light Goes Out.ogg",
),
},
Track {
track_number: Some(4),
name: Some("Between Twilight".to_owned()),
album: Some("Artemis".to_owned()),
artist: Some("Lindsey Stirling".to_owned()),
path: PathBuf::from("/mnt/music/Lindsey Stirling/Artemis/04 - Between Twilight.ogg"),
},
Track {
track_number: Some(5),
name: Some("Foreverglow".to_owned()),
album: Some("Artemis".to_owned()),
artist: Some("Lindsey Stirling".to_owned()),
path: PathBuf::from("/mnt/music/Lindsey Stirling/Artemis/05 - Foreverglow.ogg"),
},
]
}
*/
#[tokio::main]
pub async fn main() {
/*
let connection = Connection::new_session().expect("to connect to dbus");
for player in list_players(connection) {
println!("player found: {}", player.identity());
}
*/
/*
let devices = warp::path!("api" / "v1" / "devices")
.and(warp::get())
.map(|| {
let conn = Connection::new_session().expect("to connect to dbus");
warp::reply::json(&list_devices(conn))
});
let track_list = warp::path!("api" / "v1" / "tracks")
.and(warp::get())
.map(|| warp::reply::json(&tracks()));
let tracks_for_artist = warp::path!("api" / "v1" / "artist" / String)
.and(warp::get())
.map(|_artist: String| warp::reply::json(&tracks()));
let tracks_for_album = warp::path!("api" / "v1" / "album" / String)
.and(warp::get())
.map(|_album: String| warp::reply::json(&tracks()));
let queue = warp::path!("api" / "v1" / "queue")
.and(warp::get())
.map(|| {
let conn = Connection::new_session().expect("to connect to dbus");
warp::reply::json(&list_tracks(conn))
});
let playing_status = warp::path!("api" / "v1" / "play-pause")
.and(warp::get())
.map(|| warp::reply::json(&PlayPause::Paused));
let routes = devices
.or(track_list)
.or(tracks_for_album)
.or(tracks_for_artist)
.or(queue)
.or(playing_status);
let server = warp::serve(routes);
server
.run(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
8002,
))
.await;
*/
}