Retry Tidal download at lower quality when hi-res returns garbage file
This commit is contained in:
parent
a512d6ae70
commit
c3eecc88ad
1 changed files with 108 additions and 100 deletions
|
|
@ -439,133 +439,141 @@ class TidalDownloadClient:
|
|||
quality_info = QUALITY_MAP.get(quality_key, QUALITY_MAP['lossless'])
|
||||
|
||||
# Try quality fallback chain: hires → lossless → high → low
|
||||
# The entire download+validation is inside the loop so that garbage
|
||||
# files (stubs, empty HiRes responses) trigger a retry at the next tier.
|
||||
quality_chain = ['hires', 'lossless', 'high', 'low']
|
||||
start_idx = quality_chain.index(quality_key) if quality_key in quality_chain else 1
|
||||
chain = quality_chain[start_idx:]
|
||||
|
||||
stream = None
|
||||
actual_quality = None
|
||||
MIN_AUDIO_SIZE = 100 * 1024 # 100KB
|
||||
|
||||
for q_key in chain:
|
||||
q_info = QUALITY_MAP[q_key]
|
||||
|
||||
# --- Step 1: Get stream ---
|
||||
try:
|
||||
# Set session quality before requesting stream
|
||||
self.session.audio_quality = q_info['tidal_quality']
|
||||
stream = track.get_stream()
|
||||
if stream and stream.manifest_mime_type:
|
||||
actual_quality = q_info
|
||||
logger.info(f"Got Tidal stream at quality: {q_key}")
|
||||
break
|
||||
if not stream or not stream.manifest_mime_type:
|
||||
logger.warning(f"Quality {q_key} returned no stream, trying next")
|
||||
continue
|
||||
logger.info(f"Got Tidal stream at quality: {q_key}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Quality {q_key} unavailable: {e}")
|
||||
continue
|
||||
|
||||
if not stream:
|
||||
logger.error("No Tidal stream available at any quality")
|
||||
return None
|
||||
# --- Step 2: Parse manifest ---
|
||||
manifest = stream.get_stream_manifest()
|
||||
urls = manifest.get_urls()
|
||||
if not urls:
|
||||
logger.warning(f"No download URLs for quality {q_key}, trying next")
|
||||
continue
|
||||
|
||||
# Parse manifest to get download URL
|
||||
manifest = stream.get_stream_manifest()
|
||||
urls = manifest.get_urls()
|
||||
if not urls:
|
||||
logger.error("No download URLs in Tidal stream manifest")
|
||||
return None
|
||||
download_url = urls[0]
|
||||
|
||||
download_url = urls[0]
|
||||
# Determine file extension from manifest
|
||||
codec = manifest.get_codecs()
|
||||
if codec and 'flac' in codec.lower():
|
||||
extension = 'flac'
|
||||
elif codec and ('mp4a' in codec.lower() or 'aac' in codec.lower()):
|
||||
extension = 'm4a'
|
||||
elif codec and 'alac' in codec.lower():
|
||||
extension = 'm4a'
|
||||
else:
|
||||
extension = q_info.get('extension', 'flac')
|
||||
|
||||
# Determine file extension from manifest
|
||||
codec = manifest.get_codecs()
|
||||
if codec and 'flac' in codec.lower():
|
||||
extension = 'flac'
|
||||
elif codec and ('mp4a' in codec.lower() or 'aac' in codec.lower()):
|
||||
extension = 'm4a'
|
||||
elif codec and 'alac' in codec.lower():
|
||||
extension = 'm4a'
|
||||
else:
|
||||
# Default based on quality
|
||||
extension = actual_quality.get('extension', 'flac') if actual_quality else 'flac'
|
||||
# Build output filename
|
||||
safe_name = re.sub(r'[<>:"/\\|?*]', '_', display_name)
|
||||
out_filename = f"{safe_name}.{extension}"
|
||||
out_path = self.download_path / out_filename
|
||||
|
||||
# Build output filename: "Artist - Title.ext"
|
||||
safe_name = re.sub(r'[<>:"/\\|?*]', '_', display_name)
|
||||
out_filename = f"{safe_name}.{extension}"
|
||||
out_path = self.download_path / out_filename
|
||||
# Check for shutdown before downloading
|
||||
if self.shutdown_check and self.shutdown_check():
|
||||
logger.info("Server shutting down, aborting Tidal download")
|
||||
return None
|
||||
|
||||
# Check for shutdown before downloading
|
||||
if self.shutdown_check and self.shutdown_check():
|
||||
logger.info("Server shutting down, aborting Tidal download")
|
||||
return None
|
||||
# --- Step 3: Download ---
|
||||
try:
|
||||
logger.info(f"Downloading from Tidal ({q_key}): {out_filename}")
|
||||
response = http_requests.get(download_url, stream=True, timeout=120)
|
||||
response.raise_for_status()
|
||||
|
||||
# Download with progress tracking
|
||||
logger.info(f"Downloading from Tidal: {out_filename}")
|
||||
response = http_requests.get(download_url, stream=True, timeout=120)
|
||||
response.raise_for_status()
|
||||
|
||||
total_size = int(response.headers.get('content-length', 0))
|
||||
downloaded = 0
|
||||
chunk_size = 64 * 1024 # 64KB chunks
|
||||
|
||||
with self._download_lock:
|
||||
if download_id in self.active_downloads:
|
||||
self.active_downloads[download_id]['size'] = total_size
|
||||
|
||||
with open(out_path, 'wb') as f:
|
||||
for chunk in response.iter_content(chunk_size=chunk_size):
|
||||
if not chunk:
|
||||
continue
|
||||
|
||||
# Check for shutdown
|
||||
if self.shutdown_check and self.shutdown_check():
|
||||
logger.info("Server shutting down, aborting Tidal download mid-stream")
|
||||
f.close()
|
||||
out_path.unlink(missing_ok=True)
|
||||
return None
|
||||
|
||||
f.write(chunk)
|
||||
downloaded += len(chunk)
|
||||
|
||||
# Update progress
|
||||
if total_size > 0:
|
||||
progress = (downloaded / total_size) * 100
|
||||
else:
|
||||
progress = 0
|
||||
total_size = int(response.headers.get('content-length', 0))
|
||||
downloaded = 0
|
||||
chunk_size = 64 * 1024 # 64KB chunks
|
||||
|
||||
with self._download_lock:
|
||||
if download_id in self.active_downloads:
|
||||
self.active_downloads[download_id]['transferred'] = downloaded
|
||||
self.active_downloads[download_id]['progress'] = round(progress, 1)
|
||||
self.active_downloads[download_id]['size'] = total_size
|
||||
|
||||
# Validate download produced a real audio file (not a stub/preview)
|
||||
# A valid audio file should be at least 100KB — anything less is likely
|
||||
# a DRM stub, preview clip, or empty response from Tidal.
|
||||
MIN_AUDIO_SIZE = 100 * 1024 # 100KB
|
||||
if downloaded < MIN_AUDIO_SIZE:
|
||||
logger.error(
|
||||
f"Tidal download too small ({downloaded} bytes) — likely a stub or "
|
||||
f"preview. Expected audio file for '{display_name}'. Deleting."
|
||||
)
|
||||
out_path.unlink(missing_ok=True)
|
||||
return None
|
||||
with open(out_path, 'wb') as f:
|
||||
for chunk in response.iter_content(chunk_size=chunk_size):
|
||||
if not chunk:
|
||||
continue
|
||||
|
||||
# HiRes FLAC in MP4 container: extract raw FLAC with FFmpeg if available
|
||||
if extension == 'flac' and self._is_mp4_container(out_path):
|
||||
extracted = self._extract_flac_from_mp4(out_path)
|
||||
if extracted:
|
||||
out_path = Path(extracted)
|
||||
else:
|
||||
# FFmpeg extraction failed — the MP4 container is not playable as-is.
|
||||
# Delete it rather than leaving an unplayable file.
|
||||
logger.error(f"Cannot extract FLAC from MP4 container and file is not playable — deleting {out_path.name}")
|
||||
if self.shutdown_check and self.shutdown_check():
|
||||
logger.info("Server shutting down, aborting Tidal download mid-stream")
|
||||
f.close()
|
||||
out_path.unlink(missing_ok=True)
|
||||
return None
|
||||
|
||||
f.write(chunk)
|
||||
downloaded += len(chunk)
|
||||
|
||||
if total_size > 0:
|
||||
progress = (downloaded / total_size) * 100
|
||||
else:
|
||||
progress = 0
|
||||
|
||||
with self._download_lock:
|
||||
if download_id in self.active_downloads:
|
||||
self.active_downloads[download_id]['transferred'] = downloaded
|
||||
self.active_downloads[download_id]['progress'] = round(progress, 1)
|
||||
|
||||
except Exception as dl_err:
|
||||
logger.warning(f"Download failed at quality {q_key}: {dl_err}")
|
||||
out_path.unlink(missing_ok=True)
|
||||
return None
|
||||
continue
|
||||
|
||||
# Final size check after any extraction
|
||||
final_size = out_path.stat().st_size if out_path.exists() else 0
|
||||
if final_size < MIN_AUDIO_SIZE:
|
||||
logger.error(f"Final file too small after processing ({final_size} bytes) — deleting {out_path.name}")
|
||||
out_path.unlink(missing_ok=True)
|
||||
return None
|
||||
# --- Step 4: Validate ---
|
||||
if downloaded < MIN_AUDIO_SIZE:
|
||||
logger.warning(
|
||||
f"Tidal download too small at {q_key} ({downloaded} bytes) — "
|
||||
f"likely a stub/preview for '{display_name}'. Trying next quality."
|
||||
)
|
||||
out_path.unlink(missing_ok=True)
|
||||
continue
|
||||
|
||||
logger.info(f"Tidal download complete: {out_path} ({final_size / (1024*1024):.1f} MB)")
|
||||
return str(out_path)
|
||||
# HiRes FLAC in MP4 container: extract raw FLAC with FFmpeg
|
||||
if extension == 'flac' and self._is_mp4_container(out_path):
|
||||
extracted = self._extract_flac_from_mp4(out_path)
|
||||
if extracted:
|
||||
out_path = Path(extracted)
|
||||
else:
|
||||
logger.warning(
|
||||
f"Cannot extract FLAC from MP4 container at {q_key} — "
|
||||
f"deleting and trying next quality"
|
||||
)
|
||||
out_path.unlink(missing_ok=True)
|
||||
continue
|
||||
|
||||
# Final size check after any extraction
|
||||
final_size = out_path.stat().st_size if out_path.exists() else 0
|
||||
if final_size < MIN_AUDIO_SIZE:
|
||||
logger.warning(
|
||||
f"Final file too small after processing at {q_key} "
|
||||
f"({final_size} bytes) — trying next quality"
|
||||
)
|
||||
out_path.unlink(missing_ok=True)
|
||||
continue
|
||||
|
||||
# Success — file is valid
|
||||
logger.info(f"Tidal download complete ({q_key}): {out_path} ({final_size / (1024*1024):.1f} MB)")
|
||||
return str(out_path)
|
||||
|
||||
# All quality tiers exhausted
|
||||
logger.error(f"No Tidal quality tier produced a valid download for '{display_name}'")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Tidal download failed: {e}")
|
||||
|
|
|
|||
Loading…
Reference in a new issue