Top-level imports; catch only expected exceptions
velopack/tracerite/fastapi-vue are always available in the GUI build, so drop the in-function import guards. The update checker now catches only RuntimeError (not a Velopack install: dev/portable) and OSError (network). Build script and PyInstaller spec use platformdirs for the persistent tool cache.
This commit is contained in:
+18
-36
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user