mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 14:55:43 +00:00
feat(cli): relative volume and quieter CLI startup (#1238)
This commit is contained in:
@@ -20,6 +20,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
* Toggle **Album artists** vs **Track artists** on the Artists page — album mode lists indexed album artists; track mode includes performers from the local artist index (featured/guest credits). Star filter works in both modes; the choice persists across app restarts like **Show artist images**.
|
* Toggle **Album artists** vs **Track artists** on the Artists page — album mode lists indexed album artists; track mode includes performers from the local artist index (featured/guest credits). Star filter works in both modes; the choice persists across app restarts like **Show artist images**.
|
||||||
* Letter bucket filter (`A`–`Z`, `#`, `OTHER`) runs in local SQL instead of scanning catalog chunks client-side, so late-alphabet picks load promptly on large libraries.
|
* Letter bucket filter (`A`–`Z`, `#`, `OTHER`) runs in local SQL instead of scanning catalog chunks client-side, so late-alphabet picks load promptly on large libraries.
|
||||||
|
|
||||||
|
### CLI — relative volume and quieter scripting output
|
||||||
|
|
||||||
|
**By [@cucadmuh](https://github.com/cucadmuh), PR [#1238](https://github.com/Psychotoxical/psysonic/pull/1238)**
|
||||||
|
|
||||||
|
* `psysonic --player volume +5` / `volume -10` adjust the current level by that many percent; `volume 80` still sets an absolute level (use `-q` before `--player` when the delta is negative so it is not parsed as a flag).
|
||||||
|
* CLI invocations no longer print WebKit/NVIDIA workaround notes on stderr; on Linux, remote `--player` forwarding runs before WebKit startup so helper processes exit with less noise.
|
||||||
|
|
||||||
### Square corners — sharp-edged cards and covers
|
### Square corners — sharp-edged cards and covers
|
||||||
|
|
||||||
**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#1215](https://github.com/Psychotoxical/psysonic/pull/1215)**
|
**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#1215](https://github.com/Psychotoxical/psysonic/pull/1215)**
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ case ${sub[1]} in
|
|||||||
(( n == 1 )) && _message -e descriptions 'integer delta (seconds, e.g. -5)'
|
(( n == 1 )) && _message -e descriptions 'integer delta (seconds, e.g. -5)'
|
||||||
;;
|
;;
|
||||||
volume)
|
volume)
|
||||||
(( n == 1 )) && _message -e descriptions 'percent 0–100'
|
(( n == 1 )) && _message -e descriptions 'percent 0–100, or ±N for relative change'
|
||||||
;;
|
;;
|
||||||
play)
|
play)
|
||||||
(( n == 1 )) && _message -e descriptions 'Subsonic id (song, album, or artist)'
|
(( n == 1 )) && _message -e descriptions 'Subsonic id (song, album, or artist)'
|
||||||
|
|||||||
@@ -165,12 +165,17 @@ _psysonic_complete() {
|
|||||||
rating)
|
rating)
|
||||||
(( n == 1 )) && _psysonic_compreply_from_compgen '0 1 2 3 4 5' "$cur"
|
(( n == 1 )) && _psysonic_compreply_from_compgen '0 1 2 3 4 5' "$cur"
|
||||||
;;
|
;;
|
||||||
seek|volume)
|
seek)
|
||||||
if (( n == 1 )); then
|
if (( n == 1 )); then
|
||||||
_psysonic_compopt -o default
|
_psysonic_compopt -o default
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
volume)
|
||||||
|
if (( n == 1 )); then
|
||||||
|
_psysonic_compreply_from_compgen '+5 +10 -5 -10 50' "$cur"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
play)
|
play)
|
||||||
if (( n == 1 )); then
|
if (( n == 1 )); then
|
||||||
_psysonic_compopt -o default
|
_psysonic_compopt -o default
|
||||||
|
|||||||
@@ -117,7 +117,8 @@ pub fn print_help(program: &str) {
|
|||||||
eprintln!(" Global flags (place before --player when needed):");
|
eprintln!(" Global flags (place before --player when needed):");
|
||||||
eprintln!(" --quiet | -q Suppress \"OK: …\" lines (stderr errors are always shown).");
|
eprintln!(" --quiet | -q Suppress \"OK: …\" lines (stderr errors are always shown).");
|
||||||
eprintln!(" --json With `audio-device list`, `library list`, `server list`, or `search`: JSON on stdout.");
|
eprintln!(" --json With `audio-device list`, `library list`, `server list`, or `search`: JSON on stdout.");
|
||||||
eprintln!(" Use {program} -q --player seek -5 so the seek delta is not parsed as a flag.\n");
|
eprintln!(" Use {program} -q --player seek -5 so the seek delta is not parsed as a flag.");
|
||||||
|
eprintln!(" Same for relative volume: {program} -q --player volume -5\n");
|
||||||
eprintln!(" Playback");
|
eprintln!(" Playback");
|
||||||
eprintln!(" {program} [--quiet|-q] --player <action>");
|
eprintln!(" {program} [--quiet|-q] --player <action>");
|
||||||
for entry in cli_action_registry_entries() {
|
for entry in cli_action_registry_entries() {
|
||||||
@@ -129,6 +130,7 @@ pub fn print_help(program: &str) {
|
|||||||
eprintln!(" {program} [--quiet|-q] --player play <id> Track, album, or artist id (artist → shuffled library).");
|
eprintln!(" {program} [--quiet|-q] --player play <id> Track, album, or artist id (artist → shuffled library).");
|
||||||
eprintln!(" {program} [--quiet|-q] --player seek <seconds> Integer delta, e.g. 15 or -10");
|
eprintln!(" {program} [--quiet|-q] --player seek <seconds> Integer delta, e.g. 15 or -10");
|
||||||
eprintln!(" {program} [--quiet|-q] --player volume <0-100> Absolute volume percent.");
|
eprintln!(" {program} [--quiet|-q] --player volume <0-100> Absolute volume percent.");
|
||||||
|
eprintln!(" {program} [--quiet|-q] --player volume <±N> Relative change in percent, e.g. +5 or -10.");
|
||||||
eprintln!(" {program} [--quiet|-q] --player repeat off|all|one");
|
eprintln!(" {program} [--quiet|-q] --player repeat off|all|one");
|
||||||
eprintln!(" {program} [--quiet|-q] --player rating <0-5> Set song rating (0 clears).");
|
eprintln!(" {program} [--quiet|-q] --player rating <0-5> Set song rating (0 clears).");
|
||||||
eprintln!();
|
eprintln!();
|
||||||
@@ -457,6 +459,7 @@ pub fn describe_player_cli_cmd(cmd: &PlayerCliCmd) -> String {
|
|||||||
PlayerCliCmd::PlayOpaqueId(id) => format!("play {id}"),
|
PlayerCliCmd::PlayOpaqueId(id) => format!("play {id}"),
|
||||||
PlayerCliCmd::Seek { delta_secs } => format!("seek {delta_secs:+} s"),
|
PlayerCliCmd::Seek { delta_secs } => format!("seek {delta_secs:+} s"),
|
||||||
PlayerCliCmd::Volume { percent } => format!("volume {percent}%"),
|
PlayerCliCmd::Volume { percent } => format!("volume {percent}%"),
|
||||||
|
PlayerCliCmd::VolumeRelative { delta_percent } => format!("volume {delta_percent:+}%"),
|
||||||
PlayerCliCmd::Repeat(m) => match m {
|
PlayerCliCmd::Repeat(m) => match m {
|
||||||
RepeatCliMode::Off => "repeat off".into(),
|
RepeatCliMode::Off => "repeat off".into(),
|
||||||
RepeatCliMode::All => "repeat all".into(),
|
RepeatCliMode::All => "repeat all".into(),
|
||||||
@@ -510,6 +513,15 @@ pub fn emit_player_cli_cmd<R: Runtime>(app: &AppHandle<R>, cmd: PlayerCliCmd) {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
PlayerCliCmd::VolumeRelative { delta_percent } => {
|
||||||
|
emit_cli_player_command(
|
||||||
|
app,
|
||||||
|
serde_json::json!({
|
||||||
|
"command": "volume-relative",
|
||||||
|
"deltaPercent": delta_percent
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
PlayerCliCmd::Repeat(mode) => {
|
PlayerCliCmd::Repeat(mode) => {
|
||||||
let s = match mode {
|
let s = match mode {
|
||||||
RepeatCliMode::Off => "off",
|
RepeatCliMode::Off => "off",
|
||||||
|
|||||||
+72
-10
@@ -20,6 +20,7 @@ pub enum PlayerCliCmd {
|
|||||||
PlayOpaqueId(String),
|
PlayOpaqueId(String),
|
||||||
Seek { delta_secs: i32 },
|
Seek { delta_secs: i32 },
|
||||||
Volume { percent: u8 },
|
Volume { percent: u8 },
|
||||||
|
VolumeRelative { delta_percent: i32 },
|
||||||
Repeat(RepeatCliMode),
|
Repeat(RepeatCliMode),
|
||||||
Rating { stars: u8 },
|
Rating { stars: u8 },
|
||||||
}
|
}
|
||||||
@@ -172,6 +173,17 @@ pub fn wants_logs(args: &[String]) -> bool {
|
|||||||
args.iter().skip(1).any(|a| a == "--logs")
|
args.iter().skip(1).any(|a| a == "--logs")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// True when argv uses any CLI surface (`--player`, `--info`, `--help`, …), not a plain GUI launch.
|
||||||
|
pub fn wants_cli_argv(args: &[String]) -> bool {
|
||||||
|
wants_version(args)
|
||||||
|
|| wants_help(args)
|
||||||
|
|| args.get(1).is_some_and(|a| a == "completions")
|
||||||
|
|| wants_info(args)
|
||||||
|
|| wants_logs(args)
|
||||||
|
|| wants_tail(args)
|
||||||
|
|| parse_cli_command(args).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn wants_follow(args: &[String]) -> bool {
|
pub fn wants_follow(args: &[String]) -> bool {
|
||||||
args.iter().skip(1).any(|a| a == "-f" || a == "--follow")
|
args.iter().skip(1).any(|a| a == "-f" || a == "--follow")
|
||||||
}
|
}
|
||||||
@@ -219,6 +231,24 @@ fn parse_repeat_mode(arg: &str) -> Option<RepeatCliMode> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_volume_cli_arg(raw: &str) -> Option<PlayerCliCmd> {
|
||||||
|
let trimmed = raw.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if trimmed.starts_with('+') || trimmed.starts_with('-') {
|
||||||
|
let delta_percent: i32 = trimmed.parse().ok()?;
|
||||||
|
return Some(PlayerCliCmd::VolumeRelative { delta_percent });
|
||||||
|
}
|
||||||
|
let v: i64 = trimmed.parse().ok()?;
|
||||||
|
if !(0..=100).contains(&v) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(PlayerCliCmd::Volume {
|
||||||
|
percent: v as u8,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_player_cli_at(args: &[String], pos: usize) -> Option<PlayerCliCmd> {
|
fn parse_player_cli_at(args: &[String], pos: usize) -> Option<PlayerCliCmd> {
|
||||||
let verb = args.get(pos + 1)?.as_str();
|
let verb = args.get(pos + 1)?.as_str();
|
||||||
if let Some(entry) = cli_registry_entry_by_verb(verb).filter(|entry| entry.command == "play") {
|
if let Some(entry) = cli_registry_entry_by_verb(verb).filter(|entry| entry.command == "play") {
|
||||||
@@ -251,16 +281,7 @@ fn parse_player_cli_at(args: &[String], pos: usize) -> Option<PlayerCliCmd> {
|
|||||||
let delta_secs: i32 = raw.parse().ok()?;
|
let delta_secs: i32 = raw.parse().ok()?;
|
||||||
Some(PlayerCliCmd::Seek { delta_secs })
|
Some(PlayerCliCmd::Seek { delta_secs })
|
||||||
}
|
}
|
||||||
"volume" => {
|
"volume" => parse_volume_cli_arg(args.get(pos + 2)?),
|
||||||
let raw = args.get(pos + 2)?;
|
|
||||||
let v: i64 = raw.parse().ok()?;
|
|
||||||
if !(0..=100).contains(&v) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some(PlayerCliCmd::Volume {
|
|
||||||
percent: v as u8,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
_ => cli_registry_entry_by_verb(verb)
|
_ => cli_registry_entry_by_verb(verb)
|
||||||
.map(|entry| PlayerCliCmd::NoArgCommand(entry.command.clone())),
|
.map(|entry| PlayerCliCmd::NoArgCommand(entry.command.clone())),
|
||||||
}
|
}
|
||||||
@@ -338,3 +359,44 @@ pub fn parse_cli_command(args: &[String]) -> Option<CliCommand> {
|
|||||||
_ => parse_player_cli_at(args, pos).map(CliCommand::Player),
|
_ => parse_player_cli_at(args, pos).map(CliCommand::Player),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn parse_player(args: &[&str]) -> Option<PlayerCliCmd> {
|
||||||
|
let argv: Vec<String> = std::iter::once("psysonic")
|
||||||
|
.chain(args.iter().copied())
|
||||||
|
.map(String::from)
|
||||||
|
.collect();
|
||||||
|
match parse_cli_command(&argv) {
|
||||||
|
Some(CliCommand::Player(cmd)) => Some(cmd),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn volume_absolute_without_sign() {
|
||||||
|
assert_eq!(
|
||||||
|
parse_player(&["--player", "volume", "50"]),
|
||||||
|
Some(PlayerCliCmd::Volume { percent: 50 })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn volume_relative_with_sign() {
|
||||||
|
assert_eq!(
|
||||||
|
parse_player(&["--player", "volume", "+5"]),
|
||||||
|
Some(PlayerCliCmd::VolumeRelative { delta_percent: 5 })
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_player(&["-q", "--player", "volume", "-10"]),
|
||||||
|
Some(PlayerCliCmd::VolumeRelative { delta_percent: -10 })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn volume_rejects_out_of_range_absolute() {
|
||||||
|
assert_eq!(parse_player(&["--player", "volume", "101"]), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -342,22 +342,6 @@ pub fn run() {
|
|||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
set_app_user_model_id();
|
set_app_user_model_id();
|
||||||
|
|
||||||
// Linux: second `psysonic --player …` forwards over D-Bus before heavy startup.
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
{
|
|
||||||
let argv: Vec<String> = std::env::args().collect();
|
|
||||||
if crate::cli::parse_cli_command(&argv).is_some() {
|
|
||||||
match crate::cli::linux_try_forward_player_cli_secondary(&argv) {
|
|
||||||
Ok(crate::cli::LinuxPlayerForwardResult::Forwarded) => std::process::exit(0),
|
|
||||||
Ok(crate::cli::LinuxPlayerForwardResult::ContinueStartup) => {}
|
|
||||||
Err(msg) => {
|
|
||||||
crate::app_eprintln!("NOT OK: {msg}");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let (audio_engine, _audio_thread) = audio::create_engine();
|
let (audio_engine, _audio_thread) = audio::create_engine();
|
||||||
|
|
||||||
let builder = tauri::Builder::default()
|
let builder = tauri::Builder::default()
|
||||||
|
|||||||
+59
-12
@@ -8,7 +8,20 @@ use webkit2gtk_nvidia_quirk::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
fn apply_linux_webkit_nvidia_quirk() {
|
fn apply_nv_workaround_kind_silent(kind: WorkaroundKind) {
|
||||||
|
match kind {
|
||||||
|
WorkaroundKind::None => {}
|
||||||
|
WorkaroundKind::DisableWebkitDmabufRenderer => {
|
||||||
|
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
|
||||||
|
}
|
||||||
|
WorkaroundKind::DisableNvExplicitSync => {
|
||||||
|
std::env::set_var("__NV_DISABLE_EXPLICIT_SYNC", "1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn apply_linux_webkit_nvidia_quirk(silent: bool) {
|
||||||
if std::env::var("PSYSONIC_WEBKIT_GPU_ACCEL").is_ok() {
|
if std::env::var("PSYSONIC_WEBKIT_GPU_ACCEL").is_ok() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -25,7 +38,19 @@ fn apply_linux_webkit_nvidia_quirk() {
|
|||||||
let forced_x11_gdk = std::env::var("GDK_BACKEND").ok().is_some_and(|s| {
|
let forced_x11_gdk = std::env::var("GDK_BACKEND").ok().is_some_and(|s| {
|
||||||
matches!(s.split(',').next().map(str::trim), Some("x11"))
|
matches!(s.split(',').next().map(str::trim), Some("x11"))
|
||||||
});
|
});
|
||||||
if forced_x11_gdk {
|
if silent {
|
||||||
|
if forced_x11_gdk {
|
||||||
|
match kind {
|
||||||
|
WorkaroundKind::None => {}
|
||||||
|
WorkaroundKind::DisableWebkitDmabufRenderer
|
||||||
|
| WorkaroundKind::DisableNvExplicitSync => {
|
||||||
|
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
apply_nv_workaround_kind_silent(kind);
|
||||||
|
}
|
||||||
|
} else if forced_x11_gdk {
|
||||||
match kind {
|
match kind {
|
||||||
WorkaroundKind::None => {}
|
WorkaroundKind::None => {}
|
||||||
WorkaroundKind::DisableWebkitDmabufRenderer | WorkaroundKind::DisableNvExplicitSync => {
|
WorkaroundKind::DisableWebkitDmabufRenderer | WorkaroundKind::DisableNvExplicitSync => {
|
||||||
@@ -51,18 +76,26 @@ fn apply_pipewire_latency() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn try_forward_linux_cli_player_argv(args: &[String]) {
|
||||||
|
use psysonic_lib::cli::{linux_try_forward_player_cli_secondary, parse_cli_command, LinuxPlayerForwardResult};
|
||||||
|
|
||||||
|
if parse_cli_command(args).is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
match linux_try_forward_player_cli_secondary(args) {
|
||||||
|
Ok(LinuxPlayerForwardResult::Forwarded) => std::process::exit(0),
|
||||||
|
Ok(LinuxPlayerForwardResult::ContinueStartup) => {}
|
||||||
|
Err(msg) => {
|
||||||
|
psysonic_lib::app_eprintln!("NOT OK: {msg}");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// Linux audio: cap the pipewire-alsa client latency so play/pause/seek/volume
|
|
||||||
// respond promptly (issue #862). Must run before any audio stream is opened.
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
apply_pipewire_latency();
|
|
||||||
|
|
||||||
// Linux GTK/WebKit: `webkit2gtk-nvidia-quirk` (skipped when `PSYSONIC_WEBKIT_GPU_ACCEL` is set).
|
|
||||||
// Forced `GDK_BACKEND=x11` uses the X11-only mitigation path — see `apply_linux_webkit_nvidia_quirk`.
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
apply_linux_webkit_nvidia_quirk();
|
|
||||||
|
|
||||||
let args: Vec<String> = std::env::args().collect();
|
let args: Vec<String> = std::env::args().collect();
|
||||||
|
|
||||||
if psysonic_lib::cli::wants_version(&args) {
|
if psysonic_lib::cli::wants_version(&args) {
|
||||||
psysonic_lib::cli::print_version();
|
psysonic_lib::cli::print_version();
|
||||||
return;
|
return;
|
||||||
@@ -87,5 +120,19 @@ fn main() {
|
|||||||
std::process::exit(2);
|
std::process::exit(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
try_forward_linux_cli_player_argv(&args);
|
||||||
|
|
||||||
|
// Linux audio: cap the pipewire-alsa client latency so play/pause/seek/volume
|
||||||
|
// respond promptly (issue #862). Must run before any audio stream is opened.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
apply_pipewire_latency();
|
||||||
|
|
||||||
|
// Linux GTK/WebKit: `webkit2gtk-nvidia-quirk` (skipped when `PSYSONIC_WEBKIT_GPU_ACCEL` is set).
|
||||||
|
// Forced `GDK_BACKEND=x11` uses the X11-only mitigation path — see `apply_linux_webkit_nvidia_quirk`.
|
||||||
|
// Skip the crate's stderr notes when argv is a CLI invocation (scripting noise).
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
apply_linux_webkit_nvidia_quirk(psysonic_lib::cli::wants_cli_argv(&args));
|
||||||
|
|
||||||
psysonic_lib::run();
|
psysonic_lib::run();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -184,6 +184,7 @@ const CONTRIBUTOR_ENTRIES = [
|
|||||||
'Playlists — batch playlist writes past the GET URL limit (add >341 tracks), in-memory membership cache for fast dedup, offline↔playlist layering detangle (PR #1235)',
|
'Playlists — batch playlist writes past the GET URL limit (add >341 tracks), in-memory membership cache for fast dedup, offline↔playlist layering detangle (PR #1235)',
|
||||||
'Queue — resolve thin-state rows off the visible range so off-window items stop rendering as "…" placeholders (desktop panel, mobile drawer, fullscreen up-next) (PR #1236)',
|
'Queue — resolve thin-state rows off the visible range so off-window items stop rendering as "…" placeholders (desktop panel, mobile drawer, fullscreen up-next) (PR #1236)',
|
||||||
'Artists browse — case-insensitive Cyrillic/non-ASCII name search when local index is enabled (PR #1237)',
|
'Artists browse — case-insensitive Cyrillic/non-ASCII name search when local index is enabled (PR #1237)',
|
||||||
|
'CLI — relative volume via signed `volume` argument (+/− percent delta); suppress WebKit NVIDIA stderr notes on CLI argv; faster Linux CLI forward before WebKit init (PR #1238)',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
// • cli? — exposes the action to `psysonic --player <verb>`. No-arg CLI
|
// • cli? — exposes the action to `psysonic --player <verb>`. No-arg CLI
|
||||||
// verbs are auto-collected and dispatched by
|
// verbs are auto-collected and dispatched by
|
||||||
// `executeCliPlayerCommand`; arg-carrying commands (play-id,
|
// `executeCliPlayerCommand`; arg-carrying commands (play-id,
|
||||||
// seek-relative, set-volume, set-repeat, set-rating-current)
|
// seek-relative, set-volume, volume-relative, set-repeat, set-rating-current)
|
||||||
// are handled explicitly there.
|
// are handled explicitly there.
|
||||||
// • run(ctx) — the handler. `ctx.previewPolicy` ('stop' | 'ignore') decides
|
// • run(ctx) — the handler. `ctx.previewPolicy` ('stop' | 'ignore') decides
|
||||||
// whether an active track-preview is interrupted: media keys
|
// whether an active track-preview is interrupted: media keys
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
const hoisted = vi.hoisted(() => {
|
||||||
|
const player = {
|
||||||
|
volume: 0.5,
|
||||||
|
setVolume: vi.fn((v: number) => {
|
||||||
|
player.volume = v;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
return { player };
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock('@/features/playback/store/playerStore', () => ({
|
||||||
|
usePlayerStore: { getState: () => hoisted.player },
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { executeCliPlayerCommand } from '@/config/shortcutDispatch';
|
||||||
|
|
||||||
|
const navigate = vi.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
hoisted.player.volume = 0.5;
|
||||||
|
hoisted.player.setVolume.mockClear();
|
||||||
|
navigate.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('executeCliPlayerCommand volume-relative', () => {
|
||||||
|
it('raises volume by delta percent and clamps at 1', () => {
|
||||||
|
executeCliPlayerCommand({
|
||||||
|
payload: { command: 'volume-relative', deltaPercent: 10 },
|
||||||
|
navigate,
|
||||||
|
});
|
||||||
|
expect(hoisted.player.setVolume).toHaveBeenCalledWith(0.6);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lowers volume by delta percent and clamps at 0', () => {
|
||||||
|
hoisted.player.volume = 0.03;
|
||||||
|
executeCliPlayerCommand({
|
||||||
|
payload: { command: 'volume-relative', deltaPercent: -10 },
|
||||||
|
navigate,
|
||||||
|
});
|
||||||
|
expect(hoisted.player.setVolume).toHaveBeenCalledWith(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('executeCliPlayerCommand set-volume', () => {
|
||||||
|
it('sets absolute percent', () => {
|
||||||
|
executeCliPlayerCommand({
|
||||||
|
payload: { command: 'set-volume', percent: 40 },
|
||||||
|
navigate,
|
||||||
|
});
|
||||||
|
expect(hoisted.player.setVolume).toHaveBeenCalledWith(0.4);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -76,6 +76,13 @@ export function executeCliPlayerCommand(ctx: CliContext): void | Promise<void> {
|
|||||||
usePlayerStore.getState().setVolume(Math.min(1, Math.max(0, p / 100)));
|
usePlayerStore.getState().setVolume(Math.min(1, Math.max(0, p / 100)));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (command === 'volume-relative') {
|
||||||
|
const delta = Number(ctx.payload.deltaPercent);
|
||||||
|
if (!Number.isFinite(delta)) return;
|
||||||
|
const state = usePlayerStore.getState();
|
||||||
|
state.setVolume(Math.min(1, Math.max(0, state.volume + delta / 100)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (command === 'set-repeat') {
|
if (command === 'set-repeat') {
|
||||||
const modeRaw = typeof ctx.payload.mode === 'string' ? ctx.payload.mode : '';
|
const modeRaw = typeof ctx.payload.mode === 'string' ? ctx.payload.mode : '';
|
||||||
const mode = modeRaw === 'all' ? 'all' : modeRaw === 'one' ? 'one' : 'off';
|
const mode = modeRaw === 'all' ? 'all' : modeRaw === 'one' ? 'one' : 'off';
|
||||||
|
|||||||
Reference in New Issue
Block a user