fix(manifests): skip filtered dash periods after the main content and keep them over export

A manifest snapshot module now saves and restores DASH/ISM parser state for --remote and export/import, so an import keeps the service's period filter without fetching the manifest.
This commit is contained in:
imSp4rky
2026-11-02 20:41:27 -07:00
parent eeff83b820
commit 842ece3802
13 changed files with 588 additions and 338 deletions
@@ -7,6 +7,7 @@ from enum import Enum
import pytest
from unshackle.core.api.handlers import serialize_audio_track, serialize_subtitle_track, serialize_video_track
from unshackle.core.manifests.snapshot import match_track
from unshackle.core.remote_service import (
RemoteService,
build_title,
@@ -15,7 +16,6 @@ from unshackle.core.remote_service import (
deserialize_subtitle,
deserialize_video,
enum_get,
match_track,
reconstruct_drm,
)
from unshackle.core.titles.episode import Episode
@@ -524,7 +524,7 @@ def test_resolve_manifest_data_matches_across_per_range_manifests() -> None:
from tests.core.test_ism_init import VIDEO_HEVC_PQ_CPD, VIDEO_HEVC_SDR_CPD
from tests.core.test_ism_range import manifest_xml
from unshackle.core.manifests import ISM
from unshackle.core.remote_service import resolve_manifest_data
from unshackle.core.manifests.snapshot import restore
urls = {"sdr": "https://x/sdr/manifest", "hdr": "https://x/hdr/manifest"}
xml = {"sdr": manifest_xml(VIDEO_HEVC_SDR_CPD), "hdr": manifest_xml(VIDEO_HEVC_PQ_CPD)}
@@ -549,7 +549,7 @@ def test_resolve_manifest_data_matches_across_per_range_manifests() -> None:
for k in ("sdr", "hdr")
]
resolve_manifest_data(tracks, manifests)
restore(tracks, {"manifests": manifests})
assert remote_hdr.data["ism"]["quality_level"].get("CodecPrivateData") == VIDEO_HEVC_PQ_CPD
@@ -680,9 +680,8 @@ def test_no_dl_params_when_the_ctx_has_no_selection() -> None:
def test_resolve_manifest_data_keeps_the_periods_the_server_filtered() -> None:
"""A service drops periods (a mid-roll ad) on the server only. The client must skip the
same periods after it re-parses the manifest, or a remote download joins them back in."""
from unshackle.core.api.handlers import extract_manifests
from unshackle.core.manifests import DASH
from unshackle.core.remote_service import resolve_manifest_data
from unshackle.core.manifests.snapshot import capture, restore
periods = "".join(
f"""<Period id="{pid}" duration="PT1S"><AdaptationSet contentType="video" mimeType="video/mp4" lang="en">
@@ -700,6 +699,6 @@ def test_resolve_manifest_data_keeps_the_periods_the_server_filtered() -> None:
remote = deserialize_video(serialize_video_track(server.videos[0]))
tracks = build_tracks({"video": [], "audio": [], "subtitles": []})
tracks.videos.append(remote)
resolve_manifest_data(tracks, extract_manifests(server))
restore(tracks, capture(server))
assert remote.data["dash"]["filtered_period_ids"] == ["ad1"]
@@ -6,8 +6,7 @@ import pytest
from langcodes import Language
from lxml import etree
from unshackle.core.api.handlers import extract_track_manifests
from unshackle.core.remote_service import resolve_track_manifests
from unshackle.core.manifests.snapshot import capture, restore
from unshackle.core.tracks import Subtitle, Tracks
pytestmark = pytest.mark.unit
@@ -53,8 +52,8 @@ def test_detached_adaptation_set_round_trips() -> None:
)
tracks = Tracks([track])
fragments = extract_track_manifests(tracks)
assert len(fragments) == 1
snapshot = capture(tracks)
assert len(snapshot["track_manifests"]) == 1
client_track = Subtitle(
id_="abc123",
@@ -63,7 +62,7 @@ def test_detached_adaptation_set_round_trips() -> None:
language=Language.get("en"),
descriptor=Subtitle.Descriptor.URL,
)
resolve_track_manifests(Tracks([client_track]), fragments)
restore(Tracks([client_track]), snapshot)
dash = client_track.data["dash"]
assert dash["manifest"].find("BaseURL").text == "https://cdn.example/base/"
@@ -101,7 +100,7 @@ def test_single_track_manifest_round_trips_by_id() -> None:
sdh=True,
descriptor=Subtitle.Descriptor.URL,
)
resolve_track_manifests(Tracks([client_track]), extract_track_manifests(Tracks([track])))
restore(Tracks([client_track]), capture(Tracks([track])))
assert client_track.descriptor == Subtitle.Descriptor.DASH
assert client_track.data["dash"]["representation_id"] == "sub-0"
@@ -127,7 +126,7 @@ def test_in_tree_adaptation_set_is_not_shipped() -> None:
}
},
)
assert extract_track_manifests(Tracks([track])) == []
assert capture(Tracks([track]))["track_manifests"] == []
def test_fragment_keeps_only_the_track_representation() -> None:
@@ -153,7 +152,7 @@ def test_fragment_keeps_only_the_track_representation() -> None:
language=Language.get("en"),
descriptor=Subtitle.Descriptor.URL,
)
resolve_track_manifests(Tracks([client_track]), extract_track_manifests(Tracks([track])))
restore(Tracks([client_track]), capture(Tracks([track])))
dash = client_track.data["dash"]
assert dash["representation_id"] == "sub-1"