diff --git a/api/search.py b/api/search.py index 48a237c7..c2290fd3 100644 --- a/api/search.py +++ b/api/search.py @@ -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 diff --git a/api/system.py b/api/system.py index d561cc19..c462d5f1 100644 --- a/api/system.py +++ b/api/system.py @@ -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", diff --git a/beatport_unified_scraper.py b/beatport_unified_scraper.py index 8202981c..950edded 100644 --- a/beatport_unified_scraper.py +++ b/beatport_unified_scraper.py @@ -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 diff --git a/core/album_consistency.py b/core/album_consistency.py index e913b8cc..e277d02c 100644 --- a/core/album_consistency.py +++ b/core/album_consistency.py @@ -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}'") diff --git a/core/api_call_tracker.py b/core/api_call_tracker.py index f90a6dae..7fb561e1 100644 --- a/core/api_call_tracker.py +++ b/core/api_call_tracker.py @@ -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.""" diff --git a/core/artists/liked_match.py b/core/artists/liked_match.py index 9607e31d..324d8cbc 100644 --- a/core/artists/liked_match.py +++ b/core/artists/liked_match.py @@ -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() diff --git a/core/artists/map.py b/core/artists/map.py index 09e880ec..5e6f130a 100644 --- a/core/artists/map.py +++ b/core/artists/map.py @@ -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']: diff --git a/core/audiodb_worker.py b/core/audiodb_worker.py index cfb323bc..37c404d9 100644 --- a/core/audiodb_worker.py +++ b/core/audiodb_worker.py @@ -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 diff --git a/core/auto_import_worker.py b/core/auto_import_worker.py index b5c25a81..be80a7d2 100644 --- a/core/auto_import_worker.py +++ b/core/auto_import_worker.py @@ -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 diff --git a/core/automation/progress.py b/core/automation/progress.py index 8f3e8f50..9306f19f 100644 --- a/core/automation/progress.py +++ b/core/automation/progress.py @@ -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': diff --git a/core/automation/signals.py b/core/automation/signals.py index a9ce9214..9b82c5a1 100644 --- a/core/automation/signals.py +++ b/core/automation/signals.py @@ -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) diff --git a/core/automation_engine.py b/core/automation_engine.py index a1789af4..d19cbb58 100644 --- a/core/automation_engine.py +++ b/core/automation_engine.py @@ -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) diff --git a/core/connection_detect.py b/core/connection_detect.py index fd23361f..ab19e3a9 100644 --- a/core/connection_detect.py +++ b/core/connection_detect.py @@ -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: diff --git a/core/database_update_worker.py b/core/database_update_worker.py index 943cd444..3262ee30 100644 --- a/core/database_update_worker.py +++ b/core/database_update_worker.py @@ -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") diff --git a/core/debug_info.py b/core/debug_info.py index 7674422a..88a5ece9 100644 --- a/core/debug_info.py +++ b/core/debug_info.py @@ -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 diff --git a/core/deezer_client.py b/core/deezer_client.py index 6b2a26c1..0da94c76 100644 --- a/core/deezer_client.py +++ b/core/deezer_client.py @@ -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 diff --git a/core/deezer_download_client.py b/core/deezer_download_client.py index d3a13d26..727c7006 100644 --- a/core/deezer_download_client.py +++ b/core/deezer_download_client.py @@ -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): diff --git a/core/deezer_worker.py b/core/deezer_worker.py index bdaf2fb2..aba5c6fb 100644 --- a/core/deezer_worker.py +++ b/core/deezer_worker.py @@ -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 diff --git a/core/discogs_client.py b/core/discogs_client.py index 33fa1d8e..4147277d 100644 --- a/core/discogs_client.py +++ b/core/discogs_client.py @@ -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 diff --git a/core/discogs_worker.py b/core/discogs_worker.py index 84270de9..8663934f 100644 --- a/core/discogs_worker.py +++ b/core/discogs_worker.py @@ -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.""" diff --git a/core/discovery/playlist.py b/core/discovery/playlist.py index 5386dd49..4bbbf1c7 100644 --- a/core/discovery/playlist.py +++ b/core/discovery/playlist.py @@ -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, diff --git a/core/discovery/quality_scanner.py b/core/discovery/quality_scanner.py index c7c38343..27457ca7 100644 --- a/core/discovery/quality_scanner.py +++ b/core/discovery/quality_scanner.py @@ -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}") diff --git a/core/discovery/sync.py b/core/discovery/sync.py index 9d92b938..1f068450 100644 --- a/core/discovery/sync.py +++ b/core/discovery/sync.py @@ -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 diff --git a/core/download_engine/rate_limit.py b/core/download_engine/rate_limit.py index 57b86642..65d33c86 100644 --- a/core/download_engine/rate_limit.py +++ b/core/download_engine/rate_limit.py @@ -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): diff --git a/core/downloads/candidates.py b/core/downloads/candidates.py index cb2e6ee8..d74cfa9d 100644 --- a/core/downloads/candidates.py +++ b/core/downloads/candidates.py @@ -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 diff --git a/core/downloads/lifecycle.py b/core/downloads/lifecycle.py index aa784d8f..43e1e970 100644 --- a/core/downloads/lifecycle.py +++ b/core/downloads/lifecycle.py @@ -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 diff --git a/core/downloads/master.py b/core/downloads/master.py index 5291f23b..4620a467 100644 --- a/core/downloads/master.py +++ b/core/downloads/master.py @@ -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: diff --git a/core/downloads/monitor.py b/core/downloads/monitor.py index ff939352..94b4a727 100644 --- a/core/downloads/monitor.py +++ b/core/downloads/monitor.py @@ -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 diff --git a/core/downloads/wishlist_failed.py b/core/downloads/wishlist_failed.py index fcf2ce93..1b1f30bb 100644 --- a/core/downloads/wishlist_failed.py +++ b/core/downloads/wishlist_failed.py @@ -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") diff --git a/core/enrichment/api.py b/core/enrichment/api.py index e79d6fa1..e4d097d7 100644 --- a/core/enrichment/api.py +++ b/core/enrichment/api.py @@ -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: diff --git a/core/genius_worker.py b/core/genius_worker.py index 63de47a6..e2e731fd 100644 --- a/core/genius_worker.py +++ b/core/genius_worker.py @@ -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}") diff --git a/core/hydrabase_client.py b/core/hydrabase_client.py index f30324b2..df204d38 100644 --- a/core/hydrabase_client.py +++ b/core/hydrabase_client.py @@ -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]: diff --git a/core/imports/file_ops.py b/core/imports/file_ops.py index 2a082247..c3620d78 100644 --- a/core/imports/file_ops.py +++ b/core/imports/file_ops.py @@ -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)}") diff --git a/core/imports/guards.py b/core/imports/guards.py index a95fbd6d..657576f0 100644 --- a/core/imports/guards.py +++ b/core/imports/guards.py @@ -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) diff --git a/core/imports/paths.py b/core/imports/paths.py index 9fd9dea7..bdc12616 100644 --- a/core/imports/paths.py +++ b/core/imports/paths.py @@ -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. diff --git a/core/imports/pipeline.py b/core/imports/pipeline.py index 1943816e..d7b2b7d0 100644 --- a/core/imports/pipeline.py +++ b/core/imports/pipeline.py @@ -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: diff --git a/core/imports/resolution.py b/core/imports/resolution.py index 2df38dbb..e37b3d3a 100644 --- a/core/imports/resolution.py +++ b/core/imports/resolution.py @@ -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) diff --git a/core/imports/side_effects.py b/core/imports/side_effects.py index d577db91..6252abd7 100644 --- a/core/imports/side_effects.py +++ b/core/imports/side_effects.py @@ -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) diff --git a/core/itunes_client.py b/core/itunes_client.py index a62fb035..95d64669 100644 --- a/core/itunes_client.py +++ b/core/itunes_client.py @@ -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 diff --git a/core/itunes_worker.py b/core/itunes_worker.py index eebca734..50cf0df0 100644 --- a/core/itunes_worker.py +++ b/core/itunes_worker.py @@ -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) diff --git a/core/jellyfin_client.py b/core/jellyfin_client.py index 2439e1cf..1257d4ab 100644 --- a/core/jellyfin_client.py +++ b/core/jellyfin_client.py @@ -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: diff --git a/core/lastfm_worker.py b/core/lastfm_worker.py index 2509f4aa..69a4c360 100644 --- a/core/lastfm_worker.py +++ b/core/lastfm_worker.py @@ -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) diff --git a/core/library/duplicate_cleaner.py b/core/library/duplicate_cleaner.py index 1b484caf..29fe4729 100644 --- a/core/library/duplicate_cleaner.py +++ b/core/library/duplicate_cleaner.py @@ -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}") diff --git a/core/library/path_resolver.py b/core/library/path_resolver.py index 8e556cd8..44a157d8 100644 --- a/core/library/path_resolver.py +++ b/core/library/path_resolver.py @@ -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() diff --git a/core/library/retag.py b/core/library/retag.py index 82cd949f..48be9800 100644 --- a/core/library/retag.py +++ b/core/library/retag.py @@ -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')) diff --git a/core/library/track_identity.py b/core/library/track_identity.py index 3d2ff982..2b43650d 100644 --- a/core/library/track_identity.py +++ b/core/library/track_identity.py @@ -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 diff --git a/core/library_reorganize.py b/core/library_reorganize.py index 6d62da98..a0fb86f4 100644 --- a/core/library_reorganize.py +++ b/core/library_reorganize.py @@ -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 diff --git a/core/lidarr_download_client.py b/core/lidarr_download_client.py index a0542bc0..52bee817 100644 --- a/core/lidarr_download_client.py +++ b/core/lidarr_download_client.py @@ -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}") diff --git a/core/listenbrainz_manager.py b/core/listenbrainz_manager.py index ad964646..914a85ce 100644 --- a/core/listenbrainz_manager.py +++ b/core/listenbrainz_manager.py @@ -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 diff --git a/core/listening_stats_worker.py b/core/listening_stats_worker.py index ec70e1fd..e2445aac 100644 --- a/core/listening_stats_worker.py +++ b/core/listening_stats_worker.py @@ -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): diff --git a/core/metadata/album_mbid_cache.py b/core/metadata/album_mbid_cache.py index 2d92247e..ba32889f 100644 --- a/core/metadata/album_mbid_cache.py +++ b/core/metadata/album_mbid_cache.py @@ -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 diff --git a/core/metadata/artwork.py b/core/metadata/artwork.py index 795a3fdf..13276104 100644 --- a/core/metadata/artwork.py +++ b/core/metadata/artwork.py @@ -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: diff --git a/core/metadata/cache.py b/core/metadata/cache.py index c52c91ac..2974c5eb 100644 --- a/core/metadata/cache.py +++ b/core/metadata/cache.py @@ -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 diff --git a/core/metadata/common.py b/core/metadata/common.py index 9220cd75..ebf6ee5d 100644 --- a/core/metadata/common.py +++ b/core/metadata/common.py @@ -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 diff --git a/core/metadata/registry.py b/core/metadata/registry.py index 4a6de606..d2dc86f7 100644 --- a/core/metadata/registry.py +++ b/core/metadata/registry.py @@ -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": diff --git a/core/metadata/source.py b/core/metadata/source.py index 4db86101..bbe8dab9 100644 --- a/core/metadata/source.py +++ b/core/metadata/source.py @@ -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) diff --git a/core/musicbrainz_worker.py b/core/musicbrainz_worker.py index 08e02248..fe6461fc 100644 --- a/core/musicbrainz_worker.py +++ b/core/musicbrainz_worker.py @@ -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 diff --git a/core/navidrome_client.py b/core/navidrome_client.py index 5dd29136..11995dff 100644 --- a/core/navidrome_client.py +++ b/core/navidrome_client.py @@ -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: diff --git a/core/playlists/explorer.py b/core/playlists/explorer.py index 991f6915..7a4603d8 100644 --- a/core/playlists/explorer.py +++ b/core/playlists/explorer.py @@ -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 diff --git a/core/plex_client.py b/core/plex_client.py index 0cd31a35..e1d8a498 100644 --- a/core/plex_client.py +++ b/core/plex_client.py @@ -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]: diff --git a/core/qobuz_worker.py b/core/qobuz_worker.py index 31d26c78..c6ba2be7 100644 --- a/core/qobuz_worker.py +++ b/core/qobuz_worker.py @@ -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 diff --git a/core/reorganize_runner.py b/core/reorganize_runner.py index b5fba687..8ef7817b 100644 --- a/core/reorganize_runner.py +++ b/core/reorganize_runner.py @@ -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, diff --git a/core/repair_jobs/album_tag_consistency.py b/core/repair_jobs/album_tag_consistency.py index 3a8abd14..1ae32254 100644 --- a/core/repair_jobs/album_tag_consistency.py +++ b/core/repair_jobs/album_tag_consistency.py @@ -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 diff --git a/core/repair_jobs/discography_backfill.py b/core/repair_jobs/discography_backfill.py index 2887a711..9b7890ca 100644 --- a/core/repair_jobs/discography_backfill.py +++ b/core/repair_jobs/discography_backfill.py @@ -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 diff --git a/core/repair_jobs/library_reorganize.py b/core/repair_jobs/library_reorganize.py index e984c97e..6d7e68d2 100644 --- a/core/repair_jobs/library_reorganize.py +++ b/core/repair_jobs/library_reorganize.py @@ -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): diff --git a/core/repair_jobs/mbid_mismatch_detector.py b/core/repair_jobs/mbid_mismatch_detector.py index ef6ad77c..e35e9683 100644 --- a/core/repair_jobs/mbid_mismatch_detector.py +++ b/core/repair_jobs/mbid_mismatch_detector.py @@ -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") diff --git a/core/repair_jobs/orphan_file_detector.py b/core/repair_jobs/orphan_file_detector.py index 48717f91..80c5a237 100644 --- a/core/repair_jobs/orphan_file_detector.py +++ b/core/repair_jobs/orphan_file_detector.py @@ -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) diff --git a/core/repair_jobs/unknown_artist_fixer.py b/core/repair_jobs/unknown_artist_fixer.py index 53216ed0..bf037748 100644 --- a/core/repair_jobs/unknown_artist_fixer.py +++ b/core/repair_jobs/unknown_artist_fixer.py @@ -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) diff --git a/core/repair_worker.py b/core/repair_worker.py index 2d431c05..50d90b27 100644 --- a/core/repair_worker.py +++ b/core/repair_worker.py @@ -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}'} diff --git a/core/replaygain.py b/core/replaygain.py index a10469df..22e25c17 100644 --- a/core/replaygain.py +++ b/core/replaygain.py @@ -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 diff --git a/core/runtime_state.py b/core/runtime_state.py index f952aee1..dfa11798 100644 --- a/core/runtime_state.py +++ b/core/runtime_state.py @@ -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 diff --git a/core/seasonal_discovery.py b/core/seasonal_discovery.py index 0e7dfadc..159776f7 100644 --- a/core/seasonal_discovery.py +++ b/core/seasonal_discovery.py @@ -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]: diff --git a/core/soulid_worker.py b/core/soulid_worker.py index 5dc011bb..aef60f6d 100644 --- a/core/soulid_worker.py +++ b/core/soulid_worker.py @@ -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() diff --git a/core/soulseek_client.py b/core/soulseek_client.py index 2f7e3261..1f7cb9c7 100644 --- a/core/soulseek_client.py +++ b/core/soulseek_client.py @@ -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() diff --git a/core/soulsync_client.py b/core/soulsync_client.py index 41e6bb7d..2c43d7d3 100644 --- a/core/soulsync_client.py +++ b/core/soulsync_client.py @@ -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 ── diff --git a/core/spotify_client.py b/core/spotify_client.py index 124aa7b5..c0d8ca4f 100644 --- a/core/spotify_client.py +++ b/core/spotify_client.py @@ -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: diff --git a/core/spotify_worker.py b/core/spotify_worker.py index 3fa503c4..9349bb74 100644 --- a/core/spotify_worker.py +++ b/core/spotify_worker.py @@ -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) diff --git a/core/stats/queries.py b/core/stats/queries.py index c0021265..cb505e07 100644 --- a/core/stats/queries.py +++ b/core/stats/queries.py @@ -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 diff --git a/core/tag_writer.py b/core/tag_writer.py index cbdfdf11..a1589cc4 100644 --- a/core/tag_writer.py +++ b/core/tag_writer.py @@ -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 diff --git a/core/tidal_client.py b/core/tidal_client.py index ce6b0ef7..320a91aa 100644 --- a/core/tidal_client.py +++ b/core/tidal_client.py @@ -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 diff --git a/core/tidal_worker.py b/core/tidal_worker.py index a9f7d41c..4bcb434a 100644 --- a/core/tidal_worker.py +++ b/core/tidal_worker.py @@ -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 diff --git a/core/watchlist/auto_scan.py b/core/watchlist/auto_scan.py index 56550f3b..759d43f7 100644 --- a/core/watchlist/auto_scan.py +++ b/core/watchlist/auto_scan.py @@ -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 diff --git a/core/watchlist_scanner.py b/core/watchlist_scanner.py index e7528697..c90e95bd 100644 --- a/core/watchlist_scanner.py +++ b/core/watchlist_scanner.py @@ -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) diff --git a/core/wishlist/presence.py b/core/wishlist/presence.py index 16eddc2b..f7b9cee1 100644 --- a/core/wishlist/presence.py +++ b/core/wishlist/presence.py @@ -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 diff --git a/core/wishlist/processing.py b/core/wishlist/processing.py index e774da4d..542827b3 100644 --- a/core/wishlist/processing.py +++ b/core/wishlist/processing.py @@ -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 diff --git a/core/workers/metadata_update.py b/core/workers/metadata_update.py index 085015f1..7b237f0b 100644 --- a/core/workers/metadata_update.py +++ b/core/workers/metadata_update.py @@ -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) diff --git a/core/youtube_client.py b/core/youtube_client.py index b40f76cb..4f1c4b76 100644 --- a/core/youtube_client.py +++ b/core/youtube_client.py @@ -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...") diff --git a/database/music_database.py b/database/music_database.py index 2f48a4bc..857d16b0 100644 --- a/database/music_database.py +++ b/database/music_database.py @@ -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() diff --git a/pyproject.toml b/pyproject.toml index 3ef6e9d2..f82a69fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 = [ diff --git a/services/sync_service.py b/services/sync_service.py index a9b645a8..86b12920 100644 --- a/services/sync_service.py +++ b/services/sync_service.py @@ -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: diff --git a/web_server.py b/web_server.py index 1f8a093d..9599fef7 100644 --- a/web_server.py +++ b/web_server.py @@ -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"

No Access Token

Deezer response: {resp.text[:200]}

", 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'

Internal Server Error

{str(e)}

'.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)") diff --git a/webui/static/helper.js b/webui/static/helper.js index fdd4a3ed..6ed334cd 100644 --- a/webui/static/helper.js +++ b/webui/static/helper.js @@ -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(": %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' },