This commit is contained in:
Moritz Ruth 2024-02-01 17:06:23 +01:00
parent ebb0552a13
commit bf48f12c8c
Signed by: moritzruth
GPG key ID: C9BBAB79405EE56D
5 changed files with 370 additions and 2 deletions

14
Cargo.lock generated
View file

@ -1161,6 +1161,20 @@ version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69d3587f8a9e599cc7ec2c00e331f71c4e69a5f9a4b8a6efd5b07466b9736f9a"
[[package]]
name = "playerctl"
version = "0.1.0"
dependencies = [
"clap",
"color-eyre",
"deckster_mode",
"env_logger",
"log",
"once_cell",
"serde",
"tokio",
]
[[package]]
name = "png"
version = "0.17.10"

View file

@ -4,7 +4,6 @@ use log::warn;
use parse_display::Display;
use regex::Regex;
use serde::Deserialize;
use tokio::select;
use tokio::sync::broadcast;
use deckster_mode::shared::handler_communication::{
@ -246,7 +245,7 @@ async fn manage_knob(path: KnobPath, config: KnobConfig, mut events: broadcast::
}
loop {
select! {
tokio::select! {
Ok(volume_state) = volume_states.recv() => {
entity_state = volume_state.entities_by_id().values().find(|entity| state_matches(&config.target, entity)).map(Arc::clone);
update_knob_style(&entity_state);

View file

@ -0,0 +1,14 @@
[package]
name = "playerctl"
version = "0.1.0"
edition = "2021"
[dependencies]
deckster_mode = { path = "../../crates/deckster_mode" }
clap = { version = "4.4.18", features = ["derive"] }
color-eyre = "0.6.2"
serde = { version = "1.0.196", features = ["derive"] }
env_logger = "0.11.1"
log = "0.4.20"
tokio = { version = "1.35.1", features = ["macros", "parking_lot", "rt-multi-thread", "sync"] }
once_cell = "1.19.0"

View file

@ -0,0 +1,318 @@
use std::fmt::Debug;
use std::hash::Hash;
use std::io::{BufRead, BufReader};
use std::process::Stdio;
use std::sync::{Arc, RwLock};
use std::thread;
use std::thread::sleep;
use std::time::Duration;
use log::{error, warn};
use once_cell::sync::Lazy;
use serde::Deserialize;
use tokio::sync::broadcast;
use tokio::sync::broadcast::error::RecvError;
use deckster_mode::shared::handler_communication::{HandlerCommand, HandlerEvent, InitialHandlerMessage, KeyEvent};
use deckster_mode::shared::path::KeyPath;
use deckster_mode::shared::state::KeyStyleByStateMap;
use deckster_mode::{send_command, DecksterHandler};
#[derive(Debug, Deserialize)]
pub struct ButtonConfig {
#[serde(default)]
pub style: KeyStyleByStateMap<ButtonState>,
pub command: ButtonCommand,
}
#[derive(Debug, Eq, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ButtonCommand {
PlayPause,
Play,
Pause,
Previous,
Next,
}
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ButtonState {
Inactive,
Playing,
Paused,
}
#[derive(Debug, Deserialize)]
pub struct ShuffleConfig {
#[serde(default)]
pub style: KeyStyleByStateMap<ShuffleState>,
}
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ShuffleState {
Inactive,
Off,
On,
}
#[derive(Debug, Deserialize)]
pub struct LoopConfig {
#[serde(default)]
pub style: KeyStyleByStateMap<LoopState>,
}
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum LoopState {
Inactive,
None,
Single,
All,
}
struct PlayerctlStateWatcher<S: Clone + Debug + Send + Sync + 'static> {
state: Arc<RwLock<S>>,
new_states: broadcast::Sender<S>,
}
impl<S: Clone + Debug + Send + Sync + 'static> PlayerctlStateWatcher<S> {
pub fn new(initial_state: S, subcommand: &'static str, parse_state: impl 'static + Fn(&String) -> S + Send) -> Self {
let state = Arc::new(RwLock::new(initial_state));
let cloned_state = Arc::clone(&state);
let new_states = broadcast::Sender::new(1);
let cloned_new_states = new_states.clone();
thread::spawn(move || {
loop {
let players = std::process::Command::new("playerctl")
.args(["-s", "-l"])
.stderr(Stdio::null())
.stdin(Stdio::null())
.output()
.unwrap();
if players.stdout.iter().filter(|c| **c == b'\n').count() > 0 {
break;
}
sleep(Duration::from_secs(1));
}
let command = std::process::Command::new("playerctl")
.args(["-s", "-F", subcommand])
.stdout(Stdio::piped())
.stderr(Stdio::null())
.stdin(Stdio::null())
.spawn()
.unwrap();
let stdout = BufReader::new(command.stdout.unwrap());
for line in stdout.lines() {
let line = line.unwrap();
let new_state = parse_state(&line);
{
let mut g = cloned_state.write().unwrap();
*g = new_state.clone();
if cloned_new_states.send(new_state).is_err() {
warn!("No one is listening to player state changes")
}
}
}
});
PlayerctlStateWatcher { state, new_states }
}
fn subscribe_to_state(&self) -> broadcast::Receiver<S> {
let r = self.new_states.subscribe();
self.new_states.send(self.state.read().unwrap().clone()).unwrap();
r
}
}
static STATE_WATCHER_PLAYING: Lazy<PlayerctlStateWatcher<ButtonState>> = Lazy::new(|| {
PlayerctlStateWatcher::new(ButtonState::Inactive, "status", |s| match s.as_ref() {
"Playing" => ButtonState::Playing,
"Paused" => ButtonState::Paused,
"" | "Stopped" => ButtonState::Inactive,
_ => panic!("Unknown state: {}", s),
})
});
static STATE_WATCHER_SHUFFLE: Lazy<PlayerctlStateWatcher<ShuffleState>> = Lazy::new(|| {
PlayerctlStateWatcher::new(ShuffleState::Inactive, "shuffle", |s| match s.as_ref() {
"Off" => ShuffleState::Off,
"On" => ShuffleState::On,
"" => ShuffleState::Inactive,
_ => panic!("Unknown state: {}", s),
})
});
static STATE_WATCHER_LOOP: Lazy<PlayerctlStateWatcher<LoopState>> = Lazy::new(|| {
PlayerctlStateWatcher::new(LoopState::Inactive, "loop", |s| match s.as_ref() {
"Track" => LoopState::Single,
"Playlist" => LoopState::All,
"None" => LoopState::None,
"" => LoopState::Inactive,
_ => panic!("Unknown state: {}", s),
})
});
pub async fn handle_button(path: KeyPath, config: Arc<ButtonConfig>, mut events: broadcast::Receiver<KeyEvent>) {
let mut is_active = false;
let mut state = STATE_WATCHER_PLAYING.subscribe_to_state();
let command = match config.command {
ButtonCommand::PlayPause => "play-pause",
ButtonCommand::Play => "play",
ButtonCommand::Pause => "pause",
ButtonCommand::Previous => "previous",
ButtonCommand::Next => "next",
};
loop {
tokio::select! {
result = state.recv() => {
match result {
Err(RecvError::Closed) => { result.unwrap(); },
Err(RecvError::Lagged(_)) => { /* mission failed, we'll get 'em next time */ },
Ok(state) => {
is_active = state != ButtonState::Inactive;
send_command(HandlerCommand::SetKeyStyle {
path: path.clone(),
value: config.style.get(&state).cloned()
});
}
}
}
Ok(event) = events.recv() => {
if event == KeyEvent::Press && is_active {
let status = std::process::Command::new("playerctl").arg(command).status().unwrap();
if !status.success() {
error!("`playerctl {}` failed with exit code {}", command, status)
}
}
}
}
}
}
pub async fn handle_shuffle(path: KeyPath, config: Arc<ShuffleConfig>, mut events: broadcast::Receiver<KeyEvent>) {
let mut state = STATE_WATCHER_SHUFFLE.subscribe_to_state();
loop {
tokio::select! {
result = state.recv() => {
match result {
Err(RecvError::Closed) => { result.unwrap(); },
Err(RecvError::Lagged(_)) => { /* mission failed, we'll get 'em next time */ },
Ok(state) => {
send_command(HandlerCommand::SetKeyStyle {
path: path.clone(),
value: config.style.get(&state).cloned()
})
}
}
}
Ok(event) = events.recv() => {
if event == KeyEvent::Press {
let current = *STATE_WATCHER_SHUFFLE.state.read().unwrap();
let new = match current {
ShuffleState::Inactive | ShuffleState::Off => "On",
ShuffleState::On => "Off",
};
let status = std::process::Command::new("playerctl").args(["shuffle", new]).status().unwrap();
if !status.success() {
error!("`playerctl shuffle {}` failed with exit code {}", new, status)
}
}
}
}
}
}
pub async fn handle_loop(path: KeyPath, config: Arc<LoopConfig>, mut events: broadcast::Receiver<KeyEvent>) {
let mut state = STATE_WATCHER_LOOP.subscribe_to_state();
loop {
tokio::select! {
result = state.recv() => {
match result {
Err(RecvError::Closed) => { result.unwrap(); },
Err(RecvError::Lagged(_)) => { /* mission failed, we'll get 'em next time */ },
Ok(state) => {
send_command(HandlerCommand::SetKeyStyle {
path: path.clone(),
value: config.style.get(&state).cloned()
})
}
}
}
Ok(event) = events.recv() => {
if event == KeyEvent::Press {
let current = *STATE_WATCHER_LOOP.state.read().unwrap();
let new = match current {
LoopState::Inactive | LoopState::None => "Playlist",
LoopState::All => "Track",
LoopState::Single => "None",
};
let status = std::process::Command::new("playerctl").args(["loop", new]).status().unwrap();
if !status.success() {
error!("`playerctl loop {}` failed with exit code {}", new, status)
}
}
}
}
}
}
pub struct Handler {
events_sender: broadcast::Sender<(KeyPath, KeyEvent)>,
#[allow(unused)]
runtime: tokio::runtime::Runtime,
}
impl Handler {
pub fn new(data: InitialHandlerMessage<()>) -> Result<Self, HandlerInitializationError> {
let (events_sender, _) = broadcast::channel::<(KnobPath, KnobEvent)>(5);
let pa_volume_interface = Arc::new(PaVolumeInterface::spawn_thread("deckster handler".to_owned()));
let runtime = tokio::runtime::Builder::new_multi_thread().worker_threads(1).build().unwrap();
for (path, (mode, config)) in data.knob_configs {
if !mode.is_empty() {
return Err(HandlerInitializationError::InvalidModeString {
message: "No mode string allowed.".into(),
});
}
let events_receiver = events_sender.subscribe();
let a = Arc::clone(&pa_volume_interface);
runtime.spawn(manage_knob(path, config, events_receiver, a));
}
Ok(Handler { events_sender, runtime })
}
}
impl DecksterHandler for Handler {
fn handle(&mut self, event: HandlerEvent) {
if let HandlerEvent::Knob { path, event } = event {
self.events_sender.send((path, event)).unwrap();
}
}
}

View file

@ -0,0 +1,23 @@
use clap::Parser;
use color_eyre::Result;
mod handler;
#[derive(Debug, Parser)]
#[command(name = "playerctl")]
enum CliCommand {
#[command(name = "deckster-run", hide = true)]
Run,
}
fn main() -> Result<()> {
let command = CliCommand::parse();
match command {
CliCommand::Run => {
deckster_mode::run(Handler::new)?;
}
}
Ok(())
}