refactor(Netflix): improve audio and subtitle track hydration logic

- Add primary audio track storage for subtitle-only hydration cases
- Introduce helper methods for track validation, empty track tuple, and logging hydration attempts
- Enhance hydration loop to reuse last successful or primary audio tracks for context
- Log detailed hydration attempt information including track IDs and request types
- Use None in API calls instead of 'N/A' for missing tracks to prevent errors
- Comment out debug log for video profiles to reduce noise
- Simplify handling of mismatched audio and subtitle hydration lengths with improved track fallback logic
This commit is contained in:
2025-09-02 04:11:53 +07:00
parent ae3f896348
commit 0cf2367781

View File

@@ -180,6 +180,7 @@ class Netflix(Service):
def get_tracks(self, title: Title_T) -> Tracks:
tracks = Tracks()
@@ -571,7 +572,7 @@ class Netflix(Service):
self.log.debug("Profiles:\n\t" + "\n\t".join(video_profiles))
# self.log.debug("Profiles:\n\t" + "\n\t".join(video_profiles))
if not self.msl:
self.log.error(f"MSL Client is not initialized for title_id: {title_id}")
@@ -777,6 +778,7 @@ class Netflix(Service):
# Process audio tracks
unavailable_audio_tracks: List[Tuple[str, str]] = []
primary_audio_tracks: List[Tuple[str, str]] = [] # Store primary audio tracks with streams
if "audio_tracks" in manifest:
for audio_index, audio in enumerate(manifest["audio_tracks"]):
try:
@@ -789,6 +791,11 @@ class Netflix(Service):
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")
continue
# Store primary audio track info (new_track_id, id) for potential use in hydration
if "new_track_id" in audio and "id" in audio:
primary_audio_tracks.append((audio["new_track_id"], audio["id"]))
# self.log.debug(f"Adding audio lang: {audio["language"]} with profile: {audio["content_profile"]}")
is_original_lang = audio.get("language") == original_language.language
# self.log.info(f"is audio {audio["languageDescription"]} original language: {is_original_lang}")
@@ -901,15 +908,31 @@ class Netflix(Service):
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]
# Handle mismatched lengths - use last successful tracks when needed
last_successful_subtitle = self._get_empty_track_tuple() if not unavailable_subtitle else unavailable_subtitle[-1]
last_successful_audio = self._get_empty_track_tuple() if not unavailable_audio_tracks else unavailable_audio_tracks[-1]
# For subtitle-only hydration, use primary audio track if available
primary_audio_for_subtitle_hydration = primary_audio_tracks[0] if primary_audio_tracks and not unavailable_audio_tracks and unavailable_subtitle else self._get_empty_track_tuple()
# 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")
# Get audio track info for this index, or use last successful one if available
if hydration_index < len(unavailable_audio_tracks):
audio_hydration = unavailable_audio_tracks[hydration_index]
is_real_audio_request = True # This is a real audio to be added to tracks
elif unavailable_audio_tracks: # Use last successful audio track for context only
audio_hydration = last_successful_audio
is_real_audio_request = False # This is just for context, don't add to tracks
elif primary_audio_for_subtitle_hydration[0] is not None: # Use primary audio for subtitle hydration
audio_hydration = primary_audio_for_subtitle_hydration
is_real_audio_request = False # This is just for context, don't add to tracks
self.log.debug(f"Using primary audio track for subtitle hydration: {audio_hydration[1]}")
else:
audio_hydration = self._get_empty_track_tuple()
is_real_audio_request = False
# Get subtitle track info for this index, or use last successful one if available
if hydration_index < len(unavailable_subtitle):
@@ -919,33 +942,38 @@ class Netflix(Service):
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")
subtitle_hydration = self._get_empty_track_tuple()
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}")
# Only call get_manifest if we have audio to hydrate
should_hydrate_audio = audio_hydration[0] != 'N/A' and audio_hydration[1] != 'N/A'
# Prepare track IDs for API request - convert None to None for proper API handling
audio_track_id = audio_hydration[0] if audio_hydration[0] is not None else None
subtitle_track_id = subtitle_hydration[0] if subtitle_hydration[0] is not None else None
# Log what we're trying to hydrate
self._log_hydration_attempt(hydration_index, audio_hydration, subtitle_hydration,
is_real_audio_request, is_real_subtitle_request)
# Only call get_manifest if we have valid tracks to hydrate
should_hydrate_audio = self._is_valid_track_for_hydration(audio_hydration)
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:
if subtitle_track_id 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:
hydrated_manifest = self.get_manifest(title, self.profiles, subtitle_track_id, audio_track_id)
# Handle hydrated audio tracks (only if it's a real audio request, not reused)
if is_real_audio_request and 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:
@@ -976,9 +1004,11 @@ class Netflix(Service):
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}")
elif not is_real_audio_request and audio_hydration[1] is not None:
self.log.debug(f"Used audio track context for API request at hydration_index {hydration_index}, audio_track_id: {audio_hydration[1]} (not adding to tracks)")
# 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:
if is_real_subtitle_request and self._is_valid_track_for_hydration(subtitle_hydration) 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:
@@ -1009,11 +1039,11 @@ class Netflix(Service):
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':
elif not is_real_subtitle_request and subtitle_hydration[1] is not None:
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}")
self.log.warning(f"Failed to hydrate tracks at hydration_index {hydration_index}, audio_track_id: {audio_hydration[1] or 'None'}, subtitle_track_id: {subtitle_hydration[1] or 'None'}, error: {e}")
continue
else:
self.log.info("No tracks need hydration")
@@ -1064,3 +1094,22 @@ class Netflix(Service):
# If profile not found, return SDR as default
return Video.Range.SDR
def _is_valid_track_for_hydration(self, track_data: tuple) -> bool:
"""Check if track data is valid for hydration (not None values)."""
return track_data[0] is not None and track_data[1] is not None
def _get_empty_track_tuple(self) -> tuple:
"""Return an empty track tuple with None values."""
return (None, None)
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."""
audio_id = audio_data[1] if audio_data[1] is not None else 'None'
subtitle_id = subtitle_data[1] if subtitle_data[1] is not None else 'None'
self.log.debug(
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}"
)