from vinetrimmer.objects.tracks import Tracks from vinetrimmer.utils.widevine.device import LocalDevice def _hydrate_hidden_tracks(self, title, manifest, tracks, license_url): """ Hydrate hidden audio and subtitle tracks. Netflix does not include all download URLs for audio/subtitle tracks in the initial manifest for performance optimization. This method sequentially requests these hidden tracks. """ from itertools import zip_longest # Collect unhydrated audio tracks unavailable_audio_tracks = [] for audio in manifest.get("audio_tracks", []): if len(audio.get("streams", [])) == 0: # Audio is available but has no stream information self.log.info(f"Found hidden audio track: {audio.get('languageDescription', 'Unknown')}") unavailable_audio_tracks.append(( audio.get("new_track_id"), audio.get("id") )) # Collect unhydrated subtitle tracks unavailable_subtitle_tracks = [] for subtitle in manifest.get("timedtexttracks", []): if subtitle.get("isNoneTrack"): continue if subtitle.get("hydrated") == False: # Subtitle is not hydrated # self.log.info(f"Found hidden subtitle track: {subtitle.get('languageDescription', 'Unknown')}") unavailable_subtitle_tracks.append(( subtitle.get("new_track_id"), subtitle.get("id") )) if not unavailable_audio_tracks and not unavailable_subtitle_tracks: self.log.info("No hidden audio or subtitle tracks found") return self.log.info(f"Fetching {len(unavailable_audio_tracks)} hidden audio track(s) and {len(unavailable_subtitle_tracks)} hidden subtitle track(s)...") # Hydrate hidden tracks one by one for audio_info, subtitle_info in zip_longest( unavailable_audio_tracks, unavailable_subtitle_tracks, fillvalue=(None, None) ): audio_track_id, audio_id = audio_info if audio_info[0] else (None, None) subtitle_track_id, subtitle_id = subtitle_info if subtitle_info[0] else (None, None) try: # Request specific hidden track hydrated_manifest = self.get_manifest( title, self.profiles, required_audio_track_id=audio_track_id, required_text_track_id=subtitle_track_id ) # Parse hydrated tracks hydrated_tracks = self.manifest_as_tracks(hydrated_manifest) # Add audio track if audio_track_id: for audio in manifest.get("audio_tracks", []): if audio.get("id") == audio_id: for track in hydrated_tracks.audio: if track.encrypted: track.extra["license_url"] = license_url tracks.add(track, warn_only=True) # self.log.info(f"Added hidden audio track: {track.language}") break # Add subtitle track if subtitle_track_id: for subtitle in manifest.get("timedtexttracks", []): if subtitle.get("id") == subtitle_id: for track in hydrated_tracks.subtitles: tracks.add(track, warn_only=True) # self.log.info(f"Added hidden subtitle track: {track.language}") break except Exception as e: self.log.warning(f"Error hydrating hidden track: {e}") continue def get_tracks(self, title): if self.vcodec == "H264": # If H.264, get both MPL and HPL tracks as they alternate in terms of bitrate tracks = Tracks() self.config["profiles"]["video"]["H264"]["MPL+HPL+QC"] = ( self.config["profiles"]["video"]["H264"]["MPL"] + self.config["profiles"]["video"]["H264"]["HPL"] + self.config["profiles"]["video"]["H264"]["QC"] ) if self.audio_only or self.subs_only or self.chapters_only: profiles = ["MPL+HPL+QC"] else: profiles = self.profile.split("+") for profile in profiles: try: manifest = self.get_manifest(title, self.config["profiles"]["video"]["H264"][profile]) except: manifest = self.get_manifest(title, self.config["profiles"]["video"]["H264"]["MPL"] + self.config["profiles"]["video"]["H264"]["HPL"]) manifest_tracks = self.manifest_as_tracks(manifest) license_url = manifest["links"]["license"]["href"] if self.cdm.device.security_level == 3 and self.cdm.device.type == LocalDevice.Types.CHROME: self.cdm.uuid = self.cdm.chrome_uuid self.cdm.urn = f"urn:uuid:{self.cdm.uuid}" max_quality = max(x.height for x in manifest_tracks.videos) # if profile == "MPL" and max_quality >= 720: # manifest_sd = self.get_manifest(title, self.config["profiles"]["video"]["H264"]["BPL"]) # license_url_sd = manifest_sd["links"]["license"]["href"] # if "SD_LADDER" in manifest_sd["video_tracks"][0]["streams"][0]["tags"]: # # SD manifest is new encode encrypted with different keys that won't work for HD # continue # license_url = license_url_sd if profile == "HPL" and max_quality >= 1080: if "SEGMENT_MAP_2KEY" in manifest["video_tracks"][0]["streams"][0]["tags"]: # 1080p license restricted from Android L3, 720p license will work for 1080p manifest_720 = self.get_manifest( title, [x for x in self.config["profiles"]["video"]["H264"]["HPL"] if "l40" not in x] ) license_url = manifest_720["links"]["license"]["href"] else: # Older encode, can't use 720p keys for 1080p continue for track in manifest_tracks: if track.encrypted: track.extra["license_url"] = license_url # Fix PSSH with correct KID # track.pssh.init_data = b'\x08\x01\x12\x10' + bytes.fromhex(track.kid) tracks.add(manifest_tracks, warn_only=True) # Hydrate hidden audio and subtitle tracks (using the last manifest) _hydrate_hidden_tracks(title, manifest, tracks, license_url) return tracks else: pass