Spotify: read the whole playlist so one wrong song shows WRONG PLAYLIST; refresh hourly and on unknown songs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-13 10:23:15 -04:00
co-authored by Claude Opus 5.5
parent 9a108b2508
commit 38b6d1b014
2 changed files with 22 additions and 10 deletions
+1 -1
View File
@@ -120,7 +120,7 @@ The **page number key** on every page follows the ALL button. It's green when ev
| RESI | decoder (RossTalk connection and ping, 10.1.10.203); **encoder** (ping and its web page, 10.1.10.202, LAOEncoder); resi-studio-cues; Resi cloud reachable. Only shows they are on the network, not that the encoder is streaming. | | RESI | decoder (RossTalk connection and ping, 10.1.10.203); **encoder** (ping and its web page, 10.1.10.202, LAOEncoder); resi-studio-cues; Resi cloud reachable. Only shows they are on the network, not that the encoder is streaming. |
| NET | NA Ingest's link; gateway 10.1.0.1 pings; **4+ connections from 3+ groups dropping within 10 s** (what happened Sunday 10/5 at 10:21). A blip **flashes amber** for 5 minutes, then goes by itself; press NET to stop the flashing, hold NET 2 s to clear it now. | | NET | NA Ingest's link; gateway 10.1.0.1 pings; **4+ connections from 3+ groups dropping within 10 s** (what happened Sunday 10/5 at 10:21). A blip **flashes amber** for 5 minutes, then goes by itself; press NET to stop the flashing, hold NET 2 s to clear it now. |
| INGEST | the bridge (makes ProPresenter's stage screen count down to the next Resi cue), lobby captions server and caption screen, **ProdCom** (transcription, answering on :24480), ProdDash (running and its page on :24500); free disk | | INGEST | the bridge (makes ProPresenter's stage screen count down to the next Resi cue), lobby captions server and caption screen, **ProdCom** (transcription, answering on :24480), ProdDash (running and its page on :24500); free disk |
| SPOTIFY | Spotify on NA Ingest must be playing the **Christian EDM 2026** playlist (`spotify:playlist:2uTpJVYQLFnKF1WnSOuco8`): **SPOTIFY CLOSED**, **NOT PLAYING**, or **WRONG PLAYLIST** (the song list comes from Spotify's public playlist page, refreshed every 6 h; it shows the first 100 of the playlist's songs, so the alarm needs 3 songs in a row that are not on it). Hold 2 s (and Sunday auto-fix) plays the playlist, opening Spotify first if needed. | | SPOTIFY | Spotify on NA Ingest must be playing the **Christian EDM 2026** playlist (`spotify:playlist:2uTpJVYQLFnKF1WnSOuco8`): **SPOTIFY CLOSED**, **NOT PLAYING**, or **WRONG PLAYLIST** (it reads all of the playlist's songs from Spotify every hour, and again whenever a song plays that isn't on the list, since the playlist keeps getting new songs). Hold 2 s (and Sunday auto-fix) plays the playlist, opening Spotify first if needed. |
| RF (group) | Shure connections; battery ≤1 bar red, 2 bars amber; RF interference (only while that transmitter is on); PSM RF muted; each receiver pinged every 3 s ("AXIENT 1 NO PING" in about 6–9 s; Companion's Shure modules take 18–27 s to notice). The address is the one set on its Companion connection, read from Companion's settings every minute, so changing it there is enough; `"host"` in `config.json` is only the fallback. | | RF (group) | Shure connections; battery ≤1 bar red, 2 bars amber; RF interference (only while that transmitter is on); PSM RF muted; each receiver pinged every 3 s ("AXIENT 1 NO PING" in about 6–9 s; Companion's Shure modules take 18–27 s to notice). The address is the one set on its Companion connection, read from Companion's settings every minute, so changing it there is enough; `"host"` in `config.json` is only the fallback. |
A problem has to last 3 seconds before it shows, so one bad reading doesn't trigger an alarm. Recovery shows immediately. A problem has to last 3 seconds before it shows, so one bad reading doesn't trigger an alarm. Recovery shows immediately.
+21 -9
View File
@@ -1595,29 +1595,39 @@ SPOTIFY_STATE = ('tell application "Spotify" to return (player state as text) &
def playlist_tracks(playlist_uri): def playlist_tracks(playlist_uri):
"""(set of track uris, complete?) from Spotify's public pages: the embed preview lists up to """(set of track uris, complete?) for a public playlist. The embed preview page lists the first
100 songs, the playlist page says how many there are.""" 100 songs and carries an anonymous token, which reads the whole list from Spotify's playlist service."""
pid = playlist_uri.split(":")[-1] pid = playlist_uri.split(":")[-1]
req = lambda url: urllib.request.urlopen(urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}), req = lambda url, **h: urllib.request.urlopen(urllib.request.Request(url, headers=dict({"User-Agent": "Mozilla/5.0"}, **h)),
timeout=10, context=_UNVERIFIED).read().decode("utf-8", "replace") timeout=10, context=_UNVERIFIED).read().decode("utf-8", "replace")
m = re.search(r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', m = re.search(r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>',
req("https://open.spotify.com/embed/playlist/" + pid)) req("https://open.spotify.com/embed/playlist/" + pid))
entity = json.loads(m.group(1))["props"]["pageProps"]["state"]["data"]["entity"] state = json.loads(m.group(1))["props"]["pageProps"]["state"]
tracks = {t["uri"] for t in entity.get("trackList", []) if t.get("uri")} tracks = {t["uri"] for t in state["data"]["entity"].get("trackList", []) if t.get("uri")}
n = re.search(r'music:song_count" content="(\d+)"', req("https://open.spotify.com/playlist/" + pid)) try:
return tracks, bool(n) and len(tracks) >= int(n.group(1)) token = state["settings"]["session"]["accessToken"]
full = json.loads(req("https://spclient.wg.spotify.com/playlist/v2/playlist/%s?from=0&length=1000" % pid,
Authorization="Bearer " + token, Accept="application/json"))
items = {i["uri"] for i in full.get("contents", {}).get("items", []) if i.get("uri")}
if items and len(items) >= int(full.get("length") or 0):
return items, True
except (OSError, ValueError, KeyError, TypeError) as e:
log.warning("Spotify full playlist: %s (using the first %d songs)", e, len(tracks))
return tracks, False
def poll_spotify(snap, cfg): def poll_spotify(snap, cfg):
checks = [c for t in cfg["tiles"] for c in t.get("checks", []) if c["type"] == "spotify"] checks = [c for t in cfg["tiles"] for c in t.get("checks", []) if c["type"] == "spotify"]
if not checks: if not checks:
return return
if time.time() - snap.spotify_tracks_time > 6 * 3600: # the playlist changes weekly def refresh():
try: try:
snap.spotify_tracks, snap.spotify_complete = playlist_tracks(checks[0]["playlist"]) snap.spotify_tracks, snap.spotify_complete = playlist_tracks(checks[0]["playlist"])
except (OSError, ValueError, KeyError, AttributeError, TypeError) as e: except (OSError, ValueError, KeyError, AttributeError, TypeError) as e:
log.warning("Spotify playlist list: %s", e) log.warning("Spotify playlist list: %s", e)
snap.spotify_tracks_time = time.time() snap.spotify_tracks_time = time.time()
if time.time() - snap.spotify_tracks_time > 3600:
refresh()
# Never "tell" Spotify unless it is running: that would open it. # Never "tell" Spotify unless it is running: that would open it.
running = subprocess.run(["/usr/bin/pgrep", "-xq", "Spotify"], timeout=5).returncode == 0 running = subprocess.run(["/usr/bin/pgrep", "-xq", "Spotify"], timeout=5).returncode == 0
if not running: if not running:
@@ -1630,6 +1640,8 @@ def poll_spotify(snap, cfg):
parts = (r.stdout.strip().split("|") + ["", "", "", ""])[:4] parts = (r.stdout.strip().split("|") + ["", "", "", ""])[:4]
snap.spotify = {"running": True, "state": parts[0], "track": parts[1], "name": parts[2], "artist": parts[3]} snap.spotify = {"running": True, "state": parts[0], "track": parts[1], "name": parts[2], "artist": parts[3]}
if parts[1] and (not snap.spotify_recent or snap.spotify_recent[-1] != parts[1]): if parts[1] and (not snap.spotify_recent or snap.spotify_recent[-1] != parts[1]):
if parts[1] not in snap.spotify_tracks and time.time() - snap.spotify_tracks_time > 60:
refresh() # a song just added to the playlist?
snap.spotify_recent.append(parts[1]) snap.spotify_recent.append(parts[1])