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