From d12a943d966080883beaf8e2006159a980152e92 Mon Sep 17 00:00:00 2001 From: imSp4rky Date: Wed, 28 Oct 2026 15:34:52 -0600 Subject: [PATCH] fix(api): show filename codec and range labels in codec_display and range_display --- docs/dev/rest-api/endpoints.md | 6 +++--- docs/reference/configuration/output.md | 3 +++ tests/remote/unit/test_handlers_serialize.py | 17 +++++++++++++++++ unshackle/core/api/handlers.py | 9 ++++++--- unshackle/core/constants.py | 9 ++++++++- 5 files changed, 37 insertions(+), 7 deletions(-) diff --git a/docs/dev/rest-api/endpoints.md b/docs/dev/rest-api/endpoints.md index f9f9f52b..ec7ab1cd 100644 --- a/docs/dev/rest-api/endpoints.md +++ b/docs/dev/rest-api/endpoints.md @@ -333,10 +333,10 @@ When you give both `season` and `episode`, the server combines them into a `"{se "title": { "type": "movie", "name": "Example Movie", "id": "12345", "year": 2021 }, "video": [ { - "id": "v-1", "codec": "H265", "codec_display": "HEVC", + "id": "v-1", "codec": "HEVC", "codec_display": "H.265", "bitrate": 8000, "bitrate_bps": 8000000, "width": 3840, "height": 2160, "resolution": "2160p", "fps": "24", "range": "HDR10", - "range_display": "HDR10", "scan_type": null, "closed_captions": [], + "range_display": "HDR", "scan_type": null, "closed_captions": [], "dv_compatible_bitstream": false, "hdr10plus": false, "hdr10_base": false, "language": "en", "is_original_lang": true, "name": null, "track_name": "English", "needs_repack": false, "edition": [], "data": null, @@ -345,7 +345,7 @@ When you give both `season` and `episode`, the server combines them into a `"{se ], "audio": [ { - "id": "a-1", "codec": "EC3", "codec_display": "DD+", + "id": "a-1", "codec": "EC3", "codec_display": "DDP", "bitrate": 640, "bitrate_bps": 640000, "channels": "5.1", "language": "en", "is_original": true, "is_original_lang": true, "atmos": false, "joc": 0, "descriptive": false, "name": null, "track_name": "English", "needs_repack": false, "edition": [], diff --git a/docs/reference/configuration/output.md b/docs/reference/configuration/output.md index 148ac617..a94a6eb1 100644 --- a/docs/reference/configuration/output.md +++ b/docs/reference/configuration/output.md @@ -91,6 +91,9 @@ For `hdr`, unshackle renames each part of a Dolby Vision label separately, so `H changes `DV.HDR10P` to `DV.HDR10+`. When an audio name ends in a digit, `audio_full` puts a separator before the channels: `EAC3.5.1`, not `EAC35.1`. +The REST API fields `codec_display` and `range_display` use the same names and the same map. +The `dl --export` file name does not use `codec_names`. + !!! note "`tag_rules` see the new names" unshackle evaluates [`tag_rules`](#tag_rules) after the rename. With the example above, a rule must match `audio: EAC3`, not `audio: DDP`. diff --git a/tests/remote/unit/test_handlers_serialize.py b/tests/remote/unit/test_handlers_serialize.py index 9b650e17..ffa01627 100644 --- a/tests/remote/unit/test_handlers_serialize.py +++ b/tests/remote/unit/test_handlers_serialize.py @@ -22,6 +22,7 @@ from unshackle.core.api.handlers import ( validate_download_parameters, validate_service, ) +from unshackle.core.config import config from unshackle.core.titles.episode import Episode from unshackle.core.titles.movie import Movie from unshackle.core.tracks import Audio, Subtitle, Video @@ -172,6 +173,22 @@ def test_serialize_video_track_basic() -> None: assert "url" not in d +def test_display_labels_match_the_filename_labels(monkeypatch: pytest.MonkeyPatch) -> None: + """codec_display and range_display use the filename names, after any codec_names override.""" + hevc = video(codec=Video.Codec.HEVC, range_=Video.Range.DV) + hevc.hdr10_base = True + assert serialize_audio_track(audio(codec=Audio.Codec.EC3))["codec_display"] == "DDP" + assert (serialize_video_track(hevc)["codec_display"], serialize_video_track(hevc)["range_display"]) == ( + "H.265", + "DV+HDR", + ) + assert serialize_video_track(video(codec=Video.Codec.VC1))["codec_display"] == "VC-1" + + monkeypatch.setattr(config, "codec_names", {"DDP": "EAC3", "H.265": "HEVC"}) + assert serialize_audio_track(audio(codec=Audio.Codec.EC3))["codec_display"] == "EAC3" + assert serialize_video_track(hevc)["codec_display"] == "HEVC" + + def test_serialize_video_track_include_url() -> None: d = serialize_video_track(video(), include_url=True) assert d["url"] == "https://example.com/v.mpd" diff --git a/unshackle/core/api/handlers.py b/unshackle/core/api/handlers.py index 3d9e426e..9e8a7e9b 100644 --- a/unshackle/core/api/handlers.py +++ b/unshackle/core/api/handlers.py @@ -37,6 +37,7 @@ from unshackle.core.services import Services from unshackle.core.session import RnetSession, utf8_text from unshackle.core.titles import Episode, Movie, Song, Title_T from unshackle.core.titles.music import SONG_FIELDS +from unshackle.core.titles.title import rename from unshackle.core.tracks import Audio, Subtitle, Tracks, Video from unshackle.core.tunnel import HEARTBEAT, MAX_MESSAGE, MAX_TUNNELS, TUNNELS, Tunnel, tunnel_proxy from unshackle.core.utilities import declared_kwargs, kind_only_conflict, original_tag @@ -1734,12 +1735,13 @@ def serialize_track_common(track: Any) -> Dict[str, Any]: def serialize_video_track(track: Video, include_url: bool = False) -> Dict[str, Any]: """Convert video track to JSON-serializable dict.""" codec_name = enum_name(track.codec) + codec_value = track.codec.value if track.codec else codec_name range_name = enum_name(track.range) result = { "id": str(track.id), "codec": codec_name, - "codec_display": VIDEO_CODEC_MAP.get(codec_name, codec_name), + "codec_display": rename(VIDEO_CODEC_MAP.get(codec_name, codec_value)), "bitrate": int(track.bitrate / 1000) if track.bitrate else None, "bitrate_bps": track.bitrate or None, "width": track.width, @@ -1747,7 +1749,7 @@ def serialize_video_track(track: Video, include_url: bool = False) -> Dict[str, "resolution": f"{track.width}x{track.height}" if track.width and track.height else None, "fps": track.fps if track.fps else None, "range": range_name, - "range_display": DYNAMIC_RANGE_MAP.get(track.range_label, track.range_label), + "range_display": "+".join(rename(DYNAMIC_RANGE_MAP.get(p, p)) for p in track.range_label.split("+")), "scan_type": enum_name(track.scan_type) if track.scan_type else None, "closed_captions": list(getattr(track, "closed_captions", None) or []), "dv_compatible_bitstream": bool(getattr(track, "dv_compatible_bitstream", False)), @@ -1779,11 +1781,12 @@ def serialize_audio_track(track: Audio, include_url: bool = False, is_original: track 'orig' would download. """ codec_name = enum_name(track.codec) + codec_value = track.codec.value if track.codec else codec_name result = { "id": str(track.id), "codec": codec_name, - "codec_display": AUDIO_CODEC_MAP.get(codec_name, codec_name), + "codec_display": rename(AUDIO_CODEC_MAP.get(codec_value, codec_value)), "bitrate": int(track.bitrate / 1000) if track.bitrate else None, "bitrate_bps": track.bitrate or None, "channels": track.channels if track.channels else None, diff --git a/unshackle/core/constants.py b/unshackle/core/constants.py index 8e6669c2..4083665d 100644 --- a/unshackle/core/constants.py +++ b/unshackle/core/constants.py @@ -28,7 +28,14 @@ DYNAMIC_RANGE_MAP = { "HDR10 / HDR10+": "HDR10P", "HDR10 / HDR10": "HDR", } -AUDIO_CODEC_MAP = {"E-AC-3": "DDP", "AC-3": "DD", "DTS-UHD": "DTS-X", "DTS LBR": "DTS-LBR", "MPEG-H 3D Audio": "MPEG-H"} +AUDIO_CODEC_MAP = { + "E-AC-3": "DDP", + "DD+": "DDP", # Audio.Codec.EC3's value, which the REST API names a track by + "AC-3": "DD", + "DTS-UHD": "DTS-X", + "DTS LBR": "DTS-LBR", + "MPEG-H 3D Audio": "MPEG-H", +} SPACED_AUDIO_CODECS = {"DTS-X"}