diff --git a/src/app/config/settings.py b/src/app/config/settings.py index a289135..b3839ed 100644 --- a/src/app/config/settings.py +++ b/src/app/config/settings.py @@ -1,10 +1,13 @@ from __future__ import annotations import json +import logging import os from pathlib import Path from typing import Any +logger = logging.getLogger(__name__) + def _default_ffmpeg_path() -> str: if os.name == "nt": @@ -40,9 +43,9 @@ class Settings: def _load_from_path(self, path: Path) -> None: try: self.data.update(json.loads(path.read_text(encoding="utf-8"))) - except Exception: + except (OSError, TypeError, UnicodeError, ValueError) as exc: # Leave defaults if invalid JSON; validation can be added later. - pass + logger.debug("Failed to load config %s: %s", path, exc) def _write_default_config(self, path: Path) -> None: path.parent.mkdir(parents=True, exist_ok=True) diff --git a/src/app/gui/app_icon.py b/src/app/gui/app_icon.py index a2665ce..763b3e7 100644 --- a/src/app/gui/app_icon.py +++ b/src/app/gui/app_icon.py @@ -1,10 +1,13 @@ from __future__ import annotations +import logging import sys from pathlib import Path from PySide6 import QtGui, QtWidgets +logger = logging.getLogger(__name__) + def _resource_base() -> Path: # PyInstaller sets sys._MEIPASS to the temp extraction dir. @@ -31,16 +34,16 @@ def load_app_icon() -> QtGui.QIcon: icon = QtGui.QIcon(str(p)) if not icon.isNull(): return icon - except Exception: - pass + except Exception as exc: # noqa: BLE001 - icon loading is best effort + logger.debug("Failed to load app icon from %s: %s", p, exc) # Fallback to a platform theme icon (Linux) or a generic icon. try: themed = QtGui.QIcon.fromTheme("applications-multimedia") if not themed.isNull(): return themed - except Exception: - pass + except Exception as exc: # noqa: BLE001 - themed icon loading is best effort + logger.debug("Failed to load themed app icon: %s", exc) return QtWidgets.QApplication.style().standardIcon(QtWidgets.QStyle.StandardPixmap.SP_ComputerIcon) diff --git a/src/app/gui/config_store.py b/src/app/gui/config_store.py index 2d32276..5bbc210 100644 --- a/src/app/gui/config_store.py +++ b/src/app/gui/config_store.py @@ -15,7 +15,7 @@ class AppConfig: def load_config(path: Path) -> AppConfig: raw = json.loads(path.read_text(encoding="utf-8")) if not isinstance(raw, dict): - raise ValueError("config root must be a JSON object") + raise TypeError("config root must be a JSON object") return AppConfig(data=raw, path=path) diff --git a/src/app/gui/main.py b/src/app/gui/main.py index 2c641b2..1363559 100644 --- a/src/app/gui/main.py +++ b/src/app/gui/main.py @@ -1,5 +1,6 @@ from __future__ import annotations +import logging import sys import threading @@ -17,6 +18,8 @@ from .pages.queue import QueuePage from .pages.settings import SettingsPage from .runner import SyncRequest, SyncRunner +logger = logging.getLogger(__name__) + class MainWindow(QtWidgets.QMainWindow): def __init__(self) -> None: @@ -90,8 +93,8 @@ class MainWindow(QtWidgets.QMainWindow): if cfg_path is not None: try: self._settings_page.set_config_path(cfg_path) - except Exception: - pass + except Exception as exc: # noqa: BLE001 - optional settings-page wiring + logger.debug("Failed to set settings config path: %s", exc) self._playlists_page.cancel_requested.connect(self._cancel_sync) self._queue_page.cancel_sync_requested.connect(self._cancel_sync) @@ -138,7 +141,8 @@ class MainWindow(QtWidgets.QMainWindow): tray = ui.get("tray") tray = tray if isinstance(tray, dict) else {} return dict(tray) - except Exception: + except Exception as exc: # noqa: BLE001 - config fallback keeps the UI usable + logger.debug("Failed to load tray configuration: %s", exc) return {} def _close_to_tray_enabled(self) -> bool: @@ -158,7 +162,8 @@ class MainWindow(QtWidgets.QMainWindow): try: if not QtWidgets.QSystemTrayIcon.isSystemTrayAvailable(): return - except Exception: + except Exception as exc: # noqa: BLE001 - tray support is optional + logger.debug("Failed to check system tray availability: %s", exc) return icon = load_app_icon() @@ -211,27 +216,30 @@ class MainWindow(QtWidgets.QMainWindow): QtWidgets.QSystemTrayIcon.MessageIcon.Information, 3000, ) - except Exception: - pass + except Exception as exc: # noqa: BLE001 - tray notification is best effort + logger.debug("Failed to show tray notification: %s", exc) return if self._tray is not None and not self._close_to_tray_enabled(): # Explicitly quit, because the app may be configured to keep running without windows. try: event.accept() - except Exception: - pass + except Exception as exc: # noqa: BLE001 - close event acceptance is best effort + logger.debug("Failed to accept close event: %s", exc) QtWidgets.QApplication.quit() return super().closeEvent(event) def changeEvent(self, event: QtCore.QEvent) -> None: # type: ignore[override] try: - if event.type() == QtCore.QEvent.Type.WindowStateChange: - if self._tray is not None and self._minimize_to_tray_enabled(): - if bool(self.windowState() & QtCore.Qt.WindowState.WindowMinimized): - QtCore.QTimer.singleShot(0, self.hide) - except Exception: - pass + if ( + event.type() == QtCore.QEvent.Type.WindowStateChange + and self._tray is not None + and self._minimize_to_tray_enabled() + and bool(self.windowState() & QtCore.Qt.WindowState.WindowMinimized) + ): + QtCore.QTimer.singleShot(0, self.hide) + except Exception as exc: # noqa: BLE001 - window state handling is best effort + logger.debug("Failed to handle window state change: %s", exc) super().changeEvent(event) def _refresh_queue_labels(self) -> None: @@ -244,24 +252,24 @@ class MainWindow(QtWidgets.QMainWindow): pid = extract_playlist_id(url) or url labels[pid] = str(pl.get("name") or f"Playlist {idx}") self._queue_page.set_playlist_labels(labels) - except Exception: - pass + except Exception as exc: # noqa: BLE001 - queue labels are best effort + logger.debug("Failed to refresh queue labels: %s", exc) @QtCore.Slot(str, dict) def _on_bus_event(self, name: str, payload: dict) -> None: # Fan out to interested pages. try: self._queue_page.on_event(name, payload) - except Exception: - pass + except Exception as exc: # noqa: BLE001 - page event fan-out is isolated + logger.debug("Queue page event handler failed: %s", exc) try: self._logs_page.on_event(name, payload) - except Exception: - pass + except Exception as exc: # noqa: BLE001 - page event fan-out is isolated + logger.debug("Logs page event handler failed: %s", exc) try: self._playlists_page.on_event(name, payload) - except Exception: - pass + except Exception as exc: # noqa: BLE001 - page event fan-out is isolated + logger.debug("Playlists page event handler failed: %s", exc) # Auto-pause on YouTube bot-check/rate-limit surface. if name == "SyncPaused": @@ -315,8 +323,8 @@ class MainWindow(QtWidgets.QMainWindow): try: self._thread.quit() self._thread.wait(2000) - except Exception: - pass + except Exception as exc: # noqa: BLE001 - thread shutdown is best effort + logger.debug("Failed to stop sync thread: %s", exc) self._runner = None self._cancel_flag = None self._pause_flag = None @@ -330,8 +338,8 @@ class MainWindow(QtWidgets.QMainWindow): if cfg_path is not None: self._settings_page.set_config_path(cfg_path) self._refresh_queue_labels() - except Exception: - pass + except Exception as exc: # noqa: BLE001 - config refresh is best effort + logger.debug("Failed to refresh configuration after sync: %s", exc) # Continue "sync all" chain if active. if hasattr(self, "_sync_queue") and self._sync_queue: diff --git a/src/app/gui/pages/logs.py b/src/app/gui/pages/logs.py index 2e297d3..847175e 100644 --- a/src/app/gui/pages/logs.py +++ b/src/app/gui/pages/logs.py @@ -38,7 +38,7 @@ class LogsPage(QtWidgets.QWidget): # Keep this lightweight: append a single-line JSON entry. try: line = json.dumps({"event": name, **payload}, ensure_ascii=False) - except Exception: + except Exception: # noqa: BLE001 - fall back to a readable representation line = f"{name}: {payload}" self._text.appendPlainText(line) self._text.moveCursor(self._text.textCursor().End) diff --git a/src/app/gui/pages/playlists.py b/src/app/gui/pages/playlists.py index ca0ac53..a50f695 100644 --- a/src/app/gui/pages/playlists.py +++ b/src/app/gui/pages/playlists.py @@ -137,7 +137,7 @@ class PlaylistManagerPage(QtWidgets.QWidget): raise RuntimeError("Config path not available") self._config = normalize_config(load_config(self._config_path).data) rows = self._rows_from_settings() - except Exception as exc: + except Exception as exc: # noqa: BLE001 - show config load failures in the UI self._status.setText(f"Failed to load config: {exc}") return finally: @@ -152,7 +152,7 @@ class PlaylistManagerPage(QtWidgets.QWidget): ls = db.get_playlist_last_sync(pid) if ls: last_sync_by_id[pid] = str(ls) - except Exception: + except Exception: # noqa: BLE001 - database metadata is optional last_sync_by_id = {} self._list.clear() @@ -256,7 +256,7 @@ class PlaylistManagerPage(QtWidgets.QWidget): self._status.setText(f"Saved {len(data['playlists'])} playlists to {self._config_path}.") # Reload settings to reflect merged defaults self.reload_from_config() - except Exception as exc: + except Exception as exc: # noqa: BLE001 - show config save failures in the UI self._status.setText(f"Failed to save config: {exc}") def _reindex_cards(self) -> None: @@ -302,7 +302,7 @@ class PlaylistManagerPage(QtWidgets.QWidget): data["playlists"] = self._table_to_playlists() save_config(self._config_path, data) self._status.setText(f"Autosaved to {self._config_path}.") - except Exception as exc: + except Exception as exc: # noqa: BLE001 - show autosave failures in the UI self._status.setText(f"Autosave failed: {exc}") def on_event(self, name: str, payload: dict) -> None: @@ -355,7 +355,7 @@ class PlaylistManagerPage(QtWidgets.QWidget): self._download_state_by_pid[pid]["video_id"] = vid self._download_state_by_pid[pid]["status"] = str(payload.get("status") or "downloading") self._set_card_progress(pid, p) - pct = int(round(max(0.0, min(1.0, p)) * 100)) + pct = round(max(0.0, min(1.0, p)) * 100) st = str(payload.get("status") or "downloading") tail = f"{vid} {pct}%" if vid else f"{pct}%" self._set_card_status(pid, f"{st} {tail}".strip()) @@ -392,9 +392,8 @@ class PlaylistManagerPage(QtWidgets.QWidget): for i in range(self._list.count()): item = self._list.item(i) w = self._list.itemWidget(item) - if isinstance(w, _PlaylistCard): - if w.playlist_id() == playlist_id: - w.set_status(text) + if isinstance(w, _PlaylistCard) and w.playlist_id() == playlist_id: + w.set_status(text) def _set_card_last_sync(self, playlist_id: str, last_sync: str) -> None: for i in range(self._list.count()): @@ -604,7 +603,7 @@ class _PlaylistCard(QtWidgets.QFrame): return extract_playlist_id(url) or url def set_progress(self, progress: float) -> None: - pct = max(0, min(100, int(round(progress * 100)))) + pct = max(0, min(100, round(progress * 100))) self._progress.setValue(pct) def set_last_sync(self, last_sync: str) -> None: @@ -613,7 +612,7 @@ class _PlaylistCard(QtWidgets.QFrame): def validate(self) -> list[str]: errs: list[str] = [] url = self._url.text().strip() - if not url or not (url.startswith("http://") or url.startswith("https://")): + if not url or not url.startswith(("http://", "https://")): errs.append("URL required") mode = self._mode.currentText().strip() if mode not in {"video", "audio", "both"}: diff --git a/src/app/gui/pages/queue.py b/src/app/gui/pages/queue.py index b0a9a9d..518fd70 100644 --- a/src/app/gui/pages/queue.py +++ b/src/app/gui/pages/queue.py @@ -141,7 +141,7 @@ class QueuePage(QtWidgets.QWidget): status_item.setText(str(payload.get("status") or "downloading")) prog = payload.get("progress") if isinstance(prog, (int, float)): - pct = max(0, min(100, int(round(prog * 100)))) + pct = max(0, min(100, round(prog * 100))) bar = self._table.cellWidget(row, 3) if bar is None: bar = QtWidgets.QProgressBar() diff --git a/src/app/gui/pages/settings.py b/src/app/gui/pages/settings.py index a6e9027..7172eaf 100644 --- a/src/app/gui/pages/settings.py +++ b/src/app/gui/pages/settings.py @@ -126,7 +126,7 @@ class SettingsPage(QtWidgets.QWidget): self._start_minimized_to_tray.setChecked(bool(tray.get("start_minimized_to_tray", False))) self._status.setText(f"Loaded settings from {self._config_path}.") - except Exception as exc: + except Exception as exc: # noqa: BLE001 - show load failures in the UI self._status.setText(f"Failed to load settings: {exc}") finally: self._suppress_autosave = False @@ -161,5 +161,5 @@ class SettingsPage(QtWidgets.QWidget): save_config(self._config_path, data) self._status.setText(f"Saved settings to {self._config_path}.") - except Exception as exc: + except Exception as exc: # noqa: BLE001 - show save failures in the UI self._status.setText(f"Failed to save settings: {exc}") diff --git a/src/app/gui/runner.py b/src/app/gui/runner.py index fdf0c3c..b9c611a 100644 --- a/src/app/gui/runner.py +++ b/src/app/gui/runner.py @@ -70,5 +70,5 @@ class SyncRunner(QtCore.QObject): self.finished.emit(False, "cancelled") else: self.finished.emit(True, "done") - except Exception as exc: + except Exception as exc: # noqa: BLE001 - report worker failures through the UI signal self.finished.emit(False, str(exc)) diff --git a/src/app/gui/smooth_scroll.py b/src/app/gui/smooth_scroll.py index b9d5d78..d17aabe 100644 --- a/src/app/gui/smooth_scroll.py +++ b/src/app/gui/smooth_scroll.py @@ -19,7 +19,7 @@ class _SmoothWheelFilter(QtCore.QObject): try: angle_y = wheel.angleDelta().y() pixel_y = wheel.pixelDelta().y() - except Exception: + except Exception: # noqa: BLE001 - fall back to native scrolling for unknown event types return super().eventFilter(obj, event) dy = pixel_y if pixel_y else angle_y @@ -34,7 +34,7 @@ class _SmoothWheelFilter(QtCore.QObject): if pixel_y: delta = -dy else: - delta = int(round(-dy / 120.0 * base_step)) + delta = round(-dy / 120.0 * base_step) target = max(sb.minimum(), min(sb.maximum(), start + delta)) if target == start: diff --git a/tests/test_event_surface.py b/tests/test_event_surface.py index 59c8760..1b95e1e 100644 --- a/tests/test_event_surface.py +++ b/tests/test_event_surface.py @@ -37,7 +37,7 @@ def test_executor_emits_sync_events(tmp_path): assert "SyncSummary" in names assert "SyncFinished" in names - summary = [p for n, p in published if n == "SyncSummary"][0] + summary = next(p for n, p in published if n == "SyncSummary") assert summary["playlist_id"] == "p" assert "duration_s" in summary assert isinstance(summary["counts"], dict)