forked from kenzuya/unshackle
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
This commit is contained in:
@@ -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,7 +856,8 @@ 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
|
||||
self.log.debug(f"Audio track at index {audio_index}, audio_id: {audio_id}, language: {audio_lang} has no streams available")
|
||||
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
|
||||
|
||||
# Store primary audio track info (new_track_id, id) for potential use in hydration
|
||||
@@ -903,8 +910,8 @@ 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.")
|
||||
self.log.debug(f"Subtitle track at index {subtitle_index}, subtitle_id: {subtitle_id}, language: {subtitle_lang} 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
|
||||
|
||||
if subtitle.get("languageDescription") == 'Off' and self.descriptive_subtitles == False:
|
||||
@@ -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."""
|
||||
@@ -1198,4 +1230,4 @@ class Netflix(Service):
|
||||
f"Hydrating tracks at index {hydration_index}, "
|
||||
f"audio_track_id: {audio_id}, subtitle_track_id: {subtitle_id}, "
|
||||
f"is_real_audio: {is_real_audio}, is_real_subtitle: {is_real_subtitle}"
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user