feat(tracks): add a display-only note shown in --list and over --remote

This commit is contained in:
imSp4rky
2026-10-30 16:17:36 -06:00
parent 5206b3cab6
commit e6a627118f
8 changed files with 40 additions and 6 deletions
+4 -4
View File
@@ -340,7 +340,7 @@ When you give both `season` and `episode`, the server combines them into a `"{se
"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,
"name": null, "track_name": "English", "needs_repack": false, "edition": [], "note": null, "data": null,
"drm": [], "descriptor": "DASH"
}
],
@@ -350,13 +350,13 @@ When you give both `season` and `episode`, the server combines them into a `"{se
"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": [],
"data": null, "drm": [], "descriptor": "DASH"
"note": null, "data": null, "drm": [], "descriptor": "DASH"
}
],
"subtitles": [
{ "id": "s-1", "codec": "vtt", "language": "en",
"forced": false, "sdh": false, "cc": false, "is_original_lang": false,
"name": null, "track_name": "English", "needs_repack": false, "edition": [], "data": null,
"name": null, "track_name": "English", "needs_repack": false, "edition": [], "note": null, "data": null,
"drm": [], "descriptor": "DASH" }
]
}
@@ -384,7 +384,7 @@ The server sorts tracks by bitrate, descending. Episodes the server cannot get g
The `drm` array on a track is a list of `{type, pssh?, kids?, content_keys?, license_url?}` objects.
Every track carries the same fields as a local track: `is_original_lang`, `name`, `needs_repack`, `edition`, and `data`. `name` is the name the service set, or null. `track_name` is the name `--list` shows and the mux writes to an audio or subtitle track: `name` when the service set one, else the name of the language (English, or in the language itself when the server sets `muxing.track_names: native`, or no name when it sets `none`), then the server's `muxing.original_label` on an original-language track, then the SDH, CC, Forced or Descriptive label. `bitrate` is in kb/s, and `bitrate_bps` gives the exact bits/s. `data` holds the part of the service-set `track.data` that fits in JSON, without the `hls`, `dash`, and `ism` parser keys. Audio keeps `is_original` and `atmos` for clients that read them, and adds the exact `joc` object count.
Every track carries the same fields as a local track: `is_original_lang`, `name`, `needs_repack`, `edition`, `note`, and `data`. `name` is the name the service set, or null. `note` is display text the service set, such as an encoding profile, or null: `--list` shows it, but the filename and the mux do not use it. `track_name` is the name `--list` shows and the mux writes to an audio or subtitle track: `name` when the service set one, else the name of the language (English, or in the language itself when the server sets `muxing.track_names: native`, or no name when it sets `none`), then the server's `muxing.original_label` on an original-language track, then the SDH, CC, Forced or Descriptive label. `bitrate` is in kb/s, and `bitrate_bps` gives the exact bits/s. `data` holds the part of the service-set `track.data` that fits in JSON, without the `hls`, `dash`, and `ism` parser keys. Audio keeps `is_original` and `atmos` for clients that read them, and adds the exact `joc` object count.
| Status | Error code | Meaning |
| --- | --- | --- |
@@ -6,7 +6,7 @@ from enum import Enum
import pytest
from unshackle.core.api.handlers import serialize_video_track
from unshackle.core.api.handlers import serialize_audio_track, serialize_subtitle_track, serialize_video_track
from unshackle.core.remote_service import (
RemoteService,
build_title,
@@ -58,6 +58,24 @@ def test_a_dv_track_keeps_its_hdr10_base_label_over_remote() -> None:
assert deserialize_video(serialize_video_track(v)).range_label == "DV+HDR10"
def test_a_track_note_shows_over_remote_and_export_and_is_not_an_edition() -> None:
from unshackle.core.tracks.track import Track
v = Video(url="https://x/v.mp4", language="ja", codec=Video.Codec.AVC, note="h264mpl40-dash")
a = Audio(url="https://x/a.mp4", language="ja", codec=Audio.Codec.AAC, note="heaac-2-dash")
s = Subtitle(url="https://x/s.vtt", language="en", codec=Subtitle.Codec.WebVTT, note="webvtt-lssdh")
copies = [deserialize_video(serialize_video_track(v)), deserialize_audio(serialize_audio_track(a))]
copies += [deserialize_subtitle(serialize_subtitle_track(s))]
copies += [Track.from_dict(t.to_dict()) for t in (v, a, s)]
for copy in copies:
assert copy.note and str(copy).endswith(f"| {copy.note}")
assert copy.edition == []
def test_a_note_that_is_not_text_is_dropped_not_fatal() -> None:
assert deserialize_video({"id": "v1", "codec": "AVC", "width": 1, "height": 1, "note": {"x": 1}}).note is None
def test_deserialize_video_unknown_codec_falls_back_to_none() -> None:
v = deserialize_video({"id": "v2", "codec": "MADE_UP", "width": 0, "height": 0})
assert v.codec is None
+1
View File
@@ -1722,6 +1722,7 @@ def serialize_track_common(track: Any) -> Dict[str, Any]:
"track_name": track.get_track_name(),
"needs_repack": bool(getattr(track, "needs_repack", False)),
"edition": list(getattr(track, "edition", None) or []),
"note": getattr(track, "note", None),
"descriptor": descriptor_name(track),
"drm": serialize_drm(track.drm) if getattr(track, "drm", None) else None,
"drm_preference": getattr(track, "drm_preference", None),
+1
View File
@@ -288,6 +288,7 @@ def base_track_kwargs(data: Dict[str, Any]) -> Dict[str, Any]:
"needs_repack": bool(data.get("needs_repack", False)),
"name": data.get("name"),
"edition": data.get("edition") or None,
"note": data["note"] if isinstance(data.get("note"), str) else None,
"id_": data.get("id"),
}
+1
View File
@@ -214,6 +214,7 @@ class Audio(Track):
f"{self.bitrate // 1000} kb/s" if self.bitrate else None,
self.get_track_name(),
", ".join(self.edition) if self.edition else None,
self.note,
],
)
)
+7 -1
View File
@@ -231,7 +231,13 @@ class Subtitle(Track):
return " | ".join(
filter(
bool,
["SUB", f"[{self.codec.value}]" if self.codec else None, str(self.language), self.get_track_name()],
[
"SUB",
f"[{self.codec.value}]" if self.codec else None,
str(self.language),
self.get_track_name(),
self.note,
],
)
)
+6
View File
@@ -427,6 +427,7 @@ class Track:
name: Optional[str] = None,
drm: Optional[Iterable[DRM_T]] = None,
edition: Optional[str] = None,
note: Optional[str] = None,
session: Optional[Union[Session, "RnetSession"]] = None,
downloader: Optional[Callable] = None,
downloader_args: Optional[dict] = None,
@@ -451,6 +452,8 @@ class Track:
raise TypeError(f"Expected id_ to be a {str}, not {type(id_)}")
if not isinstance(edition, (str, list, type(None))):
raise TypeError(f"Expected edition to be a {str}, {list}, or None, not {type(edition)}")
if not isinstance(note, (str, type(None))):
raise TypeError(f"Expected note to be a {str}, not {type(note)}")
if not isinstance(downloader, (Callable, type(None))):
raise TypeError(f"Expected downloader to be a {Callable}, not {type(downloader)}")
if not isinstance(downloader_args, (dict, type(None))):
@@ -483,6 +486,7 @@ class Track:
self.drm = drm
self._drm_preference: Optional[str] = None
self.edition: list[str] = [edition] if isinstance(edition, str) else (edition or [])
self.note = note
self.session = session
self.downloader = downloader
self.downloader_args = downloader_args
@@ -981,6 +985,7 @@ class Track:
"needs_repack": self.needs_repack,
"name": self.name,
"edition": self.edition,
"note": self.note,
}
return data
@@ -999,6 +1004,7 @@ class Track:
"needs_repack": data.get("needs_repack", False),
"name": data.get("name"),
"edition": data.get("edition") or None,
"note": data.get("note"),
"id_": data.get("id"),
}
+1
View File
@@ -412,6 +412,7 @@ class Video(Track):
)
),
", ".join(self.edition) if self.edition else None,
self.note,
],
)
)