Bridge: extend mpd:state with nextTitles[] for queue peek
Adds nextTitles (array of up to 3 display strings) plus mirrored single nextTitle/nextArtist/nextTrackTitle/nextFile for the immediate next track. Drives the Music Box overlays' 3-up "coming next" lines without overlays needing to query MPD themselves. Reads MPD status.nextsong, walks playlistinfo for the next 3 indices, applies the existing _flatten_tag + display-string cascade. Fields are None / [] at end of queue. Backwards-compatible: overlays that don't read the new keys are unaffected.
This commit is contained in:
@@ -9,7 +9,13 @@ the custom music/index.html browser daemon as the source of truth.
|
||||
|
||||
Payload shape matches the original daemon's broadcastState() so the overlay
|
||||
needs zero changes:
|
||||
{ _type, index, total, title, file, currentTime, duration, paused }
|
||||
{ _type, index, total, title, file, currentTime, duration, paused,
|
||||
nextTitle, nextArtist, nextTrackTitle, nextFile, nextTitles }
|
||||
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
|
||||
"coming up" lines. The single `nextTitle`/`nextArtist`/etc. fields mirror
|
||||
nextTitles[0] for overlays that only need one.
|
||||
|
||||
Reads OBS WS credentials from ../vendor/obs-config.js (auto-generated by
|
||||
scripts/setup.sh from plugin_config/obs-websocket/config.json — single source of truth).
|
||||
@@ -74,18 +80,12 @@ def _flatten_tag(v):
|
||||
return v or None
|
||||
|
||||
|
||||
def mpd_to_event(status: dict, song: dict) -> dict:
|
||||
"""Translate `status` + `currentsong` into the overlay's expected payload.
|
||||
|
||||
Display string in `title` is built `Artist - Title` from ID3/Vorbis tags
|
||||
when both are present, then degrades through tag-only → filename stem
|
||||
(which is already "Artist - Title" for the NCS Directory naming convention)
|
||||
→ None. The separate `artist` field is emitted for future overlay uses
|
||||
that want to style artist + title differently."""
|
||||
paused = status.get("state", "stop") != "play"
|
||||
elapsed = float(status.get("elapsed") or 0)
|
||||
duration = float(status.get("duration") or song.get("duration") or 0)
|
||||
def _song_display(song: dict) -> tuple:
|
||||
"""Extract (display, artist, track_title) from an MPD song dict.
|
||||
|
||||
Display string is built `Artist - Title` from ID3/Vorbis tags when both
|
||||
are present, then degrades through tag-only → filename stem (already
|
||||
"Artist - Title" for the NCS Directory naming convention) → None."""
|
||||
artist = _flatten_tag(song.get("artist") or song.get("albumartist"))
|
||||
track_name = _flatten_tag(song.get("title"))
|
||||
|
||||
@@ -98,6 +98,30 @@ def mpd_to_event(status: dict, song: dict) -> dict:
|
||||
else:
|
||||
display = None
|
||||
|
||||
return display, artist, track_name
|
||||
|
||||
|
||||
def mpd_to_event(status: dict, song: dict, next_songs: list | None = None) -> dict:
|
||||
"""Translate `status` + `currentsong` (+ optional `next_songs`, the next
|
||||
up to 3 queued tracks) into the overlay payload. The single `nextTitle`
|
||||
family mirrors `nextTitles[0]` for overlays that only need one."""
|
||||
paused = status.get("state", "stop") != "play"
|
||||
elapsed = float(status.get("elapsed") or 0)
|
||||
duration = float(status.get("duration") or song.get("duration") or 0)
|
||||
|
||||
display, artist, track_name = _song_display(song)
|
||||
|
||||
next_titles = []
|
||||
next_display, next_artist, next_track, next_file = None, None, None, None
|
||||
if next_songs:
|
||||
for ns in next_songs:
|
||||
d, a, t = _song_display(ns)
|
||||
if d is not None:
|
||||
next_titles.append(d)
|
||||
if next_songs[0]:
|
||||
next_display, next_artist, next_track = _song_display(next_songs[0])
|
||||
next_file = next_songs[0].get("file")
|
||||
|
||||
return {
|
||||
"index": int(status["song"]) if "song" in status else -1,
|
||||
"total": int(status.get("playlistlength") or 0),
|
||||
@@ -108,6 +132,11 @@ def mpd_to_event(status: dict, song: dict) -> dict:
|
||||
"currentTime": elapsed,
|
||||
"duration": duration,
|
||||
"paused": paused,
|
||||
"nextTitle": next_display,
|
||||
"nextArtist": next_artist,
|
||||
"nextTrackTitle": next_track,
|
||||
"nextFile": next_file,
|
||||
"nextTitles": next_titles,
|
||||
}
|
||||
|
||||
|
||||
@@ -117,6 +146,8 @@ class SharedState:
|
||||
"index": -1, "total": 0,
|
||||
"title": None, "artist": None, "trackTitle": None, "file": None,
|
||||
"currentTime": 0, "duration": 0, "paused": True,
|
||||
"nextTitle": None, "nextArtist": None, "nextTrackTitle": None, "nextFile": None,
|
||||
"nextTitles": [],
|
||||
}
|
||||
self.changed = asyncio.Event()
|
||||
|
||||
@@ -134,7 +165,21 @@ async def mpd_loop(state: SharedState):
|
||||
while True:
|
||||
status = await client.status()
|
||||
song = await client.currentsong()
|
||||
state.data = mpd_to_event(status, song)
|
||||
# Peek the next up-to-3 tracks. MPD's `nextsong` is the index
|
||||
# of the immediate successor; we walk forward from there until
|
||||
# we hit the queue end or 3 entries, whichever comes first.
|
||||
next_songs = []
|
||||
if "nextsong" in status:
|
||||
start = int(status["nextsong"])
|
||||
total = int(status.get("playlistlength") or 0)
|
||||
for i in range(start, min(start + 3, total)):
|
||||
try:
|
||||
rows = await client.playlistinfo(i)
|
||||
if rows:
|
||||
next_songs.append(rows[0])
|
||||
except Exception:
|
||||
break # transient — let it retry next tick
|
||||
state.data = mpd_to_event(status, song, next_songs)
|
||||
state.changed.set()
|
||||
await asyncio.sleep(TICK_SECONDS)
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user