From 4177cb226790f8abd09163490e972deeaf0f7ae0 Mon Sep 17 00:00:00 2001 From: imSp4rky Date: Wed, 19 Aug 2026 14:39:09 -0600 Subject: [PATCH] fix(hls): keep ad-filtered segments out of DRM key state --- docs/dev/service-architecture.md | 3 +++ unshackle/core/downloaders/requests.py | 2 +- unshackle/core/manifests/hls.py | 11 +++++++---- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/dev/service-architecture.md b/docs/dev/service-architecture.md index 36465957..d3fa0f0d 100644 --- a/docs/dev/service-architecture.md +++ b/docs/dev/service-architecture.md @@ -346,6 +346,9 @@ A typical `get_tracks` therefore looks like fetching the manifest text with ### Segment-filtering hooks - **HLS**: set `track.OnSegmentFilter(segment)` to drop unwanted segments (ads/bumpers). + A dropped segment is invisible to the whole download: its `EXT-X-KEY` and its + `EXT-X-DISCONTINUITY` are ignored, so a clear ad break inside encrypted content does + not disturb decryption. - **DASH**: pass a `period_filter` callable to `to_tracks` to drop whole periods. Caching-breadth periods some services emit and non-`content` segment types are dropped automatically. diff --git a/unshackle/core/downloaders/requests.py b/unshackle/core/downloaders/requests.py index 3ad133b6..01012d61 100644 --- a/unshackle/core/downloaders/requests.py +++ b/unshackle/core/downloaders/requests.py @@ -1391,7 +1391,7 @@ def requests( output_dir / filename.format( i=i * index_stride + index_offset, - ext=get_extension(url["url"] if isinstance(url, dict) else url) if needs_ext else "", + ext=(get_extension(url["url"] if isinstance(url, dict) else url) or "") if needs_ext else "", ) ] ] diff --git a/unshackle/core/manifests/hls.py b/unshackle/core/manifests/hls.py index 7f49994a..d46b6966 100644 --- a/unshackle/core/manifests/hls.py +++ b/unshackle/core/manifests/hls.py @@ -789,13 +789,16 @@ class HLS: """ drm = encryption_data[1] first_segment_i = next( - int(file.stem) for file in sorted(segment_save_dir.iterdir()) if file.stem.isdigit() + (int(file.stem) for file in sorted(segment_save_dir.iterdir()) if file.stem.isdigit()), None ) last_segment_i = max(0, i - int(not include_this_segment)) + if first_segment_i is None or first_segment_i > last_segment_i: + return None range_len = (last_segment_i - first_segment_i) + 1 segment_range = f"{str(first_segment_i).zfill(name_len)}-{str(last_segment_i).zfill(name_len)}" - merged_path = segment_save_dir / f"{segment_range}{get_extension(master.segments[last_segment_i].uri)}" + ext = get_extension(wanted_segments[last_segment_i].uri) or "" + merged_path = segment_save_dir / f"{segment_range}{ext}" decrypted_path = segment_save_dir / f"{merged_path.stem}_decrypted{merged_path.suffix}" files = [ @@ -864,7 +867,7 @@ class HLS: if segment not in unwanted_segments: if isinstance(track, Subtitle): - segment_file_ext = get_extension(segment.uri) + segment_file_ext = get_extension(segment.uri) or "" segment_file_path = segment_save_dir / f"{str(i).zfill(name_len)}{segment_file_ext}" segment_data = try_ensure_utf8(segment_file_path.read_bytes()) if track.codec not in (Subtitle.Codec.fVTT, Subtitle.Codec.fTTML): @@ -909,7 +912,7 @@ class HLS: map_data = (segment.init_section, init_content) segment_keys = getattr(segment, "keys", None) - if segment_keys: + if segment_keys and segment not in unwanted_segments: if cdm: cdm_segment_keys = HLS.filter_keys_for_cdm(segment_keys, cdm) key = (