Merge pull request #516 from Nezreka/fix/silent-exception-swallowing

Fix/silent exception swallowing
This commit is contained in:
BoulderBadgeDad 2026-05-07 11:28:43 -07:00 committed by GitHub
commit 627d32cebd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
92 changed files with 743 additions and 704 deletions

View file

@ -2,10 +2,14 @@
Search endpoints search external sources (Spotify, iTunes, Hydrabase).
"""
import logging
from flask import request, current_app
from .auth import require_api_key
from .helpers import api_success, api_error
logger = logging.getLogger(__name__)
def register_routes(bp):
@ -38,8 +42,8 @@ def register_routes(bp):
if hydra_results:
tracks = [_serialize_track(t) for t in hydra_results]
return api_success({"tracks": tracks, "source": "hydrabase"})
except Exception:
pass
except Exception as e:
logger.debug("hydrabase search failed: %s", e)
spotify = ctx.get("spotify_client")
from core.metadata_service import get_primary_source, get_primary_client

View file

@ -2,12 +2,15 @@
System endpoints status, activity feed, stats.
"""
import logging
import time
from flask import current_app
from .auth import require_api_key
from .helpers import api_success, api_error
logger = logging.getLogger(__name__)
def register_routes(bp):
@ -35,8 +38,8 @@ def register_routes(bp):
try:
ws, _ = hydrabase.get_ws_and_lock()
hydrabase_ok = ws is not None and ws.connected
except Exception:
pass
except Exception as e:
logger.debug("hydrabase status probe failed: %s", e)
return api_success({
"uptime": f"{hours}h {minutes}m {seconds}s",

View file

@ -2612,7 +2612,7 @@ class BeatportUnifiedScraper:
result['title'] = title
except Exception as e:
pass # Silently handle URL extraction errors
logger.debug("URL slug extraction: %s", e)
return result
@ -3186,8 +3186,8 @@ class BeatportUnifiedScraper:
except Exception:
continue
except Exception:
pass
except Exception as e:
logger.debug("parse release tracks failed: %s", e)
return tracks

View file

@ -195,8 +195,8 @@ def _find_best_release(album_name, artist_name, track_count, mb_service):
sr_id = sr.get('id', '')
if sr_id and sr_id not in candidate_mbids:
candidate_mbids.append(sr_id)
except Exception:
pass
except Exception as e:
logger.debug("search_release fallback failed: %s", e)
if not candidate_mbids:
logger.info(f"No MB release found for '{album_name}' by '{artist_name}'")

View file

@ -302,8 +302,8 @@ class ApiCallTracker:
try:
if os.path.exists(_PERSIST_PATH + '.tmp'):
os.remove(_PERSIST_PATH + '.tmp')
except Exception:
pass
except Exception as e:
logger.debug("remove stale tmp file failed: %s", e)
def _load(self):
"""Restore 24h minute history from disk. Called on init."""

View file

@ -89,20 +89,20 @@ def _match_liked_artists_to_all_sources(database, profile_id: int):
search_clients['spotify'] = spotify_client
try:
search_clients['itunes'] = _get_itunes_client()
except Exception:
pass
except Exception as e:
logger.debug("itunes client init failed: %s", e)
try:
search_clients['deezer'] = _get_deezer_client()
except Exception:
pass
except Exception as e:
logger.debug("deezer client init failed: %s", e)
try:
dc = _get_discogs_client()
# Only use Discogs if token is configured
from config.settings import config_manager as _cm
if _cm.get('discogs.token', ''):
search_clients['discogs'] = dc
except Exception:
pass
except Exception as e:
logger.debug("discogs client init failed: %s", e)
# Reuse watchlist scanner's fuzzy matching logic
from core.watchlist_scanner import WatchlistScanner
@ -167,8 +167,8 @@ def _match_liked_artists_to_all_sources(database, profile_id: int):
harvested_ids[col] = str(r[col])
if _valid_image(r.get('thumb_url')):
best_image = r['thumb_url']
except Exception:
pass
except Exception as e:
logger.debug("library artist lookup failed: %s", e)
# 2. Watchlist artists
try:
@ -186,8 +186,8 @@ def _match_liked_artists_to_all_sources(database, profile_id: int):
harvested_ids[col] = str(wl[col])
if _valid_image(wl.get('image_url')) and not best_image:
best_image = wl['image_url']
except Exception:
pass
except Exception as e:
logger.debug("watchlist artist lookup failed: %s", e)
# 3. Metadata cache (all sources)
try:
@ -203,8 +203,8 @@ def _match_liked_artists_to_all_sources(database, profile_id: int):
harvested_ids[col] = row['entity_id']
if _valid_image(row['image_url']) and not best_image:
best_image = row['image_url']
except Exception:
pass
except Exception as e:
logger.debug("metadata cache lookup failed: %s", e)
# --- API STRATEGIES (search each missing source) ---
# Same pattern as watchlist scanner's _backfill_missing_ids
@ -287,8 +287,8 @@ def _backfill_liked_artist_images(database, profile_id: int, search_clients: dic
artist_data = sp.sp.artist(r['spotify_artist_id'])
if artist_data and artist_data.get('images'):
image_url = artist_data['images'][0]['url']
except Exception:
pass
except Exception as e:
logger.debug("spotify artist image fetch failed: %s", e)
# Try Deezer (direct image URL from ID)
if not image_url and r.get('deezer_artist_id'):
@ -302,8 +302,8 @@ def _backfill_liked_artist_images(database, profile_id: int, search_clients: dic
(image_url, r['id'])
)
filled += 1
except Exception:
pass
except Exception as e:
logger.debug("liked artist image update failed: %s", e)
time.sleep(0.3)
conn.commit()

View file

@ -158,8 +158,8 @@ def get_artist_map_data():
if r.get('genres'):
try:
genres = json.loads(r['genres'])
except Exception:
pass
except Exception as e:
logger.debug("similar node genres parse failed: %s", e)
nodes.append({
'id': idx,
'name': r['similar_artist_name'],
@ -232,8 +232,8 @@ def get_artist_map_data():
if cr['genres']:
try:
genres = json.loads(cr['genres']) if isinstance(cr['genres'], str) else []
except Exception:
pass
except Exception as e:
logger.debug("backfill cache genres parse failed: %s", e)
cache_by_name[cn][source] = {
'id': cr['entity_id'],
'image_url': cr['image_url'] or '',
@ -280,8 +280,8 @@ def get_artist_map_data():
an = _norm(r['artist_name'])
if an and an not in _album_art:
_album_art[an] = r['image_url']
except Exception:
pass
except Exception as e:
logger.debug("artist map album-art cache build failed: %s", e)
for n in nodes:
if not n.get('image_url') or not n['image_url'].startswith('http'):
nn = _norm(n['name'])
@ -330,8 +330,8 @@ def get_artist_map_genre_list():
if g and isinstance(g, str):
gl = g.lower().strip()
genre_counts[gl] = genre_counts.get(gl, 0) + 1
except Exception:
pass
except Exception as e:
logger.debug("genre count row parse failed: %s", e)
# Sort by count descending
sorted_genres = sorted(genre_counts.items(), key=lambda x: -x[1])
@ -404,8 +404,8 @@ def get_artist_map_genres():
if r['genres']:
try:
genres = json.loads(r['genres']) if isinstance(r['genres'], str) else []
except Exception:
pass
except Exception as e:
logger.debug("cache artist genres parse failed: %s", e)
src_map = {'spotify': 'spotify_id', 'itunes': 'itunes_id', 'deezer': 'deezer_id', 'discogs': 'discogs_id'}
kwargs = {src_map.get(r['source'], 'spotify_id'): r['entity_id']}
_add(r['name'], image_url=r['image_url'], genres=genres, source='cache', popularity=r['popularity'] or 0, **kwargs)
@ -421,8 +421,8 @@ def get_artist_map_genres():
if r['genres']:
try:
genres = json.loads(r['genres']) if isinstance(r['genres'], str) else []
except Exception:
pass
except Exception as e:
logger.debug("similar artist genres parse failed: %s", e)
_add(r['similar_artist_name'], image_url=r['image_url'], genres=genres,
spotify_id=r['similar_artist_spotify_id'], itunes_id=r['similar_artist_itunes_id'],
deezer_id=r['similar_artist_deezer_id'], source='similar', popularity=r['popularity'] or 0)
@ -445,8 +445,8 @@ def get_artist_map_genres():
if r['genres']:
try:
genres = json.loads(r['genres']) if isinstance(r['genres'], str) else []
except Exception:
pass
except Exception as e:
logger.debug("library artist genres parse failed: %s", e)
img = r['thumb_url'] if r['thumb_url'] and r['thumb_url'].startswith('http') else None
_add(r['name'], image_url=img, genres=genres, source='library')
@ -550,8 +550,8 @@ def get_artist_map_genres():
nn = (r['artist_name'] or '').lower().strip()
if nn and nn not in _album_art_cache:
_album_art_cache[nn] = r['image_url']
except Exception:
pass
except Exception as e:
logger.debug("genre map cache build failed: %s", e)
for n in nodes:
img = n.get('image_url', '')
@ -650,8 +650,8 @@ def get_artist_map_explore():
if row['genres']:
try:
center_genres = json.loads(row['genres']) if isinstance(row['genres'], str) else []
except Exception:
pass
except Exception as e:
logger.debug("initial center genres parse failed: %s", e)
# Check watchlist + library if not in cache
if not artist_found and not artist_id:
@ -722,8 +722,8 @@ def get_artist_map_explore():
if r['genres'] and not center_genres:
try:
center_genres = json.loads(r['genres']) if isinstance(r['genres'], str) else []
except Exception:
pass
except Exception as e:
logger.debug("center genres parse failed: %s", e)
# Add center node
center_idx = 0
@ -787,8 +787,8 @@ def get_artist_map_explore():
popularity=sa.get('popularity', 0),
similar_artist_deezer_id=sa.get('deezer_id')
)
except Exception:
pass
except Exception as e:
logger.debug("similar artist insert failed: %s", e)
# Re-query from DB to get consistent format
if id_values:
placeholders = ','.join(['?'] * len(id_values))
@ -828,8 +828,8 @@ def get_artist_map_explore():
if r['genres']:
try:
genres = json.loads(r['genres']) if isinstance(r['genres'], str) else []
except Exception:
pass
except Exception as e:
logger.debug("ring1 genres parse failed: %s", e)
img = r['image_url'] if r['image_url'] and r['image_url'].startswith('http') else ''
nodes.append({
'id': idx, 'name': r['similar_artist_name'], 'image_url': img,
@ -889,8 +889,8 @@ def get_artist_map_explore():
if r['genres']:
try:
genres = json.loads(r['genres']) if isinstance(r['genres'], str) else []
except Exception:
pass
except Exception as e:
logger.debug("ring2 genres parse failed: %s", e)
img = r['image_url'] if r['image_url'] and r['image_url'].startswith('http') else ''
nodes.append({
'id': idx, 'name': r['similar_artist_name'], 'image_url': img,
@ -928,8 +928,8 @@ def get_artist_map_explore():
if not n['genres'] and cr['genres']:
try:
n['genres'] = json.loads(cr['genres'])[:5] if isinstance(cr['genres'], str) else []
except Exception:
pass
except Exception as e:
logger.debug("explorer node genres parse failed: %s", e)
# Harvest missing IDs from cache
src_map = {'spotify': 'spotify_id', 'itunes': 'itunes_id', 'deezer': 'deezer_id', 'discogs': 'discogs_id'}
k = src_map.get(cr['source'])
@ -951,8 +951,8 @@ def get_artist_map_explore():
n['image_url'] = artist_data['images'][0]['url']
if not n['genres'] and artist_data.get('genres'):
n['genres'] = artist_data['genres'][:5]
except Exception:
pass
except Exception as e:
logger.debug("spotify artist image fallback failed: %s", e)
# Album art fallback (iTunes artists have no artist images)
if not n['image_url']:

View file

@ -140,8 +140,8 @@ class AudioDBWorker:
itype = item.get('type', '')
table = 'artists' if 'artist' in itype else ('albums' if 'album' in itype else 'tracks')
# Can't mark status without an ID — just skip
except Exception:
pass
except Exception as e:
logger.debug("null id table resolve failed: %s", e)
continue

View file

@ -596,8 +596,8 @@ class AutoImportWorker:
# Keep weak AcoustID result as fallback
if fp_result2 and (not result or fp_result2.get('identification_confidence', 0) > result.get('identification_confidence', 0)):
result = fp_result2
except Exception:
pass
except Exception as e:
logger.debug("acoustid fingerprint fallback failed: %s", e)
# If we have good tag data (artist + title), prefer tag-based identification
# over a weak metadata/AcoustID result — tags from post-processed files are reliable
@ -1100,8 +1100,8 @@ class AutoImportWorker:
if folder_artist and folder_artist.lower() != artist_name.lower():
logger.info(f"[Auto-Import] Parent folder artist '{folder_artist}' differs from tag artist '{artist_name}' — using folder artist")
artist_name = folder_artist
except Exception:
pass
except Exception as e:
logger.debug("folder artist override failed: %s", e)
release_date = identification.get('release_date', '') or album_data.get('release_date', '')
# Compute total discs
@ -1202,8 +1202,8 @@ class AutoImportWorker:
'completed_tracks': str(processed),
'failed_tracks': str(len(errors)),
})
except Exception:
pass
except Exception as e:
logger.debug("automation emit failed: %s", e)
return processed > 0

View file

@ -77,8 +77,8 @@ def update_progress(
if socketio_emit is not None:
try:
socketio_emit('automation:progress', {str(automation_id): dict(state)})
except Exception:
pass
except Exception as e:
logger.debug("socketio progress emit: %s", e)
def get_running_progress() -> dict[str, dict]:
@ -121,8 +121,8 @@ def record_history(
t0 = datetime.fromisoformat(started_at)
t1 = datetime.fromisoformat(finished_at)
duration = (t1 - t0).total_seconds()
except Exception:
pass
except Exception as e:
logger.debug("duration parse: %s", e)
r_status = result.get('status', 'completed') if result else 'completed'
if r_status == 'error':

View file

@ -8,6 +8,9 @@ names from the saved automation set so the builder UI can autocomplete.
from __future__ import annotations
import json
import logging
logger = logging.getLogger(__name__)
def collect_known_signals(database) -> list[str]:
@ -38,6 +41,6 @@ def collect_known_signals(database) -> list[str]:
signals.add(sig)
except (json.JSONDecodeError, TypeError):
pass
except Exception:
pass
except Exception as e:
logger.debug("collect known signals failed: %s", e)
return sorted(signals)

View file

@ -455,8 +455,10 @@ class AutomationEngine:
if delay_minutes and delay_minutes > 0:
# Initialize progress BEFORE delay so card glows during wait
if self._progress_init_fn:
try: self._progress_init_fn(automation_id, auto.get('name', ''), action_type)
except Exception: pass
try:
self._progress_init_fn(automation_id, auto.get('name', ''), action_type)
except Exception as e:
logger.debug("event progress init (delay): %s", e)
_delay_already_inited = True
delay_seconds = int(delay_minutes) * 60
@ -486,13 +488,17 @@ class AutomationEngine:
logger.info(f"Event automation '{auto.get('name')}' skipped — {action_type} busy")
# If progress was initialized during delay, finalize it
if _delay_already_inited and self._progress_finish_fn:
try: self._progress_finish_fn(automation_id, result)
except Exception: pass
try:
self._progress_finish_fn(automation_id, result)
except Exception as e:
logger.debug("event progress finish (skipped): %s", e)
else:
# Initialize progress tracking (skip if already done during delay)
if not _delay_already_inited and self._progress_init_fn:
try: self._progress_init_fn(automation_id, auto.get('name', ''), action_type)
except Exception: pass
try:
self._progress_init_fn(automation_id, auto.get('name', ''), action_type)
except Exception as e:
logger.debug("event progress init: %s", e)
try:
result = handler_info['handler'](action_config) or {}
logger.info(f"Event automation '{auto.get('name')}' executed: {result.get('status', 'ok')}")
@ -501,8 +507,10 @@ class AutomationEngine:
logger.error(f"Event automation '{auto.get('name')}' action failed: {e}")
# Finalize progress tracking
if self._progress_finish_fn:
try: self._progress_finish_fn(automation_id, result)
except Exception: pass
try:
self._progress_finish_fn(automation_id, result)
except Exception as e:
logger.debug("event progress finish: %s", e)
# Merge event data into result for then-action variables
merged = {**event_data, **result}
@ -531,8 +539,8 @@ class AutomationEngine:
if self._history_record_fn:
try:
self._history_record_fn(automation_id, result)
except Exception:
pass
except Exception as e:
logger.debug("history record failed: %s", e)
# --- Schedule Execution (timer-based) ---
@ -580,8 +588,10 @@ class AutomationEngine:
if not skip_delay and delay_minutes and delay_minutes > 0:
# Initialize progress BEFORE delay so card glows during wait
if self._progress_init_fn:
try: self._progress_init_fn(automation_id, auto.get('name', ''), action_type)
except Exception: pass
try:
self._progress_init_fn(automation_id, auto.get('name', ''), action_type)
except Exception as e:
logger.debug("scheduled progress init (delay): %s", e)
_delay_already_inited = True
delay_seconds = int(delay_minutes) * 60
@ -603,15 +613,19 @@ class AutomationEngine:
logger.info(f"Automation '{auto['name']}' skipped — {action_type} already running")
# If progress was initialized during delay, finalize it
if _delay_already_inited and self._progress_finish_fn:
try: self._progress_finish_fn(automation_id, result)
except Exception: pass
try:
self._progress_finish_fn(automation_id, result)
except Exception as e:
logger.debug("scheduled progress finish (skipped): %s", e)
self._finish_run(auto, automation_id, result, error=None)
return
# Initialize progress tracking (skip if already done during delay)
if not _delay_already_inited and self._progress_init_fn:
try: self._progress_init_fn(automation_id, auto.get('name', ''), action_type)
except Exception: pass
try:
self._progress_init_fn(automation_id, auto.get('name', ''), action_type)
except Exception as e:
logger.debug("scheduled progress init: %s", e)
# Execute the action
error = None
@ -637,8 +651,10 @@ class AutomationEngine:
# Finalize progress tracking
if self._progress_finish_fn:
try: self._progress_finish_fn(automation_id, result)
except Exception: pass
try:
self._progress_finish_fn(automation_id, result)
except Exception as e:
logger.debug("scheduled progress finish: %s", e)
# Execute then-actions (notifications + fire_signal)
try:
@ -673,8 +689,8 @@ class AutomationEngine:
delay = self._calc_delay_seconds(trigger_config)
if delay:
next_run_str = _utc_after(delay)
except Exception:
pass
except Exception as e:
logger.debug("next run calc failed: %s", e)
last_result = json.dumps(result) if result else None
self.db.update_automation_run(automation_id, next_run=next_run_str, error=error, last_result=last_result)
@ -682,8 +698,8 @@ class AutomationEngine:
if self._history_record_fn:
try:
self._history_record_fn(automation_id, result)
except Exception:
pass
except Exception as e:
logger.debug("history record failed: %s", e)
if self._running:
self.schedule_automation(automation_id)

View file

@ -79,9 +79,9 @@ def run_detection(server_type):
api_response = requests.get(api_url, timeout=1)
if api_response.status_code == 200 and 'MediaContainer' in api_response.text:
return f"http://{ip}:{port}"
except:
pass
except Exception as e:
logger.debug("plex probe %s: %s", ip, e)
return None
def test_jellyfin_server(ip, port=8096):
@ -101,9 +101,9 @@ def run_detection(server_type):
web_response = requests.get(web_url, timeout=1)
if web_response.status_code == 200 and 'jellyfin' in web_response.text.lower():
return f"http://{ip}:{port}"
except:
pass
except Exception as e:
logger.debug("jellyfin probe %s: %s", ip, e)
return None
def test_slskd_server(ip, port=5030):
@ -117,8 +117,8 @@ def run_detection(server_type):
if response.status_code in [200, 401]:
return f"http://{ip}:{port}"
except:
pass
except Exception as e:
logger.debug("slskd probe %s: %s", ip, e)
return None
def test_navidrome_server(ip, port=4533):
@ -140,8 +140,8 @@ def run_detection(server_type):
# Check for Subsonic/Navidrome API response structure
if 'subsonic-response' in data:
return f"http://{ip}:{port}"
except:
pass
except Exception as e:
logger.debug("navidrome json parse: %s", e)
# Also try the web interface
web_url = f"http://{ip}:{port}/"
@ -149,8 +149,8 @@ def run_detection(server_type):
if web_response.status_code == 200 and 'navidrome' in web_response.text.lower():
return f"http://{ip}:{port}"
except:
pass
except Exception as e:
logger.debug("navidrome probe %s: %s", ip, e)
return None
try:

View file

@ -1051,8 +1051,8 @@ class DatabaseUpdateWorker:
batch + [self.server_type])
cascade_album_ids.update(row[0] for row in cursor.fetchall())
removed_album_ids -= cascade_album_ids
except Exception:
pass # If this optimization fails, double-delete is harmless
except Exception as e:
logger.debug("cascade album cleanup optimization: %s", e)
if not removed_artist_ids and not removed_album_ids:
logger.info("Removal detection: no stale content found")

View file

@ -260,8 +260,8 @@ def get_debug_info():
for _pid, st in list(tidal_discovery_states.items()):
if st.get('phase') == 'syncing':
active_syncs += 1
except Exception:
pass
except Exception as e:
logger.debug("count active syncs failed: %s", e)
info['active_downloads'] = active_downloads
info['active_syncs'] = active_syncs

View file

@ -307,8 +307,8 @@ class DeezerClient:
for raw in cached_results:
try:
tracks.append(Track.from_deezer_track(raw))
except Exception:
pass
except Exception as e:
logger.debug("Track.from_deezer_track cache parse: %s", e)
if tracks:
return tracks
@ -341,8 +341,8 @@ class DeezerClient:
for raw in cached_results:
try:
artists.append(Artist.from_deezer_artist(raw))
except Exception:
pass
except Exception as e:
logger.debug("Artist.from_deezer_artist cache parse: %s", e)
if artists:
return artists
@ -375,8 +375,8 @@ class DeezerClient:
for raw in cached_results:
try:
albums.append(Album.from_deezer_album(raw))
except Exception:
pass
except Exception as e:
logger.debug("Album.from_deezer_album cache parse: %s", e)
if albums:
return albums
@ -842,8 +842,8 @@ class DeezerClient:
try:
cache = get_metadata_cache()
cache.store_entity('deezer', 'artist', str(result.get('id', '')), result)
except Exception:
pass
except Exception as e:
logger.debug("cache store_entity artist search: %s", e)
logger.debug(f"Found artist for query: {artist_name}")
return result
@ -887,8 +887,8 @@ class DeezerClient:
try:
cache = get_metadata_cache()
cache.store_entity('deezer', 'album', str(result.get('id', '')), result)
except Exception:
pass
except Exception as e:
logger.debug("cache store_entity album search: %s", e)
logger.debug(f"Found album for query: {artist_name} - {album_title}")
return result
@ -932,8 +932,8 @@ class DeezerClient:
try:
cache = get_metadata_cache()
cache.store_entity('deezer', 'track', str(result.get('id', '')), result)
except Exception:
pass
except Exception as e:
logger.debug("cache store_entity track search: %s", e)
logger.debug(f"Found track for query: {artist_name} - {track_title}")
return result
@ -965,8 +965,8 @@ class DeezerClient:
# Cache hit with full details (has label = was a get_album response, not just search)
logger.debug(f"Cache hit for album {album_id}")
return cached
except Exception:
pass
except Exception as e:
logger.debug("cache get_entity album: %s", e)
try:
response = self.session.get(
@ -984,8 +984,8 @@ class DeezerClient:
try:
cache = get_metadata_cache()
cache.store_entity('deezer', 'album', str(album_id), data)
except Exception:
pass
except Exception as e:
logger.debug("cache store_entity album full: %s", e)
logger.debug(f"Got full album details for ID: {album_id}")
return data
@ -1013,8 +1013,8 @@ class DeezerClient:
if cached and cached.get('bpm'):
logger.debug(f"Cache hit for track {track_id}")
return cached
except Exception:
pass
except Exception as e:
logger.debug("cache get_entity track: %s", e)
try:
response = self.session.get(
@ -1032,8 +1032,8 @@ class DeezerClient:
try:
cache = get_metadata_cache()
cache.store_entity('deezer', 'track', str(track_id), data)
except Exception:
pass
except Exception as e:
logger.debug("cache store_entity track full: %s", e)
logger.debug(f"Got full track details for ID: {track_id}")
return data

View file

@ -430,8 +430,8 @@ class DeezerDownloadClient(DownloadSourcePlugin):
if cached and cached.get('release_date'):
album_release_dates[aid] = cached['release_date']
continue
except Exception:
pass
except Exception as e:
logger.debug("cache get_entity album release_date: %s", e)
# Cache miss — fetch from API
try:
time.sleep(0.3) # Respect rate limits
@ -443,10 +443,10 @@ class DeezerDownloadClient(DownloadSourcePlugin):
if cache:
try:
cache.store_entity('deezer', 'album', aid, a_data)
except Exception:
pass
except Exception:
pass
except Exception as e:
logger.debug("cache store_entity album release_date: %s", e)
except Exception as e:
logger.debug("fetch deezer album release_date %s: %s", aid, e)
tracks = []
for i, t in enumerate(raw_tracks, start=1):

View file

@ -141,8 +141,8 @@ class DeezerWorker:
itype = item.get('type', '')
table = 'artists' if 'artist' in itype else ('albums' if 'album' in itype else 'tracks')
# Can't mark status without an ID — just skip
except Exception:
pass
except Exception as e:
logger.debug("null id table resolve failed: %s", e)
continue

View file

@ -320,8 +320,8 @@ class DiscogsClient:
try:
from config.settings import config_manager
self.token = config_manager.get('discogs.token', '')
except Exception:
pass
except Exception as e:
logger.debug("load discogs.token from config: %s", e)
if self.token:
self.session.headers['Authorization'] = f'Discogs token={self.token}'
@ -499,8 +499,8 @@ class DiscogsClient:
for raw in cached_results:
try:
artists.append(Artist.from_discogs_artist(raw))
except Exception:
pass
except Exception as e:
logger.debug("Artist.from_discogs_artist cache parse: %s", e)
if artists:
return artists
@ -536,8 +536,8 @@ class DiscogsClient:
for raw in cached_results:
try:
albums.append(Album.from_discogs_release(raw))
except Exception:
pass
except Exception as e:
logger.debug("Album.from_discogs_release cache parse: %s", e)
if albums:
return albums

View file

@ -298,8 +298,8 @@ class DiscogsWorker:
self.stats['errors'] += 1
try:
self._mark_status(item['type'], item['id'], 'error')
except Exception:
pass
except Exception as e:
logger.debug("mark item status error failed: %s", e)
def _get_existing_id(self, entity_type: str, entity_id) -> Optional[str]:
"""Check if entity already has a discogs_id."""

View file

@ -196,8 +196,8 @@ def run_playlist_discovery_worker(playlists, automation_id=None, deps: PlaylistD
current_item=track_name,
log_line=f'{track_name}{cached_match.get("name", "?")} (cache)', log_type='success')
continue
except Exception:
pass
except Exception as e:
logger.debug("discovery cache lookup failed: %s", e)
# Step 2: Generate search queries
try:
@ -252,8 +252,8 @@ def run_playlist_discovery_worker(playlists, automation_id=None, deps: PlaylistD
if match and confidence > best_confidence:
best_confidence = confidence
best_match = match
except Exception:
pass
except Exception as e:
logger.debug("extended discovery search failed: %s", e)
# Step 4: Store results
if best_match and best_confidence >= min_confidence:
@ -290,8 +290,8 @@ def run_playlist_discovery_worker(playlists, automation_id=None, deps: PlaylistD
if _raw:
track_number = _raw.get('track_number')
disc_number = _raw.get('disc_number')
except Exception:
pass
except Exception as e:
logger.debug("metadata cache lookup for album enrichment failed: %s", e)
matched_data = {
'id': best_match.id if hasattr(best_match, 'id') else '',
@ -323,8 +323,8 @@ def run_playlist_discovery_worker(playlists, automation_id=None, deps: PlaylistD
best_confidence, matched_data,
track_name, artist_name
)
except Exception:
pass
except Exception as e:
logger.debug("save discovery cache match failed: %s", e)
logger.info(f"[{i+1}/{len(undiscovered_tracks)}] {track_name}{matched_data['name']} ({best_confidence:.2f})")
deps.update_automation_progress(automation_id,
@ -366,8 +366,8 @@ def run_playlist_discovery_worker(playlists, automation_id=None, deps: PlaylistD
'failed_count': str(total_failed),
'skipped_count': str(total_skipped),
})
except Exception:
pass
except Exception as e:
logger.debug("discovery_completed emit failed: %s", e)
logger.error(f"Playlist discovery complete: {total_discovered} discovered, {total_failed} failed, {total_skipped} skipped")
deps.update_automation_progress(automation_id, status='finished', progress=100,

View file

@ -648,8 +648,8 @@ def run_quality_scanner(scope='watchlist', profile_id=1, deps: QualityScannerDep
'low_quality': str(deps.quality_scanner_state.get('low_quality', 0)),
'total_scanned': str(deps.quality_scanner_state.get('processed', 0)),
})
except Exception:
pass
except Exception as e:
logger.debug("emit quality_scan_completed failed: %s", e)
except Exception as e:
logger.error(f"[Quality Scanner] Critical error: {e}")

View file

@ -290,8 +290,8 @@ def run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None, p
logger.debug(f"Sync cache hit: '{original_title}' → server track {cached['server_track_id']}")
return DatabaseTrackCached(db_track_check), cached['confidence']
logger.warning(f"Sync cache stale for '{original_title}' — track gone")
except Exception:
pass
except Exception as e:
logger.debug("sync match cache fast-path failed: %s", e)
# --- End cache fast-path ---
# Try each artist (same logic as original)
@ -322,8 +322,8 @@ def run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None, p
spotify_id, me.clean_title(original_title), me.clean_artist(artist_name),
active_server, db_track.id, db_track.title, confidence
)
except Exception:
pass
except Exception as e:
logger.debug("save sync match cache failed: %s", e)
# Create mock track object for playlist creation
class DatabaseTrackMock:
@ -429,8 +429,8 @@ def run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None, p
entry = db.get_sync_history_entry(_resync_entry_id)
if entry:
target_batch_id = entry.get('batch_id', sync_batch_id)
except Exception:
pass
except Exception as e:
logger.debug("resync history lookup failed: %s", e)
else:
db.update_sync_history_completion(sync_batch_id, matched, synced, failed)
@ -466,8 +466,8 @@ def run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None, p
'synced_tracks': str(getattr(result, 'synced_tracks', 0)),
'failed_tracks': str(getattr(result, 'failed_tracks', 0)),
})
except Exception:
pass
except Exception as e:
logger.debug("playlist_synced emit failed: %s", e)
# Save sync status with match counts and track hash for smart-skip on next scheduled sync
import hashlib as _hl

View file

@ -22,8 +22,11 @@ module-level constant in the client file.
from __future__ import annotations
import logging
from dataclasses import dataclass
logger = logging.getLogger(__name__)
@dataclass(frozen=True)
class RateLimitPolicy:
@ -63,8 +66,8 @@ def resolve_policy(plugin) -> RateLimitPolicy:
policy = method()
if isinstance(policy, RateLimitPolicy):
return policy
except Exception:
pass
except Exception as e:
logger.debug("plugin rate_limit_policy() call failed: %s", e)
declared = getattr(plugin, 'RATE_LIMIT_POLICY', None)
if isinstance(declared, RateLimitPolicy):

View file

@ -97,8 +97,8 @@ def attempt_download_with_candidates(task_id, candidates, track, batch_id=None,
if _bl_db.is_blacklisted(candidate.username, candidate.filename):
logger.info(f"[Modal Worker] Skipping blacklisted source: {source_key}")
continue
except Exception:
pass
except Exception as e:
logger.debug("blacklist check failed: %s", e)
# CRITICAL: Add source to used_sources IMMEDIATELY to prevent race conditions
# This must happen BEFORE starting download to prevent multiple retries from picking same source

View file

@ -233,8 +233,8 @@ def on_download_completed(batch_id: str, task_id: str, success: bool, deps: Life
'title': track_info.get('track_name', ''),
'reason': track_info.get('failure_reason', 'Unknown'),
})
except Exception:
pass
except Exception as e:
logger.debug("download_failed emit failed: %s", e)
# WISHLIST REMOVAL: Handle successful downloads for wishlist removal
if success and task_id in download_tasks:
@ -364,8 +364,8 @@ def on_download_completed(batch_id: str, task_id: str, success: bool, deps: Life
'completed_tracks': str(successful_downloads),
'failed_tracks': str(failed_count),
})
except Exception:
pass
except Exception as e:
logger.debug("batch_complete emit failed: %s", e)
# Update YouTube playlist phase to 'download_complete' if this is a YouTube playlist
playlist_id = batch.get('playlist_id')
@ -569,8 +569,8 @@ def check_batch_completion_v2(batch_id: str, deps: LifecycleDeps) -> Optional[bo
'completed_tracks': str(successful_downloads),
'failed_tracks': str(failed_count),
})
except Exception:
pass
except Exception as e:
logger.debug("batch_complete emit failed: %s", e)
else:
logger.warning(f"[Completion Check V2] Batch {batch_id} already marked complete - skipping duplicate processing")
return True # Already complete

View file

@ -296,8 +296,8 @@ def run_full_missing_tracks_process(batch_id, playlist_id, tracks_json, deps: Ma
})
if track_results:
db_sh.update_sync_history_track_results(batch_id, json.dumps(track_results))
except Exception:
pass
except Exception as e:
logger.debug("update sync_history track results failed: %s", e)
is_auto_batch = False
with tasks_lock:

View file

@ -280,8 +280,8 @@ class WebUIDownloadMonitor:
all_downloads = run_async(
download_orchestrator.engine.get_all_downloads(exclude=('soulseek',))
)
except Exception:
pass
except Exception as e:
logger.debug("get_all_downloads failed: %s", e)
for download in all_downloads:
key = _make_context_key(download.username, download.filename)
# Convert DownloadStatus to transfer dict format for monitor compatibility

View file

@ -146,8 +146,8 @@ def _process_failed_tracks_to_wishlist_exact(batch_id):
'title': track_name,
'reason': failed_track_info.get('failure_reason', ''),
})
except Exception:
pass
except Exception as e:
logger.debug("emit wishlist_item_added failed: %s", e)
else:
logger.error(f"[Wishlist Processing] Failed to add {track_name} to wishlist")

View file

@ -67,8 +67,8 @@ def _drop_auto_pause_marker(service: EnrichmentService) -> None:
return
try:
_auto_paused_discard(service.auto_pause_token)
except Exception:
pass
except Exception as e:
logger.debug("auto-pause marker discard: %s", e)
def _add_yield_override(service: EnrichmentService) -> None:
@ -76,8 +76,8 @@ def _add_yield_override(service: EnrichmentService) -> None:
return
try:
_yield_override_add(service.auto_pause_token)
except Exception:
pass
except Exception as e:
logger.debug("yield override add: %s", e)
def create_blueprint() -> Blueprint:

View file

@ -154,8 +154,8 @@ class GeniusWorker:
itype = item.get('type', '')
table = 'artists' if 'artist' in itype else ('albums' if 'album' in itype else 'tracks')
# Can't mark status without an ID — just skip
except Exception:
pass
except Exception as e:
logger.debug("null-id item type lookup: %s", e)
continue
self._process_item(item)
@ -367,8 +367,8 @@ class GeniusWorker:
if song_url:
try:
lyrics = self.client.get_lyrics(song_url)
except Exception:
pass
except Exception as _e:
logger.debug("genius lyrics scrape: %s", _e)
self._update_track(track_id, full_song, full_song, lyrics)
self.stats['matched'] += 1
logger.info(f"Enriched track '{track_name}' from existing Genius ID: {existing_id}")

View file

@ -525,8 +525,8 @@ class HydrabaseClient:
for album in albums:
if album.image_url:
return album.image_url
except Exception:
pass
except Exception as e:
logger.debug("get artist image from albums failed: %s", e)
return None
def get_artist_albums(self, artist_id: str, album_type: str = 'album,single', limit: int = 50) -> List[Album]:

View file

@ -362,8 +362,8 @@ def downsample_hires_flac(final_path, context):
if os.path.exists(temp_path):
try:
os.remove(temp_path)
except Exception:
pass
except Exception as _e:
logger.debug("cleanup downsample temp: %s", _e)
return None
@ -468,8 +468,8 @@ def create_lossy_copy(final_path):
if os.path.exists(out_path):
try:
os.remove(out_path)
except Exception:
pass
except Exception as _e:
logger.debug("cleanup lossy copy artifact: %s", _e)
return None
except subprocess.TimeoutExpired:
logger.warning(f"[Lossy Copy] Conversion timed out for: {os.path.basename(final_path)}")

View file

@ -82,8 +82,8 @@ def move_to_quarantine(file_path: str, context: dict, reason: str, automation_en
"reason": reason or "Unknown",
},
)
except Exception:
pass
except Exception as e:
logger.debug("emit download_quarantined failed: %s", e)
return str(quarantine_path)

View file

@ -188,8 +188,8 @@ def _replace_template_variables(template: str, context: dict) -> str:
resolved = resolved_client.resolve_primary_artist(itunes_artist_id)
if resolved and resolved != album_artist_value:
album_artist_value = resolved
except Exception:
pass
except Exception as e:
logger.debug("resolve primary artist failed: %s", e)
# $cdnum — smart CD label for multi-disc filenames. Produces "CD01" /
# "CD02" etc. when the album has 2+ discs, empty string otherwise.

View file

@ -434,8 +434,8 @@ def post_process_matched_download(context_key, context, file_path, runtime, meta
logger.error(f"Quarantine failed ({quarantine_error}), deleting file: {file_path}")
try:
os.remove(file_path)
except Exception:
pass
except Exception as e:
logger.debug("delete quarantine fallback: %s", e)
context['_bitdepth_rejected'] = True
with matched_context_lock:
@ -562,8 +562,8 @@ def post_process_matched_download(context_key, context, file_path, runtime, meta
logger.error(f"Quarantine failed ({quarantine_error}), deleting file: {file_path}")
try:
os.remove(file_path)
except Exception:
pass
except Exception as e:
logger.debug("delete quarantine fallback: %s", e)
context['_bitdepth_rejected'] = True
with matched_context_lock:

View file

@ -407,8 +407,8 @@ def get_single_track_import_context(
'genres',
default=[],
) or []
except Exception:
pass
except Exception as e:
logger.debug("override artist genres: %s", e)
return payload
except Exception as exc:
logger.debug("Override track lookup failed on %s for %s: %s", chosen_source, override_id, exc)
@ -461,8 +461,8 @@ def get_single_track_import_context(
'genres',
default=[],
) or []
except Exception:
pass
except Exception as e:
logger.debug("artist genres lookup: %s", e)
return payload
return _build_single_import_fallback_context(title, artist, source_priority)

View file

@ -72,8 +72,8 @@ def emit_track_downloaded(context: Dict[str, Any], automation_engine=None) -> No
"quality": context.get("_audio_quality", "Unknown"),
},
)
except Exception:
pass
except Exception as e:
logger.debug("track_downloaded emit failed: %s", e)
def record_library_history_download(context: Dict[str, Any]) -> None:
@ -143,8 +143,8 @@ def record_library_history_download(context: Dict[str, Any]) -> None:
acoustid_result=acoustid_result,
source_artist=source_artist,
)
except Exception:
pass
except Exception as e:
logger.debug("library history record failed: %s", e)
def record_download_provenance(context: Dict[str, Any]) -> None:
@ -188,8 +188,8 @@ def record_download_provenance(context: Dict[str, Any]) -> None:
sample_rate = getattr(audio.info, "sample_rate", None)
bitrate = getattr(audio.info, "bitrate", None)
bit_depth = getattr(audio.info, "bits_per_sample", None)
except Exception:
pass
except Exception as e:
logger.debug("audio info probe failed: %s", e)
# Pull the metadata-source IDs out of context. ``embed_source_ids``
# in core/metadata/source.py wrote them to ``_embedded_id_tags``
@ -239,8 +239,8 @@ def record_download_provenance(context: Dict[str, Any]) -> None:
soul_id=soul_id,
isrc=isrc,
)
except Exception:
pass
except Exception as e:
logger.debug("record_download_provenance failed: %s", e)
def record_soulsync_library_entry(context: Dict[str, Any], artist_context: Dict[str, Any], album_info: Dict[str, Any]) -> None:
@ -321,8 +321,8 @@ def record_soulsync_library_entry(context: Dict[str, Any], artist_context: Dict[
audio = MutagenFile(final_path)
if audio and hasattr(audio, "info") and audio.info and hasattr(audio.info, "bitrate"):
bitrate = int(audio.info.bitrate / 1000) if audio.info.bitrate else 0
except Exception:
pass
except Exception as e:
logger.debug("bitrate read failed: %s", e)
# File size on disk (powers Library Disk Usage card on Stats).
# SoulSync standalone is the only path where the file is local
@ -371,8 +371,8 @@ def record_soulsync_library_entry(context: Dict[str, Any], artist_context: Dict[
f"UPDATE artists SET {artist_source_col} = ? WHERE id = ?",
(artist_source_id, artist_id),
)
except Exception:
pass
except Exception as e:
logger.debug("artist source-id update failed: %s", e)
cursor.execute("SELECT id FROM albums WHERE id = ? AND server_source = 'soulsync'", (album_id,))
if not cursor.fetchone():
@ -402,8 +402,8 @@ def record_soulsync_library_entry(context: Dict[str, Any], artist_context: Dict[
f"UPDATE albums SET {album_source_col} = ? WHERE id = ?",
(album_source_id, album_id),
)
except Exception:
pass
except Exception as e:
logger.debug("album source-id update failed: %s", e)
track_artist = None
track_artists_list = track_info.get("artists", []) or original_search.get("artists", [])
@ -451,8 +451,8 @@ def record_soulsync_library_entry(context: Dict[str, Any], artist_context: Dict[
f"UPDATE tracks SET {track_album_col} = ? WHERE id = ?",
(album_source_id, track_id),
)
except Exception:
pass
except Exception as e:
logger.debug("track source-id update failed: %s", e)
conn.commit()
logger.info("[SoulSync Library] Added: %s / %s / %s", artist_name, album_name, track_name)

View file

@ -380,8 +380,8 @@ class iTunesClient:
for raw in cached_results:
try:
tracks.append(Track.from_itunes_track(raw))
except Exception:
pass
except Exception as e:
logger.debug("Track.from_itunes_track cache parse: %s", e)
if tracks:
return tracks
@ -569,8 +569,8 @@ class iTunesClient:
for raw in cached_results:
try:
albums.append(Album.from_itunes_album(raw))
except Exception:
pass
except Exception as e:
logger.debug("Album.from_itunes_album cache parse: %s", e)
if albums:
return albums
@ -893,8 +893,8 @@ class iTunesClient:
for item in results:
if item.get('wrapperType') == 'artist' and item.get('artistName'):
return item['artistName']
except Exception:
pass
except Exception as e:
logger.debug("itunes lookup artistId %s: %s", artist_id, e)
return None
def search_artists(self, query: str, limit: int = 20) -> List[Artist]:
@ -911,8 +911,8 @@ class iTunesClient:
for raw in cached_results:
try:
artists.append(Artist.from_itunes_artist(raw))
except Exception:
pass
except Exception as e:
logger.debug("Artist.from_itunes_artist cache parse: %s", e)
if artists:
return artists

View file

@ -144,8 +144,8 @@ class iTunesWorker:
itype = item.get('type', '')
table = 'artists' if 'artist' in itype else ('albums' if 'album' in itype else 'tracks')
# Can't mark status without an ID — just skip
except Exception:
pass
except Exception as e:
logger.debug("null id table resolve failed: %s", e)
continue
self._process_item(item)

View file

@ -1441,8 +1441,8 @@ class JellyfinClient(MediaServerClient):
response = requests.delete(url, headers=headers, timeout=10)
if response.status_code in [200, 204]:
logger.info(f"Deleted existing backup playlist '{target_name}'")
except Exception:
pass # Target doesn't exist, which is fine
except Exception as e:
logger.debug("backup playlist precheck: %s", e)
# Create new playlist with copied tracks
try:

View file

@ -155,8 +155,8 @@ class LastFMWorker:
itype = item.get('type', '')
table = 'artists' if 'artist' in itype else ('albums' if 'album' in itype else 'tracks')
# Can't mark status without an ID — just skip
except Exception:
pass
except Exception as e:
logger.debug("null id table resolve failed: %s", e)
continue
self._process_item(item)

View file

@ -209,8 +209,8 @@ def _run_duplicate_cleaner():
'duplicates_found': str(duplicates_found),
'space_freed': f"{space_mb:.1f} MB",
})
except Exception:
pass
except Exception as e:
logger.debug("emit duplicate_scan_completed failed: %s", e)
except Exception as e:
logger.error(f"[Duplicate Cleaner] Critical error: {e}")

View file

@ -83,8 +83,8 @@ def _collect_base_dirs(
candidates.append(_docker_resolve_path(transfer_cfg))
if download_cfg:
candidates.append(_docker_resolve_path(download_cfg))
except Exception:
pass
except Exception as e:
logger.debug("soulseek paths read failed: %s", e)
# Plex-reported library locations (handles "Plex scanned at /music but
# SoulSync mounts at /library" cases).
@ -96,8 +96,8 @@ def _collect_base_dirs(
for loc in getattr(music_library, "locations", []) or []:
if loc:
candidates.append(loc)
except Exception:
pass
except Exception as e:
logger.debug("plex locations read failed: %s", e)
# User-configured library music paths (Settings → Library → Music Paths).
if config_manager is not None:
@ -107,8 +107,8 @@ def _collect_base_dirs(
for p in music_paths:
if isinstance(p, str) and p.strip():
candidates.append(_docker_resolve_path(p.strip()))
except Exception:
pass
except Exception as e:
logger.debug("music paths read failed: %s", e)
# De-duplicate while preserving order, drop empties / non-existent dirs.
seen: set[str] = set()

View file

@ -265,8 +265,8 @@ def execute_retag(group_id, album_id, deps: RetagDeps):
try:
os.remove(old_cover)
logger.warning("[Retag] Removed orphaned cover.jpg from old directory")
except Exception:
pass
except Exception as e:
logger.debug("remove orphaned cover failed: %s", e)
# Cleanup old empty directories
transfer_dir = deps.docker_resolve_path(deps.config_manager.get('soulseek.transfer_path', './Transfer'))

View file

@ -208,7 +208,7 @@ def find_library_track_by_external_id(
if conn is not None:
try:
conn.close()
except Exception:
except Exception: # noqa: S110 — finally-block cleanup, logger may be torn down
pass
@ -287,7 +287,7 @@ def find_provenance_by_external_id(
if conn is not None:
try:
conn.close()
except Exception:
except Exception: # noqa: S110 — finally-block cleanup, logger may be torn down
pass

View file

@ -506,7 +506,7 @@ def load_album_and_tracks(db, album_id):
if conn is not None:
try:
conn.close()
except Exception:
except Exception: # noqa: S110 — finally-block cleanup, logger may be torn down
pass
@ -938,8 +938,8 @@ class _RunContext:
return
try:
self.on_progress(updates)
except Exception:
pass
except Exception as e:
logger.debug("progress emit failed: %s", e)
def record_error(self, track_id, title, message, kind: str = 'skipped') -> None:
with self.state_lock:
@ -1185,8 +1185,8 @@ def reorganize_album(
return
try:
on_progress(updates)
except Exception:
pass
except Exception as e:
logger.debug("reorganize progress callback failed: %s", e)
# Load album + tracks
album_data, tracks = load_album_and_tracks(db, album_id)
@ -1329,7 +1329,7 @@ def reorganize_album(
try:
if os.path.isdir(staging_album_dir):
shutil.rmtree(staging_album_dir, ignore_errors=True)
except Exception:
except Exception: # noqa: S110 — finally-block cleanup, logger may be torn down
pass
# Best-effort cleanup of source directories. For each touched dir
@ -1343,14 +1343,14 @@ def reorganize_album(
if _has_remaining_audio(src_dir):
continue
_delete_album_sidecars(src_dir)
except Exception:
except Exception: # noqa: S110 — finally-block cleanup, logger may be torn down
pass
if cleanup_empty_dir_fn:
for src_dir in src_dirs_touched:
try:
cleanup_empty_dir_fn(src_dir)
except Exception:
except Exception: # noqa: S110 — finally-block cleanup, logger may be torn down
pass
# Prune empty *destination* siblings — e.g. when a previous

View file

@ -490,8 +490,8 @@ class LidarrDownloadClient(DownloadSourcePlugin):
try:
self._api_delete(f'album/{lidarr_album_id}', params={'deleteFiles': 'false'})
logger.debug(f"Cleaned up album {lidarr_album_id} from Lidarr")
except Exception:
pass
except Exception as e:
logger.debug("Lidarr album cleanup failed: %s", e)
except Exception as e:
logger.error(f"Lidarr download thread failed: {e}")

View file

@ -302,8 +302,8 @@ class ListenBrainzManager:
# Fallback to first image
if images:
return images[0].get('thumbnails', {}).get('small') or images[0].get('image')
except:
pass
except Exception as e:
logger.debug("cover-art fetch: %s", e)
return None

View file

@ -507,7 +507,7 @@ class ListeningStatsWorker:
if conn:
try:
conn.close()
except Exception:
except Exception: # noqa: S110 — finally-block cleanup, logger may be torn down
pass
def _resolve_db_track_id(self, title, artist):

View file

@ -102,7 +102,7 @@ def lookup(normalized_album_key: str, artist_key: str) -> Optional[str]:
if conn is not None:
try:
conn.close()
except Exception:
except Exception: # noqa: S110 — finally-block cleanup, logger may be torn down
pass
return None
@ -142,7 +142,7 @@ def record(normalized_album_key: str, artist_key: str, release_mbid: str) -> boo
if conn is not None:
try:
conn.close()
except Exception:
except Exception: # noqa: S110 — finally-block cleanup, logger may be torn down
pass
@ -168,7 +168,7 @@ def clear_all() -> bool:
if conn is not None:
try:
conn.close()
except Exception:
except Exception: # noqa: S110 — finally-block cleanup, logger may be torn down
pass

View file

@ -320,8 +320,8 @@ def download_cover_art(album_info: dict, target_dir: str, context: dict = None):
from core.spotify_client import _upgrade_spotify_image_url
art_url = _upgrade_spotify_image_url(art_url)
except Exception:
pass
except Exception as e:
logger.debug("upgrade spotify image url failed: %s", e)
elif art_url and "mzstatic.com" in art_url:
art_url = re.sub(r"\d+x\d+bb", "3000x3000bb", art_url)
if not art_url:

View file

@ -33,8 +33,8 @@ def get_metadata_cache():
try:
import threading
threading.Thread(target=_cache_instance.backfill_deezer_album_genres, daemon=True).start()
except Exception:
pass
except Exception as e:
logger.debug("start deezer genres backfill failed: %s", e)
return _cache_instance

View file

@ -171,8 +171,8 @@ def get_image_dimensions(data: bytes):
return w, h
length = struct.unpack(">H", data[i + 2 : i + 4])[0]
i += 2 + length
except Exception:
pass
except Exception as e:
logger.debug("parse JPEG dimensions failed: %s", e)
return None, None

View file

@ -278,8 +278,8 @@ def get_hydrabase_client(allow_fallback: bool = True, require_enabled: bool = Tr
if client and client.is_connected():
if not require_enabled or bool(_dev_mode_enabled_provider()):
return client
except Exception:
pass
except Exception as e:
logger.debug("hydrabase client lookup: %s", e)
if allow_fallback:
return get_itunes_client()
@ -394,8 +394,8 @@ def get_client_for_source(
client = get_spotify_client(client_factory=spotify_client_factory)
if client and client.is_spotify_authenticated():
return client
except Exception:
pass
except Exception as e:
logger.debug("spotify client get_for_source: %s", e)
return None
if source == "deezer":

View file

@ -296,8 +296,8 @@ def _process_musicbrainz_source(pp: dict, metadata: dict, cfg, runtime, track_ti
try:
from core.metadata import album_mbid_cache as _persisted_cache
_persisted_cache.record(normalized_album_key, artist_key, release_mbid)
except Exception:
pass
except Exception as e:
logger.debug("MBID cache persist failed: %s", e)
pp["release_mbid"] = release_mbid or ""
if pp["release_mbid"]:
pp["id_tags"]["MUSICBRAINZ_RELEASE_ID"] = pp["release_mbid"]
@ -959,8 +959,8 @@ def extract_source_metadata(context: dict, artist: dict, album_info: dict) -> di
resolved = itunes_client.resolve_primary_artist(artist_id)
if resolved and resolved != raw_album_artist:
raw_album_artist = resolved
except Exception:
pass
except Exception as e:
logger.debug("itunes primary artist resolve failed: %s", e)
metadata["album_artist"] = raw_album_artist
if album_info.get("is_album"):
@ -1136,8 +1136,8 @@ def embed_source_ids(audio_file, metadata: dict, context: dict = None, runtime=N
)
if isrc_value:
context["_isrc"] = str(isrc_value)
except Exception:
pass
except Exception as e:
logger.debug("context isrc copy failed: %s", e)
except Exception as exc:
logger.error("Error embedding source IDs (non-fatal): %s", exc)

View file

@ -141,8 +141,8 @@ class MusicBrainzWorker:
itype = item.get('type', '')
table = 'artists' if 'artist' in itype else ('albums' if 'album' in itype else 'tracks')
# Can't mark status without an ID — just skip
except Exception:
pass
except Exception as e:
logger.debug("null id table resolve failed: %s", e)
continue

View file

@ -922,8 +922,8 @@ class NavidromeClient(MediaServerClient):
if target_playlist:
self._make_request('deletePlaylist', {'id': target_playlist.id})
logger.info(f"Deleted existing backup playlist '{target_name}'")
except Exception:
pass # Target doesn't exist, which is fine
except Exception as e:
logger.debug("backup playlist precheck: %s", e)
# Create new playlist with copied tracks
try:

View file

@ -176,8 +176,8 @@ def playlist_explorer_build_tree(deps: PlaylistExplorerDeps):
artist_image = images[0].get('url') if images else None
elif hasattr(artist_info, 'image_url'):
artist_image = artist_info.image_url
except Exception:
pass
except Exception as e:
logger.debug("artist image resolve: %s", e)
else:
# No pre-resolved ID — search by name
try:
@ -224,8 +224,8 @@ def playlist_explorer_build_tree(deps: PlaylistExplorerDeps):
cursor.execute("SELECT title FROM albums WHERE artist_id = ?", (ar['id'],))
for alb_row in cursor.fetchall():
owned_titles.add((alb_row['title'] or '').strip().lower())
except Exception:
pass # Non-critical — owned badges just won't show
except Exception as e:
logger.debug("owned-titles lookup: %s", e)
# Build release list
releases = []
@ -256,8 +256,8 @@ def playlist_explorer_build_tree(deps: PlaylistExplorerDeps):
if cache and releases:
try:
cache.store_entity(source_name, 'artist_discography', cache_key, result)
except Exception:
pass
except Exception as e:
logger.debug("cache discography write: %s", e)
return result

View file

@ -405,8 +405,8 @@ class PlexClient(MediaServerClient):
new_count = getattr(a, 'leafCount', 0) or 0
if new_count > existing_count:
by_name[name_key] = a
except Exception:
pass
except Exception as e:
logger.debug("artist leafCount compare failed: %s", e)
return list(by_name.values())
def _dedupe_albums(self, albums: List[PlexAlbum]) -> List[PlexAlbum]:
@ -424,8 +424,8 @@ class PlexClient(MediaServerClient):
artist = ''
try:
artist = (getattr(alb, 'parentTitle', '') or '').strip().lower()
except Exception:
pass
except Exception as e:
logger.debug("album parentTitle read failed: %s", e)
key = (artist, title)
existing = by_key.get(key)
if existing is None:
@ -436,8 +436,8 @@ class PlexClient(MediaServerClient):
new_count = getattr(alb, 'leafCount', 0) or 0
if new_count > existing_count:
by_key[key] = alb
except Exception:
pass
except Exception as e:
logger.debug("album leafCount compare failed: %s", e)
return list(by_key.values())
def get_all_playlists(self) -> List[PlaylistInfo]:

View file

@ -104,8 +104,8 @@ class QobuzWorker:
try:
if self.client:
authenticated = self.client.is_authenticated()
except Exception:
pass
except Exception as e:
logger.debug("qobuz auth status check: %s", e)
return {
'enabled': True,
@ -163,8 +163,8 @@ class QobuzWorker:
itype = item.get('type', '')
table = 'artists' if 'artist' in itype else ('albums' if 'album' in itype else 'tracks')
# Can't mark status without an ID — just skip
except Exception:
pass
except Exception as e:
logger.debug("null-id item type lookup: %s", e)
continue

View file

@ -95,15 +95,15 @@ def build_runner(
def _cleanup_empty(src_dir):
try:
cleanup_empty_directories_fn(transfer_dir, os.path.join(src_dir, '_'))
except Exception:
pass
except Exception as e:
logger.debug("cleanup empty dirs failed: %s", e)
def _on_progress(updates):
try:
get_queue().update_active_progress(queue_id=item.queue_id, **updates)
except Exception:
except Exception as e:
# Progress fan-out failures must never break a run.
pass
logger.debug("reorganize progress fan-out: %s", e)
return reorganize_album(
album_id=item.album_id,

View file

@ -62,8 +62,8 @@ def _read_tag(audio, tag_name):
if vals:
return vals[0].decode('utf-8') if isinstance(vals[0], bytes) else str(vals[0])
return None
except Exception:
pass
except Exception as e:
logger.debug("read tag value failed: %s", e)
return None

View file

@ -299,8 +299,8 @@ class DiscographyBackfillJob(RepairJob):
track_id = track_item.get('id', '')
if track_id and self._is_in_wishlist(context.db, track_id):
continue
except Exception:
pass
except Exception as e:
logger.debug("wishlist membership check failed: %s", e)
# Build wishlist-ready track data. album is a dict (required by
# add_to_wishlist and by the download pipeline's cover-art

View file

@ -337,8 +337,8 @@ class LibraryReorganizeJob(RepairJob):
if context.config_manager:
try:
active_server = context.config_manager.get_active_media_server()
except Exception:
pass
except Exception as e:
logger.debug("active media server lookup: %s", e)
try:
conn = context.db._get_connection()
try:
@ -399,7 +399,7 @@ class LibraryReorganizeJob(RepairJob):
if conn:
try:
conn.close()
except Exception:
except Exception: # noqa: S110 — finally-block cleanup, logger may be torn down
pass
def _get_setting(self, context: JobContext, key: str, default):

View file

@ -366,8 +366,8 @@ class MbidMismatchDetectorJob(RepairJob):
try:
from core.musicbrainz_client import MusicBrainzClient
mb_client = MusicBrainzClient()
except Exception:
pass
except Exception as e:
logger.debug("MusicBrainz client init failed: %s", e)
if not mb_client:
logger.warning("MusicBrainz client not available, skipping MBID mismatch scan")

View file

@ -159,8 +159,8 @@ class OrphanFileDetectorJob(RepairJob):
(first_artist and (clean_title, first_artist) in known_titles_clean)
):
is_known = True
except Exception:
pass
except Exception as e:
logger.debug("tag-based orphan check: %s", e)
# Last resort: parse title from filename pattern "NN - Title [Quality].ext"
# and match against known titles. Catches files with unreadable tags.
@ -186,8 +186,8 @@ class OrphanFileDetectorJob(RepairJob):
if clean_fn and (clean_fn, clean_fa) in known_titles_clean:
is_known = True
break
except Exception:
pass
except Exception as e:
logger.debug("filename-pattern orphan check: %s", e)
if not is_known:
orphan_files.append(fpath)

View file

@ -496,8 +496,8 @@ class UnknownArtistFixerJob(RepairJob):
if not os.path.exists(sidecar_dst):
try:
shutil.move(sidecar_src, sidecar_dst)
except Exception:
pass
except Exception as e:
logger.debug("move sidecar file: %s", e)
# Also move cover.jpg from old album folder
cover_src = os.path.join(src_dir, 'cover.jpg')
@ -505,8 +505,8 @@ class UnknownArtistFixerJob(RepairJob):
if os.path.isfile(cover_src) and not os.path.exists(cover_dst):
try:
shutil.copy2(cover_src, cover_dst)
except Exception:
pass
except Exception as e:
logger.debug("copy cover.jpg: %s", e)
# Clean up empty directories
parent = os.path.dirname(current_norm)

View file

@ -579,8 +579,8 @@ class RepairWorker:
if self._on_job_start:
try:
self._on_job_start(job_id, job.display_name)
except Exception:
pass
except Exception as e:
logger.debug("on_job_start callback failed: %s", e)
# Record job start
run_id = self._record_job_start(job_id)
@ -590,8 +590,8 @@ class RepairWorker:
if self._on_job_progress:
try:
self._on_job_progress(job_id, **kwargs)
except Exception:
pass
except Exception as e:
logger.debug("on_job_progress callback failed: %s", e)
# Build context
context = JobContext(
@ -636,8 +636,8 @@ class RepairWorker:
try:
status = 'error' if result.errors > 0 and result.auto_fixed == 0 else 'finished'
self._on_job_finish(job_id, status, result)
except Exception:
pass
except Exception as e:
logger.debug("on_job_finish callback failed: %s", e)
logger.info(
"Job %s complete: scanned=%d fixed=%d findings=%d errors=%d (%.1fs)",
@ -1164,8 +1164,8 @@ class RepairWorker:
audio = MutagenFile(resolved)
if audio:
_, total_tracks = _read_track_number_tag(audio)
except Exception:
pass
except Exception as e:
logger.debug("Failed to read total_tracks tag from file: %s", e)
total_tracks = int(total_tracks or 0)
_fix_track_number_tag(resolved, int(correct_num), total_tracks)
@ -1185,8 +1185,8 @@ class RepairWorker:
cursor.execute("UPDATE tracks SET file_path = ? WHERE file_path = ?",
(new_path, resolved))
conn.commit()
except Exception:
pass
except Exception as e:
logger.debug("Failed to update DB file_path after rename: %s", e)
finally:
if conn:
conn.close()
@ -1544,8 +1544,8 @@ class RepairWorker:
if not os.path.exists(d):
try:
shutil.move(s, d)
except Exception:
pass
except Exception as e:
logger.debug("Failed to move sidecar %s: %s", s, e)
# Clean up empty dirs
self._cleanup_empty_parents(resolved)
@ -1652,8 +1652,8 @@ class RepairWorker:
if resolved and os.path.exists(resolved):
try:
os.remove(resolved)
except Exception:
pass
except Exception as e:
logger.debug("Failed to remove wrong file %s: %s", resolved, e)
if track_id:
try:
conn = self.db._get_connection()
@ -1661,8 +1661,8 @@ class RepairWorker:
cursor.execute("DELETE FROM tracks WHERE id = ?", (track_id,))
conn.commit()
conn.close()
except Exception:
pass
except Exception as e:
logger.debug("Failed to delete wrong track row from DB: %s", e)
return {'success': True, 'action': 'redownload',
'message': f'Added "{expected_title}" to wishlist, removed wrong file'}
@ -2389,8 +2389,8 @@ class RepairWorker:
album_thumb = album_row[2]
album_track_count = album_row[3]
spotify_album_id = album_row[4] if len(album_row) > 4 else None
except Exception:
pass
except Exception as e:
logger.debug("Failed to load album metadata for retag: %s", e)
finally:
if conn_meta:
conn_meta.close()
@ -2529,8 +2529,8 @@ class RepairWorker:
if not os.path.exists(sidecar_dst):
try:
shutil.move(sidecar_src, sidecar_dst)
except Exception:
pass
except Exception as e:
logger.debug("Failed to move sidecar %s: %s", sidecar_src, e)
# Update DB file path
conn = None
@ -2550,8 +2550,8 @@ class RepairWorker:
cursor.execute(
"UPDATE tracks SET file_path = ? WHERE file_path LIKE ? ESCAPE '^'",
(dst, '%/' + escaped))
except Exception:
pass
except Exception as e:
logger.debug("Suffix-match DB path update failed: %s", e)
conn.commit()
except Exception as e:
logger.debug("DB path update failed for %s: %s", src, e)
@ -2646,8 +2646,8 @@ class RepairWorker:
if os.path.exists(out_path):
try:
os.remove(out_path)
except Exception:
pass
except Exception as e:
logger.debug("Failed to remove out_path after ffmpeg failure: %s", e)
return {'success': False, 'error': f'ffmpeg conversion failed: {proc.stderr[:200] if proc.stderr else "unknown error"}'}
# Update QUALITY tag
@ -2664,8 +2664,8 @@ class RepairWorker:
from mutagen.mp4 import MP4FreeForm
audio['----:com.apple.iTunes:QUALITY'] = [MP4FreeForm(quality_label.encode('utf-8'))]
audio.save()
except Exception:
pass
except Exception as e:
logger.debug("Failed to write QUALITY tag on lossy copy: %s", e)
# Embed cover art from source FLAC
if codec in ('opus', 'aac'):
@ -2697,8 +2697,8 @@ class RepairWorker:
fmt = MP4Cover.FORMAT_JPEG if 'jpeg' in pic.mime else MP4Cover.FORMAT_PNG
dest_audio['covr'] = [MP4Cover(pic.data, imageformat=fmt)]
dest_audio.save()
except Exception:
pass
except Exception as e:
logger.debug("Failed to embed cover art in lossy copy: %s", e)
# Blasphemy Mode — uses the job's own setting, not the global lossy_copy one
delete_original = False
@ -2724,8 +2724,8 @@ class RepairWorker:
)
conn.commit()
conn.close()
except Exception:
pass
except Exception as e:
logger.debug("Failed to update DB path after lossy conversion: %s", e)
return {'success': True, 'action': 'converted_and_deleted',
'message': f'Converted to {quality_label} and deleted original'}
except Exception as e:
@ -2738,8 +2738,8 @@ class RepairWorker:
if os.path.exists(out_path):
try:
os.remove(out_path)
except Exception:
pass
except Exception as e:
logger.debug("Failed to remove out_path after timeout: %s", e)
return {'success': False, 'error': 'Conversion timed out (120s)'}
except Exception as e:
return {'success': False, 'error': f'Conversion error: {e}'}

View file

@ -7,10 +7,13 @@ Tag writing uses mutagen directly to stay consistent with the rest of the codeba
Supported formats: MP3, FLAC, OGG Vorbis, Opus, M4A/MP4
"""
import logging
import re
import subprocess
from typing import Optional, Tuple, Dict
logger = logging.getLogger(__name__)
# ReplayGain 2.0 reference level (EBU R128)
RG_REFERENCE_LUFS = -18.0
@ -189,8 +192,8 @@ def read_replaygain_tags(file_path: str) -> Dict[str, Optional[str]]:
result['track_peak'] = _mp4_rg(audio, _TAG_TRACK_PEAK)
result['album_gain'] = _mp4_rg(audio, _TAG_ALBUM_GAIN)
result['album_peak'] = _mp4_rg(audio, _TAG_ALBUM_PEAK)
except Exception:
pass
except Exception as e:
logger.debug("read replaygain tags failed: %s", e)
return result
@ -207,8 +210,8 @@ def _read_id3_txxx(audio, description: str) -> Optional[str]:
if frame_key.upper() == key.upper():
frame = audio.tags[frame_key]
return str(frame.text[0]) if frame.text else None
except Exception:
pass
except Exception as e:
logger.debug("read id3 txxx frame failed: %s", e)
return None
@ -218,8 +221,8 @@ def _vorbis_first(audio, key: str) -> Optional[str]:
vals = audio.get(key) or audio.get(key.upper())
if vals:
return str(vals[0])
except Exception:
pass
except Exception as e:
logger.debug("read vorbis comment failed: %s", e)
return None
@ -234,8 +237,8 @@ def _mp4_rg(audio, tag_name: str) -> Optional[str]:
if hasattr(val, 'decode'):
return val.decode('utf-8')
return str(val)
except Exception:
pass
except Exception as e:
logger.debug("read mp4 replaygain atom failed: %s", e)
return None

View file

@ -2,11 +2,14 @@
from __future__ import annotations
import logging
import threading
import time
from functools import wraps
from typing import Any, Dict, Optional
logger = logging.getLogger(__name__)
matched_context_lock = threading.Lock()
matched_downloads_context: Dict[str, Dict[str, Any]] = {}
tasks_lock = threading.Lock()
@ -57,8 +60,8 @@ def add_activity_item(icon, title, subtitle, time_ago="Now", show_toast=True):
if show_toast and _activity_toast_emitter is not None:
try:
_activity_toast_emitter("dashboard:toast", activity_item)
except Exception:
pass
except Exception as e:
logger.debug("emit activity toast failed: %s", e)
return activity_item

View file

@ -167,8 +167,8 @@ class SeasonalDiscoveryService:
try:
cursor.execute(f"ALTER TABLE {table} ADD COLUMN source TEXT NOT NULL DEFAULT 'spotify'")
conn.commit()
except Exception:
pass # Column already exists
except Exception as e:
logger.debug("source column migration %s: %s", table, e)
logger.info("Seasonal discovery database schema initialized")
@ -201,8 +201,8 @@ class SeasonalDiscoveryService:
val = row[0] if isinstance(row, tuple) else row['value']
if val in ('northern', 'southern'):
return val
except Exception:
pass
except Exception as e:
logger.debug("read hemisphere metadata failed: %s", e)
return 'northern'
def get_current_season(self) -> Optional[str]:

View file

@ -280,8 +280,8 @@ class SoulIDWorker:
if conn:
try:
conn.rollback()
except Exception:
pass
except Exception as _e:
logger.debug("rollback failed: %s", _e)
return 0
finally:
if conn:
@ -533,8 +533,8 @@ class SoulIDWorker:
if conn:
try:
conn.rollback()
except Exception:
pass
except Exception as _e:
logger.debug("rollback failed: %s", _e)
return 0
finally:
if conn:
@ -595,8 +595,8 @@ class SoulIDWorker:
if conn:
try:
conn.rollback()
except Exception:
pass
except Exception as _e:
logger.debug("rollback failed: %s", _e)
return 0
finally:
if conn:
@ -634,8 +634,8 @@ class SoulIDWorker:
if conn:
try:
conn.rollback()
except Exception:
pass
except Exception as _e:
logger.debug("rollback failed: %s", _e)
finally:
if conn:
conn.close()

View file

@ -216,8 +216,8 @@ class SoulseekClient(DownloadSourcePlugin):
if session:
try:
await session.close()
except:
pass
except Exception as _e:
logger.debug("aiohttp session close: %s", _e)
async def _make_direct_request(self, method: str, endpoint: str, **kwargs) -> Optional[Dict[str, Any]]:
"""Make a direct request to slskd without /api/v0/ prefix (for endpoints that work directly)"""
@ -273,9 +273,9 @@ class SoulseekClient(DownloadSourcePlugin):
if session:
try:
await session.close()
except:
pass
except Exception as _e:
logger.debug("aiohttp direct session close: %s", _e)
def _process_search_responses(self, responses_data: List[Dict[str, Any]]) -> tuple[List[TrackResult], List[AlbumResult]]:
"""Process search response data into TrackResult and AlbumResult objects"""
from collections import defaultdict
@ -1621,8 +1621,8 @@ class SoulseekClient(DownloadSourcePlugin):
if resp.status in [200, 405]: # 405 means endpoint exists but wrong method
available_endpoints[f"direct_{endpoint}"] = f"Status: {resp.status}"
logger.info(f"[OK] Direct endpoint available: {simple_url} (Status: {resp.status})")
except:
pass
except Exception as _e:
logger.debug("direct endpoint probe %s: %s", endpoint, _e)
finally:
await session.close()

View file

@ -253,8 +253,8 @@ class SoulSyncClient(MediaServerClient):
if self._progress_callback:
try:
self._progress_callback(msg)
except Exception:
pass
except Exception as e:
logger.debug("progress callback failed: %s", e)
# ── Core Scanning ──

View file

@ -39,8 +39,8 @@ def _get_min_api_interval():
val = config_manager.get('spotify.min_api_interval', None)
if val is not None:
return max(0.1, float(val)) # Floor at 100ms to prevent abuse
except Exception:
pass
except Exception as e:
logger.debug("get min_api_interval setting: %s", e)
return MIN_API_INTERVAL
# Request queuing for burst handling
@ -136,8 +136,8 @@ def _set_global_rate_limit(retry_after_seconds, endpoint_name, has_real_header=F
detail=f'{"escalation #" + str(_rate_limit_hit_count) if escalated else "initial"}'
f'{", real Retry-After" if has_real_header else ", estimated"}'
)
except Exception:
pass
except Exception as e:
logger.debug("api_call_tracker record rate_limit_ban: %s", e)
try:
from core.metadata.status import publish_spotify_status
@ -148,8 +148,8 @@ def _set_global_rate_limit(retry_after_seconds, endpoint_name, has_real_header=F
rate_limit=_get_rate_limit_info(),
post_ban_cooldown=_get_post_ban_cooldown_remaining() or None,
)
except Exception:
pass
except Exception as e:
logger.debug("publish_spotify_status set rate limit: %s", e)
def _is_globally_rate_limited():
@ -230,8 +230,8 @@ def _clear_rate_limit():
rate_limit=None,
post_ban_cooldown=None,
)
except Exception:
pass
except Exception as e:
logger.debug("publish_spotify_status clear rate limit: %s", e)
def _detect_and_set_rate_limit(exception, endpoint_name="unknown"):
@ -637,8 +637,8 @@ class SpotifyClient:
rate_limit=_get_rate_limit_info(),
post_ban_cooldown=_get_post_ban_cooldown_remaining() or None,
)
except Exception:
pass
except Exception as e:
logger.debug("publish_spotify_status no-client: %s", e)
return False
# If globally rate limited, report as NOT authenticated so callers
@ -655,8 +655,8 @@ class SpotifyClient:
rate_limit=_get_rate_limit_info(),
post_ban_cooldown=_get_post_ban_cooldown_remaining() or None,
)
except Exception:
pass
except Exception as e:
logger.debug("publish_spotify_status rate-limited: %s", e)
return False
# Post-ban cooldown: after a ban expires, don't probe Spotify immediately.
@ -675,8 +675,8 @@ class SpotifyClient:
rate_limit=None,
post_ban_cooldown=remaining or None,
)
except Exception:
pass
except Exception as e:
logger.debug("publish_spotify_status post-ban cooldown: %s", e)
return False
# Check cache first (lock only for brief read)
@ -692,8 +692,8 @@ class SpotifyClient:
rate_limit=None,
post_ban_cooldown=None,
)
except Exception:
pass
except Exception as e:
logger.debug("publish_spotify_status cache hit: %s", e)
return self._auth_cached_result
# Cache miss — make API call outside the lock.
@ -718,11 +718,11 @@ class SpotifyClient:
rate_limit=None,
post_ban_cooldown=None,
)
except Exception:
pass
except Exception as e:
logger.debug("publish_spotify_status no-token: %s", e)
return False
except Exception:
pass
except Exception as e:
logger.debug("cached token probe: %s", e)
# Use a dedicated probe client (retries=0) so a 429 here propagates
# immediately and we can detect long Retry-After bans.
@ -771,8 +771,8 @@ class SpotifyClient:
rate_limit=_get_rate_limit_info() if rate_limited_state else None,
post_ban_cooldown=None,
)
except Exception:
pass
except Exception as e:
logger.debug("publish_spotify_status auth probe: %s", e)
return result
@ -793,8 +793,8 @@ class SpotifyClient:
rate_limit=None,
post_ban_cooldown=None,
)
except Exception:
pass
except Exception as e:
logger.debug("publish_spotify_status disconnect: %s", e)
cache_path = 'config/.spotify_cache'
try:
@ -1245,8 +1245,8 @@ class SpotifyClient:
for raw in cached_results:
try:
tracks.append(Track.from_spotify_track(raw))
except Exception:
pass
except Exception as e:
logger.debug("Track.from_spotify_track cache parse: %s", e)
if tracks:
return tracks
@ -1298,8 +1298,8 @@ class SpotifyClient:
for raw in cached_results:
try:
artists.append(Artist.from_spotify_artist(raw))
except Exception:
pass
except Exception as e:
logger.debug("Artist.from_spotify_artist cache parse: %s", e)
if artists:
query_lower = query.lower().strip()
artists.sort(key=lambda a: (0 if a.name.lower().strip() == query_lower else 1))
@ -1361,8 +1361,8 @@ class SpotifyClient:
for raw in cached_results:
try:
albums.append(Album.from_spotify_album(raw))
except Exception:
pass
except Exception as e:
logger.debug("Album.from_spotify_album cache parse: %s", e)
if albums:
return albums
@ -1636,8 +1636,8 @@ class SpotifyClient:
try:
albums_list = cached.get('_albums', cached) if isinstance(cached, dict) else cached
return [Album.from_spotify_album(ad) for ad in albums_list]
except Exception:
pass # Cache data incompatible, re-fetch
except Exception as e:
logger.debug("artist albums cache reuse: %s", e)
if self.is_spotify_authenticated():
try:

View file

@ -234,8 +234,8 @@ class SpotifyWorker:
itype = item.get('type', '')
table = 'artists' if 'artist' in itype else ('albums' if 'album' in itype else 'tracks')
# Can't mark status without an ID — just skip
except Exception:
pass
except Exception as e:
logger.debug("null id table resolve failed: %s", e)
continue
self._process_item(item)

View file

@ -84,8 +84,8 @@ def get_top_artists(database, image_url_fixer: ImageUrlFixer, time_range: str, l
artist['soul_id'] = row[4]
finally:
conn.close()
except Exception:
pass
except Exception as e:
logger.debug("top artists enrich failed: %s", e)
return artists
@ -114,8 +114,8 @@ def get_top_albums(database, image_url_fixer: ImageUrlFixer, time_range: str, li
album['artist_id'] = row[2]
finally:
conn.close()
except Exception:
pass
except Exception as e:
logger.debug("top albums enrich failed: %s", e)
return albums
@ -146,8 +146,8 @@ def get_top_tracks(database, image_url_fixer: ImageUrlFixer, time_range: str, li
track['artist_id'] = row[2]
finally:
conn.close()
except Exception:
pass
except Exception as e:
logger.debug("top tracks enrich failed: %s", e)
return tracks

View file

@ -131,8 +131,8 @@ def read_file_tags(file_path: str) -> Dict[str, Any]:
result['replaygain_track_peak'] = rg.get('track_peak')
result['replaygain_album_gain'] = rg.get('album_gain')
result['replaygain_album_peak'] = rg.get('album_peak')
except Exception:
pass
except Exception as e:
logger.debug("read replaygain tags failed: %s", e)
return result

View file

@ -640,8 +640,8 @@ class TidalClient:
image_id = image_rel.get('id', '')
if image_id:
image_url = f"https://resources.tidal.com/images/{image_id.replace('-', '/')}/640x640.jpg"
except Exception:
pass
except Exception as _e:
logger.debug("tidal v2 playlists image_url extract: %s", _e)
new_playlist = Playlist(
id=str(playlist_id),
@ -1188,8 +1188,8 @@ class TidalClient:
image_id = image_rel.get('id', '')
if image_id:
playlist.image_url = f"https://resources.tidal.com/images/{image_id.replace('-', '/')}/640x640.jpg"
except Exception:
pass
except Exception as _e:
logger.debug("tidal playlist image_url extract: %s", _e)
logger.info(f"Retrieved Tidal playlist '{playlist.name}' with {len(tracks)} tracks")
return playlist

View file

@ -124,8 +124,8 @@ class TidalWorker:
authenticated = False
try:
authenticated = self.client.is_authenticated()
except Exception:
pass
except Exception as e:
logger.debug("tidal auth status check: %s", e)
return {
'enabled': True,
@ -176,8 +176,8 @@ class TidalWorker:
itype = item.get('type', '')
table = 'artists' if 'artist' in itype else ('albums' if 'album' in itype else 'tracks')
# Can't mark status without an ID — just skip
except Exception:
pass
except Exception as e:
logger.debug("null-id item type lookup: %s", e)
continue

View file

@ -440,8 +440,8 @@ def process_watchlist_scan_automatically(automation_id=None, profile_id=None, de
'new_tracks_found': str(total_new_tracks),
'tracks_added': str(total_added_to_wishlist),
})
except Exception:
pass
except Exception as e:
logger.debug("watchlist_scan_completed emit failed: %s", e)
except Exception as e:
logger.error(f"Error in automatic watchlist scan: {e}")
@ -460,7 +460,7 @@ def process_watchlist_scan_automatically(automation_id=None, profile_id=None, de
# Clear one-time rescan cutoff after full scan cycle
try:
scanner._clear_rescan_cutoff()
except Exception:
except Exception: # noqa: S110 — finally-block cleanup, logger may be torn down
pass
# Always reset flag

View file

@ -2745,8 +2745,8 @@ class WatchlistScanner:
if release_date_str and len(release_date_str) >= 10:
release_date = datetime.strptime(release_date_str[:10], "%Y-%m-%d")
is_new = (datetime.now() - release_date).days <= 30
except Exception:
pass
except Exception as e:
logger.debug("album release_date parse failed: %s", e)
for track in tracks:
try:
@ -2778,8 +2778,8 @@ class WatchlistScanner:
synth_pop += 15
elif age_days <= 365:
synth_pop += 5
except Exception:
pass
except Exception as e:
logger.debug("synthetic popularity age calc failed: %s", e)
if similar_artist.occurrence_count >= 3:
synth_pop += 10
elif similar_artist.occurrence_count >= 2:
@ -2902,8 +2902,8 @@ class WatchlistScanner:
if release_date_str and len(release_date_str) >= 10:
release_date = datetime.strptime(release_date_str[:10], "%Y-%m-%d")
is_new = (datetime.now() - release_date).days <= 30
except Exception:
pass
except Exception as e:
logger.debug("album release_date parse failed: %s", e)
for track in tracks:
try:
@ -3107,8 +3107,8 @@ class WatchlistScanner:
release_date = datetime.strptime(release_date_str, "%Y-%m-%d")
days_old = (datetime.now() - release_date).days
is_new = days_old <= 30
except:
pass
except Exception as e:
logger.debug("new-release date parse: %s", e)
# Add each track to discovery pool
for track in tracks:
@ -3214,8 +3214,8 @@ class WatchlistScanner:
elif profile['avg_daily_plays'] > 20:
days_lookback = 21 # Heavy listener — keep it fresh
logger.info(f"Recent albums window: {days_lookback} days (avg {profile['avg_daily_plays']:.1f} plays/day)")
except Exception:
pass
except Exception as e:
logger.debug("listening profile lookback adjust failed: %s", e)
cutoff_date = datetime.now() - timedelta(days=days_lookback)
discovery_sources = self._discovery_source_priority()
if not discovery_sources:
@ -3498,8 +3498,8 @@ class WatchlistScanner:
_artist_genre_cache[_row[0].lower()] = {g.strip().lower() for g in _row[1].split(',') if g.strip()}
_conn.close()
logger.debug(f"Built genre cache for {len(_artist_genre_cache)} artists")
except Exception:
pass
except Exception as e:
logger.debug("artist genre cache build failed: %s", e)
logger.info(f"Curating playlists for sources: {sources_to_process}")
@ -3552,8 +3552,8 @@ class WatchlistScanner:
if release_date_str and len(release_date_str) >= 10:
release_date = datetime.strptime(release_date_str[:10], "%Y-%m-%d")
days_old = (datetime.now() - release_date).days
except:
pass
except Exception as e:
logger.debug("release-date parse: %s", e)
for track in album_data['tracks'].get('items', []):
track_id = track.get('id')
@ -3760,8 +3760,8 @@ class WatchlistScanner:
_wa_list = self.database.get_watchlist_artists(profile_id=profile_id)
for _wa in _wa_list:
_wa_id_to_name[str(_wa.id)] = (_wa.artist_name or '').lower()
except Exception:
pass
except Exception as e:
logger.debug("watchlist artist id-to-name map failed: %s", e)
all_similar = self.database.get_top_similar_artists(limit=200, profile_id=profile_id)

View file

@ -3,6 +3,9 @@
from __future__ import annotations
import json
import logging
logger = logging.getLogger(__name__)
def load_wishlist_keys(cursor, profile_id: int) -> set[str]:
@ -27,21 +30,21 @@ def load_wishlist_keys(cursor, profile_id: int) -> set[str]:
wa = ""
if wname:
keys.add(wname + "|||" + wa.lower().strip())
except Exception:
pass
except Exception as e:
logger.debug("parse wishlist row failed: %s", e)
try:
cursor.execute("SELECT spotify_data FROM wishlist_tracks WHERE profile_id = ?", (profile_id,))
_absorb(cursor.fetchall())
return keys
except Exception:
pass
except Exception as e:
logger.debug("profile-aware wishlist query failed: %s", e)
try:
cursor.execute("SELECT spotify_data FROM wishlist_tracks")
_absorb(cursor.fetchall())
except Exception:
pass
except Exception as e:
logger.debug("legacy wishlist query failed: %s", e)
return keys

View file

@ -215,8 +215,8 @@ def finalize_auto_wishlist_completion(
'tracks_found': str(tracks_added),
'tracks_failed': str(total_failed - tracks_added),
})
except Exception:
pass
except Exception as e:
logger.debug("emit wishlist_processing_completed failed: %s", e)
return completion_summary

View file

@ -209,8 +209,8 @@ class WebMetadataUpdateWorker:
raw = self._db.api_get_artist(best.id)
if raw:
spotify_artist_id = raw.get('spotify_artist_id')
except Exception:
pass
except Exception as e:
logger.debug("get spotify_artist_id failed: %s", e)
return best, has_genres, spotify_artist_id
except Exception:
return None, False, None
@ -419,10 +419,10 @@ class WebMetadataUpdateWorker:
try:
for album in artist.albums():
if hasattr(album, 'genres') and album.genres:
album_genres.update(genre.tag if hasattr(genre, 'tag') else str(genre)
album_genres.update(genre.tag if hasattr(genre, 'tag') else str(genre)
for genre in album.genres)
except Exception:
pass # Albums might not be accessible
except Exception as e:
logger.debug("artist album genre walk: %s", e)
# Combine all genres (prioritize Spotify genres)
all_genres = spotify_genres.union(album_genres)

View file

@ -1190,8 +1190,8 @@ class YouTubeClient(DownloadSourcePlugin):
album_data = track_details.get('album', {})
if album_data.get('artists'):
album_artist = album_data['artists'][0]
except:
pass
except Exception as e:
logger.debug("spotify album artist lookup: %s", e)
logger.debug(" Setting metadata tags...")

View file

@ -650,8 +650,8 @@ class MusicDatabase:
try:
cursor.execute("ALTER TABLE sync_history ADD COLUMN track_results TEXT")
logger.info("Added track_results column to sync_history table")
except Exception:
pass
except Exception as e:
logger.debug("Failed to add track_results column: %s", e)
# Migration: add source_page column to sync_history (UI origin context for batch panel)
try:
@ -660,8 +660,8 @@ class MusicDatabase:
try:
cursor.execute("ALTER TABLE sync_history ADD COLUMN source_page TEXT")
logger.info("Added source_page column to sync_history table")
except Exception:
pass
except Exception as e:
logger.debug("Failed to add source_page column: %s", e)
# Migration: add track_artist column for per-track artist on compilations/DJ mixes
try:
@ -670,8 +670,8 @@ class MusicDatabase:
try:
cursor.execute("ALTER TABLE tracks ADD COLUMN track_artist TEXT")
logger.info("Added track_artist column to tracks table")
except Exception:
pass
except Exception as e:
logger.debug("Failed to add track_artist column: %s", e)
# Migration: add file_size column so the Stats page can show
# total library size on disk without having to walk the
@ -687,8 +687,8 @@ class MusicDatabase:
try:
cursor.execute("ALTER TABLE tracks ADD COLUMN file_size INTEGER")
logger.info("Added file_size column to tracks table")
except Exception:
pass
except Exception as e:
logger.debug("Failed to add file_size column: %s", e)
# One-time migration: purge discovery cache entries that lack track_number.
# Prior versions cached discovery results without track_number/disc_number/release_date,
@ -704,8 +704,8 @@ class MusicDatabase:
cursor.execute("CREATE TABLE _discovery_cache_v2_migrated (applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)")
if purged > 0:
logger.info(f"Purged {purged} stale discovery cache entries (missing track_number)")
except Exception:
pass
except Exception as e:
logger.debug("Failed to purge stale discovery cache entries: %s", e)
# One-time migration: purge Deezer album/track cache entries with missing data.
# Deezer's /artist/{id}/albums returns albums without artist info, and search
@ -721,8 +721,8 @@ class MusicDatabase:
cursor.execute("CREATE TABLE _deezer_cache_v2_migrated (applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)")
if purged > 0:
logger.info(f"Purged {purged} stale Deezer cache entries (missing artist/track_position)")
except Exception:
pass
except Exception as e:
logger.debug("Failed to purge stale Deezer cache entries: %s", e)
# One-time migration: purge cached tracks/albums with junk artist names.
# The cache gate now rejects these, but existing entries need cleaning.
@ -738,8 +738,8 @@ class MusicDatabase:
cursor.execute("CREATE TABLE _cache_junk_artist_purged (applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)")
if purged > 0:
logger.info(f"Purged {purged} cached tracks/albums with junk artist names")
except Exception:
pass
except Exception as e:
logger.debug("Failed to purge cached tracks/albums with junk artist names: %s", e)
# HiFi API instances table
cursor.execute("""
@ -819,8 +819,8 @@ class MusicDatabase:
config = json.loads(row[2]) if row[2] else {}
then_actions = json.dumps([{'type': row[1], 'config': config}])
cursor.execute("UPDATE automations SET then_actions = ? WHERE id = ?", (then_actions, row[0]))
except Exception:
pass
except Exception as e:
logger.debug("Failed to migrate notify data for automation row: %s", e)
logger.info("Migrated existing notify data to then_actions")
except Exception as e:
logger.error(f"Error adding automation then_actions column: {e}")
@ -1527,8 +1527,8 @@ class MusicDatabase:
try:
cursor.execute("ALTER TABLE liked_albums_pool ADD COLUMN discogs_release_id TEXT")
logger.info("Added discogs_release_id column to liked_albums_pool")
except Exception:
pass
except Exception as e:
logger.debug("Failed to add discogs_release_id column: %s", e)
logger.info("Discovery tables added/verified successfully")
@ -2468,8 +2468,8 @@ class MusicDatabase:
if 'profile_id' not in columns:
cursor.execute(f"ALTER TABLE {table} ADD COLUMN profile_id INTEGER DEFAULT 1")
logger.info(f"Repaired missing profile_id column on {table}")
except Exception:
pass
except Exception as e:
logger.debug("Failed to repair profile_id column on %s: %s", table, e)
if already_migrated:
return # Rest of migration already done
@ -2666,8 +2666,8 @@ class MusicDatabase:
for idx_name, table in index_pairs:
try:
cursor.execute(f"CREATE INDEX IF NOT EXISTS {idx_name} ON {table} (profile_id)")
except Exception:
pass
except Exception as e:
logger.debug("Failed to create index %s on %s: %s", idx_name, table, e)
# Set migration marker
cursor.execute("""
@ -3326,8 +3326,8 @@ class MusicDatabase:
))
if cursor.rowcount > 0:
inserted += 1
except Exception:
pass
except Exception as e:
logger.debug("Failed to insert listening event: %s", e)
conn.commit()
return inserted
except Exception as e:
@ -3635,8 +3635,8 @@ class MusicDatabase:
total_size = 0
try:
total_size = os.path.getsize(str(self.database_path))
except Exception:
pass
except Exception as e:
logger.debug("Failed to stat database file size: %s", e)
conn = self._get_connection()
cursor = conn.cursor()
@ -3663,8 +3663,8 @@ class MusicDatabase:
cursor.execute(f"SELECT COUNT(*) FROM [{tbl}]")
count = cursor.fetchone()[0]
tables.append({'name': tbl, 'size': count})
except Exception:
pass
except Exception as e:
logger.debug("Failed to get row count for table %s: %s", tbl, e)
tables.sort(key=lambda x: x['size'], reverse=True)
return {
@ -4189,8 +4189,8 @@ class MusicDatabase:
'bubble_snapshots', 'recent_releases']:
try:
cursor.execute(f"DELETE FROM {table} WHERE profile_id = ?", (profile_id,))
except Exception:
pass
except Exception as e:
logger.debug("Failed to delete from %s for profile: %s", table, e)
cursor.execute("DELETE FROM profiles WHERE id = ?", (profile_id,))
conn.commit()
return cursor.rowcount > 0
@ -5152,8 +5152,8 @@ class MusicDatabase:
album_artist_name = artist_row[0] if artist_row else ''
if nav_artist and nav_artist.lower() != album_artist_name.lower():
track_artist = nav_artist
except Exception:
pass
except Exception as e:
logger.debug("Failed to load album artist for track_artist comparison: %s", e)
# Extract MusicBrainz recording ID from server if available (Navidrome provides this)
mbid = getattr(track_obj, 'musicBrainzId', None) or None
@ -5219,8 +5219,8 @@ class MusicDatabase:
file_path=file_path,
thumb_url=album_row[1] if album_row and len(album_row) > 1 else None
)
except Exception:
pass # Non-critical history logging
except Exception as e:
logger.debug("history logging: %s", e)
return True
@ -10901,8 +10901,8 @@ class MusicDatabase:
""")
for row in cursor.fetchall():
source_counts[row['download_source']] = row['cnt']
except Exception:
pass
except Exception as e:
logger.debug("Failed to load library history source counts: %s", e)
stats['source_counts'] = source_counts
return stats
@ -11193,8 +11193,8 @@ class MusicDatabase:
WHERE playlist_id = ? AND source_track_id IS NOT NULL AND extra_data IS NOT NULL
""", (playlist_id,))
old_extra_map = {row['source_track_id']: row['extra_data'] for row in cursor.fetchall()}
except Exception:
pass
except Exception as e:
logger.debug("Failed to preserve mirrored playlist extra_data: %s", e)
# Replace all tracks
cursor.execute("DELETE FROM mirrored_playlist_tracks WHERE playlist_id=?", (playlist_id,))
@ -12137,5 +12137,5 @@ def close_database():
db_instance.close()
except Exception as e:
# Ignore threading errors during shutdown
pass
logger.debug("db instance close: %s", e)
_database_instances.clear()

View file

@ -5,7 +5,7 @@ line-length = 160
[tool.ruff.lint]
# Start lenient — catch real bugs, not style nits
# E: pycodestyle errors, F: pyflakes, UP: pyupgrade, B: bugbear
select = ["F", "E9", "W6", "B"]
select = ["F", "E9", "W6", "B", "S110"]
# Ignore rules that will flag too much in an existing codebase
ignore = [
"F401", # unused imports (too noisy initially)
@ -15,7 +15,7 @@ ignore = [
[tool.ruff.lint.per-file-ignores]
# Tests can use assert, magic values, etc.
"tests/**" = ["B", "F"]
"tests/**" = ["B", "F", "S110"]
[tool.pytest.ini_options]
markers = [

View file

@ -545,8 +545,8 @@ class PlaylistSyncService:
spotify_id, me.clean_title(original_title), me.clean_artist(artist_name),
active_server, db_track.id, db_track.title, confidence
)
except Exception:
pass
except Exception as e:
logger.debug("save sync match cache failed: %s", e)
# Fetch the actual track object from active media server using the database track ID
try:

View file

@ -51,8 +51,8 @@ def _build_version_string():
result = subprocess.run(['git', 'rev-parse', 'HEAD'], capture_output=True, text=True, cwd=os.path.dirname(__file__) or '.')
if result.returncode == 0:
sha = result.stdout.strip()
except Exception:
pass
except Exception as e:
logger.debug("git rev-parse failed: %s", e)
if sha:
return f"{_SOULSYNC_BASE_VERSION}+{sha[:7]}"
return _SOULSYNC_BASE_VERSION
@ -387,8 +387,8 @@ def _set_profile_context():
session.pop('profile_id', None)
from flask import jsonify as _jsonify
return _jsonify({"error": "profile_required", "message": "Profile no longer exists"}), 401
except Exception:
pass # DB error — don't block requests, use the session value
except Exception as e:
logger.debug("profile session validate: %s", e)
g.profile_id = pid
@ -415,8 +415,8 @@ def _log_slow_request(response):
response.status_code,
elapsed_ms,
)
except Exception:
pass
except Exception as e:
logger.debug("slow request log failed: %s", e)
return response
@ -515,8 +515,8 @@ def check_download_permission():
profile = get_database().get_profile(pid)
if profile and not profile.get('can_download', True):
return jsonify({'success': False, 'error': 'Downloads are disabled for this profile.'}), 403
except Exception:
pass # DB error — don't block
except Exception as e:
logger.debug("download permission check: %s", e)
return None
# --- Docker Helper Functions ---
@ -1249,8 +1249,8 @@ def _register_automation_handlers():
'added': str(added_count),
'removed': str(removed_count),
})
except Exception:
pass
except Exception as e:
logger.debug("playlist_synced automation emit failed: %s", e)
else:
logger.warning(f"[AUTOMATION] No changes: '{pl.get('name', '')}' (tracks={len(old_ids)})")
_update_automation_progress(auto_id,
@ -1397,8 +1397,8 @@ def _register_automation_handlers():
'status': 'skipped',
'reason': f'All {len(tracks_json)} tracks unchanged since last sync',
}
except Exception:
pass # If we can't read last status, just run the sync
except Exception as e:
logger.debug("mirror sync last-status read: %s", e)
_update_automation_progress(auto_id, progress=50,
phase=f'Syncing "{pl["name"]}"',
@ -1855,8 +1855,8 @@ def _register_automation_handlers():
elif os.path.isdir(fp):
_shutil.rmtree(fp)
removed += 1
except Exception:
pass
except Exception as e:
logger.debug("quarantine entry purge failed: %s", e)
_update_automation_progress(automation_id,
log_line=f'Removed {removed} quarantined items', log_type='success' if removed > 0 else 'info')
return {'status': 'completed', 'removed': str(removed)}
@ -1961,8 +1961,8 @@ def _register_automation_handlers():
while len(existing) > max_backups:
try:
os.remove(existing.pop(0))
except Exception:
pass
except Exception as e:
logger.debug("rolling backup cleanup failed: %s", e)
_update_automation_progress(automation_id,
log_line=f'Backup created: {size_mb}MB ({os.path.basename(backup_path)})', log_type='success')
return {'status': 'completed', 'backup_path': backup_path, 'size_mb': str(size_mb)}
@ -2101,8 +2101,8 @@ def _register_automation_handlers():
elif os.path.isdir(fp):
_shutil.rmtree(fp)
q_removed += 1
except Exception:
pass
except Exception as e:
logger.debug("quarantine entry purge failed: %s", e)
steps.append(f'Quarantine: removed {q_removed} items')
_update_automation_progress(automation_id,
log_line=f'Quarantine: removed {q_removed} items', log_type='success' if q_removed else 'info')
@ -2166,8 +2166,8 @@ def _register_automation_handlers():
for hidden in entries:
try:
os.remove(os.path.join(dirpath, hidden))
except Exception:
pass
except Exception as e:
logger.debug("hidden file cleanup failed: %s", e)
try:
os.rmdir(dirpath)
s_removed += 1
@ -2515,8 +2515,8 @@ def get_cached_transfer_data():
all_downloads = run_async(
download_orchestrator.engine.get_all_downloads(exclude=('soulseek',))
)
except Exception:
pass
except Exception as e:
logger.debug("get_all_downloads failed: %s", e)
for download in all_downloads:
key = _make_context_key(download.username, download.filename)
# Convert DownloadStatus to transfer dict format
@ -2974,13 +2974,13 @@ def _atexit_save_history():
try:
from core.api_call_tracker import api_call_tracker
api_call_tracker.save()
except Exception:
except Exception: # noqa: S110 — atexit handler, log handles may be closed
pass
def _atexit_shutdown():
try:
_shutdown_runtime_components()
except Exception:
except Exception: # noqa: S110 — atexit handler, log handles may be closed
pass
@ -3446,8 +3446,8 @@ def _get_enrichment_status():
if key == 'spotify_enrichment':
try:
svc_data['daily_budget'] = worker._get_daily_budget_info()
except Exception:
pass
except Exception as e:
logger.debug("spotify daily budget read failed: %s", e)
services[key] = svc_data
else:
@ -4176,8 +4176,8 @@ def handle_settings():
# FIX: Re-instantiate the global tidal_client to pick up new settings
try:
tidal_client = TidalClient()
except Exception:
pass # Keep existing tidal_client if re-init fails
except Exception as e:
logger.debug("tidal client re-init: %s", e)
# Reload enrichment worker clients for key-based services
if lastfm_worker:
lastfm_worker._init_client()
@ -4205,8 +4205,8 @@ def handle_settings():
# Include which download sources are configured so the UI can auto-disable unconfigured ones
try:
data['_source_status'] = download_orchestrator.get_source_status()
except Exception:
pass
except Exception as e:
logger.debug("download source status read failed: %s", e)
return jsonify(data)
except Exception as e:
return jsonify({"error": str(e)}), 500
@ -4330,8 +4330,8 @@ def hydrabase_connect():
if _hydrabase_ws:
try:
_hydrabase_ws.close()
except:
pass
except Exception as _e:
logger.debug("hydrabase connect-existing close: %s", _e)
ws = websocket.create_connection(
url,
header={"x-api-key": api_key},
@ -4356,8 +4356,8 @@ def hydrabase_disconnect():
if _hydrabase_ws:
try:
_hydrabase_ws.close()
except:
pass
except Exception as e:
logger.debug("hydrabase disconnect close: %s", e)
_hydrabase_ws = None
config_manager.set('hydrabase.auto_connect', False)
# Only disable dev mode if not using Hydrabase as a regular fallback source
@ -4428,8 +4428,8 @@ def hydrabase_send():
with _hydrabase_lock:
try:
_hydrabase_ws.close()
except:
pass
except Exception as _e:
logger.debug("hydrabase send close: %s", _e)
_hydrabase_ws = None
return jsonify({"success": False, "error": str(e)}), 500
@ -6118,8 +6118,8 @@ def deezer_callback():
try:
json_data = resp.json()
access_token = json_data.get('access_token')
except Exception:
pass
except Exception as e:
logger.debug("deezer token json parse failed: %s", e)
if not access_token:
return f"<h1>No Access Token</h1><p>Deezer response: {resp.text[:200]}</p>", 400
@ -7451,8 +7451,8 @@ def get_download_status():
if transfer_id:
try:
run_async(download_orchestrator.cancel_download(str(transfer_id), username, remove=True))
except Exception:
pass
except Exception as e:
logger.debug("orphan transfer cancel failed: %s", e)
_orphaned_download_keys.discard(context_key)
continue # Skip normal post-processing either way
@ -8475,8 +8475,8 @@ def get_artist_detail(artist_id):
except Exception:
enrichment_coverage[svc] = 0
enrichment_coverage['total_tracks'] = total
except Exception:
pass
except Exception as e:
logger.debug("enrichment coverage build failed: %s", e)
response_data = {
"success": True,
@ -8780,8 +8780,8 @@ def get_artist_discography(artist_id):
if not artist_info.get('genres') and lib['genres']:
try:
artist_info['genres'] = json.loads(lib['genres'])
except Exception:
pass
except Exception as e:
logger.debug("genres json parse failed: %s", e)
# Last.fm enrichment
if lib.get('lastfm_bio'):
artist_info['lastfm_bio'] = lib['lastfm_bio']
@ -8792,8 +8792,8 @@ def get_artist_discography(artist_id):
if lib.get('lastfm_tags'):
try:
artist_info['lastfm_tags'] = json.loads(lib['lastfm_tags']) if isinstance(lib['lastfm_tags'], str) else lib['lastfm_tags']
except Exception:
pass
except Exception as e:
logger.debug("lastfm_tags json parse failed: %s", e)
if lib.get('lastfm_url'):
artist_info['lastfm_url'] = lib['lastfm_url']
if lib.get('genius_url'):
@ -9475,25 +9475,25 @@ def _build_artist_quality_deps():
sources.append(('spotify', spotify_client))
try:
sources.append(('itunes', _get_itunes_client()))
except Exception:
pass
except Exception as e:
logger.debug("itunes client init failed: %s", e)
try:
sources.append(('deezer', _get_deezer_client()))
except Exception:
pass
except Exception as e:
logger.debug("deezer client init failed: %s", e)
# Discogs needs an explicit token; only include when configured.
try:
_discogs_token = config_manager.get('discogs.token', '')
if _discogs_token:
sources.append(('discogs', _get_discogs_client(_discogs_token)))
except Exception:
pass
except Exception as e:
logger.debug("discogs client init failed: %s", e)
# Hydrabase only when connected (dev-mode + active client).
try:
if hydrabase_client and hydrabase_client.is_connected():
sources.append(('hydrabase', hydrabase_client))
except Exception:
pass
except Exception as e:
logger.debug("hydrabase client check failed: %s", e)
return sources
return _artists_quality.ArtistQualityDeps(
@ -10958,8 +10958,8 @@ def _resolve_library_file_path(file_path):
if plex and plex.server:
for loc in plex.get_music_library_locations():
library_dirs.add(loc)
except Exception:
pass
except Exception as e:
logger.debug("plex library locations lookup failed: %s", e)
# Check user-configured music library paths (Settings > Library)
try:
@ -10970,8 +10970,8 @@ def _resolve_library_file_path(file_path):
resolved_p = docker_resolve_path(p.strip())
if resolved_p:
library_dirs.add(resolved_p)
except Exception:
pass
except Exception as e:
logger.debug("library music paths read failed: %s", e)
path_parts = file_path.replace('\\', '/').split('/')
@ -11462,8 +11462,8 @@ def library_delete_track(track_id):
try:
os.remove(sidecar)
logger.info(f"Deleted sidecar file: {sidecar}")
except Exception:
pass
except Exception as e:
logger.debug("sidecar removal failed: %s", e)
except Exception as e:
logger.warning(f"Failed to delete file: {e}")
file_error = str(e)
@ -12023,8 +12023,8 @@ def library_delete_album(album_id):
if os.path.exists(sidecar):
try:
os.remove(sidecar)
except Exception:
pass
except Exception as e:
logger.debug("sidecar removal failed: %s", e)
except Exception as e:
logger.warning(f"Failed to delete track file: {e}")
files_failed += 1
@ -12042,8 +12042,8 @@ def library_delete_album(album_id):
if os.path.isdir(album_dir) and not os.listdir(album_dir):
os.rmdir(album_dir)
logger.info(f"Removed empty album directory: {album_dir}")
except Exception:
pass
except Exception as e:
logger.debug("empty album dir cleanup failed: %s", e)
# Delete all tracks belonging to this album
cursor.execute("DELETE FROM tracks WHERE album_id = ?", (album_id,))
@ -13197,8 +13197,8 @@ def _sweep_empty_download_directories():
for hidden in entries:
try:
os.remove(os.path.join(dirpath, hidden))
except Exception:
pass
except Exception as e:
logger.debug("hidden file cleanup failed: %s", e)
os.rmdir(dirpath)
removed += 1
except OSError:
@ -14010,8 +14010,8 @@ def _apply_path_template(template: str, context: dict) -> str:
resolved = _get_itunes_client().resolve_primary_artist(itunes_artist_id)
if resolved and resolved != album_artist_value:
album_artist_value = resolved
except Exception:
pass
except Exception as e:
logger.debug("itunes primary artist resolve failed: %s", e)
# $cdnum — smart CD label for multi-disc filenames. Produces "CD01" /
# "CD02" etc. when the album has 2+ discs, empty string otherwise.
# Empty output collapses gracefully via the trailing double-dash cleanup
@ -14491,8 +14491,8 @@ def _get_current_commit_sha():
result = subprocess.run(['git', 'rev-parse', 'HEAD'], capture_output=True, text=True, cwd=os.path.dirname(__file__) or '.')
if result.returncode == 0:
return result.stdout.strip()
except Exception:
pass
except Exception as e:
logger.debug("git rev-parse failed: %s", e)
return None
_current_commit_sha = _get_current_commit_sha()
@ -14809,8 +14809,8 @@ def _db_update_finished_callback(total_artists, total_albums, total_tracks, succ
'total_albums': str(total_albums),
'total_tracks': str(total_tracks),
})
except Exception:
pass
except Exception as e:
logger.debug("library_updated automation emit failed: %s", e)
# Invalidate sync match cache (track IDs may have changed)
try:
@ -14818,8 +14818,8 @@ def _db_update_finished_callback(total_artists, total_albums, total_tracks, succ
cleared = inv_db.invalidate_sync_match_cache()
if cleared:
logger.info(f"Cleared {cleared} sync match cache entries after database update")
except Exception:
pass
except Exception as e:
logger.debug("sync match cache invalidation failed: %s", e)
# WISHLIST CLEANUP: Automatically clean up wishlist after database update
try:
@ -14950,8 +14950,8 @@ def _run_soulsync_full_refresh():
INSERT OR IGNORE INTO artists (id, name, server_source, created_at, updated_at)
VALUES (?, ?, 'soulsync', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
""", (artist_id, artist_name))
except Exception:
pass
except Exception as e:
logger.debug("soulsync artist insert failed: %s", e)
for album_name, tracks in albums.items():
album_key = f"{artist_name.lower()}::{album_name.lower()}"
@ -14970,8 +14970,8 @@ def _run_soulsync_full_refresh():
INSERT OR IGNORE INTO albums (id, artist_id, title, year, track_count, server_source, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, 'soulsync', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
""", (album_id, artist_id, album_name, year, len(tracks)))
except Exception:
pass
except Exception as e:
logger.debug("soulsync album insert failed: %s", e)
# Insert tracks
for file_path, tags in tracks:
@ -15761,8 +15761,8 @@ def backup_database_endpoint():
try:
with open(meta_path, 'w') as mf:
json.dump({"version": SOULSYNC_VERSION, "created": timestamp}, mf)
except Exception:
pass # Non-critical — backup still works without metadata
except Exception as e:
logger.debug("backup meta sidecar write: %s", e)
# Rolling cleanup
existing = sorted(_glob.glob(f"{db_path}.backup_*"), key=os.path.getmtime)
# Filter out .meta.json files from the backup list
@ -15774,8 +15774,8 @@ def backup_database_endpoint():
# Also remove sidecar if present
if os.path.exists(removed + '.meta.json'):
os.remove(removed + '.meta.json')
except Exception:
pass
except Exception as e:
logger.debug("rolling backup cleanup failed: %s", e)
return jsonify({"success": True, "backup_path": backup_path, "size_mb": size_mb, "version": SOULSYNC_VERSION})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
@ -15809,8 +15809,8 @@ def list_backups_endpoint():
with open(meta_path, 'r') as mf:
meta = json.load(mf)
entry['version'] = meta.get('version')
except Exception:
pass
except Exception as e:
logger.debug("backup metadata read failed: %s", e)
backups.append(entry)
db_size_mb = round(os.path.getsize(db_path) / (1024 * 1024), 2) if os.path.exists(db_path) else 0
return jsonify({
@ -15838,8 +15838,8 @@ def delete_backup_endpoint(filename):
if os.path.exists(meta_path):
try:
os.remove(meta_path)
except Exception:
pass
except Exception as e:
logger.debug("backup sidecar removal failed: %s", e)
return jsonify({"success": True, "deleted": filename})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
@ -15865,8 +15865,8 @@ def restore_backup_endpoint(filename):
with open(meta_path, 'r') as mf:
meta = json.load(mf)
backup_version = meta.get('version')
except Exception:
pass
except Exception as e:
logger.debug("backup version metadata read failed: %s", e)
version_warning = None
# Compare base versions only (strip +commit suffix) to avoid false mismatches
@ -15898,8 +15898,8 @@ def restore_backup_endpoint(filename):
try:
with open(safety_path + '.meta.json', 'w') as mf:
json.dump({"version": SOULSYNC_VERSION, "created": safety_ts}, mf)
except Exception:
pass
except Exception as e:
logger.debug("safety backup metadata write failed: %s", e)
# Restore using SQLite backup API (handles concurrent access safely)
from database.music_database import close_database, get_database
@ -18140,13 +18140,13 @@ def get_server_playlist_tracks(playlist_id):
raw_playlist = None
try:
raw_playlist = media_server_engine.client('plex').server.fetchItem(int(playlist_id))
except Exception:
pass
except Exception as e:
logger.debug("plex playlist fetchItem failed: %s", e)
if not raw_playlist and playlist_name:
try:
raw_playlist = media_server_engine.client('plex').server.playlist(playlist_name)
except Exception:
pass
except Exception as e:
logger.debug("plex playlist by-name lookup failed: %s", e)
if not raw_playlist:
logger.warning(
f"[ServerPlaylistTracks] Plex playlist not found by "
@ -18417,13 +18417,13 @@ def server_playlist_replace_track(playlist_id):
raw_playlist = None
try:
raw_playlist = plex_server.fetchItem(int(playlist_id))
except Exception:
pass
except Exception as e:
logger.debug("plex playlist fetchItem failed: %s", e)
if not raw_playlist and playlist_name:
try:
raw_playlist = plex_server.playlist(playlist_name)
except Exception:
pass
except Exception as e:
logger.debug("plex playlist by-name lookup failed: %s", e)
if not raw_playlist:
logger.warning(f"[ServerPlaylist] replace-track: playlist not found by id={playlist_id} or name='{playlist_name}'")
return jsonify({"success": False, "error": "Playlist not found on server"}), 404
@ -18517,13 +18517,13 @@ def server_playlist_add_track(playlist_id):
raw_playlist = None
try:
raw_playlist = plex_server.fetchItem(int(playlist_id))
except Exception:
pass
except Exception as e:
logger.debug("plex playlist fetchItem failed: %s", e)
if not raw_playlist and playlist_name:
try:
raw_playlist = plex_server.playlist(playlist_name)
except Exception:
pass
except Exception as e:
logger.debug("plex playlist by-name lookup failed: %s", e)
if not raw_playlist:
logger.warning(f"[ServerPlaylist] add-track: playlist not found by id={playlist_id} or name='{playlist_name}'")
return jsonify({"success": False, "error": "Playlist not found"}), 404
@ -18601,13 +18601,13 @@ def server_playlist_remove_track(playlist_id):
raw_playlist = None
try:
raw_playlist = plex_server.fetchItem(int(playlist_id))
except Exception:
pass
except Exception as e:
logger.debug("plex playlist fetchItem failed: %s", e)
if not raw_playlist and playlist_name:
try:
raw_playlist = plex_server.playlist(playlist_name)
except Exception:
pass
except Exception as e:
logger.debug("plex playlist by-name lookup failed: %s", e)
if not raw_playlist:
logger.warning(f"[ServerPlaylist] remove-track: playlist not found by id={playlist_id} or name='{playlist_name}'")
return jsonify({"success": False, "error": "Playlist not found"}), 404
@ -20747,8 +20747,8 @@ def _pause_enrichment_workers(label='discovery'):
worker.pause()
was_running[name] = True
logger.warning(f"Paused {name} enrichment worker during {label}")
except Exception:
pass
except Exception as e:
logger.debug("enrichment worker pause failed: %s", e)
return was_running
@ -20765,8 +20765,8 @@ def _resume_enrichment_workers(was_running, label='discovery'):
if was_running.get(name) and worker:
worker.resume()
logger.info(f"Resumed {name} enrichment worker after {label}")
except Exception:
pass
except Exception as e:
logger.debug("enrichment worker resume failed: %s", e)
def _sync_discovery_results_to_mirrored(source_type, source_playlist_id, discovery_results, discovery_source, profile_id=1):
@ -24764,8 +24764,8 @@ def add_to_watchlist():
elif row['discogs_id']:
artist_id = row['discogs_id']
source = 'discogs'
except Exception:
pass
except Exception as e:
logger.debug("watchlist artist source lookup failed: %s", e)
if not source:
fallback_source = _get_metadata_fallback_source()
source = fallback_source if is_numeric_id else 'spotify'
@ -24845,16 +24845,16 @@ def add_to_watchlist():
try:
pid = get_current_profile_id()
socketio.emit('watchlist:count', _build_watchlist_count_payload(profile_id=pid), room=f'profile:{pid}')
except Exception:
pass
except Exception as e:
logger.debug("watchlist count emit failed: %s", e)
try:
if automation_engine:
automation_engine.emit('watchlist_artist_added', {
'artist': artist_name,
'artist_id': str(artist_id),
})
except Exception:
pass
except Exception as e:
logger.debug("watchlist_artist_added emit failed: %s", e)
_artmap_cache_invalidate(get_current_profile_id())
return jsonify({"success": True, "message": f"Added {artist_name} to watchlist"})
else:
@ -24882,16 +24882,16 @@ def remove_from_watchlist():
try:
pid = get_current_profile_id()
socketio.emit('watchlist:count', _build_watchlist_count_payload(profile_id=pid), room=f'profile:{pid}')
except Exception:
pass
except Exception as e:
logger.debug("watchlist count emit failed: %s", e)
try:
if automation_engine:
automation_engine.emit('watchlist_artist_removed', {
'artist': data.get('artist_name', str(artist_id)),
'artist_id': str(artist_id),
})
except Exception:
pass
except Exception as e:
logger.debug("watchlist_artist_removed emit failed: %s", e)
_artmap_cache_invalidate(get_current_profile_id())
return jsonify({"success": True, "message": "Removed artist from watchlist"})
else:
@ -25033,8 +25033,8 @@ def watchlist_all_unwatched_library_artists():
if artist.get('image_url'):
try:
database.update_watchlist_artist_image(artist_id, artist['image_url'])
except Exception:
pass
except Exception as e:
logger.debug("watchlist artist image update failed: %s", e)
total_unwatched = len(unwatched_artists)
message_parts = [f"Added {added} artist{'s' if added != 1 else ''} to watchlist"]
@ -25193,8 +25193,8 @@ def start_watchlist_scan():
try:
if config_manager.get('discogs.token', ''):
providers_to_backfill.append('discogs')
except Exception:
pass
except Exception as e:
logger.debug("discogs token backfill check failed: %s", e)
for _bf_provider in providers_to_backfill:
try:
logger.debug(f"Checking for missing {_bf_provider} IDs in watchlist...")
@ -25357,8 +25357,8 @@ def start_watchlist_scan():
# Clear one-time rescan cutoff after full scan cycle
try:
scanner._clear_rescan_cutoff()
except Exception:
pass
except Exception as e:
logger.debug("scanner rescan cutoff clear failed: %s", e)
# Always reset flag when scan completes (success or error)
with watchlist_timer_lock:
@ -26195,8 +26195,8 @@ def enrich_similar_artists():
aid, fallback_source,
image_url=img_url, genres=genres, popularity=0
)
except Exception:
pass
except Exception as e:
logger.debug("similar artist enrichment failed: %s", e)
cached_count = len(enriched) - len([aid for aid in uncached_ids if aid in enriched])
api_count = len([aid for aid in uncached_ids if aid in enriched])
@ -26363,10 +26363,10 @@ def get_discover_recent_releases():
if album_id:
try:
database.update_discovery_recent_album_cover(album_id, cover)
except Exception:
pass
except Exception:
pass
except Exception as e:
logger.debug("recent album cover update failed: %s", e)
except Exception as e:
logger.debug("recent album cover fetch failed: %s", e)
# Filter out blacklisted artists
blacklisted = database.get_discovery_blacklist_names()
@ -26500,8 +26500,8 @@ def get_discover_because_you_listen_to():
if row and row[0]:
artist_image = fix_artist_image_url(row[0])
conn.close()
except Exception:
pass
except Exception as e:
logger.debug("artist image lookup failed: %s", e)
sections.append({
'artist_name': artist_name,
@ -26686,8 +26686,8 @@ def resolve_cache_album():
if results:
r = results[0]
return jsonify({'success': True, 'entity_id': str(r.id), 'source': _get_metadata_fallback_source()})
except Exception:
pass
except Exception as e:
logger.debug("fallback album search failed: %s", e)
return jsonify({'success': False, 'error': 'Album not found in cache'})
except Exception as e:
@ -27002,8 +27002,8 @@ def get_seasonal_playlist(season_key):
try:
import json
track_dict['track_data_json'] = json.loads(track_dict['track_data_json'])
except:
pass
except Exception as e:
logger.debug("track_data_json parse: %s", e)
tracks.append(track_dict)
else:
# Try discovery_pool as fallback (filtered by source)
@ -27029,8 +27029,8 @@ def get_seasonal_playlist(season_key):
try:
import json
track_dict['track_data_json'] = json.loads(track_dict['track_data_json'])
except:
pass
except Exception as e:
logger.debug("discovery track_data_json parse: %s", e)
tracks.append(track_dict)
config = SEASONAL_CONFIG[season_key]
@ -27435,8 +27435,8 @@ def get_your_artists_sources():
try:
if tidal_client and hasattr(tidal_client, '_ensure_valid_token') and tidal_client._ensure_valid_token():
connected.append('tidal')
except Exception:
pass
except Exception as e:
logger.debug("tidal auth check failed: %s", e)
# Last.fm
if config_manager.get('lastfm.api_key', '') and config_manager.get('lastfm.session_key', ''):
connected.append('lastfm')
@ -27448,8 +27448,8 @@ def get_your_artists_sources():
and download_orchestrator.client("deezer_dl").is_authenticated())
if deezer_oauth or deezer_arl:
connected.append('deezer')
except Exception:
pass
except Exception as e:
logger.debug("deezer auth check failed: %s", e)
return jsonify({"success": True, "enabled": enabled, "connected": connected})
except Exception as e:
@ -27711,8 +27711,8 @@ def get_your_albums_sources():
try:
if tidal_client and hasattr(tidal_client, '_ensure_valid_token') and tidal_client._ensure_valid_token():
connected.append('tidal')
except Exception:
pass
except Exception as e:
logger.debug("tidal auth check failed: %s", e)
try:
deezer_cl = _get_deezer_client()
deezer_oauth = deezer_cl and hasattr(deezer_cl, 'is_user_authenticated') and deezer_cl.is_user_authenticated()
@ -27720,8 +27720,8 @@ def get_your_albums_sources():
and download_orchestrator.client("deezer_dl").is_authenticated())
if deezer_oauth or deezer_arl:
connected.append('deezer')
except Exception:
pass
except Exception as e:
logger.debug("deezer auth check failed: %s", e)
# Discogs: counts as "connected" when a personal access token is
# configured. Username comes from /oauth/identity at fetch time;
@ -27729,8 +27729,8 @@ def get_your_albums_sources():
try:
if config_manager.get('discogs.token', ''):
connected.append('discogs')
except Exception:
pass
except Exception as e:
logger.debug("discogs token check failed: %s", e)
return jsonify({"success": True, "enabled": enabled, "connected": connected})
except Exception as e:
@ -27913,8 +27913,8 @@ def get_your_artist_info(artist_id):
'lastfm_playcount': r.get('lastfm_playcount', 0),
})
return jsonify(result)
except Exception:
pass
except Exception as e:
logger.debug("library artist lookup failed: %s", e)
# 2. Try metadata cache
try:
@ -27935,8 +27935,8 @@ def get_your_artist_info(artist_id):
'followers': cached.get('followers', {}).get('total', 0) if isinstance(cached.get('followers'), dict) else cached.get('followers', 0),
})
return jsonify(result)
except Exception:
pass
except Exception as e:
logger.debug("metadata cache lookup failed: %s", e)
# 3. Try Spotify API directly (genres, image, followers)
try:
@ -31402,8 +31402,8 @@ def mirror_playlist_endpoint():
'source': source,
'track_count': str(len(tracks)),
})
except Exception:
pass
except Exception as e:
logger.debug("mirrored_playlist_created emit failed: %s", e)
return jsonify({"success": True, "playlist_id": playlist_id})
except Exception as e:
@ -31577,8 +31577,8 @@ def fix_discovery_pool_track():
cache_key[0], cache_key[1], _get_active_discovery_source(), 1.0, matched_data,
row['track_name'], row['artist_name']
)
except Exception:
pass
except Exception as e:
logger.debug("discovery cache match save failed: %s", e)
return jsonify({"success": True})
except Exception as e:
@ -32002,8 +32002,8 @@ def playlist_explorer_album_tracks(album_id):
if cache and tracks:
try:
cache.store_entity(source_name, 'album_tracks', cache_key, result)
except Exception:
pass
except Exception as e:
logger.debug("album_tracks cache store failed: %s", e)
return jsonify(result)
except Exception as e:
@ -32212,8 +32212,8 @@ def start_oauth_callback_servers():
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(f'<h1>Internal Server Error</h1><p>{str(e)}</p>'.encode())
except Exception:
pass # Connection already broken, nothing more we can do
except Exception as _e:
_oauth_logger.debug("oauth response write: %s", _e)
def log_message(self, format, *args):
pass # Suppress BaseHTTPRequestHandler access logs (we use our own logger)
@ -33153,8 +33153,8 @@ try:
state['log'].append({'type': 'success' if status == 'finished' else 'error', 'text': summary})
try:
socketio.emit('repair:progress', {job_id: dict(state)})
except Exception:
pass
except Exception as e:
logger.debug("repair progress emit failed: %s", e)
repair_worker.register_progress_callbacks(_repair_job_start, _repair_job_progress, _repair_job_finish)
# Store refs for WebSocket push loop
@ -33639,8 +33639,8 @@ def import_staging_hints():
if album:
key = (album.strip(), (artist or '').strip())
tag_albums[key] = tag_albums.get(key, 0) + 1
except Exception:
pass
except Exception as e:
logger.debug("tag read failed: %s", e)
# Build search queries, prioritizing tag-based hints (more specific)
queries = []
@ -33796,8 +33796,8 @@ def import_album_process():
'completed_tracks': str(processed),
'failed_tracks': str(len(errors)),
})
except Exception:
pass
except Exception as e:
logger.debug("album import automation emit failed: %s", e)
# Rebuild suggestions cache since staging contents changed
if processed > 0:
@ -33960,8 +33960,8 @@ def import_singles_process():
'completed_tracks': str(processed),
'failed_tracks': str(len(errors)),
})
except Exception:
pass
except Exception as e:
logger.debug("singles import automation emit failed: %s", e)
# Rebuild suggestions cache since staging contents changed
if processed > 0:
@ -34147,8 +34147,8 @@ def _build_status_payload():
for t in download_tasks.values():
if t.get('status') in ('downloading', 'searching', 'post_processing', 'queued', 'pending'):
active_dl_count += 1
except Exception:
pass
except Exception as e:
logger.debug("active download count failed: %s", e)
return {
'metadata_source': metadata_status['metadata_source'],
@ -34193,8 +34193,8 @@ def _hydrabase_reconnect_loop():
if _hydrabase_ws is not None and _hydrabase_ws.connected:
_consecutive_failures = 0
continue
except Exception:
pass # Socket in bad state — treat as disconnected
except Exception as e:
logger.debug("hydrabase socket check: %s", e)
# Disconnected with auto_connect enabled — try to reconnect
# Back off: 30s, 60s, 120s, max 300s between attempts
@ -34208,8 +34208,8 @@ def _hydrabase_reconnect_loop():
if _hydrabase_ws:
try:
_hydrabase_ws.close()
except:
pass
except Exception as e:
logger.debug("hydrabase reconnect close: %s", e)
ws = websocket.create_connection(
hydra_cfg['url'],
header={"x-api-key": hydra_cfg['api_key']},
@ -34224,8 +34224,8 @@ def _hydrabase_reconnect_loop():
logger.error(f"[Hydrabase] Reconnect attempt failed ({_consecutive_failures}): {e}")
elif _consecutive_failures == 4:
logger.error("[Hydrabase] Reconnect failing repeatedly — suppressing further logs until success")
except Exception:
pass # Don't crash the monitor loop
except Exception as e:
logger.debug("hydrabase monitor loop: %s", e)
def _emit_service_status_loop():
"""Background thread that pushes service status every 5 seconds."""
@ -34368,8 +34368,8 @@ def _has_active_downloads():
for batch_data in download_batches.values():
if batch_data.get('phase') not in ('complete', 'error', 'cancelled', None):
return True
except Exception:
pass
except Exception as e:
logger.debug("active downloads check failed: %s", e)
return False
@ -34403,8 +34403,8 @@ def _spotify_resume_pre_check():
try:
if _spotify_rate_limited():
return (429, 'Cannot resume while Spotify is rate limited')
except Exception:
pass
except Exception as e:
logger.debug("spotify rate-limit pre-check failed: %s", e)
return None
@ -34511,8 +34511,8 @@ def _emit_rate_monitor_loop():
}
if svc_key == 'spotify' and enr.get('daily_budget'):
entry['worker']['daily_budget'] = enr['daily_budget']
except Exception:
pass
except Exception as e:
logger.debug("enrichment worker status build failed: %s", e)
# Add Spotify rate limit state
try:
@ -34522,8 +34522,8 @@ def _emit_rate_monitor_loop():
payload['spotify']['rate_limited'] = True
payload['spotify']['rl_remaining'] = rl_info.get('remaining_seconds', 0)
payload['spotify']['rl_endpoint'] = rl_info.get('endpoint', '')
except Exception:
pass
except Exception as e:
logger.debug("spotify rate-limit status read failed: %s", e)
socketio.emit('rate-monitor:update', payload)
except Exception as e:
@ -34763,8 +34763,8 @@ def _emit_sync_progress_loop():
socketio.emit('sync:progress', {
'playlist_id': pid, **state
}, room=f'sync:{pid}')
except Exception:
pass
except Exception as e:
logger.debug("sync progress emit failed: %s", e)
except Exception as e:
logger.debug(f"Error in sync progress loop: {e}")
@ -34801,8 +34801,8 @@ def _emit_discovery_progress_loop():
'complete': state.get('phase') == 'discovered',
}
socketio.emit('discovery:progress', payload, room=f'discovery:{pid}')
except Exception:
pass
except Exception as e:
logger.debug("discovery progress emit failed: %s", e)
except Exception as e:
logger.debug(f"Error in {platform_name} discovery loop: {e}")
@ -34945,8 +34945,8 @@ def start_runtime_services():
logger.info("Automation engine started")
try:
automation_engine.emit('app_started', {})
except Exception:
pass
except Exception as e:
logger.debug("app_started emit failed: %s", e)
except AttributeError as e:
logger.error(f"Automation engine failed to start: {e}")
logger.info(" If using Docker, check that your volume mount is /app/data (not /app/database)")

View file

@ -3432,6 +3432,7 @@ const WHATS_NEW = {
'2.4.2': [
// --- post-2.4.1 dev work — entries hidden by _getLatestWhatsNewVersion until the build version bumps ---
{ date: 'Unreleased — 2.4.2 dev cycle' },
{ title: 'Internal: Stop Swallowing Exceptions Silently', desc: 'github issue #369 (johnbaumb): the codebase had ~300 `except Exception: pass` blocks — and another ~30 bare `except: pass` ones — across web_server.py, every metadata client, every download/import worker, the repair jobs, and most service modules. when one of those paths failed at runtime, the failure was completely invisible: no log line, no telemetry, nothing. you\'d see "downloads stopped working after a few hours" or "enrichment never finishes" and there was nothing to grep for in app.log because the exception had been thrown straight into the void. swept all of them. converted to `except Exception as e: logger.debug("<context>: %s", e)` so failures land in the log with enough context to grep. bare `except:` cases (which also swallow KeyboardInterrupt and SystemExit — actively wrong) got upgraded to `except Exception:` first so ctrl-c works correctly. ~14 cleanup-path sites (atexit handlers, finally-block conn.close calls) were intentionally left silent with explicit `# noqa: S110` comments — logging during shutdown can itself crash because file handles get torn down before the handler fires. and added ruff S110 to the lint config so this pattern fails CI going forward — drift fails at PR review instead of at runtime against a wedged worker thread. zero behavior change to any happy path; just made the failure paths inspectable. test suite (2188 tests) green throughout the sweep.' },
{ title: 'Fix: Repair Job Card "X Findings" Badge Was Misleading After Bulk-Fix', desc: 'discord report: duplicate detector card said "372 findings" and cover art filler said "60 findings", but clicking the findings tab pending filter showed 0 — read like a bug ("findings aren\'t being created"). actual cause: job-card badge displayed `last_run.findings_created` (historical "found in last scan") which doesn\'t reflect current state when those findings have since been bulk-fixed and moved to status="resolved". fix: api response now includes `pending_findings_count` per job (current pending count from a single sql aggregation). badge now shows "X pending" when pending count > 0 (urgent red color), or "X found in last scan" with a muted grey color when pending = 0 but the last scan did surface something. user can tell at a glance whether something needs review vs whether it\'s a historical reminder. 3 new tests pin the per-job pending count helper.', page: 'stats' },
{ title: 'Fix: Downloads Stop After 2-3 Hours (slskd HTTP Timeout)', desc: 'github issue #499 (bafoed): big initial sync of spotify playlists worked for 2-3 hours then downloads silently stopped. 3 active tasks stuck in "searching" state, replaced every ~10 min by different ones, but slskd ui showed no actual searches happening. only fix was restarting the soulsync container — which would buy another 2-3 hours. root cause: `core/soulseek_client.py` constructed `aiohttp.ClientSession()` with no timeout at four sites. when slskd hung on a request (overloaded, transient network blip, internal stall), the http call blocked indefinitely — and the worker thread blocked with it. download executor only has `max_workers=3` for download workers. once 3 worker threads were wedged on hung calls, no further downloads could start. batch-level "stuck detection" (10-min) was correctly marking tasks `not_found` and trying to start replacements, but the executor pool was exhausted — replacements queued forever. fix: bounded `aiohttp.ClientTimeout` (total 120s, connect 15s, sock_read 60s) on every slskd `ClientSession` construction. legitimate metadata calls (search submission, status polls, download enqueue) finish in seconds — slskd doesn\'t stream files through these requests, so the timeout can\'t kill a real operation. when timeout fires, the request raises `asyncio.TimeoutError` which is now explicitly caught + logged + returns None to the caller (treats as a normal failure, same code path as a 5xx response). worker thread unblocks → executor pool stays healthy → downloads keep flowing. 3 new tests pin the timeout config + the `asyncio.TimeoutError` handler so future drift fails at the test boundary instead of at runtime against a wedged executor.', page: 'downloads' },
{ title: 'Fix: Library Reorganize Job Misclassified Album Tracks As Singles', desc: 'github issue #500 (bafoed): library reorganize repair job moved tracks like `Surf Curse/Surf Curse - Nothing Yet (2017)/01 - Christine F.flac` to single-template paths like `Surf Curse/Surf Curse - Christine F/Surf Curse - Christine F.flac`. root cause: the job used `is_album = (group_size > 1)` where `group_size` was the count of tracks for the same album currently sitting in the transfer folder being scanned — when only one track of an album was in transfer (rest already moved to library, or album tags varied across tracks like "Buds" vs "Buds (Bonus)"), each track became a 1-element group → all routed through single template. fix: rewrote the job to delegate to the per-album planner (`core.library_reorganize.preview_album_reorganize` / `reorganize_queue`) — the same planner the artist-detail "reorganize" modal uses. db-driven: the planner knows the album has multiple tracks regardless of how many sit in the transfer folder, so the album-vs-single classification is structurally correct. apply mode delegates to the existing reorganize queue → file move + post-processing + db update + sidecar handling all flow through one code path. only iterates albums for the ACTIVE media server (matches the artist-detail modal\'s scope) — multi-server users (plex + jellyfin etc) won\'t accidentally have the job touch the inactive server\'s files. albums missing a metadata source id get a single "needs enrichment first" finding instead of n per-track "no source" findings. dropped ~500 loc of tag-reading + transfer-walk + template logic that was duplicated against the per-album path. files in transfer with no db entry are now exclusively the orphan_file_detector\'s domain (clean separation). 12 tests pin the delegation contract.', page: 'library' },