From 7fe4be4542bb286be2343836caf763d3c03a94ef Mon Sep 17 00:00:00 2001 From: kenzuyaa Date: Mon, 8 Sep 2025 20:42:33 +0700 Subject: [PATCH] refactor(netflix): support multiple video ranges and improve profile handling - Add support for processing multiple video ranges in parallel - Handle HYBRID range by fetching HDR10 and DV profiles separately - Introduce get_profiles_for_range method to retrieve profiles for given range - Refactor profile fetching logic with detailed logging and error handling - Validate all requested video ranges are supported by the current codec - Allow H.264 codec only with SDR range and enforce checks for multiple ranges - Improve track hydration logic to avoid duplicates across ranges and profiles - Add logging for multi-range processing and profile fetching details --- unshackle/services/Netflix/__init__.py | 149 +++++++++++++++++-------- 1 file changed, 105 insertions(+), 44 deletions(-) diff --git a/unshackle/services/Netflix/__init__.py b/unshackle/services/Netflix/__init__.py index e7d8f55..58fd861 100644 --- a/unshackle/services/Netflix/__init__.py +++ b/unshackle/services/Netflix/__init__.py @@ -207,49 +207,66 @@ class Netflix(Service): except Exception as e: self.log.error(e) else: - if self.range[0] == Video.Range.HYBRID: - # Handle HYBRID mode by getting HDR10 and DV profiles separately + # Handle multiple video ranges + for range_index, video_range in enumerate(self.range): try: - # Get HDR10 profiles for the current codec - hdr10_profiles = self.config["profiles"]["video"][self.vcodec.extension.upper()].get("HDR10", []) - if hdr10_profiles: - self.log.info("Fetching HDR10 tracks for hybrid processing") - hdr10_manifest = self.get_manifest(title, hdr10_profiles) - hdr10_tracks = self.manifest_as_tracks(hdr10_manifest, title, self.hydrate_track) - tracks.add(hdr10_tracks) - else: - self.log.warning(f"No HDR10 profiles found for codec {self.vcodec.extension.upper()}") + # Only hydrate tracks on the first range to avoid duplicates + should_hydrate = self.hydrate_track and range_index == 0 - # Get DV profiles for the current codec - dv_profiles = self.config["profiles"]["video"][self.vcodec.extension.upper()].get("DV", []) - if dv_profiles: - self.log.info("Fetching DV tracks for hybrid processing") - dv_manifest = self.get_manifest(title, dv_profiles) - dv_tracks = self.manifest_as_tracks(dv_manifest, title, False) # Don't hydrate again - tracks.add(dv_tracks.videos) + if video_range == Video.Range.HYBRID: + # Handle HYBRID mode by getting HDR10 and DV profiles separately + # Get HDR10 profiles for the current codec + hdr10_profiles = self.config["profiles"]["video"][self.vcodec.extension.upper()].get("HDR10", []) + if hdr10_profiles: + self.log.info(f"Fetching HDR10 tracks for HYBRID processing (range {range_index + 1}/{len(self.range)})") + hdr10_manifest = self.get_manifest(title, hdr10_profiles) + hdr10_tracks = self.manifest_as_tracks(hdr10_manifest, title, should_hydrate) + tracks.add(hdr10_tracks) + else: + self.log.warning(f"No HDR10 profiles found for codec {self.vcodec.extension.upper()}") + + # Get DV profiles for the current codec + dv_profiles = self.config["profiles"]["video"][self.vcodec.extension.upper()].get("DV", []) + if dv_profiles: + self.log.info(f"Fetching DV tracks for HYBRID processing (range {range_index + 1}/{len(self.range)})") + dv_manifest = self.get_manifest(title, dv_profiles) + dv_tracks = self.manifest_as_tracks(dv_manifest, title, False) # Don't hydrate DV tracks + tracks.add(dv_tracks.videos) + else: + self.log.warning(f"No DV profiles found for codec {self.vcodec.extension.upper()}") + + elif self.high_bitrate: + # Get profiles for the current range + range_profiles = self.get_profiles_for_range(video_range) + if not range_profiles: + self.log.warning(f"No profiles found for range {video_range.name}") + continue + + splitted_profiles = self.split_profiles(range_profiles) + for profile_index, profile_list in enumerate(splitted_profiles): + try: + 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) + except Exception: + self.log.error(f"Error getting profile: {profile_list} for range {video_range.name}. Skipping") + continue else: - self.log.warning(f"No DV profiles found for codec {self.vcodec.extension.upper()}") + # Get profiles for the current range + range_profiles = self.get_profiles_for_range(video_range) + if not range_profiles: + self.log.warning(f"No profiles found for range {video_range.name}") + continue + + 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) except Exception as e: - self.log.error(f"Error in HYBRID mode processing: {e}") - elif self.high_bitrate: - splitted_profiles = self.split_profiles(self.profiles) - for index, profile_list in enumerate(splitted_profiles): - try: - self.log.debug(f"Index: {index}. Getting profiles: {profile_list}") - manifest = self.get_manifest(title, profile_list) - manifest_tracks = self.manifest_as_tracks(manifest, title, self.hydrate_track if index == 0 else False) - tracks.add(manifest_tracks if index == 0 else manifest_tracks.videos) - except Exception: - self.log.error(f"Error getting profile: {profile_list}. Skipping") - continue - else: - try: - manifest = self.get_manifest(title, self.profiles) - manifest_tracks = self.manifest_as_tracks(manifest, title, self.hydrate_track) - tracks.add(manifest_tracks) - except Exception as e: - self.log.error(e) + self.log.error(f"Error processing range {video_range.name}: {e}") + continue @@ -400,15 +417,25 @@ class Netflix(Service): self.log.error(f"Video range {self.range[0].name} is not supported by Video Codec: {self.vcodec}") sys.exit(1) - if len(self.range) > 1: - self.log.error(f"Multiple video range is not supported right now.") - sys.exit(1) + # Validate all ranges are supported + for video_range in self.range: + if video_range.name not in list(self.config["profiles"]["video"][self.vcodec.extension.upper()].keys()) and video_range != Video.Range.HYBRID and self.vcodec != Video.Codec.AVC and self.vcodec != Video.Codec.VP9: + self.log.error(f"Video range {video_range.name} is not supported by Video Codec: {self.vcodec}") + sys.exit(1) - if self.vcodec == Video.Codec.AVC and self.range[0] != Video.Range.SDR: - self.log.error(f"H.264 Video Codec only supports SDR") - sys.exit(1) + if self.vcodec == Video.Codec.AVC: + for video_range in self.range: + if video_range != Video.Range.SDR: + self.log.error(f"H.264 Video Codec only supports SDR, but {video_range.name} was requested") + sys.exit(1) self.profiles = self.get_profiles() + + # Log information about video ranges being processed + if len(self.range) > 1: + range_names = [r.name for r in self.range] + self.log.info(f"Processing multiple video ranges: {', '.join(range_names)}") + self.log.info("Intializing a MSL client") self.get_esn() # if self.cdm.security_level == 1: @@ -472,6 +499,40 @@ class Netflix(Service): self.log.debug(f"Result_profiles: {result_profiles}") return result_profiles + def get_profiles_for_range(self, video_range: Video.Range) -> List[str]: + """ + Get profiles for a specific video range. + + Args: + video_range: The video range to get profiles for + + Returns: + List of profile strings for the specified range + """ + result_profiles = [] + + # Handle case for codec VP9 + if self.vcodec == Video.Codec.VP9 and video_range != Video.Range.HDR10: + result_profiles.extend(self.config["profiles"]["video"][self.vcodec.extension.upper()].values()) + return result_profiles + + # Get profiles for the specific range + codec_profiles = self.config["profiles"]["video"][self.vcodec.extension.upper()] + + if video_range.name in codec_profiles: + result_profiles.extend(codec_profiles[video_range.name]) + elif video_range == Video.Range.HYBRID: + # For hybrid, use HDR10 profiles + if "HDR10" in codec_profiles: + result_profiles.extend(codec_profiles["HDR10"]) + else: + self.log.warning(f"No HDR10 profiles found for HYBRID range in codec {self.vcodec.extension.upper()}") + else: + self.log.warning(f"Range {video_range.name} not found in codec {self.vcodec.extension.upper()} profiles") + + self.log.debug(f"Profiles for range {video_range.name}: {result_profiles}") + return result_profiles + def get_esn(self): if self.cdm.device_type == DeviceTypes.ANDROID: try: