Merge branch 'main' into dev

This commit is contained in:
Andy
2026-01-22 14:47:25 -07:00
10 changed files with 1062 additions and 896 deletions

View File

@@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2.3.1] - 2026-01-22
### Fixed
- **Vulnerable Dependencies**: Upgraded dependencies to address security alerts
- urllib3: 2.5.0 → 2.6.3 (CVE-2025-66418, CVE-2025-66471, CVE-2026-21441)
- aiohttp: 3.13.2 → 3.13.3 (8 CVEs including CVE-2025-69223, CVE-2025-69227)
- fonttools: 4.60.1 → 4.61.1 (CVE-2025-66034)
- filelock: 3.19.1 → 3.20.3 (CVE-2025-68146, CVE-2026-22701)
- virtualenv: 20.34.0 → 20.36.1 (CVE-2026-22702)
- **HLS Key Selection**: Prefer media playlist keys over session keys for accurate KID matching
- Session keys from master playlists often contain PSSHs with multiple KIDs covering all tracks
- Unified DRM licensing logic for all downloaders
- Added `filter_keys_for_cdm()` to select keys matching configured CDM type
- Added `get_track_kid_from_init()` to extract KID from init segment with fallback
- Fixed PlayReady keyformat matching using strict `PR_PSSH.SYSTEM_ID` URN
- Fixes download failures where track KID was null or mismatched
- **DASH Audio Track Selection**: Include language in N_m3u8DL-RE track selection
- Fixes duplicate audio downloads when DASH manifests have multiple adaptation sets with same representation IDs
- **SubtitleEdit Compatibility**: Update CLI syntax for SubtitleEdit 4.x
- Use lowercase format names (subrip, webvtt, advancedsubstationalpha)
- Respect `conversion_method` config setting when stripping SDH
## [2.3.0] - 2026-01-18 ## [2.3.0] - 2026-01-18
### Added ### Added

View File

