Fix grouped AVR / room card + add movie info
Deploy HEOS panel / deploy (push) Successful in 25s

This commit is contained in:
2026-09-16 20:36:29 +02:00
parent 69476134f7
commit 532b48ad37
7 changed files with 316 additions and 123 deletions
+66 -12
View File
@@ -12,7 +12,7 @@ an AVR input, not something the rest of the panel depends on.
"""
import json
import urllib.error
import urllib.parse
import urllib.request
@@ -20,20 +20,74 @@ class ZidooClient:
def __init__(self, host, port=9529, timeout=1.5):
self.base_url = f"http://{host}:{port}"
self.timeout = timeout
self._film_for = (None, {}) # (file path, _film()'s answer) last looked up
def _get(self, route, **params):
"""The raw body of a GET, or None if the Zidoo did not answer."""
query = f"?{urllib.parse.urlencode(params)}" if params else ""
try:
with urllib.request.urlopen(f"{self.base_url}/{route}{query}", timeout=self.timeout) as response:
return response.read()
except OSError: # URLError, and a timeout mid-read, which urlopen does not wrap
return None
def _get_json(self, route, **params):
body = self._get(route, **params)
try:
return json.loads(body) if body is not None else None
except ValueError:
return None
def now_playing(self):
"""{"song", "artist", "image"} for whatever video is loaded, in the
same shape a room's now-playing card already expects -- or None."""
try:
with urllib.request.urlopen(
f"{self.base_url}/ZidooVideoPlay/getPlayStatus", timeout=self.timeout
) as response:
payload = json.loads(response.read())
except (urllib.error.URLError, ValueError):
same shape a room's now-playing card already expects -- or None.
A film has no artist, so that line under the title carries its year
instead. "image" is always None: the poster lives on the Zidoo, which
the phone cannot load over plain http, so "poster_id" names it
instead for app.py to serve. "position_ms" and "duration_ms" come
along whenever the length is known, as a room's do."""
payload = self._get_json("ZidooVideoPlay/getPlayStatus")
if not payload or payload.get("status") != 200:
return None
if payload.get("status") != 200:
return None
title = (payload.get("video") or {}).get("title")
video = payload.get("video") or {}
title = video.get("title")
if not title:
return None
return {"song": title, "artist": None, "image": None}
film = self._film(video.get("path"))
year = film.get("year")
track = {"song": title, "artist": str(year) if year else None, "image": None,
"poster_id": film.get("id")}
if video.get("duration"):
track["position_ms"] = video.get("currentPosition") or 0
track["duration_ms"] = video["duration"]
return track
def _film(self, path):
"""The poster wall's entry for the film a file belongs to -- its
"id" (which is also its poster's) and "year" among the rest -- or {}
when the file is not in the library. Looked up once per file rather
than on every poll: the answer comes wrapped in the film's whole cast
and crew, and does not change halfway through it."""
if not path:
return {}
if self._film_for[0] != path:
payload = self._get_json("ZidooPoster/v2/getAggregationOfFile", path=path)
if payload is None:
return {} # unreachable for now -- ask again next poll
# "type" names the key holding the item itself: "movie" for a
# film. Anything else without one simply gets no poster or year.
self._film_for = (path, payload.get(payload.get("type")) or {})
return self._film_for[1]
def poster(self, poster_id, width=200, height=300):
"""(bytes, mimetype) for a film's poster, or None. The Zidoo sends
no Content-Type, and answers an id it has no poster for with a JSON
error instead, so the image is told apart by its first bytes."""
body = self._get("ZidooPoster/getFile/getPoster", id=poster_id, w=width, h=height)
if body is None:
return None
if body.startswith(b"\x89PNG"):
return body, "image/png"
if body.startswith(b"\xff\xd8"):
return body, "image/jpeg"
return None