2 Commits

Author SHA1 Message Date
6a15cd0a5d refactor(netflix): unify DRM handling and improve track hydration logic
- Added DRM system detection method to distinguish Widevine and PlayReady CDMs
- Implemented create_drm method to instantiate appropriate DRM objects based on system
- Updated track hydration to add all tracks on first profile/range, only videos otherwise
- Changed get_playready_license to reuse Widevine license retrieval method
- Replaced direct Widevine instantiations with create_drm calls for DRM object creation
- Added conditional debug logs for unavailable audio and subtitle tracks when hydrating
- Cleaned up DRM imports and type annotations for drm_system attribute in Netflix class
2025-09-09 18:03:14 +07:00
5d4b71b388 fix(MSL): raise exception on error in MSL response message
- Replace silent log of error with raising an exception
- Ensure errors in MSL response messages do not go unnoticed
- Prevent continuation on critical MSL response errors
- Improve error handling robustness in MSL class
2025-09-09 18:01:50 +07:00
2 changed files with 52 additions and 20 deletions

View File

@@ -342,7 +342,7 @@ class MSL:
if not (error_display or error_detail):
self.log.critical(f"- {error}")
raise Exception(f"- MSL response message contains an error: {error}")
# sys.exit(1)
return data["result"]

View File

@@ -18,12 +18,13 @@ from Crypto.Random import get_random_bytes
import jsonpickle
from pymp4.parser import Box
from pywidevine import PSSH, Cdm, DeviceTypes
from pyplayready import PSSH as PlayReadyPSSH
import requests
from langcodes import Language
from unshackle.core.constants import AnyTrack
from unshackle.core.credential import Credential
from unshackle.core.drm.widevine import Widevine
from unshackle.core.drm import Widevine, PlayReady
from unshackle.core.service import Service
from unshackle.core.titles import Titles_T, Title_T
from unshackle.core.titles.episode import Episode, Series
@@ -36,6 +37,7 @@ from unshackle.core.tracks.subtitle import Subtitle
from unshackle.core.tracks.track import Track
from unshackle.core.tracks.video import Video
from unshackle.core.utils.collections import flatten, as_list
from unshackle.core.drm import DRM_T
from unshackle.core.tracks.attachment import Attachment
from unshackle.core.drm.playready import PlayReady
@@ -83,7 +85,7 @@ class Netflix(Service):
self.profile = profile
self.meta_lang = meta_lang
self.hydrate_track = hydrate_track
self.drm_system = drm_system
self.drm_system: Literal["widevine", "playready"] = drm_system
self.profiles: List[str] = []
self.requested_profiles: List[str] = []
self.high_bitrate = high_bitrate
@@ -248,7 +250,10 @@ class Netflix(Service):
self.log.debug(f"Range {range_index + 1}/{len(self.range)} ({video_range.name}), Profile Index: {profile_index}. Getting profiles: {profile_list}")
manifest = self.get_manifest(title, profile_list)
manifest_tracks = self.manifest_as_tracks(manifest, title, should_hydrate and profile_index == 0)
tracks.add(manifest_tracks if should_hydrate and profile_index == 0 else manifest_tracks.videos)
if should_hydrate and profile_index == 0:
tracks.add(manifest_tracks) # Add all tracks (video, audio, subtitles) on first hydrated profile
else:
tracks.add(manifest_tracks.videos) # Add only videos for additional profiles
except Exception:
self.log.error(f"Error getting profile: {profile_list} for range {video_range.name}. Skipping")
continue
@@ -262,7 +267,12 @@ class Netflix(Service):
self.log.info(f"Processing range {range_index + 1}/{len(self.range)}: {video_range.name}")
manifest = self.get_manifest(title, range_profiles)
manifest_tracks = self.manifest_as_tracks(manifest, title, should_hydrate)
tracks.add(manifest_tracks if should_hydrate else manifest_tracks.videos)
if should_hydrate:
tracks.add(manifest_tracks) # Add all tracks (video, audio, subtitles) when hydrating
elif range_index == 0:
tracks.add(manifest_tracks) # Add all tracks on first range even without hydration
else:
tracks.add(manifest_tracks.videos) # Add only videos for additional ranges
except Exception as e:
self.log.error(f"Error processing range {video_range.name}: {e}")
@@ -392,17 +402,14 @@ class Netflix(Service):
return payload_data[0]["licenseResponseBase64"]
def get_playready_license(self, *, challenge: bytes, title: Movie | Episode | Song, track: AnyTrack) -> bytes | str | None:
return None
return self.get_widevine_license(challenge=challenge, title=title, track=track)
# return super().get_widevine_license(challenge=challenge, title=title, track=track)
def configure(self):
# self.log.info(ctx)
# if profile is none from argument let's use them all profile in video codec scope
# self.log.info(f"Requested profiles: {self.profile}")
if self.profile is None:
self.profiles = self.config["profiles"]["video"][self.vcodec.extension.upper()]
if self.profile is not None:
self.requested_profiles = self.profile.split('+')
self.log.info(f"Requested profile: {self.requested_profiles}")
@@ -430,7 +437,7 @@ class Netflix(Service):
sys.exit(1)
self.profiles = self.get_profiles()
self.drm_system = self.get_drm_system()
# Log information about video ranges being processed
if len(self.range) > 1:
range_names = [r.name for r in self.range]
@@ -808,6 +815,10 @@ class Netflix(Service):
# self.log.info(video)
id = video["downloadable_id"]
# self.log.info(f"Adding video {video["res_w"]}x{video["res_h"]}, bitrate: {(float(video["framerate_value"]) / video["framerate_scale"]) if "framerate_value" in video else None} with profile {video["content_profile"]}. kid: {video["drmHeaderId"]}")
drm = Widevine(
pssh=PSSH(manifest["video_tracks"][0]["drmHeader"]["bytes"]),
pssh_b64=video["drmHeaderId"],
)
tracks.add(
Video(
id_=video["downloadable_id"],
@@ -821,12 +832,7 @@ class Netflix(Service):
edition=video["content_profile"],
range_=self.parse_video_range_from_profile(video["content_profile"]),
is_original_lang=True,
drm=[Widevine(
pssh=PSSH(
manifest["video_tracks"][0]["drmHeader"]["bytes"]
),
kid=video["drmHeaderId"]
)] if manifest["video_tracks"][0].get("drmHeader", {}).get("bytes") else [],
drm=[self.create_drm(manifest["video_tracks"][0]["drmHeader"]["bytes"], video["drmHeaderId"])] if manifest["video_tracks"][0].get("drmHeader", {}).get("bytes") else [],
data={
'license_url': license_url
}
@@ -850,6 +856,7 @@ class Netflix(Service):
# self.log.debug(f"Audio lang {audio["languageDescription"]} is available but no stream available.")
if "new_track_id" in audio and "id" in audio:
unavailable_audio_tracks.append((audio["new_track_id"], audio["id"])) # Assign to `unavailable_subtitle` for request missing audio tracks later
if hydrate_tracks:
self.log.debug(f"Audio track at index {audio_index}, audio_id: {audio_id}, language: {audio_lang} has no streams available")
continue
@@ -903,7 +910,7 @@ class Netflix(Service):
# This subtitles is there but has to request stream first
if "new_track_id" in subtitle and "id" in subtitle:
unavailable_subtitle.append((subtitle["new_track_id"], subtitle["id"])) # Assign to `unavailable_subtitle` for request missing subtitles later
# self.log.debug(f"Audio language: {subtitle["languageDescription"]} id: {subtitle["new_track_id"]} is not hydrated.")
if hydrate_tracks:
self.log.debug(f"Subtitle track at index {subtitle_index}, subtitle_id: {subtitle_id}, language: {subtitle_lang} is not hydrated")
continue
@@ -1189,6 +1196,31 @@ class Netflix(Service):
return hydrated_tracks
def create_drm(self, pssh: str, kid: str) -> DRM_T:
if self.drm_system == "widevine":
return Widevine(PSSH(pssh), kid)
elif self.drm_system == "playready":
return PlayReady(PlayReadyPSSH(pssh), kid, pssh)
else:
raise ValueError("Unknown DRM system while creating DRM")
def get_drm_system(self) -> Literal["widevine", "playready"]:
# This is widevine?
if isinstance(self.cdm, Widevine):
return "widevine"
elif isinstance(self.cdm, PlayReady):
return "playready"
else:
# Maybe this is DecryptLabsRemoteCDM
from unshackle.core.cdm import DecryptLabsRemoteCDM
if (isinstance(self.cdm, DecryptLabsRemoteCDM)):
# Is Decrypt Labs using PlayReady?
if self.cdm.is_playready:
return "playready"
else:
return "widevine"
raise ValueError("Unknown DRM system")
def _log_hydration_attempt(self, hydration_index: int, audio_data: tuple, subtitle_data: tuple,
is_real_audio: bool, is_real_subtitle: bool) -> None:
"""Log hydration attempt details."""