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"