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'])
|
quality_info = QUALITY_MAP.get(quality_key, QUALITY_MAP['lossless'])
|
||||||
|
|
||||||
# Try quality fallback chain: hires → lossless → high → low
|
# 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']
|
quality_chain = ['hires', 'lossless', 'high', 'low']
|
||||||
start_idx = quality_chain.index(quality_key) if quality_key in quality_chain else 1
|
start_idx = quality_chain.index(quality_key) if quality_key in quality_chain else 1
|
||||||
chain = quality_chain[start_idx:]
|
chain = quality_chain[start_idx:]
|
||||||
|
|
||||||
stream = None
|
MIN_AUDIO_SIZE = 100 * 1024 # 100KB
|
||||||
actual_quality = None
|
|
||||||
for q_key in chain:
|
for q_key in chain:
|
||||||
q_info = QUALITY_MAP[q_key]
|
q_info = QUALITY_MAP[q_key]
|
||||||
|
|
||||||
|
# --- Step 1: Get stream ---
|
||||||
try:
|
try:
|
||||||
# Set session quality before requesting stream
|
|
||||||
self.session.audio_quality = q_info['tidal_quality']
|
self.session.audio_quality = q_info['tidal_quality']
|
||||||
stream = track.get_stream()
|
stream = track.get_stream()
|
||||||
if stream and stream.manifest_mime_type:
|
if not stream or not stream.manifest_mime_type:
|
||||||
actual_quality = q_info
|
logger.warning(f"Quality {q_key} returned no stream, trying next")
|
||||||
logger.info(f"Got Tidal stream at quality: {q_key}")
|
continue
|
||||||
break
|
logger.info(f"Got Tidal stream at quality: {q_key}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Quality {q_key} unavailable: {e}")
|
logger.warning(f"Quality {q_key} unavailable: {e}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not stream:
|
# --- Step 2: Parse manifest ---
|
||||||
logger.error("No Tidal stream available at any quality")
|
manifest = stream.get_stream_manifest()
|
||||||
return None
|
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
|
download_url = urls[0]
|
||||||
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]
|
# 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
|
# Build output filename
|
||||||
codec = manifest.get_codecs()
|
safe_name = re.sub(r'[<>:"/\\|?*]', '_', display_name)
|
||||||
if codec and 'flac' in codec.lower():
|
out_filename = f"{safe_name}.{extension}"
|
||||||
extension = 'flac'
|
out_path = self.download_path / out_filename
|
||||||
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: "Artist - Title.ext"
|
# Check for shutdown before downloading
|
||||||
safe_name = re.sub(r'[<>:"/\\|?*]', '_', display_name)
|
if self.shutdown_check and self.shutdown_check():
|
||||||
out_filename = f"{safe_name}.{extension}"
|
logger.info("Server shutting down, aborting Tidal download")
|
||||||
out_path = self.download_path / out_filename
|
return None
|
||||||
|
|
||||||
# Check for shutdown before downloading
|
# --- Step 3: Download ---
|
||||||
if self.shutdown_check and self.shutdown_check():
|
try:
|
||||||
logger.info("Server shutting down, aborting Tidal download")
|
logger.info(f"Downloading from Tidal ({q_key}): {out_filename}")
|
||||||
return None
|
response = http_requests.get(download_url, stream=True, timeout=120)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
# Download with progress tracking
|
total_size = int(response.headers.get('content-length', 0))
|
||||||
logger.info(f"Downloading from Tidal: {out_filename}")
|
downloaded = 0
|
||||||
response = http_requests.get(download_url, stream=True, timeout=120)
|
chunk_size = 64 * 1024 # 64KB chunks
|
||||||
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
|
|
||||||
|
|
||||||
with self._download_lock:
|
with self._download_lock:
|
||||||
if download_id in self.active_downloads:
|
if download_id in self.active_downloads:
|
||||||
self.active_downloads[download_id]['transferred'] = downloaded
|
self.active_downloads[download_id]['size'] = total_size
|
||||||
self.active_downloads[download_id]['progress'] = round(progress, 1)
|
|
||||||
|
|
||||||
# Validate download produced a real audio file (not a stub/preview)
|
with open(out_path, 'wb') as f:
|
||||||
# A valid audio file should be at least 100KB — anything less is likely
|
for chunk in response.iter_content(chunk_size=chunk_size):
|
||||||
# a DRM stub, preview clip, or empty response from Tidal.
|
if not chunk:
|
||||||
MIN_AUDIO_SIZE = 100 * 1024 # 100KB
|
continue
|
||||||
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
|
|
||||||
|
|
||||||
# HiRes FLAC in MP4 container: extract raw FLAC with FFmpeg if available
|
if self.shutdown_check and self.shutdown_check():
|
||||||
if extension == 'flac' and self._is_mp4_container(out_path):
|
logger.info("Server shutting down, aborting Tidal download mid-stream")
|
||||||
extracted = self._extract_flac_from_mp4(out_path)
|
f.close()
|
||||||
if extracted:
|
out_path.unlink(missing_ok=True)
|
||||||
out_path = Path(extracted)
|
return None
|
||||||
else:
|
|
||||||
# FFmpeg extraction failed — the MP4 container is not playable as-is.
|
f.write(chunk)
|
||||||
# Delete it rather than leaving an unplayable file.
|
downloaded += len(chunk)
|
||||||
logger.error(f"Cannot extract FLAC from MP4 container and file is not playable — deleting {out_path.name}")
|
|
||||||
|
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)
|
out_path.unlink(missing_ok=True)
|
||||||
return None
|
continue
|
||||||
|
|
||||||
# Final size check after any extraction
|
# --- Step 4: Validate ---
|
||||||
final_size = out_path.stat().st_size if out_path.exists() else 0
|
if downloaded < MIN_AUDIO_SIZE:
|
||||||
if final_size < MIN_AUDIO_SIZE:
|
logger.warning(
|
||||||
logger.error(f"Final file too small after processing ({final_size} bytes) — deleting {out_path.name}")
|
f"Tidal download too small at {q_key} ({downloaded} bytes) — "
|
||||||
out_path.unlink(missing_ok=True)
|
f"likely a stub/preview for '{display_name}'. Trying next quality."
|
||||||
return None
|
)
|
||||||
|
out_path.unlink(missing_ok=True)
|
||||||
|
continue
|
||||||
|
|
||||||
logger.info(f"Tidal download complete: {out_path} ({final_size / (1024*1024):.1f} MB)")
|
# HiRes FLAC in MP4 container: extract raw FLAC with FFmpeg
|
||||||
return str(out_path)
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Tidal download failed: {e}")
|
logger.error(f"Tidal download failed: {e}")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue