MPD and Scene fixes + twitch-bot cleanup

This commit is contained in:
Jakub Zych
2026-04-28 12:13:42 +02:00
parent 4c818e327b
commit 32be7e4073
16 changed files with 858 additions and 972 deletions

View File

@@ -12,7 +12,12 @@ needs zero changes:
{ _type, index, total, title, artist, trackTitle, album, year, genre,
file, currentTime, duration, paused,
nextTitle, nextArtist, nextTrackTitle, nextFile, nextTitles,
coverPath, coverHash }
coverPath, coverHash,
audio: { format, samplerate, bits, channels, bitrate } }
File-level audio info comes from MPD `status` (`audio` is "rate:bits:ch",
`bitrate` is kbps) and the file extension (FORMAT label — FLAC/MP3/OPUS/…).
Lossless tracks (FLAC, ALAC, WAV) report their actual bit depth; MP3/Opus
report their decode word size, which is informational only.
Next-track fields are populated when MPD reports `nextsong` in its status.
`nextTitles` is up to 3 display strings peeked from the queue starting at
the current song's successor — used by the Music Box overlay to show
@@ -158,6 +163,72 @@ def _status_volume(status: dict) -> int | None:
return volume if volume >= 0 else None
# Map file extension → display label. Anything not listed falls through to
# the uppercased extension so unusual codecs still render something readable.
_FORMAT_LABELS = {
"flac": "FLAC",
"mp3": "MP3",
"opus": "OPUS",
"ogg": "OGG",
"oga": "OGG",
"m4a": "AAC",
"mp4": "AAC",
"aac": "AAC",
"wav": "WAV",
"wv": "WAVPACK",
"ape": "APE",
"alac": "ALAC",
"webm": "WEBM",
"wma": "WMA",
}
def _detect_format(uri: str | None) -> str | None:
if not uri:
return None
ext = Path(uri).suffix.lower().lstrip(".")
if not ext:
return None
return _FORMAT_LABELS.get(ext, ext.upper())
def _parse_audio(audio_str: str | None) -> tuple[int | None, int | None, int | None]:
"""Decode MPD's `audio` status string `samplerate:bits:channels`.
`bits` may be `f` (float) or `dsd<N>` for DSD streams — return None for
anything we can't render as a plain integer; the overlay just skips it."""
if not audio_str:
return None, None, None
parts = audio_str.split(":")
if len(parts) < 3:
return None, None, None
def _maybe_int(v):
try: return int(v)
except (TypeError, ValueError): return None
return _maybe_int(parts[0]), _maybe_int(parts[1]), _maybe_int(parts[2])
def _audio_info(status: dict, song: dict) -> dict:
sr, bits, ch = _parse_audio(status.get("audio"))
bitrate = None
raw_br = status.get("bitrate")
if raw_br is not None:
try:
bitrate = int(raw_br)
except (TypeError, ValueError):
bitrate = None
# MPD reports 0 kbps for paused/stopped; surface as missing instead
# of a misleading "0 kbps".
if bitrate is not None and bitrate <= 0:
bitrate = None
return {
"format": _detect_format(song.get("file")),
"samplerate": sr,
"bits": bits,
"channels": ch,
"bitrate": bitrate,
}
# Module-level cache: MPD stats barely move between ticks (only on file
# add/remove), so we refresh every STATS_REFRESH_SEC and reuse otherwise.
_stats_cache = {"data": None, "next_refresh": 0.0}
@@ -246,6 +317,7 @@ def mpd_to_event(status: dict, song: dict, next_songs: list | None = None,
"coverHash": cover_hash,
"playback": playback,
"stats": stats,
"audio": _audio_info(status, song),
}
@@ -261,6 +333,7 @@ class SharedState:
"coverPath": None, "coverHash": None,
"playback": {"volume": None, "repeat": False, "random": False, "single": False, "consume": False},
"stats": None,
"audio": {"format": None, "samplerate": None, "bits": None, "channels": None, "bitrate": None},
}
self.changed = asyncio.Event()