diff --git a/apps/cli/.gitignore b/apps/cli/.gitignore index ef42835d..52e7b5b0 100644 --- a/apps/cli/.gitignore +++ b/apps/cli/.gitignore @@ -1,3 +1,4 @@ node_modules/ dist/ result +imports/ diff --git a/apps/cli/src/mpd/commands.ts b/apps/cli/src/mpd/commands.ts index 1aeedc80..c37554d6 100644 --- a/apps/cli/src/mpd/commands.ts +++ b/apps/cli/src/mpd/commands.ts @@ -56,6 +56,14 @@ function parseRange(arg: string | undefined, len: number): [number, number] { // --- status ---------------------------------------------------------------- +// The player volume as a valid MPD integer 0..100. Guards against NaN / out-of +// -range values (which clients like rmpc reject, discarding the whole status). +export function mpdVolume(): number { + const v = Math.round(playerController.volume() * 100); + if (!Number.isFinite(v)) return 0; + return Math.max(0, Math.min(100, v)); +} + // The queue index MPD should treat as "current". While playing/paused it's the // engine's index; when stopped with a session restored on startup it's the // restored index — so a client that connects before the TUI hits play still @@ -86,7 +94,7 @@ const statusHandler: Handler = () => { : "pause"; const lines = [ - kv("volume", Math.round(playerController.volume() * 100)), + kv("volume", mpdVolume()), kv("repeat", repeat ? 1 : 0), kv("random", playerController.isShuffle() ? 1 : 0), kv("single", single ? 1 : 0), @@ -230,9 +238,7 @@ const setVolHandler: Handler = (args) => { return []; }; -const getVolHandler: Handler = () => [ - kv("volume", Math.round(playerController.volume() * 100)), -]; +const getVolHandler: Handler = () => [kv("volume", mpdVolume())]; const volumeHandler: Handler = (args) => { const delta = parseInt(args[0], 10); diff --git a/apps/cli/src/mpd/server.ts b/apps/cli/src/mpd/server.ts index 89d0051a..f472e003 100644 --- a/apps/cli/src/mpd/server.ts +++ b/apps/cli/src/mpd/server.ts @@ -8,7 +8,7 @@ import net from "net"; import { itemUri, MpdDb } from "./db"; import { playerController } from "../tui/player"; import { bus, SUBSYSTEMS, type Subsystem } from "./bus"; -import { commandNames, type Ctx, handlers } from "./commands"; +import { commandNames, type Ctx, handlers, mpdVolume } from "./commands"; import { Ack, ackLine, GREETING, kv, MpdError, tokenize } from "./protocol"; export interface MpdServerOptions { @@ -324,7 +324,7 @@ function snapshot() { state: st?.state ?? "stopped", index: idx, curId: items[idx] ? itemUri(items[idx]) : "", - vol: Math.round(playerController.volume() * 100), + vol: mpdVolume(), shuffle: playerController.isShuffle(), repeat: playerController.repeat(), qlen: items.length, diff --git a/apps/cli/src/tui/player.ts b/apps/cli/src/tui/player.ts index e5490f90..7b4fe32f 100644 --- a/apps/cli/src/tui/player.ts +++ b/apps/cli/src/tui/player.ts @@ -186,7 +186,11 @@ class PlayerController { replaygainClip: boolean; }; }) { - this._volume = s.volume; + // Clamp to 0..1 so a bad persisted value can't produce an out-of-range + // volume downstream (e.g. the MPD 0..100 report). + this._volume = Number.isFinite(s.volume) + ? Math.max(0, Math.min(1, s.volume)) + : 0.9; this._shuffle = s.shuffle; this._repeat = { off: 0, one: 1, all: 2 }[s.repeat] ?? 0; this.sound = { @@ -530,11 +534,21 @@ class PlayerController { return this._volume; } + // Last successful native status, held so a transient throw from the native + // status() call doesn't momentarily read as "stopped / no song". That glitch + // (which can happen while applying a volume/EQ change mid-playback) otherwise + // made external consumers — the MPD server especially — flip to Stopped and + // drop the current track for a tick. + private lastStatus: PlayerStatus | null = null; + status(): PlayerStatus | null { + if (!this.player) return null; try { - return this.player ? this.player.status() : null; + const s = this.player.status(); + if (s) this.lastStatus = s; + return s; } catch { - return null; + return this.lastStatus; // transient native hiccup — hold last known } } @@ -553,6 +567,7 @@ class PlayerController { } this.player = null; this.queueItems = []; + this.lastStatus = null; } }