diff --git a/mediahive/winmain.py b/mediahive/winmain.py index 1ea75c9..9b0d55e 100644 --- a/mediahive/winmain.py +++ b/mediahive/winmain.py @@ -26,9 +26,13 @@ from pathlib import Path import msgspec.structs import uvicorn +import velopack import webview +from fastapi_vue.logging import patch_log_config +from fastapi_vue.startupbox import print_box +from tracerite.html import html_traceback -from mediahive.config import load_config, save_config +from mediahive.config import load_config, log_dir, save_config from mediahive.volume_control import get_volume, set_volume, volume_max logger = logging.getLogger("mediahive.winmain") @@ -872,18 +876,16 @@ def _wait_for_previous_instance(log_path: Path, timeout: float = 15.0): def _setup_logging() -> Path: - """Redirect stdout/stderr and configure logging to a file in %APPDATA%/mediahive/. + """Redirect stdout/stderr and configure logging to a file in the platform log dir. In a PyInstaller --windowed build there is no console, so any print() or unhandled exception traceback would be lost. This ensures everything ends up in a persistent log file the user can send for bug reports. Returns the path to the log file. """ - from mediahive.config import config_dir - - log_dir = config_dir() - log_dir.mkdir(parents=True, exist_ok=True) - log_path = log_dir / "mediahive.log" + log_directory = log_dir() + log_directory.mkdir(parents=True, exist_ok=True) + log_path = log_directory / "mediahive.log" try: log_file = _rotate_and_open_log(log_path) @@ -900,7 +902,7 @@ def _setup_logging() -> Path: log_file = _wait_for_previous_instance(log_path) if log_file is None: # Never fail startup over logging: fall back to a per-process file. - log_path = log_dir / f"mediahive-{os.getpid()}.log" + log_path = log_directory / f"mediahive-{os.getpid()}.log" with contextlib.suppress(OSError): fd = os.open(log_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC) log_file = os.fdopen(fd, "w", encoding="utf-8", buffering=1) @@ -959,22 +961,14 @@ def _show_fatal_error(exc: BaseException) -> None: Frozen --windowed builds otherwise surface crashes only as PyInstaller's plain-text error dialog (or nothing at all). """ - try: - from tracerite.html import html_traceback - - fragment = str(html_traceback(exc)) - except Exception: # noqa: BLE001 - error reporting must never raise - return + fragment = str(html_traceback(exc)) page = ( "" "MediaHive — Error" f"{fragment}" ) - try: - webview.create_window("MediaHive — Error", html=page, width=1100, height=750) - webview.start(icon=_icon_path(), **_webview_start_kwargs()) - except Exception: - logger.exception("Could not display the error window") + webview.create_window("MediaHive — Error", html=page, width=1100, height=750) + webview.start(icon=_icon_path(), **_webview_start_kwargs()) def _velopack_startup() -> None: @@ -986,14 +980,7 @@ def _velopack_startup() -> None: Applies downloaded-but-pending updates. No-op in development and portable-ZIP runs. """ - try: - import velopack - except ImportError: - return - try: - velopack.App().run() - except Exception: - logger.exception("Velopack startup hook failed") + velopack.App().run() def _check_for_updates() -> None: @@ -1001,11 +988,10 @@ def _check_for_updates() -> None: Downloaded updates are applied automatically by Velopack on the next app start (via _velopack_startup), so the running session is never - interrupted. + interrupted. Not a Velopack install (dev/portable) and network failures + are expected and skipped quietly. """ try: - import velopack - mgr = velopack.UpdateManager(velopack.GiteaSource(VELOPACK_REPO_URL)) info = mgr.check_for_updates() if info is None: @@ -1015,7 +1001,7 @@ def _check_for_updates() -> None: logger.info("Velopack: downloading update %s", version) mgr.download_updates(info) logger.info("Velopack: update %s staged, applies on next launch", version) - except Exception as exc: # not a Velopack install (dev/portable), network, ... + except (RuntimeError, OSError) as exc: logger.info("Velopack update check skipped: %s", exc) @@ -1184,7 +1170,7 @@ def _strip_mark_of_the_web() -> None: meipass = Path(sys._MEIPASS) # type: ignore[attr-defined] for dll in meipass.rglob("*.dll"): with contextlib.suppress(OSError): - os.remove(f"{dll}:Zone.Identifier") + Path(f"{dll}:Zone.Identifier").unlink() def winmain() -> None: @@ -1242,8 +1228,6 @@ def winmain() -> None: # Startup banner, same as fastapi-vue's server.run() prints in CLI mode. # Goes to stderr, which frozen builds redirect to the log file. - from fastapi_vue.startupbox import print_box - try: version = importlib.metadata.version("mediahive") except importlib.metadata.PackageNotFoundError: @@ -1254,8 +1238,6 @@ def winmain() -> None: # log config wires up its access-log middleware, emoji level prefixes and # tracerite tracebacks (colors are auto-disabled when stderr is not a tty, # e.g. redirected to the log file in frozen builds). - from fastapi_vue.logging import patch_log_config - config = uvicorn.Config( "mediahive.server:app", host=BACKEND_HOST, diff --git a/scripts/MediaHive.spec b/scripts/MediaHive.spec index 2f8ddc1..99f1f44 100644 --- a/scripts/MediaHive.spec +++ b/scripts/MediaHive.spec @@ -7,7 +7,6 @@ # Or use the build script (recommended—handles versioning and packaging): # uv run scripts/guibuild.py -import os import sys import mediahive.winmain import mediahive.server @@ -23,11 +22,9 @@ _icon_win = _pkg / "assets" / "mediahive.ico" _icon_mac = _pkg / "assets" / "mediahive.icns" # ffmpeg staging lives in the persistent build cache (same logic as # scripts/guibuild.py); fall back to the legacy build/ffmpeg location. -if sys.platform == "win32": - _cache_base = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData" / "Local")) -else: - _cache_base = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache")) -_tools_dir = _cache_base / "mediahive-build" / "ffmpeg" +from platformdirs import user_cache_path + +_tools_dir = user_cache_path("mediahive-build") / "ffmpeg" if not _tools_dir.exists(): _tools_dir = Path(SPECPATH).parent / "build" / "ffmpeg" _tool_names = ["ffmpeg.exe"] if sys.platform == "win32" else ["ffmpeg"] diff --git a/scripts/guibuild.py b/scripts/guibuild.py index 820eda8..b1201ee 100755 --- a/scripts/guibuild.py +++ b/scripts/guibuild.py @@ -31,6 +31,7 @@ import zipfile from pathlib import Path import setuptools_scm +from platformdirs import user_cache_path # BtbN automated builds always publish a 'latest' tag with this asset. _FFMPEG_URL = ( @@ -46,11 +47,7 @@ _ASSETS_DIR = _REPO_ROOT / "mediahive" / "assets" def _build_cache_dir() -> Path: """Return the persistent cross-build cache dir for downloaded tools (CI wipes build/).""" - if sys.platform == "win32": - base = os.environ.get("LOCALAPPDATA") or (Path.home() / "AppData" / "Local") - return Path(base) / "mediahive-build" - base = os.environ.get("XDG_CACHE_HOME") or (Path.home() / ".cache") - return Path(base) / "mediahive-build" + return user_cache_path("mediahive-build") _FFMPEG_STAGING = _build_cache_dir() / "ffmpeg"