fix(api): show filename codec and range labels in codec_display and range_display

This commit is contained in:
imSp4rky
2026-10-28 15:34:52 -06:00
parent b0c006d7f6
commit d12a943d96
5 changed files with 37 additions and 7 deletions
+3 -3
View File
@@ -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 }, "title": { "type": "movie", "name": "Example Movie", "id": "12345", "year": 2021 },
"video": [ "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, "bitrate": 8000, "bitrate_bps": 8000000, "width": 3840, "height": 2160,
"resolution": "2160p", "fps": "24", "range": "HDR10", "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", "dv_compatible_bitstream": false, "hdr10plus": false, "hdr10_base": false, "language": "en",
"is_original_lang": true, "is_original_lang": true,
"name": null, "track_name": "English", "needs_repack": false, "edition": [], "data": null, "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": [ "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", "bitrate": 640, "bitrate_bps": 640000, "channels": "5.1", "language": "en",
"is_original": true, "is_original_lang": true, "atmos": false, "joc": 0, "is_original": true, "is_original_lang": true, "atmos": false, "joc": 0,
"descriptive": false, "name": null, "track_name": "English", "needs_repack": false, "edition": [], "descriptive": false, "name": null, "track_name": "English", "needs_repack": false, "edition": [],
+3
View File
@@ -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 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`. 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" !!! note "`tag_rules` see the new names"
unshackle evaluates [`tag_rules`](#tag_rules) after the rename. With the example above, a unshackle evaluates [`tag_rules`](#tag_rules) after the rename. With the example above, a
rule must match `audio: EAC3`, not `audio: DDP`. rule must match `audio: EAC3`, not `audio: DDP`.
@@ -22,6 +22,7 @@ from unshackle.core.api.handlers import (
validate_download_parameters, validate_download_parameters,
validate_service, validate_service,
) )
from unshackle.core.config import config
from unshackle.core.titles.episode import Episode from unshackle.core.titles.episode import Episode
from unshackle.core.titles.movie import Movie from unshackle.core.titles.movie import Movie
from unshackle.core.tracks import Audio, Subtitle, Video from unshackle.core.tracks import Audio, Subtitle, Video
@@ -172,6 +173,22 @@ def test_serialize_video_track_basic() -> None:
assert "url" not in d 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: def test_serialize_video_track_include_url() -> None:
d = serialize_video_track(video(), include_url=True) d = serialize_video_track(video(), include_url=True)
assert d["url"] == "https://example.com/v.mpd" assert d["url"] == "https://example.com/v.mpd"
+6 -3
View File
@@ -37,6 +37,7 @@ from unshackle.core.services import Services
from unshackle.core.session import RnetSession, utf8_text from unshackle.core.session import RnetSession, utf8_text
from unshackle.core.titles import Episode, Movie, Song, Title_T from unshackle.core.titles import Episode, Movie, Song, Title_T
from unshackle.core.titles.music import SONG_FIELDS 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.tracks import Audio, Subtitle, Tracks, Video
from unshackle.core.tunnel import HEARTBEAT, MAX_MESSAGE, MAX_TUNNELS, TUNNELS, Tunnel, tunnel_proxy 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 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]: def serialize_video_track(track: Video, include_url: bool = False) -> Dict[str, Any]:
"""Convert video track to JSON-serializable dict.""" """Convert video track to JSON-serializable dict."""
codec_name = enum_name(track.codec) codec_name = enum_name(track.codec)
codec_value = track.codec.value if track.codec else codec_name
range_name = enum_name(track.range) range_name = enum_name(track.range)
result = { result = {
"id": str(track.id), "id": str(track.id),
"codec": codec_name, "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": int(track.bitrate / 1000) if track.bitrate else None,
"bitrate_bps": track.bitrate or None, "bitrate_bps": track.bitrate or None,
"width": track.width, "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, "resolution": f"{track.width}x{track.height}" if track.width and track.height else None,
"fps": track.fps if track.fps else None, "fps": track.fps if track.fps else None,
"range": range_name, "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, "scan_type": enum_name(track.scan_type) if track.scan_type else None,
"closed_captions": list(getattr(track, "closed_captions", None) or []), "closed_captions": list(getattr(track, "closed_captions", None) or []),
"dv_compatible_bitstream": bool(getattr(track, "dv_compatible_bitstream", False)), "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. track 'orig' would download.
""" """
codec_name = enum_name(track.codec) codec_name = enum_name(track.codec)
codec_value = track.codec.value if track.codec else codec_name
result = { result = {
"id": str(track.id), "id": str(track.id),
"codec": codec_name, "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": int(track.bitrate / 1000) if track.bitrate else None,
"bitrate_bps": track.bitrate or None, "bitrate_bps": track.bitrate or None,
"channels": track.channels if track.channels else None, "channels": track.channels if track.channels else None,
+8 -1
View File
@@ -28,7 +28,14 @@ DYNAMIC_RANGE_MAP = {
"HDR10 / HDR10+": "HDR10P", "HDR10 / HDR10+": "HDR10P",
"HDR10 / HDR10": "HDR", "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"} SPACED_AUDIO_CODECS = {"DTS-X"}