forked from kenzuya/unshackle
fix(netflix): improve audio and subtitle track hydration logic
- Update joc value for atmos content profile from 6 to 16 - Add informative log message summarizing total audio and subtitle tracks to hydrate - Refactor hydration loop to handle mismatched lengths of audio and subtitle tracks more clearly - Skip hydration if no audio tracks are available for the current index - Ensure valid subtitle track ID is used in manifest request to avoid API errors - Add detailed debug logs for processing hydrated audio and subtitle streams - Handle exceptions gracefully for each stream and track hydration step with warnings - Log when no tracks need hydration to improve observability
This commit is contained in:
@@ -750,7 +750,7 @@ class Netflix(Service):
|
|||||||
channels=stream["channels"],
|
channels=stream["channels"],
|
||||||
descriptive=audio.get("rawTrackType", "").lower() == "assistive",
|
descriptive=audio.get("rawTrackType", "").lower() == "assistive",
|
||||||
name="[Original]" if Language.get(audio["language"]).language == original_language.language else None,
|
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:
|
except Exception as e:
|
||||||
@@ -831,124 +831,136 @@ class Netflix(Service):
|
|||||||
return tracks
|
return tracks
|
||||||
|
|
||||||
# Hydrate missing tracks
|
# Hydrate missing tracks
|
||||||
self.log.info(f"Getting all missing audio and subtitle tracks")
|
if unavailable_audio_tracks or unavailable_subtitle:
|
||||||
|
# Show hydration information once
|
||||||
# Handle mismatched lengths - use last successful subtitle track when needed
|
audio_count = len(unavailable_audio_tracks)
|
||||||
last_successful_subtitle = ("N/A", "N/A") if not unavailable_subtitle else unavailable_subtitle[-1]
|
subtitle_count = len(unavailable_subtitle)
|
||||||
|
|
||||||
# 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 subtitle track info for this index, or use last successful one if available
|
hydration_parts = []
|
||||||
if hydration_index < len(unavailable_subtitle):
|
if audio_count > 0:
|
||||||
subtitle_hydration = unavailable_subtitle[hydration_index]
|
hydration_parts.append(f"audio ({audio_count})")
|
||||||
is_real_subtitle_request = True # This is a real subtitle to be added to tracks
|
if subtitle_count > 0:
|
||||||
elif unavailable_subtitle: # Use last successful subtitle track for context only
|
hydration_parts.append(f"subtitle ({subtitle_count})")
|
||||||
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:
|
hydration_info = " and ".join(hydration_parts)
|
||||||
# Log what we're trying to hydrate
|
self.log.info(f"Hydrating {hydration_info} tracks. Total: {audio_count + subtitle_count}")
|
||||||
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}")
|
|
||||||
|
# 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
|
# Get subtitle track info for this index, or use last successful one if available
|
||||||
should_hydrate_audio = audio_hydration[0] != 'N/A' and audio_hydration[1] != 'N/A'
|
if hydration_index < len(unavailable_subtitle):
|
||||||
|
subtitle_hydration = unavailable_subtitle[hydration_index]
|
||||||
if not should_hydrate_audio:
|
is_real_subtitle_request = True # This is a real subtitle to be added to tracks
|
||||||
self.log.debug(f"Skipping hydration at index {hydration_index} - no audio tracks to hydrate")
|
elif unavailable_subtitle: # Use last successful subtitle track for context only
|
||||||
continue
|
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
|
# Only call get_manifest if we have audio to hydrate
|
||||||
# Use the subtitle track (real or reused) if available, otherwise use N/A
|
should_hydrate_audio = audio_hydration[0] != 'N/A' and audio_hydration[1] != '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])
|
if not should_hydrate_audio:
|
||||||
|
self.log.debug(f"Skipping hydration at index {hydration_index} - no audio tracks to hydrate")
|
||||||
# Handle hydrated audio tracks
|
continue
|
||||||
if should_hydrate_audio and "audio_tracks" in hydrated_manifest:
|
|
||||||
try:
|
# Always use a valid subtitle track ID for the manifest request to avoid API errors
|
||||||
audios = next((item for item in hydrated_manifest["audio_tracks"] if 'id' in item and item["id"] == audio_hydration[1]), None)
|
# Use the subtitle track (real or reused) if available, otherwise use N/A
|
||||||
if audios and "streams" in audios:
|
subtitle_track_for_request = subtitle_hydration[0] if subtitle_hydration[0] != 'N/A' else None
|
||||||
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'])}")
|
# If we still don't have a subtitle track ID, skip this hydration to avoid API error
|
||||||
for stream_index, stream in enumerate(audios["streams"]):
|
if subtitle_track_for_request is None:
|
||||||
try:
|
self.log.warning(f"Skipping hydration at index {hydration_index} - no subtitle track available for API request context")
|
||||||
stream_id = stream.get("downloadable_id", "unknown")
|
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(
|
tracks.add(
|
||||||
Audio(
|
Subtitle(
|
||||||
id_=stream["downloadable_id"],
|
id_=id[0],
|
||||||
url=stream["urls"][0]["url"],
|
url=tt_downloadables["urls"][0]["url"],
|
||||||
codec=Audio.Codec.from_netflix_profile(stream["content_profile"]),
|
codec=Subtitle.Codec.from_netflix_profile(profile),
|
||||||
language=Language.get(self.NF_LANG_MAP.get(audios["language"]) or audios["language"]),
|
language=language,
|
||||||
is_original_lang=audios["language"] == original_language.language,
|
forced=subtitles.get("isForcedNarrative", False),
|
||||||
bitrate=stream["bitrate"] * 1000,
|
cc=subtitles.get("rawTrackType") == "closedcaptions",
|
||||||
channels=stream["channels"],
|
sdh=subtitles.get("trackVariant") == 'STRIPPED_SDH' if "trackVariant" in subtitles else False,
|
||||||
descriptive=audios.get("rawTrackType", "").lower() == "assistive",
|
is_original_lang=subtitles.get("language") == original_language.language,
|
||||||
name="[Original]" if Language.get(audios["language"]).language == original_language.language else None,
|
name=("[Original]" if language.language == original_language.language else None or "[Dubbing]" if "trackVariant" in subtitles and subtitles["trackVariant"] == "DUBTITLE" else None),
|
||||||
joc=16 if "atmos" in stream["content_profile"] else None
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
except Exception as e:
|
else:
|
||||||
stream_id = stream.get("downloadable_id", "unknown") if isinstance(stream, dict) else "unknown"
|
self.log.warning(f"No downloadable IDs found for hydrated subtitle_track_id: {subtitle_hydration[1]} at hydration_index {hydration_index}")
|
||||||
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:
|
else:
|
||||||
self.log.warning(f"No downloadable IDs 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}")
|
||||||
else:
|
except Exception as e:
|
||||||
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"Failed to process hydrated subtitle track at hydration_index {hydration_index}, subtitle_track_id: {subtitle_hydration[1]}, error: {e}")
|
||||||
except Exception as e:
|
elif not is_real_subtitle_request and subtitle_hydration[1] != 'N/A':
|
||||||
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.debug(f"Used subtitle track context for API request at hydration_index {hydration_index}, subtitle_track_id: {subtitle_hydration[1]} (not adding to tracks)")
|
||||||
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}")
|
||||||
except Exception as e:
|
continue
|
||||||
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}")
|
else:
|
||||||
continue
|
self.log.info("No tracks need hydration")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.error(f"Exception in manifest_as_tracks: {e}")
|
self.log.error(f"Exception in manifest_as_tracks: {e}")
|
||||||
|
|||||||
Reference in New Issue
Block a user