diff --git a/unshackle/services/Netflix/__init__.py b/unshackle/services/Netflix/__init__.py index 970cdeb..f59cf2f 100644 --- a/unshackle/services/Netflix/__init__.py +++ b/unshackle/services/Netflix/__init__.py @@ -750,7 +750,7 @@ class Netflix(Service): channels=stream["channels"], descriptive=audio.get("rawTrackType", "").lower() == "assistive", name="[Original]" if Language.get(audio["language"]).language == original_language.language else None, - joc=6 if "atmos" in stream["content_profile"] else None + joc=16 if "atmos" in stream["content_profile"] else None ) ) except Exception as e: @@ -831,124 +831,136 @@ class Netflix(Service): return tracks # Hydrate missing tracks - self.log.info(f"Getting all missing audio and subtitle tracks") - - # Handle mismatched lengths - use last successful subtitle track when needed - last_successful_subtitle = ("N/A", "N/A") if not unavailable_subtitle else unavailable_subtitle[-1] - - # Process audio tracks first, then handle subtitles separately if needed - max_length = max(len(unavailable_audio_tracks), len(unavailable_subtitle)) - - for hydration_index in range(max_length): - # Get audio track info for this index - audio_hydration = unavailable_audio_tracks[hydration_index] if hydration_index < len(unavailable_audio_tracks) else ("N/A", "N/A") + if unavailable_audio_tracks or unavailable_subtitle: + # Show hydration information once + audio_count = len(unavailable_audio_tracks) + subtitle_count = len(unavailable_subtitle) - # Get subtitle track info for this index, or use last successful one if available - if hydration_index < len(unavailable_subtitle): - subtitle_hydration = unavailable_subtitle[hydration_index] - is_real_subtitle_request = True # This is a real subtitle to be added to tracks - elif unavailable_subtitle: # Use last successful subtitle track for context only - subtitle_hydration = last_successful_subtitle - is_real_subtitle_request = False # This is just for context, don't add to tracks - else: - subtitle_hydration = ("N/A", "N/A") - is_real_subtitle_request = False + hydration_parts = [] + if audio_count > 0: + hydration_parts.append(f"audio ({audio_count})") + if subtitle_count > 0: + hydration_parts.append(f"subtitle ({subtitle_count})") - try: - # Log what we're trying to hydrate - self.log.debug(f"Hydrating tracks at index {hydration_index}, audio_track_id: {audio_hydration[1] if audio_hydration[1] != 'N/A' else 'N/A'}, subtitle_track_id: {subtitle_hydration[1] if subtitle_hydration[1] != 'N/A' else 'N/A'}, is_real_subtitle: {is_real_subtitle_request}") + hydration_info = " and ".join(hydration_parts) + self.log.info(f"Hydrating {hydration_info} tracks. Total: {audio_count + subtitle_count}") + + # Handle mismatched lengths - use last successful subtitle track when needed + last_successful_subtitle = ("N/A", "N/A") if not unavailable_subtitle else unavailable_subtitle[-1] + + # Process audio tracks first, then handle subtitles separately if needed + max_length = max(len(unavailable_audio_tracks), len(unavailable_subtitle)) + + for hydration_index in range(max_length): + # Get audio track info for this index + audio_hydration = unavailable_audio_tracks[hydration_index] if hydration_index < len(unavailable_audio_tracks) else ("N/A", "N/A") - # Only call get_manifest if we have audio to hydrate - should_hydrate_audio = audio_hydration[0] != 'N/A' and audio_hydration[1] != 'N/A' - - if not should_hydrate_audio: - self.log.debug(f"Skipping hydration at index {hydration_index} - no audio tracks to hydrate") - continue + # Get subtitle track info for this index, or use last successful one if available + if hydration_index < len(unavailable_subtitle): + subtitle_hydration = unavailable_subtitle[hydration_index] + is_real_subtitle_request = True # This is a real subtitle to be added to tracks + elif unavailable_subtitle: # Use last successful subtitle track for context only + subtitle_hydration = last_successful_subtitle + is_real_subtitle_request = False # This is just for context, don't add to tracks + else: + subtitle_hydration = ("N/A", "N/A") + is_real_subtitle_request = False + + try: + # Log what we're trying to hydrate + self.log.debug(f"Hydrating tracks at index {hydration_index}, audio_track_id: {audio_hydration[1] if audio_hydration[1] != 'N/A' else 'N/A'}, subtitle_track_id: {subtitle_hydration[1] if subtitle_hydration[1] != 'N/A' else 'N/A'}, is_real_subtitle: {is_real_subtitle_request}") - # Always use a valid subtitle track ID for the manifest request to avoid API errors - # Use the subtitle track (real or reused) if available, otherwise use N/A - subtitle_track_for_request = subtitle_hydration[0] if subtitle_hydration[0] != 'N/A' else None - - # If we still don't have a subtitle track ID, skip this hydration to avoid API error - if subtitle_track_for_request is None: - self.log.warning(f"Skipping hydration at index {hydration_index} - no subtitle track available for API request context") - continue + # Only call get_manifest if we have audio to hydrate + should_hydrate_audio = audio_hydration[0] != 'N/A' and audio_hydration[1] != 'N/A' - hydrated_manifest = self.get_manifest(title, self.profiles, subtitle_track_for_request, audio_hydration[0]) - - # Handle hydrated audio tracks - if should_hydrate_audio and "audio_tracks" in hydrated_manifest: - try: - audios = next((item for item in hydrated_manifest["audio_tracks"] if 'id' in item and item["id"] == audio_hydration[1]), None) - if audios and "streams" in audios: - audio_lang = audios.get("language", "unknown") - self.log.debug(f"Processing hydrated audio track_id: {audio_hydration[1]}, language: {audio_lang}, streams_count: {len(audios['streams'])}") - for stream_index, stream in enumerate(audios["streams"]): - try: - stream_id = stream.get("downloadable_id", "unknown") + if not should_hydrate_audio: + self.log.debug(f"Skipping hydration at index {hydration_index} - no audio tracks to hydrate") + continue + + # Always use a valid subtitle track ID for the manifest request to avoid API errors + # Use the subtitle track (real or reused) if available, otherwise use N/A + subtitle_track_for_request = subtitle_hydration[0] if subtitle_hydration[0] != 'N/A' else None + + # If we still don't have a subtitle track ID, skip this hydration to avoid API error + if subtitle_track_for_request is None: + self.log.warning(f"Skipping hydration at index {hydration_index} - no subtitle track available for API request context") + continue + + hydrated_manifest = self.get_manifest(title, self.profiles, subtitle_track_for_request, audio_hydration[0]) + + # Handle hydrated audio tracks + if should_hydrate_audio and "audio_tracks" in hydrated_manifest: + try: + audios = next((item for item in hydrated_manifest["audio_tracks"] if 'id' in item and item["id"] == audio_hydration[1]), None) + if audios and "streams" in audios: + audio_lang = audios.get("language", "unknown") + self.log.debug(f"Processing hydrated audio track_id: {audio_hydration[1]}, language: {audio_lang}, streams_count: {len(audios['streams'])}") + for stream_index, stream in enumerate(audios["streams"]): + try: + stream_id = stream.get("downloadable_id", "unknown") + tracks.add( + Audio( + id_=stream["downloadable_id"], + url=stream["urls"][0]["url"], + codec=Audio.Codec.from_netflix_profile(stream["content_profile"]), + language=Language.get(self.NF_LANG_MAP.get(audios["language"]) or audios["language"]), + is_original_lang=audios["language"] == original_language.language, + bitrate=stream["bitrate"] * 1000, + channels=stream["channels"], + descriptive=audios.get("rawTrackType", "").lower() == "assistive", + name="[Original]" if Language.get(audios["language"]).language == original_language.language else None, + joc=16 if "atmos" in stream["content_profile"] else None + ) + ) + except Exception as e: + stream_id = stream.get("downloadable_id", "unknown") if isinstance(stream, dict) else "unknown" + self.log.warning(f"Failed to process hydrated audio stream at hydration_index {hydration_index}, stream_index {stream_index}, audio_track_id: {audio_hydration[1]}, stream_id: {stream_id}, error: {e}") + continue + else: + self.log.warning(f"No audio streams found for hydrated audio_track_id: {audio_hydration[1]} at hydration_index {hydration_index}") + except Exception as e: + self.log.warning(f"Failed to find hydrated audio track at hydration_index {hydration_index}, audio_track_id: {audio_hydration[1]}, error: {e}") + + # Handle hydrated subtitle tracks (only if it's a real subtitle request, not reused) + if is_real_subtitle_request and subtitle_hydration[0] != 'N/A' and subtitle_hydration[1] != 'N/A' and "timedtexttracks" in hydrated_manifest: + try: + subtitles = next((item for item in hydrated_manifest["timedtexttracks"] if 'id' in item and item["id"] == subtitle_hydration[1]), None) + if subtitles and "downloadableIds" in subtitles and "ttDownloadables" in subtitles: + subtitle_lang = subtitles.get("language", "unknown") + self.log.debug(f"Processing hydrated subtitle track_id: {subtitle_hydration[1]}, language: {subtitle_lang}") + + id = list(subtitles["downloadableIds"].values()) + if id: + language = Language.get(subtitles["language"]) + profile = next(iter(subtitles["ttDownloadables"].keys())) + tt_downloadables = next(iter(subtitles["ttDownloadables"].values())) tracks.add( - Audio( - id_=stream["downloadable_id"], - url=stream["urls"][0]["url"], - codec=Audio.Codec.from_netflix_profile(stream["content_profile"]), - language=Language.get(self.NF_LANG_MAP.get(audios["language"]) or audios["language"]), - is_original_lang=audios["language"] == original_language.language, - bitrate=stream["bitrate"] * 1000, - channels=stream["channels"], - descriptive=audios.get("rawTrackType", "").lower() == "assistive", - name="[Original]" if Language.get(audios["language"]).language == original_language.language else None, - joc=16 if "atmos" in stream["content_profile"] else None + Subtitle( + id_=id[0], + url=tt_downloadables["urls"][0]["url"], + codec=Subtitle.Codec.from_netflix_profile(profile), + language=language, + forced=subtitles.get("isForcedNarrative", False), + cc=subtitles.get("rawTrackType") == "closedcaptions", + sdh=subtitles.get("trackVariant") == 'STRIPPED_SDH' if "trackVariant" in subtitles else False, + is_original_lang=subtitles.get("language") == original_language.language, + name=("[Original]" if language.language == original_language.language else None or "[Dubbing]" if "trackVariant" in subtitles and subtitles["trackVariant"] == "DUBTITLE" else None), ) ) - except Exception as e: - stream_id = stream.get("downloadable_id", "unknown") if isinstance(stream, dict) else "unknown" - self.log.warning(f"Failed to process hydrated audio stream at hydration_index {hydration_index}, stream_index {stream_index}, audio_track_id: {audio_hydration[1]}, stream_id: {stream_id}, error: {e}") - continue - else: - self.log.warning(f"No audio streams found for hydrated audio_track_id: {audio_hydration[1]} at hydration_index {hydration_index}") - except Exception as e: - self.log.warning(f"Failed to find hydrated audio track at hydration_index {hydration_index}, audio_track_id: {audio_hydration[1]}, error: {e}") - - # Handle hydrated subtitle tracks (only if it's a real subtitle request, not reused) - if is_real_subtitle_request and subtitle_hydration[0] != 'N/A' and subtitle_hydration[1] != 'N/A' and "timedtexttracks" in hydrated_manifest: - try: - subtitles = next((item for item in hydrated_manifest["timedtexttracks"] if 'id' in item and item["id"] == subtitle_hydration[1]), None) - if subtitles and "downloadableIds" in subtitles and "ttDownloadables" in subtitles: - subtitle_lang = subtitles.get("language", "unknown") - self.log.debug(f"Processing hydrated subtitle track_id: {subtitle_hydration[1]}, language: {subtitle_lang}") - # self.log.info(jsonpickle.encode(subtitles, indent=2)) - # sel - - id = list(subtitles["downloadableIds"].values()) - if id: - language = Language.get(subtitles["language"]) - profile = next(iter(subtitles["ttDownloadables"].keys())) - tt_downloadables = next(iter(subtitles["ttDownloadables"].values())) - tracks.add( - Subtitle( - id_=id[0], - url=tt_downloadables["urls"][0]["url"], - codec=Subtitle.Codec.from_netflix_profile(profile), - language=language, - forced=subtitles.get("isForcedNarrative", False), - cc=subtitles.get("rawTrackType") == "closedcaptions", - sdh=subtitles.get("trackVariant") == 'STRIPPED_SDH' if "trackVariant" in subtitles else False, - is_original_lang=subtitles.get("language") == original_language.language, - name=("[Original]" if language.language == original_language.language else None or "[Dubbing]" if "trackVariant" in subtitles and subtitles["trackVariant"] == "DUBTITLE" else None), - ) - ) + else: + self.log.warning(f"No downloadable IDs found for hydrated subtitle_track_id: {subtitle_hydration[1]} at hydration_index {hydration_index}") else: - self.log.warning(f"No downloadable IDs found for hydrated subtitle_track_id: {subtitle_hydration[1]} at hydration_index {hydration_index}") - else: - self.log.warning(f"No subtitle data found for hydrated subtitle_track_id: {subtitle_hydration[1]} at hydration_index {hydration_index}") - except Exception as e: - self.log.warning(f"Failed to process hydrated subtitle track at hydration_index {hydration_index}, subtitle_track_id: {subtitle_hydration[1]}, error: {e}") - elif not is_real_subtitle_request and subtitle_hydration[1] != 'N/A': - self.log.debug(f"Used subtitle track context for API request at hydration_index {hydration_index}, subtitle_track_id: {subtitle_hydration[1]} (not adding to tracks)") - - except Exception as e: - self.log.warning(f"Failed to hydrate tracks at hydration_index {hydration_index}, audio_track_id: {audio_hydration[1] if audio_hydration[1] != 'N/A' else 'N/A'}, subtitle_track_id: {subtitle_hydration[1] if subtitle_hydration[1] != 'N/A' else 'N/A'}, error: {e}") - continue + self.log.warning(f"No subtitle data found for hydrated subtitle_track_id: {subtitle_hydration[1]} at hydration_index {hydration_index}") + except Exception as e: + self.log.warning(f"Failed to process hydrated subtitle track at hydration_index {hydration_index}, subtitle_track_id: {subtitle_hydration[1]}, error: {e}") + elif not is_real_subtitle_request and subtitle_hydration[1] != 'N/A': + self.log.debug(f"Used subtitle track context for API request at hydration_index {hydration_index}, subtitle_track_id: {subtitle_hydration[1]} (not adding to tracks)") + + except Exception as e: + self.log.warning(f"Failed to hydrate tracks at hydration_index {hydration_index}, audio_track_id: {audio_hydration[1] if audio_hydration[1] != 'N/A' else 'N/A'}, subtitle_track_id: {subtitle_hydration[1] if subtitle_hydration[1] != 'N/A' else 'N/A'}, error: {e}") + continue + else: + self.log.info("No tracks need hydration") except Exception as e: self.log.error(f"Exception in manifest_as_tracks: {e}")