Installers for all platforms and related fixes #1

Merged
LeoVasanko merged 38 commits from installer into main 2026-09-23 17:41:21 +00:00
3 changed files with 23 additions and 47 deletions
Showing only changes of commit e514829737 - Show all commits
+18 -36
View File
@@ -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 = (
"<!DOCTYPE html><html><head><meta charset='utf-8'>"
"<title>MediaHive — Error</title></head>"
f"<body style='margin:1.5rem'>{fragment}</body></html>"
)
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,
+3 -6
View File
@@ -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"]
+2 -5
View File
@@ -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"