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 shutil
import sys
from copy import copy
from copy import copy, deepcopy
from functools import partial
from pathlib import Path
from typing import Any, Callable, Optional, Union
@@ -18,6 +18,7 @@ from zlib import crc32
import requests
from curl_cffi.requests import Session as CurlSession
from langcodes import Language, tag_is_valid
from lxml import etree
from lxml.etree import Element, ElementTree
from pyplayready.system.pssh import PSSH as PR_PSSH
from pywidevine.cdm import Cdm as WidevineCdm
@@ -101,14 +102,22 @@ class DASH:
"""
tracks = Tracks()
filtered_period_ids: list[str] = []
for period in self.manifest.findall("Period"):
if callable(period_filter) and period_filter(period):
if period_id := period.get("id"):
filtered_period_ids.append(period_id)
continue
if next(iter(period.xpath("SegmentType/@value")), "content") != "content":
if period_id := period.get("id"):
filtered_period_ids.append(period_id)
continue
if "urn:amazon:primevideo:cachingBreadth" in [
x.get("schemeIdUri") for x in period.findall("SupplementalProperty")
]:
if period_id := period.get("id"):
filtered_period_ids.append(period_id)
continue
for adaptation_set in period.findall("AdaptationSet"):
@@ -235,6 +244,7 @@ class DASH:
"period": period,
"adaptation_set": adaptation_set,
"representation": rep,
"filtered_period_ids": filtered_period_ids,
}
},
**track_args,
@@ -541,6 +551,26 @@ class DASH:
skip_merge = False
if downloader.__name__ == "n_m3u8dl_re":
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(
{
"filename": track.id,
@@ -578,6 +608,11 @@ class DASH:
status_update["downloaded"] = f"DASH {downloaded}"
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
for control_file in save_dir.glob("*.aria2__temp"):
control_file.unlink()