forked from kenzuya/unshackle
Merge branch 'main' into dev
This commit is contained in:
23
CHANGELOG.md
23
CHANGELOG.md
@@ -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/),
|
||||
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
|
||||
|
||||
### Added
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "unshackle"
|
||||
version = "2.3.0"
|
||||
version = "2.3.1"
|
||||
description = "Modular Movie, TV, and Music Archival Software."
|
||||
authors = [{ name = "unshackle team" }]
|
||||
requires-python = ">=3.10,<3.13"
|
||||
@@ -31,7 +31,8 @@ dependencies = [
|
||||
"click>=8.1.8,<9",
|
||||
"construct>=2.8.8,<3",
|
||||
"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",
|
||||
"langcodes>=3.4.0,<4",
|
||||
"lxml>=5.2.1,<7",
|
||||
@@ -52,13 +53,14 @@ dependencies = [
|
||||
"sortedcontainers>=2.4.0,<3",
|
||||
"subtitle-filter>=1.4.9,<2",
|
||||
"Unidecode>=1.3.8,<2",
|
||||
"urllib3>=2.2.1,<3",
|
||||
"urllib3>=2.6.3,<3",
|
||||
"chardet>=5.2.0,<6",
|
||||
"curl-cffi>=0.7.0b4,<0.14",
|
||||
"pyplayready>=0.6.3,<0.7",
|
||||
"httpx>=0.28.1,<0.29",
|
||||
"cryptography>=45.0.0,<47",
|
||||
"subby",
|
||||
"aiohttp>=3.13.3,<4",
|
||||
"aiohttp-swagger3>=0.9.0,<1",
|
||||
"pysubs2>=1.7.0,<2",
|
||||
"PyExecJS>=1.5.1,<2",
|
||||
@@ -78,6 +80,7 @@ unshackle = "unshackle.core.__main__:main"
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pre-commit>=3.7.0,<5",
|
||||
"virtualenv>=20.36.1,<22",
|
||||
"mypy>=1.9.0,<2",
|
||||
"mypy-protobuf>=3.6.0,<4",
|
||||
"types-protobuf>=4.24.0.20240408,<7",
|
||||
@@ -118,4 +121,4 @@ no_implicit_optional = true
|
||||
|
||||
[tool.uv.sources]
|
||||
unshackle = { workspace = true }
|
||||
subby = { git = "https://github.com/vevv/subby.git", rev = "5a925c367ffb3f5e53fd114ae222d3be1fdff35d" }
|
||||
subby = { git = "https://github.com/vevv/subby.git", rev = "1ea6a52028c5bea8177c8abc91716d74e4d097e1" }
|
||||
|
||||
@@ -135,7 +135,7 @@ def serve(host: str, port: int, caddy: bool, api_only: bool, no_key: bool, debug
|
||||
app["config"] = {"users": []}
|
||||
else:
|
||||
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
|
||||
setup_routes(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])
|
||||
# Setup config - add API secret to users for authentication
|
||||
serve_config = dict(config.serve)
|
||||
if not serve_config.get("users"):
|
||||
serve_config["users"] = []
|
||||
if not serve_config.get("users") or not isinstance(serve_config["users"], dict):
|
||||
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.on_startup.append(pywidevine_serve._startup)
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "2.3.0"
|
||||
__version__ = "2.3.1"
|
||||
|
||||
@@ -449,8 +449,8 @@ class DecryptLabsRemoteCDM:
|
||||
error_msg = data.get("message", "Unknown error")
|
||||
if "details" in data:
|
||||
error_msg += f" - Details: {data['details']}"
|
||||
if "error" in data:
|
||||
error_msg += f" - Error: {data['error']}"
|
||||
if "Error" in data:
|
||||
error_msg += f" - Error: {data['Error']}"
|
||||
|
||||
if "service_certificate is required" in str(data) and not session["service_certificate"]:
|
||||
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}"
|
||||
if "details" in data:
|
||||
error_msg += f" - Details: {data['details']}"
|
||||
if "error" in data:
|
||||
error_msg += f" - Error: {data['error']}"
|
||||
if "Error" in data:
|
||||
error_msg += f" - Error: {data['Error']}"
|
||||
|
||||
if already_tried_cache and data.get("message") == "success":
|
||||
return b""
|
||||
@@ -612,8 +612,8 @@ class DecryptLabsRemoteCDM:
|
||||
|
||||
if data.get("message") != "success":
|
||||
error_msg = data.get("message", "Unknown error")
|
||||
if "error" in data:
|
||||
error_msg += f" - Error: {data['error']}"
|
||||
if "Error" in data:
|
||||
error_msg += f" - Error: {data['Error']}"
|
||||
if "details" in data:
|
||||
error_msg += f" - Details: {data['details']}"
|
||||
raise requests.RequestException(f"License decrypt error: {error_msg}")
|
||||
|
||||
@@ -154,7 +154,9 @@ class PlayReady:
|
||||
pssh_boxes.extend(
|
||||
Box.parse(base64.b64decode(x.uri.split(",")[-1]))
|
||||
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)
|
||||
|
||||
@@ -12,6 +12,7 @@ from functools import partial
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Optional, Union
|
||||
from urllib.parse import urljoin
|
||||
from uuid import UUID
|
||||
from zlib import crc32
|
||||
|
||||
import m3u8
|
||||
@@ -260,7 +261,9 @@ class HLS:
|
||||
sys.exit(1)
|
||||
playlist_text = response.text
|
||||
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)
|
||||
|
||||
@@ -268,23 +271,51 @@ class HLS:
|
||||
log.error("Track's HLS playlist has no segments, expecting an invariant M3U8 playlist.")
|
||||
sys.exit(1)
|
||||
|
||||
# Get session DRM as fallback but prefer media playlist keys for accurate KID matching
|
||||
if track.drm:
|
||||
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:
|
||||
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():
|
||||
progress(downloaded="[yellow]SKIPPED")
|
||||
return
|
||||
@@ -341,12 +372,15 @@ class HLS:
|
||||
|
||||
if downloader.__name__ == "n_m3u8dl_re":
|
||||
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(
|
||||
{
|
||||
"output_dir": save_dir,
|
||||
"filename": track.id,
|
||||
"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
|
||||
map_data: Optional[tuple[m3u8.model.InitializationSection, bytes]] = None
|
||||
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:
|
||||
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])
|
||||
else:
|
||||
track_kid = None
|
||||
if not track_kid:
|
||||
track_kid = drm.kid
|
||||
progress(downloaded="LICENSING")
|
||||
license_widevine(drm, track_kid=track_kid)
|
||||
progress(downloaded="[yellow]LICENSED")
|
||||
@@ -770,6 +806,60 @@ class HLS:
|
||||
|
||||
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
|
||||
def get_supported_key(keys: list[Union[m3u8.model.SessionKey, m3u8.model.Key]]) -> Optional[m3u8.Key]:
|
||||
"""
|
||||
@@ -798,9 +888,9 @@ class HLS:
|
||||
return key
|
||||
elif key.keyformat and key.keyformat.lower() == WidevineCdm.urn:
|
||||
return key
|
||||
elif key.keyformat and (
|
||||
key.keyformat.lower() == PlayReadyCdm or "com.microsoft.playready" in key.keyformat.lower()
|
||||
):
|
||||
elif key.keyformat and key.keyformat.lower() in {
|
||||
f"urn:uuid:{PR_PSSH.SYSTEM_ID}", "com.microsoft.playready"
|
||||
}:
|
||||
return key
|
||||
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]),
|
||||
**key._extra_params, # noqa
|
||||
)
|
||||
elif key.keyformat and (
|
||||
key.keyformat.lower() == PlayReadyCdm or "com.microsoft.playready" in key.keyformat.lower()
|
||||
):
|
||||
elif key.keyformat and key.keyformat.lower() in {
|
||||
f"urn:uuid:{PR_PSSH.SYSTEM_ID}", "com.microsoft.playready"
|
||||
}:
|
||||
drm = PlayReady(
|
||||
pssh=PR_PSSH(key.uri.split(",")[-1]),
|
||||
pssh_b64=key.uri.split(",")[-1],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
import subprocess
|
||||
from collections import defaultdict
|
||||
@@ -16,7 +17,7 @@ from construct import Container
|
||||
from pycaption import Caption, CaptionList, CaptionNode, WebVTTReader
|
||||
from pycaption.geometry import Layout
|
||||
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 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.utils.webvtt import merge_segmented_webvtt
|
||||
|
||||
# silence srt library INFO logging
|
||||
logging.getLogger("srt").setLevel(logging.ERROR)
|
||||
|
||||
|
||||
class Subtitle(Track):
|
||||
class Codec(str, Enum):
|
||||
@@ -595,10 +599,13 @@ class Subtitle(Track):
|
||||
|
||||
if self.codec == Subtitle.Codec.WebVTT:
|
||||
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:
|
||||
converter = SAMIConverter()
|
||||
srt_subtitles = converter.from_file(str(self.path))
|
||||
srt_subtitles = converter.from_file(self.path)
|
||||
|
||||
if srt_subtitles is not None:
|
||||
# Apply common fixes
|
||||
@@ -607,11 +614,11 @@ class Subtitle(Track):
|
||||
|
||||
# If target is SRT, we're done
|
||||
if codec == Subtitle.Codec.SubRip:
|
||||
output_path.write_text(str(fixed_srt), encoding="utf8")
|
||||
fixed_srt.save(output_path, encoding="utf8")
|
||||
else:
|
||||
# Convert from SRT to target format using existing pycaption logic
|
||||
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
|
||||
caption_set = self.parse(temp_srt_path.read_bytes(), Subtitle.Codec.SubRip)
|
||||
@@ -724,7 +731,7 @@ class Subtitle(Track):
|
||||
elif conversion_method == "pysubs2":
|
||||
return self.convert_with_pysubs2(codec)
|
||||
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)
|
||||
else:
|
||||
return self._convert_standard(codec)
|
||||
@@ -1177,9 +1184,12 @@ class Subtitle(Track):
|
||||
|
||||
if sdh_method == "subby" and self.codec == Subtitle.Codec.SubRip:
|
||||
# Use subby's SDHStripper directly on the file
|
||||
fixer = CommonIssuesFixer()
|
||||
stripper = SDHStripper()
|
||||
stripped_srt, _ = stripper.from_file(str(self.path))
|
||||
self.path.write_text(str(stripped_srt), encoding="utf8")
|
||||
srt, _ = fixer.from_file(self.path)
|
||||
stripped, status = stripper.from_srt(srt)
|
||||
if status is True:
|
||||
stripped.save(self.path)
|
||||
return
|
||||
elif sdh_method == "subtitleedit" and binaries.SubtitleEdit:
|
||||
# Force use of SubtitleEdit
|
||||
@@ -1205,9 +1215,12 @@ class Subtitle(Track):
|
||||
# Try subby first for SRT files, then fall back
|
||||
if self.codec == Subtitle.Codec.SubRip:
|
||||
try:
|
||||
fixer = CommonIssuesFixer()
|
||||
stripper = SDHStripper()
|
||||
stripped_srt, _ = stripper.from_file(str(self.path))
|
||||
self.path.write_text(str(stripped_srt), encoding="utf8")
|
||||
srt, _ = fixer.from_file(self.path)
|
||||
stripped, status = stripper.from_srt(srt)
|
||||
if status is True:
|
||||
stripped.save(self.path)
|
||||
return
|
||||
except Exception:
|
||||
pass # Fall through to other methods
|
||||
|
||||
@@ -215,7 +215,8 @@ class Track:
|
||||
# or when the subtitle has a direct file extension
|
||||
if self.downloader.__name__ == "n_m3u8dl_re" and (
|
||||
self.descriptor == self.Descriptor.URL
|
||||
or get_extension(self.url) in {
|
||||
or get_extension(self.url)
|
||||
in {
|
||||
".srt",
|
||||
".vtt",
|
||||
".ttml",
|
||||
@@ -303,7 +304,9 @@ class Track:
|
||||
try:
|
||||
self.drm = [Widevine.from_track(self, session)]
|
||||
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:
|
||||
try:
|
||||
self.drm = [Widevine.from_track(self, session)]
|
||||
@@ -311,7 +314,9 @@ class Track:
|
||||
try:
|
||||
self.drm = [PlayReady.from_track(self, session)]
|
||||
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:
|
||||
track_kid = self.get_key_id(session=session)
|
||||
@@ -548,7 +553,6 @@ class Track:
|
||||
|
||||
try:
|
||||
import m3u8
|
||||
from pyplayready.cdm import Cdm as PlayReadyCdm
|
||||
from pyplayready.system.pssh import PSSH as PR_PSSH
|
||||
from pywidevine.cdm import Cdm as WidevineCdm
|
||||
from pywidevine.pssh import PSSH as WV_PSSH
|
||||
@@ -569,7 +573,7 @@ class Track:
|
||||
pssh_b64 = key.uri.split(",")[-1]
|
||||
drm = Widevine(pssh=WV_PSSH(pssh_b64))
|
||||
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]
|
||||
drm = PlayReady(pssh=PR_PSSH(pssh_b64), pssh_b64=pssh_b64)
|
||||
drm_list.append(drm)
|
||||
|
||||
Reference in New Issue
Block a user