forked from kenzuya/unshackle
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:
@@ -179,6 +179,7 @@ class Netflix(Service):
|
|||||||
return titles
|
return titles
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_tracks(self, title: Title_T) -> Tracks:
|
def get_tracks(self, title: Title_T) -> 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:
|
if not self.msl:
|
||||||
self.log.error(f"MSL Client is not initialized for title_id: {title_id}")
|
self.log.error(f"MSL Client is not initialized for title_id: {title_id}")
|
||||||
@@ -777,6 +778,7 @@ class Netflix(Service):
|
|||||||
|
|
||||||
# Process audio tracks
|
# Process audio tracks
|
||||||
unavailable_audio_tracks: List[Tuple[str, str]] = []
|
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:
|
if "audio_tracks" in manifest:
|
||||||
for audio_index, audio in enumerate(manifest["audio_tracks"]):
|
for audio_index, audio in enumerate(manifest["audio_tracks"]):
|
||||||
try:
|
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
|
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")
|
self.log.debug(f"Audio track at index {audio_index}, audio_id: {audio_id}, language: {audio_lang} has no streams available")
|
||||||
continue
|
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"]}")
|
# self.log.debug(f"Adding audio lang: {audio["language"]} with profile: {audio["content_profile"]}")
|
||||||
is_original_lang = audio.get("language") == original_language.language
|
is_original_lang = audio.get("language") == original_language.language
|
||||||
# self.log.info(f"is audio {audio["languageDescription"]} original language: {is_original_lang}")
|
# 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)
|
hydration_info = " and ".join(hydration_parts)
|
||||||
self.log.info(f"Hydrating {hydration_info} tracks. Total: {audio_count + subtitle_count}")
|
self.log.info(f"Hydrating {hydration_info} tracks. Total: {audio_count + subtitle_count}")
|
||||||
|
|
||||||
# Handle mismatched lengths - use last successful subtitle track when needed
|
# Handle mismatched lengths - use last successful tracks when needed
|
||||||
last_successful_subtitle = ("N/A", "N/A") if not unavailable_subtitle else unavailable_subtitle[-1]
|
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
|
# Process audio tracks first, then handle subtitles separately if needed
|
||||||
max_length = max(len(unavailable_audio_tracks), len(unavailable_subtitle))
|
max_length = max(len(unavailable_audio_tracks), len(unavailable_subtitle))
|
||||||
|
|
||||||
for hydration_index in range(max_length):
|
for hydration_index in range(max_length):
|
||||||
# Get audio track info for this index
|
# Get audio track info for this index, or use last successful one if available
|
||||||
audio_hydration = unavailable_audio_tracks[hydration_index] if hydration_index < len(unavailable_audio_tracks) else ("N/A", "N/A")
|
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
|
# Get subtitle track info for this index, or use last successful one if available
|
||||||
if hydration_index < len(unavailable_subtitle):
|
if hydration_index < len(unavailable_subtitle):
|
||||||
@@ -919,33 +942,38 @@ class Netflix(Service):
|
|||||||
subtitle_hydration = last_successful_subtitle
|
subtitle_hydration = last_successful_subtitle
|
||||||
is_real_subtitle_request = False # This is just for context, don't add to tracks
|
is_real_subtitle_request = False # This is just for context, don't add to tracks
|
||||||
else:
|
else:
|
||||||
subtitle_hydration = ("N/A", "N/A")
|
subtitle_hydration = self._get_empty_track_tuple()
|
||||||
is_real_subtitle_request = False
|
is_real_subtitle_request = False
|
||||||
|
|
||||||
try:
|
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}")
|
|
||||||
|
# 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
|
||||||
|
|
||||||
# Only call get_manifest if we have audio to hydrate
|
# Log what we're trying to hydrate
|
||||||
should_hydrate_audio = audio_hydration[0] != 'N/A' and audio_hydration[1] != 'N/A'
|
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:
|
if not should_hydrate_audio:
|
||||||
self.log.debug(f"Skipping hydration at index {hydration_index} - no audio tracks to hydrate")
|
self.log.debug(f"Skipping hydration at index {hydration_index} - no audio tracks to hydrate")
|
||||||
continue
|
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 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")
|
self.log.warning(f"Skipping hydration at index {hydration_index} - no subtitle track available for API request context")
|
||||||
continue
|
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:
|
try:
|
||||||
audios = next((item for item in hydrated_manifest["audio_tracks"] if 'id' in item and item["id"] == audio_hydration[1]), None)
|
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:
|
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}")
|
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:
|
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}")
|
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)
|
# 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:
|
try:
|
||||||
subtitles = next((item for item in hydrated_manifest["timedtexttracks"] if 'id' in item and item["id"] == subtitle_hydration[1]), None)
|
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:
|
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}")
|
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:
|
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}")
|
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)")
|
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:
|
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
|
continue
|
||||||
else:
|
else:
|
||||||
self.log.info("No tracks need hydration")
|
self.log.info("No tracks need hydration")
|
||||||
@@ -1063,4 +1093,23 @@ class Netflix(Service):
|
|||||||
return Video.Range.SDR
|
return Video.Range.SDR
|
||||||
|
|
||||||
# If profile not found, return SDR as default
|
# If profile not found, return SDR as default
|
||||||
return Video.Range.SDR
|
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}"
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user