@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "unshackle" name = "unshackle"
version = "2.3.0" version = "2.3.1"
description = "Modular Movie, TV, and Music Archival Software." description = "Modular Movie, TV, and Music Archival Software."
authors = [{ name = "unshackle team" }] authors = [{ name = "unshackle team" }]
requires-python = ">=3.10,<3.13" requires-python = ">=3.10,<3.13"
@@ -31,7 +31,8 @@ dependencies = [
"click>=8.1.8,<9", "click>=8.1.8,<9",
"construct>=2.8.8,<3", "construct>=2.8.8,<3",
"crccheck>=1.3.0,<2", "crccheck>=1.3.0,<2",
"fonttools>=4.0.0,<5", "filelock>=3.20.3,<4",
"fonttools>=4.60.2,<5",
"jsonpickle>=3.0.4,<5", "jsonpickle>=3.0.4,<5",
"langcodes>=3.4.0,<4", "langcodes>=3.4.0,<4",
"lxml>=5.2.1,<7", "lxml>=5.2.1,<7",
@@ -52,13 +53,14 @@ dependencies = [
"sortedcontainers>=2.4.0,<3", "sortedcontainers>=2.4.0,<3",
"subtitle-filter>=1.4.9,<2", "subtitle-filter>=1.4.9,<2",
"Unidecode>=1.3.8,<2", "Unidecode>=1.3.8,<2",
"urllib3>=2.2.1,<3", "urllib3>=2.6.3,<3",
"chardet>=5.2.0,<6", "chardet>=5.2.0,<6",
"curl-cffi>=0.7.0b4,<0.14", "curl-cffi>=0.7.0b4,<0.14",
"pyplayready>=0.6.3,<0.7", "pyplayready>=0.6.3,<0.7",
"httpx>=0.28.1,<0.29", "httpx>=0.28.1,<0.29",
"cryptography>=45.0.0,<47", "cryptography>=45.0.0,<47",
"subby", "subby",
"aiohttp>=3.13.3,<4",
"aiohttp-swagger3>=0.9.0,<1", "aiohttp-swagger3>=0.9.0,<1",
"pysubs2>=1.7.0,<2", "pysubs2>=1.7.0,<2",
"PyExecJS>=1.5.1,<2", "PyExecJS>=1.5.1,<2",
@@ -78,6 +80,7 @@ unshackle = "unshackle.core.__main__:main"
[dependency-groups] [dependency-groups]
dev = [ dev = [
"pre-commit>=3.7.0,<5", "pre-commit>=3.7.0,<5",
"virtualenv>=20.36.1,<22",
"mypy>=1.9.0,<2", "mypy>=1.9.0,<2",
"mypy-protobuf>=3.6.0,<4", "mypy-protobuf>=3.6.0,<4",
"types-protobuf>=4.24.0.20240408,<7", "types-protobuf>=4.24.0.20240408,<7",
@@ -118,4 +121,4 @@ no_implicit_optional = true
[tool.uv.sources] [tool.uv.sources]
unshackle = { workspace = true } unshackle = { workspace = true }
subby = { git = "https://github.com/vevv/subby.git", rev = "5a925c367ffb3f5e53fd114ae222d3be1fdff35d" } subby = { git = "https://github.com/vevv/subby.git", rev = "1ea6a52028c5bea8177c8abc91716d74e4d097e1" }

View File

@@ -135,7 +135,7 @@ def serve(host: str, port: int, caddy: bool, api_only: bool, no_key: bool, debug
app["config"] = {"users": []} app["config"] = {"users": []}
else: else:
app = web.Application(middlewares=[cors_middleware, pywidevine_serve.authentication]) app = web.Application(middlewares=[cors_middleware, pywidevine_serve.authentication])
app["config"] = {"users": [api_secret]} app["config"] = {"users": {api_secret: {"devices": [], "username": "api_user"}}}
app["debug_api"] = debug_api app["debug_api"] = debug_api
setup_routes(app) setup_routes(app)
setup_swagger(app) setup_swagger(app)
@@ -156,10 +156,14 @@ def serve(host: str, port: int, caddy: bool, api_only: bool, no_key: bool, debug
app = web.Application(middlewares=[cors_middleware, pywidevine_serve.authentication]) app = web.Application(middlewares=[cors_middleware, pywidevine_serve.authentication])
# Setup config - add API secret to users for authentication # Setup config - add API secret to users for authentication
serve_config = dict(config.serve) serve_config = dict(config.serve)
if not serve_config.get("users"): if not serve_config.get("users") or not isinstance(serve_config["users"], dict):
serve_config["users"] = [] serve_config["users"] = {}
if api_secret not in serve_config["users"]: if api_secret not in serve_config["users"]:
serve_config["users"].append(api_secret) device_names = [d.stem if hasattr(d, "stem") else str(d) for d in serve_config.get("devices", [])]
serve_config["users"][api_secret] = {
"devices": device_names,
"username": "api_user"
}
app["config"] = serve_config app["config"] = serve_config
app.on_startup.append(pywidevine_serve._startup) app.on_startup.append(pywidevine_serve._startup)

View File

@@ -1 +1 @@
__version__ = "2.3.0" __version__ = "2.3.1"

View File

@@ -449,8 +449,8 @@ class DecryptLabsRemoteCDM:
error_msg = data.get("message", "Unknown error") error_msg = data.get("message", "Unknown error")
if "details" in data: if "details" in data:
error_msg += f" - Details: {data['details']}" error_msg += f" - Details: {data['details']}"
if "error" in data: if "Error" in data:
error_msg += f" - Error: {data['error']}" error_msg += f" - Error: {data['Error']}"
if "service_certificate is required" in str(data) and not session["service_certificate"]: if "service_certificate is required" in str(data) and not session["service_certificate"]:
error_msg += " (No service certificate was provided to the CDM session)" error_msg += " (No service certificate was provided to the CDM session)"
@@ -537,8 +537,8 @@ class DecryptLabsRemoteCDM:
error_msg = f"API response: {data['message']} - {error_msg}" error_msg = f"API response: {data['message']} - {error_msg}"
if "details" in data: if "details" in data:
error_msg += f" - Details: {data['details']}" error_msg += f" - Details: {data['details']}"
if "error" in data: if "Error" in data:
error_msg += f" - Error: {data['error']}" error_msg += f" - Error: {data['Error']}"
if already_tried_cache and data.get("message") == "success": if already_tried_cache and data.get("message") == "success":
return b"" return b""
@@ -612,8 +612,8 @@ class DecryptLabsRemoteCDM:
if data.get("message") != "success": if data.get("message") != "success":
error_msg = data.get("message", "Unknown error") error_msg = data.get("message", "Unknown error")
if "error" in data: if "Error" in data:
error_msg += f" - Error: {data['error']}" error_msg += f" - Error: {data['Error']}"
if "details" in data: if "details" in data:
error_msg += f" - Details: {data['details']}" error_msg += f" - Details: {data['details']}"
raise requests.RequestException(f"License decrypt error: {error_msg}") raise requests.RequestException(f"License decrypt error: {error_msg}")

View File

@@ -154,7 +154,9 @@ class PlayReady:
pssh_boxes.extend( pssh_boxes.extend(
Box.parse(base64.b64decode(x.uri.split(",")[-1])) Box.parse(base64.b64decode(x.uri.split(",")[-1]))
for x in (master.session_keys or master.keys) for x in (master.session_keys or master.keys)
if x and x.keyformat and "playready" in x.keyformat.lower() if x and x.keyformat and x.keyformat.lower() in {
f"urn:uuid:{PSSH.SYSTEM_ID}", "com.microsoft.playready"
}
) )
init_data = track.get_init_segment(session=session) init_data = track.get_init_segment(session=session)

View File

@@ -12,6 +12,7 @@ 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
from urllib.parse import urljoin from urllib.parse import urljoin
from uuid import UUID
from zlib import crc32 from zlib import crc32
import m3u8 import m3u8
@@ -260,7 +261,9 @@ class HLS:
sys.exit(1) sys.exit(1)
playlist_text = response.text playlist_text = response.text
else: else:
raise TypeError(f"Expected response to be a requests.Response or curl_cffi.Response, not {type(response)}") raise TypeError(
f"Expected response to be a requests.Response or curl_cffi.Response, not {type(response)}"
)
master = m3u8.loads(playlist_text, uri=track.url) master = m3u8.loads(playlist_text, uri=track.url)
@@ -268,23 +271,51 @@ class HLS:
log.error("Track's HLS playlist has no segments, expecting an invariant M3U8 playlist.") log.error("Track's HLS playlist has no segments, expecting an invariant M3U8 playlist.")
sys.exit(1) sys.exit(1)
# Get session DRM as fallback but prefer media playlist keys for accurate KID matching
if track.drm: if track.drm:
session_drm = track.get_drm_for_cdm(cdm) session_drm = track.get_drm_for_cdm(cdm)
if isinstance(session_drm, (Widevine, PlayReady)):
# license and grab content keys
try:
if not license_widevine:
raise ValueError("license_widevine func must be supplied to use DRM")
progress(downloaded="LICENSING")
license_widevine(session_drm)
progress(downloaded="[yellow]LICENSED")
except Exception: # noqa
DOWNLOAD_CANCELLED.set() # skip pending track downloads
progress(downloaded="[red]FAILED")
raise
else: else:
session_drm = None session_drm = None
initial_drm_licensed = False
initial_drm_key = None # Track the EXT-X-KEY used for initial licensing
media_keys = [k for k in (master.keys or []) if k is not None]
if media_keys:
cdm_media_keys = HLS.filter_keys_for_cdm(media_keys, cdm)
media_playlist_key = HLS.get_supported_key(cdm_media_keys) if cdm_media_keys else None
if media_playlist_key:
media_drm = HLS.get_drm(media_playlist_key, session)
if isinstance(media_drm, (Widevine, PlayReady)):
track_kid = HLS.get_track_kid_from_init(master, track, session) or media_drm.kid
try:
if not license_widevine:
raise ValueError("license_widevine func must be supplied to use DRM")
progress(downloaded="LICENSING")
license_widevine(media_drm, track_kid=track_kid)
progress(downloaded="[yellow]LICENSED")
initial_drm_licensed = True
initial_drm_key = media_playlist_key
track.drm = [media_drm]
session_drm = media_drm
except Exception: # noqa
DOWNLOAD_CANCELLED.set() # skip pending track downloads
progress(downloaded="[red]FAILED")
raise
# Fall back to session DRM if media playlist has no matching keys
if not initial_drm_licensed and session_drm and isinstance(session_drm, (Widevine, PlayReady)):
try:
if not license_widevine:
raise ValueError("license_widevine func must be supplied to use DRM")
progress(downloaded="LICENSING")
license_widevine(session_drm)
progress(downloaded="[yellow]LICENSED")
except Exception: # noqa
DOWNLOAD_CANCELLED.set() # skip pending track downloads
progress(downloaded="[red]FAILED")
raise
if DOWNLOAD_LICENCE_ONLY.is_set(): if DOWNLOAD_LICENCE_ONLY.is_set():
progress(downloaded="[yellow]SKIPPED") progress(downloaded="[yellow]SKIPPED")
return return
@@ -341,12 +372,15 @@ class HLS:
if downloader.__name__ == "n_m3u8dl_re": if downloader.__name__ == "n_m3u8dl_re":
skip_merge = True skip_merge = True
# session_drm already has correct content_keys from initial licensing above
n_m3u8dl_content_keys = session_drm.content_keys if session_drm else None
downloader_args.update( downloader_args.update(
{ {
"output_dir": save_dir, "output_dir": save_dir,
"filename": track.id, "filename": track.id,
"track": track, "track": track,
"content_keys": session_drm.content_keys if session_drm else None, "content_keys": n_m3u8dl_content_keys,
} }
) )
@@ -390,7 +424,7 @@ class HLS:
range_offset = 0 range_offset = 0
map_data: Optional[tuple[m3u8.model.InitializationSection, bytes]] = None map_data: Optional[tuple[m3u8.model.InitializationSection, bytes]] = None
if session_drm: if session_drm:
encryption_data: Optional[tuple[Optional[m3u8.Key], DRM_T]] = (None, session_drm) encryption_data: Optional[tuple[Optional[m3u8.Key], DRM_T]] = (initial_drm_key, session_drm)
else: else:
encryption_data: Optional[tuple[Optional[m3u8.Key], DRM_T]] = None encryption_data: Optional[tuple[Optional[m3u8.Key], DRM_T]] = None
@@ -571,6 +605,8 @@ class HLS:
track_kid = track.get_key_id(map_data[1]) track_kid = track.get_key_id(map_data[1])
else: else:
track_kid = None track_kid = None
if not track_kid:
track_kid = drm.kid
progress(downloaded="LICENSING") progress(downloaded="LICENSING")
license_widevine(drm, track_kid=track_kid) license_widevine(drm, track_kid=track_kid)
progress(downloaded="[yellow]LICENSED") progress(downloaded="[yellow]LICENSED")
@@ -770,6 +806,60 @@ class HLS:
return keys return keys
@staticmethod
def filter_keys_for_cdm(
keys: list[Union[m3u8.model.SessionKey, m3u8.model.Key]],
cdm: object,
) -> list[Union[m3u8.model.SessionKey, m3u8.model.Key]]:
"""
Filter EXT-X-KEY entries to only include those matching the CDM type.
This ensures we select the correct DRM system (Widevine vs PlayReady)
based on what CDM is configured, avoiding license request failures.
"""
playready_urn = f"urn:uuid:{PR_PSSH.SYSTEM_ID}"
playready_keyformats = {playready_urn, "com.microsoft.playready"}
if isinstance(cdm, WidevineCdm):
return [k for k in keys if k.keyformat and k.keyformat.lower() == WidevineCdm.urn]
elif isinstance(cdm, PlayReadyCdm):
return [k for k in keys if k.keyformat and k.keyformat.lower() in playready_keyformats]
elif hasattr(cdm, "is_playready"):
if cdm.is_playready:
return [k for k in keys if k.keyformat and k.keyformat.lower() in playready_keyformats]
else:
return [k for k in keys if k.keyformat and k.keyformat.lower() == WidevineCdm.urn]
return keys
@staticmethod
def get_track_kid_from_init(
master: M3U8,
track: AnyTrack,
session: Union[Session, CurlSession],
) -> Optional[UUID]:
"""
Extract the track's Key ID from its init segment (EXT-X-MAP).
Returns None if no init segment exists or KID extraction fails.
The caller should fall back to drm.kid from the PSSH if this returns None.
"""
map_section = next((seg.init_section for seg in master.segments if seg.init_section), None)
if not map_section:
return None
map_uri = urljoin(map_section.base_uri or master.base_uri or "", map_section.uri)
try:
if map_section.byterange:
byte_range = HLS.calculate_byte_range(map_section.byterange, 0)
headers = {"Range": f"bytes={byte_range}"}
else:
headers = {}
map_res = session.get(url=map_uri, headers=headers)
if map_res.ok:
return track.get_key_id(map_res.content)
except Exception:
pass
return None
@staticmethod @staticmethod
def get_supported_key(keys: list[Union[m3u8.model.SessionKey, m3u8.model.Key]]) -> Optional[m3u8.Key]: def get_supported_key(keys: list[Union[m3u8.model.SessionKey, m3u8.model.Key]]) -> Optional[m3u8.Key]:
""" """
@@ -798,9 +888,9 @@ class HLS:
return key return key
elif key.keyformat and key.keyformat.lower() == WidevineCdm.urn: elif key.keyformat and key.keyformat.lower() == WidevineCdm.urn:
return key return key
elif key.keyformat and ( elif key.keyformat and key.keyformat.lower() in {
key.keyformat.lower() == PlayReadyCdm or "com.microsoft.playready" in key.keyformat.lower() f"urn:uuid:{PR_PSSH.SYSTEM_ID}", "com.microsoft.playready"
): }:
return key return key
else: else:
unsupported_systems.append(key.method + (f" ({key.keyformat})" if key.keyformat else "")) unsupported_systems.append(key.method + (f" ({key.keyformat})" if key.keyformat else ""))
@@ -837,9 +927,9 @@ class HLS:
pssh=WV_PSSH(key.uri.split(",")[-1]), pssh=WV_PSSH(key.uri.split(",")[-1]),
**key._extra_params, # noqa **key._extra_params, # noqa
) )
elif key.keyformat and ( elif key.keyformat and key.keyformat.lower() in {
key.keyformat.lower() == PlayReadyCdm or "com.microsoft.playready" in key.keyformat.lower() f"urn:uuid:{PR_PSSH.SYSTEM_ID}", "com.microsoft.playready"
): }:
drm = PlayReady( drm = PlayReady(
pssh=PR_PSSH(key.uri.split(",")[-1]), pssh=PR_PSSH(key.uri.split(",")[-1]),
pssh_b64=key.uri.split(",")[-1], pssh_b64=key.uri.split(",")[-1],

View File

@@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import logging
import re import re
import subprocess import subprocess
from collections import defaultdict from collections import defaultdict
@@ -16,7 +17,7 @@ from construct import Container
from pycaption import Caption, CaptionList, CaptionNode, WebVTTReader from pycaption import Caption, CaptionList, CaptionNode, WebVTTReader
from pycaption.geometry import Layout from pycaption.geometry import Layout
from pymp4.parser import MP4 from pymp4.parser import MP4
from subby import CommonIssuesFixer, SAMIConverter, SDHStripper, WebVTTConverter from subby import CommonIssuesFixer, SAMIConverter, SDHStripper, WebVTTConverter, WVTTConverter
from subtitle_filter import Subtitles from subtitle_filter import Subtitles
from unshackle.core import binaries from unshackle.core import binaries
@@ -25,6 +26,9 @@ from unshackle.core.tracks.track import Track
from unshackle.core.utilities import try_ensure_utf8 from unshackle.core.utilities import try_ensure_utf8
from unshackle.core.utils.webvtt import merge_segmented_webvtt from unshackle.core.utils.webvtt import merge_segmented_webvtt
# silence srt library INFO logging
logging.getLogger("srt").setLevel(logging.ERROR)
class Subtitle(Track): class Subtitle(Track):
class Codec(str, Enum): class Codec(str, Enum):
@@ -595,10 +599,13 @@ class Subtitle(Track):
if self.codec == Subtitle.Codec.WebVTT: if self.codec == Subtitle.Codec.WebVTT:
converter = WebVTTConverter() converter = WebVTTConverter()
srt_subtitles = converter.from_file(str(self.path)) srt_subtitles = converter.from_file(self.path)
if self.codec == Subtitle.Codec.fVTT:
converter = WVTTConverter()
srt_subtitles = converter.from_file(self.path)
elif self.codec == Subtitle.Codec.SAMI: elif self.codec == Subtitle.Codec.SAMI:
converter = SAMIConverter() converter = SAMIConverter()
srt_subtitles = converter.from_file(str(self.path)) srt_subtitles = converter.from_file(self.path)
if srt_subtitles is not None: if srt_subtitles is not None:
# Apply common fixes # Apply common fixes
@@ -607,11 +614,11 @@ class Subtitle(Track):
# If target is SRT, we're done # If target is SRT, we're done
if codec == Subtitle.Codec.SubRip: if codec == Subtitle.Codec.SubRip:
output_path.write_text(str(fixed_srt), encoding="utf8") fixed_srt.save(output_path, encoding="utf8")
else: else:
# Convert from SRT to target format using existing pycaption logic # Convert from SRT to target format using existing pycaption logic
temp_srt_path = self.path.with_suffix(".temp.srt") temp_srt_path = self.path.with_suffix(".temp.srt")
temp_srt_path.write_text(str(fixed_srt), encoding="utf8") fixed_srt.save(temp_srt_path, encoding="utf8")
# Parse the SRT and convert to target format # Parse the SRT and convert to target format
caption_set = self.parse(temp_srt_path.read_bytes(), Subtitle.Codec.SubRip) caption_set = self.parse(temp_srt_path.read_bytes(), Subtitle.Codec.SubRip)
@@ -724,7 +731,7 @@ class Subtitle(Track):
elif conversion_method == "pysubs2": elif conversion_method == "pysubs2":
return self.convert_with_pysubs2(codec) return self.convert_with_pysubs2(codec)
elif conversion_method == "auto": elif conversion_method == "auto":
if self.codec in (Subtitle.Codec.WebVTT, Subtitle.Codec.SAMI): if self.codec in (Subtitle.Codec.WebVTT, Subtitle.Codec.fVTT, Subtitle.Codec.SAMI):
return self.convert_with_subby(codec) return self.convert_with_subby(codec)
else: else:
return self._convert_standard(codec) return self._convert_standard(codec)
@@ -1177,9 +1184,12 @@ class Subtitle(Track):
if sdh_method == "subby" and self.codec == Subtitle.Codec.SubRip: if sdh_method == "subby" and self.codec == Subtitle.Codec.SubRip:
# Use subby's SDHStripper directly on the file # Use subby's SDHStripper directly on the file
fixer = CommonIssuesFixer()
stripper = SDHStripper() stripper = SDHStripper()
stripped_srt, _ = stripper.from_file(str(self.path)) srt, _ = fixer.from_file(self.path)
self.path.write_text(str(stripped_srt), encoding="utf8") stripped, status = stripper.from_srt(srt)
if status is True:
stripped.save(self.path)
return return
elif sdh_method == "subtitleedit" and binaries.SubtitleEdit: elif sdh_method == "subtitleedit" and binaries.SubtitleEdit:
# Force use of SubtitleEdit # Force use of SubtitleEdit
@@ -1205,9 +1215,12 @@ class Subtitle(Track):
# Try subby first for SRT files, then fall back # Try subby first for SRT files, then fall back
if self.codec == Subtitle.Codec.SubRip: if self.codec == Subtitle.Codec.SubRip:
try: try:
fixer = CommonIssuesFixer()
stripper = SDHStripper() stripper = SDHStripper()
stripped_srt, _ = stripper.from_file(str(self.path)) srt, _ = fixer.from_file(self.path)
self.path.write_text(str(stripped_srt), encoding="utf8") stripped, status = stripper.from_srt(srt)
if status is True:
stripped.save(self.path)
return return
except Exception: except Exception:
pass # Fall through to other methods pass # Fall through to other methods

View File

@@ -215,7 +215,8 @@ class Track:
# or when the subtitle has a direct file extension # or when the subtitle has a direct file extension
if self.downloader.__name__ == "n_m3u8dl_re" and ( if self.downloader.__name__ == "n_m3u8dl_re" and (
self.descriptor == self.Descriptor.URL self.descriptor == self.Descriptor.URL
or get_extension(self.url) in { or get_extension(self.url)
in {
".srt", ".srt",
".vtt", ".vtt",
".ttml", ".ttml",
@@ -303,7 +304,9 @@ class Track:
try: try:
self.drm = [Widevine.from_track(self, session)] self.drm = [Widevine.from_track(self, session)]
except Widevine.Exceptions.PSSHNotFound: except Widevine.Exceptions.PSSHNotFound:
log.warning("No PlayReady or Widevine PSSH was found for this track, is it DRM free?") log.warning(
"No PlayReady or Widevine PSSH was found for this track, is it DRM free?"
)
else: else:
try: try:
self.drm = [Widevine.from_track(self, session)] self.drm = [Widevine.from_track(self, session)]
@@ -311,7 +314,9 @@ class Track:
try: try:
self.drm = [PlayReady.from_track(self, session)] self.drm = [PlayReady.from_track(self, session)]
except PlayReady.Exceptions.PSSHNotFound: except PlayReady.Exceptions.PSSHNotFound:
log.warning("No Widevine or PlayReady PSSH was found for this track, is it DRM free?") log.warning(
"No Widevine or PlayReady PSSH was found for this track, is it DRM free?"
)
if self.drm: if self.drm:
track_kid = self.get_key_id(session=session) track_kid = self.get_key_id(session=session)
@@ -548,7 +553,6 @@ class Track:
try: try:
import m3u8 import m3u8
from pyplayready.cdm import Cdm as PlayReadyCdm
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
from pywidevine.pssh import PSSH as WV_PSSH from pywidevine.pssh import PSSH as WV_PSSH
@@ -569,7 +573,7 @@ class Track:
pssh_b64 = key.uri.split(",")[-1] pssh_b64 = key.uri.split(",")[-1]
drm = Widevine(pssh=WV_PSSH(pssh_b64)) drm = Widevine(pssh=WV_PSSH(pssh_b64))
drm_list.append(drm) drm_list.append(drm)
elif fmt == PlayReadyCdm or "com.microsoft.playready" in fmt: elif fmt in {f"urn:uuid:{PR_PSSH.SYSTEM_ID}", "com.microsoft.playready"}:
pssh_b64 = key.uri.split(",")[-1] pssh_b64 = key.uri.split(",")[-1]
drm = PlayReady(pssh=PR_PSSH(pssh_b64), pssh_b64=pssh_b64) drm = PlayReady(pssh=PR_PSSH(pssh_b64), pssh_b64=pssh_b64)
drm_list.append(drm) drm_list.append(drm)

1715
uv.lock generated

File diff suppressed because it is too large Load Diff