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