Resolve Race Condition and Optimize Session Switching for Atmos Downloads

This commit is contained in:
Rikrdoga
2025-11-04 12:36:27 -06:00
parent 6e665bcd45
commit af58b0a83d
2 changed files with 134 additions and 45 deletions
+76 -22
View File
@@ -794,43 +794,97 @@ class Download:
stream_manifest: StreamManifest | None = None
media_stream: Stream | None = None
do_flac_extract: bool = False
file_extension: str = ""
if isinstance(media, Track):
# CRITICAL: This lock is intentionally broad and serializes all
# stream-fetching (Phase 1) to prevent a critical race condition.
#
# THE PROBLEM:
# The single, shared session (self.tidal.session) must change its
# credentials to switch between Atmos and Hi-Res/Normal streams.
#
# THE RACE CONDITION IT FIXES:
# If this lock is released *before* get_stream() is called,
# another thread could change the session (e.g., back to "Normal")
# right after this thread switched it to "Atmos". This would
# cause this thread to call get_stream() with the wrong credentials,
# resulting in the API returning AAC 320 instead of Atmos.
#
# THE TRADEOFF:
# This creates a "tollbooth" bottleneck, serializing the get_stream()
# calls. However, the *actual* segment downloads (Phase 2)
# still run in parallel, governed by `downloads_concurrent_max`.
#
# DO NOT "OPTIMIZE" THIS by making the lock more granular.
# Correctness > Performance.
with self.tidal.stream_lock:
try:
if (
self.settings.data.download_dolby_atmos
and hasattr(media, "audio_modes")
and AudioMode.dolby_atmos.value in media.audio_modes
):
with self.tidal.atmos_session_context():
atmos_track = self.session.track(media.id)
media_stream = atmos_track.get_stream()
else:
media_stream = media.get_stream()
if isinstance(media, Track):
stream_manifest, file_extension, do_flac_extract, media_stream = self._get_track_stream_info(media)
if stream_manifest is None:
return None, "", False, None
stream_manifest = media_stream.get_stream_manifest()
elif isinstance(media, Video):
# Videos always require the normal session
if not self.tidal.restore_normal_session():
self.fn_logger.error(f"Failed to restore normal session for video: {media.id}")
return None, "", False, None
file_extension = AudioExtensions.MP4 if self.settings.data.video_convert_mp4 else VideoExtensions.TS
stream_manifest = None
media_stream = None
do_flac_extract = False
else:
self.fn_logger.error(f"Unknown media type for stream info: {type(media)}")
return None, "", False, None
except TooManyRequests:
self.fn_logger.exception(
f"Too many requests against TIDAL backend. Skipping '{name_builder_item(media)}'. "
f"Consider to activate delay between downloads."
)
return None, "", False, None
except Exception:
self.fn_logger.exception(f"Something went wrong. Skipping '{name_builder_item(media)}'.")
return None, "", False, None
file_extension = stream_manifest.file_extension
return stream_manifest, file_extension, do_flac_extract, media_stream
if self.settings.data.extract_flac and (
stream_manifest.codecs.upper() == Codec.FLAC and file_extension != AudioExtensions.FLAC
):
file_extension = AudioExtensions.FLAC
do_flac_extract = True
elif isinstance(media, Video):
file_extension = AudioExtensions.MP4 if self.settings.data.video_convert_mp4 else VideoExtensions.TS
def _get_track_stream_info(self, media: Track) -> tuple[StreamManifest | None, str, bool, Stream | None]:
"""
Gets stream info for a Track, handling Atmos/Normal session switching.
This is a helper for _get_stream_info to reduce complexity.
"""
want_atmos = (
self.settings.data.download_dolby_atmos
and hasattr(media, "audio_modes")
and AudioMode.dolby_atmos.value in media.audio_modes
)
if want_atmos:
if not self.tidal.switch_to_atmos_session():
self.fn_logger.error(f"Failed to switch to Atmos session for track: {media.id}")
return None, "", False, None
else:
if not self.tidal.restore_normal_session():
self.fn_logger.error(f"Failed to restore normal session for track: {media.id}")
return None, "", False, None
media_stream = self.session.track(media.id).get_stream() if want_atmos else media.get_stream()
stream_manifest = media_stream.get_stream_manifest()
file_extension = stream_manifest.file_extension
do_flac_extract = False
if self.settings.data.extract_flac and (
stream_manifest.codecs.upper() == Codec.FLAC and file_extension != AudioExtensions.FLAC
):
file_extension = AudioExtensions.FLAC
do_flac_extract = True
return stream_manifest, file_extension, do_flac_extract, media_stream