fix(dash): pass period_filter to n_m3u8dl_re via filtered MPD file

The period_filter in DASH.to_tracks() only affected track listing but had no effect on n_m3u8dl_re downloads, which re-parsed the raw MPD and downloaded all periods including ads/pre-rolls. This caused DRM decryption failures and corrupted video output.
When periods are filtered during to_tracks(), write a filtered MPD (with rejected periods removed) to a temp file and pass it to n_m3u8dl_re via track.from_file.

Closes #51
This commit is contained in:
Andy
2026-03-01 13:18:27 -07:00
parent 2f7a3d6d1d
commit d1e6d0812c

View File

@@ -7,7 +7,7 @@ import math
import re import re
import shutil import shutil
import sys import sys
from copy import copy from copy import copy, deepcopy
from functools import partial from functools import partial
from pathlib import Path from pathlib import Path
from typing import Any, Callable, Optional, Union from typing import Any, Callable, Optional, Union
@@ -18,6 +18,7 @@ from zlib import crc32
import requests import requests
from curl_cffi.requests import Session as CurlSession from curl_cffi.requests import Session as CurlSession
from langcodes import Language, tag_is_valid from langcodes import Language, tag_is_valid
from lxml import etree
from lxml.etree import Element, ElementTree from lxml.etree import Element, ElementTree
from pyplayready.system.pssh import PSSH as PR_PSSH from pyplayready.system.pssh import PSSH as PR_PSSH
from pywidevine.cdm import Cdm as WidevineCdm from pywidevine.cdm import Cdm as WidevineCdm
@@ -101,14 +102,22 @@ class DASH:
""" """
tracks = Tracks() tracks = Tracks()
filtered_period_ids: list[str] = []
for period in self.manifest.findall("Period"): for period in self.manifest.findall("Period"):
if callable(period_filter) and period_filter(period): if callable(period_filter) and period_filter(period):
if period_id := period.get("id"):
filtered_period_ids.append(period_id)
continue continue
if next(iter(period.xpath("SegmentType/@value")), "content") != "content": if next(iter(period.xpath("SegmentType/@value")), "content") != "content":
if period_id := period.get("id"):
filtered_period_ids.append(period_id)
continue continue
if "urn:amazon:primevideo:cachingBreadth" in [ if "urn:amazon:primevideo:cachingBreadth" in [
x.get("schemeIdUri") for x in period.findall("SupplementalProperty") x.get("schemeIdUri") for x in period.findall("SupplementalProperty")
]: ]:
if period_id := period.get("id"):
filtered_period_ids.append(period_id)
continue continue
for adaptation_set in period.findall("AdaptationSet"): for adaptation_set in period.findall("AdaptationSet"):
@@ -235,6 +244,7 @@ class DASH:
"period": period, "period": period,
"adaptation_set": adaptation_set, "adaptation_set": adaptation_set,
"representation": rep, "representation": rep,
"filtered_period_ids": filtered_period_ids,
} }
}, },
**track_args, **track_args,
@@ -541,6 +551,26 @@ class DASH:
skip_merge = False skip_merge = False
if downloader.__name__ == "n_m3u8dl_re": if downloader.__name__ == "n_m3u8dl_re":
skip_merge = True skip_merge = True
# When periods were filtered out during to_tracks(), n_m3u8dl_re will re-parse
# the raw MPD and download ALL periods (including ads/pre-rolls). Write a filtered
# MPD with the rejected periods removed so n_m3u8dl_re downloads the correct content.
filtered_period_ids = track.data.get("dash", {}).get("filtered_period_ids", [])
if filtered_period_ids:
filtered_manifest = deepcopy(manifest)
for child in list(filtered_manifest):
if not hasattr(child.tag, "find"):
continue
if child.tag == "Period" and child.get("id") in filtered_period_ids:
filtered_manifest.remove(child)
filtered_mpd_path = save_dir / f".{track.id}_filtered.mpd"
filtered_mpd_path.parent.mkdir(parents=True, exist_ok=True)
etree.ElementTree(filtered_manifest).write(
str(filtered_mpd_path), xml_declaration=True, encoding="utf-8"
)
track.from_file = filtered_mpd_path
downloader_args.update( downloader_args.update(
{ {
"filename": track.id, "filename": track.id,
@@ -578,6 +608,11 @@ class DASH:
status_update["downloaded"] = f"DASH {downloaded}" status_update["downloaded"] = f"DASH {downloaded}"
progress(**status_update) progress(**status_update)
# Clean up filtered MPD temp file before enumerating segments
filtered_mpd_path = save_dir / f".{track.id}_filtered.mpd"
if filtered_mpd_path.exists():
filtered_mpd_path.unlink()
# see https://github.com/devine-dl/devine/issues/71 # see https://github.com/devine-dl/devine/issues/71
for control_file in save_dir.glob("*.aria2__temp"): for control_file in save_dir.glob("*.aria2__temp"):
control_file.unlink() control_file.unlink()