This commit is contained in:
1 parent
6bee9bd9a3
commit
9ddc13e352
2 files changed
+84
-3
No files matched your search
+25
-1
@@ -16,7 +16,7 @@ from urllib.parse import parse_qs, urlparse
|
|||||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
from controller import Controller, TargetError # noqa: E402
|
from controller import Controller, TargetError # noqa: E402
|
||||||
from zidoo import ZidooClient, ZidooError # noqa: E402
|
from zidoo import ZidooClient, ZidooError, short_audio, short_output # noqa: E402
|
||||||
|
|
||||||
PATH = "/storage/disk/Movies/A.Private.War.2018.1080p.mp4"
|
PATH = "/storage/disk/Movies/A.Private.War.2018.1080p.mp4"
|
||||||
PNG = b"\x89PNG\r\n\x1a\n" + b"\0" * 16
|
PNG = b"\x89PNG\r\n\x1a\n" + b"\0" * 16
|
||||||
@@ -112,6 +112,30 @@ class ZidooTest(unittest.TestCase):
|
|||||||
self.assertIsNone(track["poster_id"])
|
self.assertIsNone(track["poster_id"])
|
||||||
self.assertIsNone(track["artist"])
|
self.assertIsNone(track["artist"])
|
||||||
|
|
||||||
|
def test_without_a_year_it_says_how_it_plays(self):
|
||||||
|
FakeZidoo.library = {}
|
||||||
|
FakeZidoo.video["output"] = "3840X2160P @ 23.976Hz YUV422 12Bits SDR"
|
||||||
|
FakeZidoo.video["audioInfo"] = "English Dolby Digital Plus with Dolby Atmos 48.0KHz 768kbps (3/3)"
|
||||||
|
self.assertEqual(self.zidoo.now_playing()["artist"], "2160p 422 12b SDR · DD+ Atmos")
|
||||||
|
|
||||||
|
def test_the_output_shortens_to_height_chroma_bits_and_dynamic_range(self):
|
||||||
|
self.assertEqual(short_output("3840X2160P @ 23.976Hz YCbCr444 12Bits Dolby Vision"), "2160p 444 12b Dolby Vision")
|
||||||
|
self.assertEqual(short_output("3840X2160P @ 60Hz RGB 10Bits HDR10+"), "2160p RGB 10b HDR10+")
|
||||||
|
self.assertEqual(short_output("1920x1080i @ 50Hz"), "1080i")
|
||||||
|
|
||||||
|
def test_the_audio_shortens_to_its_codec(self):
|
||||||
|
# Longest name first: Dolby Digital Plus is not plain Dolby Digital.
|
||||||
|
self.assertEqual(short_audio("English Dolby Digital 48.0KHz 640kbps (1/1)"), "DD")
|
||||||
|
self.assertEqual(short_audio("English TrueHD Atmos 48.0KHz (1/2)"), "TrueHD Atmos")
|
||||||
|
self.assertEqual(short_audio("French DTS-HD MA 7.1 48.0KHz (2/2)"), "DTS-HD MA")
|
||||||
|
# A codec it does not know keeps its words, minus the numbers.
|
||||||
|
self.assertEqual(short_audio("Japanese Vorbis 48.0KHz 192kbps (1/1)"), "Japanese Vorbis")
|
||||||
|
self.assertIsNone(short_audio("Off (0/2)"))
|
||||||
|
|
||||||
|
def test_a_film_keeps_its_year_over_how_it_plays(self):
|
||||||
|
FakeZidoo.video["output"] = "3840X2160P @ 23.976Hz YUV422 12Bits SDR"
|
||||||
|
self.assertEqual(self.zidoo.now_playing()["artist"], "2018")
|
||||||
|
|
||||||
def test_nothing_loaded_is_nothing_playing(self):
|
def test_nothing_loaded_is_nothing_playing(self):
|
||||||
FakeZidoo.video = None
|
FakeZidoo.video = None
|
||||||
self.assertIsNone(self.zidoo.now_playing())
|
self.assertIsNone(self.zidoo.now_playing())
|
||||||
|
|||||||
@@ -12,10 +12,62 @@ an AVR input, not something the rest of the panel depends on.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
|
|
||||||
|
# What the card has room for, out of the Zidoo's own wording: the longest
|
||||||
|
# match wins, so "Dolby Digital Plus" is never read as plain "Dolby Digital".
|
||||||
|
DYNAMIC_RANGES = {"dolby vision": "Dolby Vision", "dovi": "Dolby Vision", "hdr10+": "HDR10+",
|
||||||
|
"hdr10": "HDR10", "hdr": "HDR", "hlg": "HLG", "sdr": "SDR"}
|
||||||
|
AUDIO_CODECS = {"dolby digital plus": "DD+", "e-ac3": "DD+", "eac3": "DD+",
|
||||||
|
"dolby digital": "DD", "ac3": "DD", "truehd": "TrueHD",
|
||||||
|
"dts:x": "DTS:X", "dts-x": "DTS:X", "dts-hd ma": "DTS-HD MA",
|
||||||
|
"dts-hd master audio": "DTS-HD MA", "dts-hd": "DTS-HD", "dts": "DTS",
|
||||||
|
"aac": "AAC", "flac": "FLAC", "lpcm": "PCM", "pcm": "PCM", "opus": "Opus"}
|
||||||
|
|
||||||
|
|
||||||
|
def _find(text, names):
|
||||||
|
"""The short name of the longest of `names` found in `text`, or None."""
|
||||||
|
lowered = text.lower()
|
||||||
|
hits = [key for key in names if re.search(rf"(?<![\w+]){re.escape(key)}(?![\w+])", lowered)]
|
||||||
|
return names[max(hits, key=len)] if hits else None
|
||||||
|
|
||||||
|
|
||||||
|
def short_output(output):
|
||||||
|
""""3840X2160P @ 23.976Hz YUV422 12Bits SDR" -> "2160p 422 12b SDR":
|
||||||
|
the picture's height, its chroma subsampling and bit depth, and its
|
||||||
|
dynamic range. Anything not in that shape is passed on as it is."""
|
||||||
|
if not output:
|
||||||
|
return None
|
||||||
|
size = re.search(r"\d+\s*[xX]\s*(\d+)\s*([PpIi])?", output)
|
||||||
|
if not size:
|
||||||
|
return output
|
||||||
|
height = size.group(1) + (size.group(2) or "p").lower()
|
||||||
|
# YUV422 or YCbCr444 down to the digits, which are what tell them apart;
|
||||||
|
# RGB has no subsampling to give, so it keeps its name.
|
||||||
|
chroma = re.search(r"(?:YUV|YCbCr)\s*(4[24][024])|\b(RGB)\b", output, re.I)
|
||||||
|
chroma = (chroma.group(1) or chroma.group(2).upper()) if chroma else None
|
||||||
|
bits = re.search(r"(\d+)\s*-?\s*bits?\b", output, re.I)
|
||||||
|
bits = f"{bits.group(1)}b" if bits else None
|
||||||
|
return " ".join(filter(None, (height, chroma, bits, _find(output, DYNAMIC_RANGES))))
|
||||||
|
|
||||||
|
|
||||||
|
def short_audio(audio):
|
||||||
|
""""English Dolby Digital Plus with Dolby Atmos 48.0KHz 768kbps (3/3)"
|
||||||
|
-> "DD+ Atmos": the codec, and Atmos or DTS:X's objects on top of it. A
|
||||||
|
codec it does not know keeps the Zidoo's own words, minus the sample
|
||||||
|
rate, bitrate and track count."""
|
||||||
|
if not audio or audio.lower().startswith("off"):
|
||||||
|
return None
|
||||||
|
codec = _find(audio, AUDIO_CODECS)
|
||||||
|
if codec is None:
|
||||||
|
return re.sub(r"\s*(\d[\d.]*\s*k?hz|\d+\s*kbps|\(\d+/\d+\))", "", audio, flags=re.I).strip() or None
|
||||||
|
atmos = "atmos" in audio.lower()
|
||||||
|
return f"{codec} Atmos" if atmos else codec
|
||||||
|
|
||||||
|
|
||||||
class ZidooError(RuntimeError):
|
class ZidooError(RuntimeError):
|
||||||
"""The Zidoo did not do what it was asked -- off, unreachable, or with
|
"""The Zidoo did not do what it was asked -- off, unreachable, or with
|
||||||
nothing loaded to do it to."""
|
nothing loaded to do it to."""
|
||||||
@@ -54,7 +106,7 @@ class ZidooClient:
|
|||||||
"""{"song", "artist", "image"} for whatever video is loaded, in the
|
"""{"song", "artist", "image"} for whatever video is loaded, in the
|
||||||
same shape a room's now-playing card already expects -- or None.
|
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
|
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
|
instead -- or, with no year to give, the video output and audio track. "image" is always None: the poster lives on the Zidoo, which
|
||||||
the phone cannot load over plain http, so "poster_id" names it
|
the phone cannot load over plain http, so "poster_id" names it
|
||||||
instead for app.py to serve. "position_ms" and "duration_ms" come
|
instead for app.py to serve. "position_ms" and "duration_ms" come
|
||||||
along whenever the length is known, as a room's do. "play_state" is
|
along whenever the length is known, as a room's do. "play_state" is
|
||||||
@@ -69,7 +121,12 @@ class ZidooClient:
|
|||||||
return None
|
return None
|
||||||
film = self._film(video.get("path"))
|
film = self._film(video.get("path"))
|
||||||
year = film.get("year")
|
year = film.get("year")
|
||||||
track = {"song": title, "artist": str(year) if year else None, "image": None,
|
# Nothing to say under the title -- an episode, or a file outside the
|
||||||
|
# library -- so say how it is playing: the picture going out to the
|
||||||
|
# TV, then the audio track.
|
||||||
|
playing_as = " · ".join(filter(None, (short_output(video.get("output")),
|
||||||
|
short_audio(video.get("audioInfo")))))
|
||||||
|
track = {"song": title, "artist": str(year) if year else playing_as or None, "image": None,
|
||||||
"poster_id": film.get("id"),
|
"poster_id": film.get("id"),
|
||||||
"play_state": "play" if video.get("status") == 1 else "pause"}
|
"play_state": "play" if video.get("status") == 1 else "pause"}
|
||||||
if video.get("duration"):
|
if video.get("duration"):
|
||||||
|
|||||||
Reference in new issue
Block a user