Begin #34
This commit is contained in:
@@ -486,11 +486,11 @@ class RadioUtil:
|
||||
)
|
||||
|
||||
"""Loading Complete"""
|
||||
self.playlists_loaded = True
|
||||
# Request skip from LS to bring streams current
|
||||
for playlist in self.playlists:
|
||||
logging.info("Skipping: %s", playlist)
|
||||
await self._ls_skip(playlist)
|
||||
self.playlists_loaded = True
|
||||
except Exception as e:
|
||||
logging.info("Playlist load failed: %s", str(e))
|
||||
traceback.print_exc()
|
||||
|
||||
@@ -51,8 +51,16 @@ logger = logging.getLogger(__name__)
|
||||
async def check_flac_stream(file_path):
|
||||
"""Check if the given file contains a FLAC stream using ffprobe."""
|
||||
cmd = [
|
||||
"ffprobe", "-v", "error", "-select_streams", "a:0", "-show_entries",
|
||||
"stream=codec_name", "-of", "default=noprint_wrappers=1:nokey=1", file_path
|
||||
"ffprobe",
|
||||
"-v",
|
||||
"error",
|
||||
"-select_streams",
|
||||
"a:0",
|
||||
"-show_entries",
|
||||
"stream=codec_name",
|
||||
"-of",
|
||||
"default=noprint_wrappers=1:nokey=1",
|
||||
file_path,
|
||||
]
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
||||
@@ -282,263 +290,292 @@ def bulk_download(track_list: list, quality: str = "FLAC"):
|
||||
|
||||
# Ensure aiohttp session is properly closed
|
||||
async with aiohttp.ClientSession(headers=HEADERS) as session:
|
||||
print(f"DEBUG: Starting process_tracks with {len(track_list)} tracks")
|
||||
# Set up a one-time rate-limit callback to notify on the first 429 seen by SRUtil
|
||||
async def _rate_limit_notify(exc: Exception):
|
||||
try:
|
||||
send_log_to_discord(
|
||||
f"Rate limit observed while fetching metadata: {exc}",
|
||||
"WARNING",
|
||||
target,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
print(f"DEBUG: Starting process_tracks with {len(track_list)} tracks")
|
||||
|
||||
# attach callback and reset notified flag for this job run
|
||||
# Set up a one-time rate-limit callback to notify on the first 429 seen by SRUtil
|
||||
async def _rate_limit_notify(exc: Exception):
|
||||
try:
|
||||
sr.on_rate_limit = _rate_limit_notify
|
||||
sr._rate_limit_notified = False
|
||||
send_log_to_discord(
|
||||
f"Rate limit observed while fetching metadata: {exc}",
|
||||
"WARNING",
|
||||
target,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
total = len(track_list or [])
|
||||
for i, track_id in enumerate(track_list or []):
|
||||
print(f"DEBUG: Processing track {i+1}/{total}: {track_id}")
|
||||
track_info = {
|
||||
"track_id": str(track_id),
|
||||
"status": "Pending",
|
||||
"file_path": None,
|
||||
"error": None,
|
||||
"attempts": 0,
|
||||
}
|
||||
attempt = 0
|
||||
|
||||
while attempt < MAX_RETRIES:
|
||||
tmp_file = None
|
||||
attempt += 1
|
||||
track_info["attempts"] = attempt
|
||||
# attach callback and reset notified flag for this job run
|
||||
try:
|
||||
sr.on_rate_limit = _rate_limit_notify
|
||||
sr._rate_limit_notified = False
|
||||
except Exception:
|
||||
pass
|
||||
total = len(track_list or [])
|
||||
for i, track_id in enumerate(track_list or []):
|
||||
print(f"DEBUG: Processing track {i + 1}/{total}: {track_id}")
|
||||
track_info = {
|
||||
"track_id": str(track_id),
|
||||
"status": "Pending",
|
||||
"file_path": None,
|
||||
"error": None,
|
||||
"attempts": 0,
|
||||
}
|
||||
attempt = 0
|
||||
|
||||
while attempt < MAX_RETRIES:
|
||||
tmp_file = None
|
||||
attempt += 1
|
||||
track_info["attempts"] = attempt
|
||||
|
||||
try:
|
||||
print(f"DEBUG: Getting downloadable for track {track_id}")
|
||||
# Fetch downloadable (handles DASH and others)
|
||||
downloadable = await sr._safe_api_call(
|
||||
sr.streamrip_client.get_downloadable,
|
||||
str(track_id),
|
||||
2 if quality == "FLAC" else 1,
|
||||
retries=3,
|
||||
)
|
||||
|
||||
print(f"DEBUG: Got downloadable: {type(downloadable)}")
|
||||
if not downloadable:
|
||||
raise RuntimeError("No downloadable created")
|
||||
|
||||
ext = f".{downloadable.extension}"
|
||||
tmp_file = Path(f"/tmp/{uuid.uuid4().hex}{ext}")
|
||||
|
||||
print(f"DEBUG: Starting download to {tmp_file}")
|
||||
# Download
|
||||
print(f"TRACK {track_id}: Starting download")
|
||||
try:
|
||||
print(f"DEBUG: Getting downloadable for track {track_id}")
|
||||
# Fetch downloadable (handles DASH and others)
|
||||
downloadable = await sr._safe_api_call(
|
||||
sr.streamrip_client.get_downloadable,
|
||||
str(track_id),
|
||||
2 if quality == "FLAC" else 1,
|
||||
retries=3,
|
||||
await downloadable._download(
|
||||
str(tmp_file), callback=lambda x=None: None
|
||||
)
|
||||
print(
|
||||
f"TRACK {track_id}: Download method completed normally"
|
||||
)
|
||||
except Exception as download_e:
|
||||
print(
|
||||
f"TRACK {track_id}: Download threw exception: {download_e}"
|
||||
)
|
||||
raise
|
||||
|
||||
print(
|
||||
f"DEBUG: Download completed, file exists: {tmp_file.exists()}"
|
||||
)
|
||||
if not tmp_file.exists():
|
||||
raise RuntimeError(
|
||||
f"Download completed but no file created: {tmp_file}"
|
||||
)
|
||||
|
||||
print(f"DEBUG: Got downloadable: {type(downloadable)}")
|
||||
if not downloadable:
|
||||
raise RuntimeError("No downloadable created")
|
||||
|
||||
ext = f".{downloadable.extension}"
|
||||
tmp_file = Path(f"/tmp/{uuid.uuid4().hex}{ext}")
|
||||
|
||||
print(f"DEBUG: Starting download to {tmp_file}")
|
||||
# Download
|
||||
print(f"TRACK {track_id}: Starting download")
|
||||
try:
|
||||
await downloadable._download(str(tmp_file), callback=lambda x=None: None)
|
||||
print(f"TRACK {track_id}: Download method completed normally")
|
||||
except Exception as download_e:
|
||||
print(f"TRACK {track_id}: Download threw exception: {download_e}")
|
||||
raise
|
||||
|
||||
print(f"DEBUG: Download completed, file exists: {tmp_file.exists()}")
|
||||
if not tmp_file.exists():
|
||||
raise RuntimeError(f"Download completed but no file created: {tmp_file}")
|
||||
|
||||
print(f"DEBUG: Fetching metadata for track {track_id}")
|
||||
# Metadata fetch
|
||||
try:
|
||||
md = await sr.get_metadata_by_track_id(track_id) or {}
|
||||
print(f"DEBUG: Metadata fetched: {bool(md)}")
|
||||
except MetadataFetchError as me:
|
||||
# Permanent metadata failure — mark failed and break
|
||||
track_info["status"] = "Failed"
|
||||
track_info["error"] = str(me)
|
||||
per_track_meta.append(track_info)
|
||||
if job:
|
||||
job.meta["tracks"] = per_track_meta
|
||||
job.meta["progress"] = int(((i + 1) / total) * 100)
|
||||
job.save_meta()
|
||||
break
|
||||
|
||||
artist_raw = md.get("artist") or "Unknown Artist"
|
||||
album_raw = md.get("album") or "Unknown Album"
|
||||
title_raw = md.get("title") or f"Track {track_id}"
|
||||
|
||||
artist = sanitize_filename(artist_raw)
|
||||
album = sanitize_filename(album_raw)
|
||||
title = sanitize_filename(title_raw)
|
||||
|
||||
print(f"TRACK {track_id}: Processing '{title}' by {artist}")
|
||||
|
||||
all_artists.add(artist)
|
||||
album_dir = staging_root / artist / album
|
||||
album_dir.mkdir(parents=True, exist_ok=True)
|
||||
final_file = ensure_unique_path(album_dir / f"{title}{ext}")
|
||||
|
||||
# Move to final location
|
||||
print(f"TRACK {track_id}: Moving to final location...")
|
||||
tmp_file.rename(final_file)
|
||||
print(f"TRACK {track_id}: File moved successfully")
|
||||
|
||||
# Fetch cover art
|
||||
try:
|
||||
album_field = md.get("album")
|
||||
album_id = md.get("album_id") or (
|
||||
album_field.get("id") if isinstance(album_field, dict) else None
|
||||
)
|
||||
except Exception:
|
||||
album_id = None
|
||||
|
||||
if album_id:
|
||||
try:
|
||||
cover_url = await sr.get_cover_by_album_id(album_id, size=640)
|
||||
except Exception:
|
||||
cover_url = None
|
||||
else:
|
||||
cover_url = md.get("cover_url")
|
||||
|
||||
# Embed tags
|
||||
embedded = False
|
||||
img_bytes = None
|
||||
if cover_url:
|
||||
try:
|
||||
timeout = aiohttp.ClientTimeout(total=15)
|
||||
async with session.get(cover_url, timeout=timeout) as img_resp:
|
||||
if img_resp.status == 200:
|
||||
img_bytes = await img_resp.read()
|
||||
else:
|
||||
img_bytes = None
|
||||
try:
|
||||
send_log_to_discord(
|
||||
f"Cover download HTTP `{img_resp.status}` for track `{track_id} album_id={album_id} url={cover_url} artist={artist} album={album}`",
|
||||
"WARNING",
|
||||
target,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
img_bytes = None
|
||||
try:
|
||||
send_log_to_discord(
|
||||
f"Cover download exception for track `{track_id} album_id={album_id} url={cover_url} artist={artist} album={album}`: `{e}`",
|
||||
"WARNING",
|
||||
target,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Try music_tag first
|
||||
try:
|
||||
from music_tag import load_file as mt_load_file # type: ignore
|
||||
|
||||
# Add validation for `mf` object
|
||||
try:
|
||||
mf = mt_load_file(str(final_file))
|
||||
if mf is not None:
|
||||
if md.get("title"):
|
||||
mf["title"] = md.get("title")
|
||||
if md.get("artist"):
|
||||
mf["artist"] = md.get("artist")
|
||||
if md.get("album"):
|
||||
mf["album"] = md.get("album")
|
||||
tracknum = md.get("track_number")
|
||||
if tracknum is not None:
|
||||
try:
|
||||
mf["tracknumber"] = int(tracknum)
|
||||
except Exception:
|
||||
pass
|
||||
if img_bytes:
|
||||
mf["artwork"] = img_bytes
|
||||
mf.save()
|
||||
embedded = True
|
||||
else:
|
||||
logger.error("Failed to load file with music_tag.")
|
||||
embedded = False
|
||||
except Exception:
|
||||
embedded = False
|
||||
except Exception:
|
||||
embedded = False
|
||||
|
||||
if not embedded:
|
||||
try:
|
||||
if cover_url and not img_bytes:
|
||||
send_log_to_discord(
|
||||
f"Cover art not available for track {track_id} album_id={album_id} url={cover_url}",
|
||||
"WARNING",
|
||||
target,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
tag_with_mediafile(str(final_file), md)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Success
|
||||
tmp_file = None
|
||||
track_info["status"] = "Success"
|
||||
track_info["file_path"] = str(final_file)
|
||||
track_info["error"] = None
|
||||
all_final_files.append(final_file)
|
||||
|
||||
print(f"TRACK {track_id}: SUCCESS! Progress: {((i + 1) / total) * 100:.0f}%")
|
||||
|
||||
print(f"DEBUG: Fetching metadata for track {track_id}")
|
||||
# Metadata fetch
|
||||
try:
|
||||
md = await sr.get_metadata_by_track_id(track_id) or {}
|
||||
print(f"DEBUG: Metadata fetched: {bool(md)}")
|
||||
except MetadataFetchError as me:
|
||||
# Permanent metadata failure — mark failed and break
|
||||
track_info["status"] = "Failed"
|
||||
track_info["error"] = str(me)
|
||||
per_track_meta.append(track_info)
|
||||
if job:
|
||||
job.meta["tracks"] = per_track_meta
|
||||
job.meta["progress"] = int(((i + 1) / total) * 100)
|
||||
job.meta["tracks"] = per_track_meta + [track_info]
|
||||
job.save_meta()
|
||||
break
|
||||
|
||||
except aiohttp.ClientResponseError as e:
|
||||
msg = f"Track {track_id} attempt {attempt} ClientResponseError: {e}"
|
||||
send_log_to_discord(msg, "WARNING", target)
|
||||
if getattr(e, "status", None) == 429:
|
||||
wait_time = min(60, 2 ** attempt)
|
||||
await asyncio.sleep(wait_time)
|
||||
else:
|
||||
await asyncio.sleep(random.uniform(THROTTLE_MIN, THROTTLE_MAX))
|
||||
artist_raw = md.get("artist") or "Unknown Artist"
|
||||
album_raw = md.get("album") or "Unknown Album"
|
||||
title_raw = md.get("title") or f"Track {track_id}"
|
||||
|
||||
except Exception as e:
|
||||
tb = traceback.format_exc()
|
||||
is_no_stream_url = isinstance(e, RuntimeError) and str(e) == "No stream URL"
|
||||
if is_no_stream_url:
|
||||
if attempt == 1 or attempt == MAX_RETRIES:
|
||||
msg = f"Track {track_id} attempt {attempt} failed: {e}\n{tb}"
|
||||
send_log_to_discord(msg, "ERROR", target)
|
||||
track_info["error"] = str(e)
|
||||
if attempt >= MAX_RETRIES:
|
||||
track_info["status"] = "Failed"
|
||||
send_log_to_discord(
|
||||
f"Track {track_id} failed after {attempt} attempts",
|
||||
"ERROR",
|
||||
target,
|
||||
)
|
||||
await asyncio.sleep(random.uniform(THROTTLE_MIN, THROTTLE_MAX))
|
||||
else:
|
||||
msg = f"Track {track_id} attempt {attempt} failed: {e}\n{tb}"
|
||||
send_log_to_discord(msg, "ERROR", target)
|
||||
track_info["error"] = str(e)
|
||||
if attempt >= MAX_RETRIES:
|
||||
track_info["status"] = "Failed"
|
||||
send_log_to_discord(
|
||||
f"Track {track_id} failed after {attempt} attempts",
|
||||
"ERROR",
|
||||
target,
|
||||
)
|
||||
await asyncio.sleep(random.uniform(THROTTLE_MIN, THROTTLE_MAX))
|
||||
artist = sanitize_filename(artist_raw)
|
||||
album = sanitize_filename(album_raw)
|
||||
title = sanitize_filename(title_raw)
|
||||
|
||||
finally:
|
||||
print(f"TRACK {track_id}: Processing '{title}' by {artist}")
|
||||
|
||||
all_artists.add(artist)
|
||||
album_dir = staging_root / artist / album
|
||||
album_dir.mkdir(parents=True, exist_ok=True)
|
||||
final_file = ensure_unique_path(album_dir / f"{title}{ext}")
|
||||
|
||||
# Move to final location
|
||||
print(f"TRACK {track_id}: Moving to final location...")
|
||||
tmp_file.rename(final_file)
|
||||
print(f"TRACK {track_id}: File moved successfully")
|
||||
|
||||
# Fetch cover art
|
||||
try:
|
||||
album_field = md.get("album")
|
||||
album_id = md.get("album_id") or (
|
||||
album_field.get("id")
|
||||
if isinstance(album_field, dict)
|
||||
else None
|
||||
)
|
||||
except Exception:
|
||||
album_id = None
|
||||
|
||||
if album_id:
|
||||
try:
|
||||
if tmp_file and tmp_file.exists():
|
||||
os.remove(tmp_file)
|
||||
cover_url = await sr.get_cover_by_album_id(
|
||||
album_id, size=640
|
||||
)
|
||||
except Exception:
|
||||
cover_url = None
|
||||
else:
|
||||
cover_url = md.get("cover_url")
|
||||
|
||||
# Embed tags
|
||||
embedded = False
|
||||
img_bytes = None
|
||||
if cover_url:
|
||||
try:
|
||||
timeout = aiohttp.ClientTimeout(total=15)
|
||||
async with session.get(
|
||||
cover_url, timeout=timeout
|
||||
) as img_resp:
|
||||
if img_resp.status == 200:
|
||||
img_bytes = await img_resp.read()
|
||||
else:
|
||||
img_bytes = None
|
||||
try:
|
||||
send_log_to_discord(
|
||||
f"Cover download HTTP `{img_resp.status}` for track `{track_id} album_id={album_id} url={cover_url} artist={artist} album={album}`",
|
||||
"WARNING",
|
||||
target,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
img_bytes = None
|
||||
try:
|
||||
send_log_to_discord(
|
||||
f"Cover download exception for track `{track_id} album_id={album_id} url={cover_url} artist={artist} album={album}`: `{e}`",
|
||||
"WARNING",
|
||||
target,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Try music_tag first
|
||||
try:
|
||||
from music_tag import load_file as mt_load_file # type: ignore
|
||||
|
||||
# Add validation for `mf` object
|
||||
try:
|
||||
mf = mt_load_file(str(final_file))
|
||||
if mf is not None:
|
||||
if md.get("title"):
|
||||
mf["title"] = md.get("title")
|
||||
if md.get("artist"):
|
||||
mf["artist"] = md.get("artist")
|
||||
if md.get("album"):
|
||||
mf["album"] = md.get("album")
|
||||
tracknum = md.get("track_number")
|
||||
if tracknum is not None:
|
||||
try:
|
||||
mf["tracknumber"] = int(tracknum)
|
||||
except Exception:
|
||||
pass
|
||||
if img_bytes:
|
||||
mf["artwork"] = img_bytes
|
||||
mf.save()
|
||||
embedded = True
|
||||
else:
|
||||
logger.error("Failed to load file with music_tag.")
|
||||
embedded = False
|
||||
except Exception:
|
||||
embedded = False
|
||||
except Exception:
|
||||
embedded = False
|
||||
|
||||
if not embedded:
|
||||
try:
|
||||
if cover_url and not img_bytes:
|
||||
send_log_to_discord(
|
||||
f"Cover art not available for track {track_id} album_id={album_id} url={cover_url}",
|
||||
"WARNING",
|
||||
target,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
tag_with_mediafile(str(final_file), md)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
per_track_meta.append(track_info)
|
||||
# Success
|
||||
tmp_file = None
|
||||
track_info["status"] = "Success"
|
||||
track_info["file_path"] = str(final_file)
|
||||
track_info["error"] = None
|
||||
all_final_files.append(final_file)
|
||||
|
||||
print(
|
||||
f"TRACK {track_id}: SUCCESS! Progress: {((i + 1) / total) * 100:.0f}%"
|
||||
)
|
||||
|
||||
if job:
|
||||
job.meta["progress"] = int(((i + 1) / total) * 100)
|
||||
job.meta["tracks"] = per_track_meta + [track_info]
|
||||
job.save_meta()
|
||||
break
|
||||
|
||||
except aiohttp.ClientResponseError as e:
|
||||
msg = f"Track {track_id} attempt {attempt} ClientResponseError: {e}"
|
||||
send_log_to_discord(msg, "WARNING", target)
|
||||
if getattr(e, "status", None) == 429:
|
||||
wait_time = min(60, 2**attempt)
|
||||
await asyncio.sleep(wait_time)
|
||||
else:
|
||||
await asyncio.sleep(
|
||||
random.uniform(THROTTLE_MIN, THROTTLE_MAX)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
tb = traceback.format_exc()
|
||||
is_no_stream_url = (
|
||||
isinstance(e, RuntimeError) and str(e) == "No stream URL"
|
||||
)
|
||||
if is_no_stream_url:
|
||||
if attempt == 1 or attempt == MAX_RETRIES:
|
||||
msg = f"Track {track_id} attempt {attempt} failed: {e}\n{tb}"
|
||||
send_log_to_discord(msg, "ERROR", target)
|
||||
track_info["error"] = str(e)
|
||||
if attempt >= MAX_RETRIES:
|
||||
track_info["status"] = "Failed"
|
||||
send_log_to_discord(
|
||||
f"Track {track_id} failed after {attempt} attempts",
|
||||
"ERROR",
|
||||
target,
|
||||
)
|
||||
await asyncio.sleep(
|
||||
random.uniform(THROTTLE_MIN, THROTTLE_MAX)
|
||||
)
|
||||
else:
|
||||
msg = (
|
||||
f"Track {track_id} attempt {attempt} failed: {e}\n{tb}"
|
||||
)
|
||||
send_log_to_discord(msg, "ERROR", target)
|
||||
track_info["error"] = str(e)
|
||||
if attempt >= MAX_RETRIES:
|
||||
track_info["status"] = "Failed"
|
||||
send_log_to_discord(
|
||||
f"Track {track_id} failed after {attempt} attempts",
|
||||
"ERROR",
|
||||
target,
|
||||
)
|
||||
await asyncio.sleep(
|
||||
random.uniform(THROTTLE_MIN, THROTTLE_MAX)
|
||||
)
|
||||
|
||||
finally:
|
||||
try:
|
||||
if tmp_file and tmp_file.exists():
|
||||
os.remove(tmp_file)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
per_track_meta.append(track_info)
|
||||
|
||||
if not all_final_files:
|
||||
if job:
|
||||
@@ -690,6 +727,7 @@ def bulk_download(track_list: list, quality: str = "FLAC"):
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
|
||||
# Correct integration of FLAC stream check
|
||||
async def process_tracks(track_list):
|
||||
for i, track_id in enumerate(track_list or []):
|
||||
|
||||
@@ -19,15 +19,21 @@ class MetadataFetchError(Exception):
|
||||
"""Raised when metadata fetch permanently fails after retries."""
|
||||
|
||||
|
||||
# Suppress all logging output from this module and its children
|
||||
for name in [__name__, "utils.sr_wrapper"]:
|
||||
# Suppress noisy logging from this module and from the `streamrip` library
|
||||
# We set propagate=False so messages don't bubble up to the root logger and
|
||||
# attach a NullHandler where appropriate to avoid "No handler found" warnings.
|
||||
for name in [__name__, "utils.sr_wrapper", "streamrip", "streamrip.client"]:
|
||||
logger = logging.getLogger(name)
|
||||
logger.setLevel(logging.INFO) # Temporarily set to INFO for debugging LRC
|
||||
# Keep default level (or raise to WARNING) so non-important logs are dropped
|
||||
try:
|
||||
logger.setLevel(logging.WARNING)
|
||||
except Exception:
|
||||
pass
|
||||
logger.propagate = False
|
||||
for handler in logger.handlers:
|
||||
handler.setLevel(logging.INFO)
|
||||
# Also set the root logger to CRITICAL as a last resort (may affect global logging)
|
||||
# logging.getLogger().setLevel(logging.CRITICAL)
|
||||
# Ensure a NullHandler is present so logs don't propagate and no missing-handler
|
||||
# warnings are printed when the package emits records.
|
||||
if not any(isinstance(h, logging.NullHandler) for h in logger.handlers):
|
||||
logger.addHandler(logging.NullHandler())
|
||||
|
||||
|
||||
load_dotenv()
|
||||
@@ -684,21 +690,22 @@ class SRUtil:
|
||||
except Exception as e:
|
||||
# Exponential backoff with jitter for 429 or other errors
|
||||
delay = self.RETRY_DELAY * (2 ** (attempt - 1)) + random.uniform(0, 0.5)
|
||||
logging.warning(
|
||||
"Metadata fetch failed for track %s (attempt %d/%d): %s. Retrying in %.2fs",
|
||||
track_id,
|
||||
attempt,
|
||||
self.MAX_METADATA_RETRIES,
|
||||
str(e),
|
||||
delay,
|
||||
)
|
||||
if attempt < self.MAX_METADATA_RETRIES:
|
||||
logging.warning(
|
||||
"Retrying metadata fetch for track %s (attempt %d/%d): %s. Next retry in %.2fs",
|
||||
track_id,
|
||||
attempt,
|
||||
self.MAX_METADATA_RETRIES,
|
||||
str(e),
|
||||
delay,
|
||||
)
|
||||
await asyncio.sleep(delay)
|
||||
else:
|
||||
logging.error(
|
||||
"Metadata fetch failed permanently for track %s after %d attempts",
|
||||
"Metadata fetch failed permanently for track %s after %d attempts: %s",
|
||||
track_id,
|
||||
self.MAX_METADATA_RETRIES,
|
||||
str(e),
|
||||
)
|
||||
# Raise a specific exception so callers can react (e.g. notify)
|
||||
raise MetadataFetchError(
|
||||
|
||||
Reference in New Issue
Block a user