diff --git a/frontend/src/api.ts b/frontend/src/api.ts index a07e3b8..848d1e6 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -44,19 +44,19 @@ export function getVideoPreviewUrl(url: string): string { export function getVideoSourceAttributes(path: string | null | undefined): VideoSourceAttributes { if (!path) { - return { type: 'video/mp4; codecs="hvc1"', codecs: 'hvc1' }; + return { type: 'video/mp4', codecs: 'hvc1' }; } if (/\.webm$/i.test(path)) { - return { type: 'video/webm; codecs="av01"', codecs: 'av01' }; + return { type: 'video/webm', codecs: 'av1' }; } if (/\.mp4$/i.test(path) || /\.m4v$/i.test(path)) { - return { type: 'video/mp4; codecs="hvc1"', codecs: 'hvc1' }; + return { type: 'video/mp4', codecs: 'hvc1' }; } if (/\.mov$/i.test(path)) { - return { type: 'video/quicktime; codecs="hvc1"', codecs: 'hvc1' }; + return { type: 'video/quicktime', codecs: 'hvc1' }; } if (/\.avi$/i.test(path)) { @@ -67,7 +67,7 @@ export function getVideoSourceAttributes(path: string | null | undefined): Video return { type: 'video/x-matroska', codecs: '' }; } - return { type: 'video/mp4; codecs="hvc1"', codecs: 'hvc1' }; + return { type: 'video/mp4', codecs: 'hvc1' }; } /** diff --git a/mediahive/hivescan/showreel.py b/mediahive/hivescan/showreel.py index a6e4e1f..242848f 100644 --- a/mediahive/hivescan/showreel.py +++ b/mediahive/hivescan/showreel.py @@ -33,6 +33,8 @@ async def _subprocess_exec(*args, **kwargs): # Showreel timestamp positions in seconds (5, 10, 15, 20, 25 minutes) SHOWREEL_TIMESTAMPS = [5 * 60, 10 * 60, 15 * 60, 20 * 60, 25 * 60] REEL_SOURCE_EXTENSIONS = [".webm", ".mp4"] +# Temporary rollout switch: keep platform-native MP4/H.265 path available but disabled. +ENABLE_PLATFORM_NATIVE_REELS = False def get_reel_source_extensions() -> list[str]: @@ -52,11 +54,15 @@ def _to_media_path(path: Path, media_root: Optional[Path] = None) -> str: def get_reel_extension() -> str: """Return the platform-native reel file extension.""" + if not ENABLE_PLATFORM_NATIVE_REELS: + return ".webm" return ".mp4" if sys.platform == "darwin" else ".webm" async def get_reel_video_encoder() -> str: """Return the platform-native reel video encoder.""" + if not ENABLE_PLATFORM_NATIVE_REELS: + return await get_av1_encoder() if sys.platform == "darwin": return "libx265" return await get_av1_encoder() @@ -73,6 +79,8 @@ def get_reel_video_options(encoder: str) -> list[str]: def get_reel_audio_options() -> list[str]: """Return ffmpeg audio and container options for the current platform.""" + if not ENABLE_PLATFORM_NATIVE_REELS: + return ["-c:a", "libopus", "-ac", "2", "-b:a", "128k"] if sys.platform == "darwin": return ["-c:a", "aac", "-ac", "2", "-b:a", "128k", "-movflags", "+faststart"] return ["-c:a", "libopus", "-ac", "2", "-b:a", "128k"] diff --git a/mediahive/models/__init__.py b/mediahive/models/__init__.py new file mode 100644 index 0000000..0402b2d --- /dev/null +++ b/mediahive/models/__init__.py @@ -0,0 +1 @@ +"""Shared data/protocol models for MediaHive.""" diff --git a/mediahive/winmain.py b/mediahive/winmain.py index 5409652..b48c6e5 100644 --- a/mediahive/winmain.py +++ b/mediahive/winmain.py @@ -28,6 +28,8 @@ import webview from mediahive.__main__ import resolve_media_root from mediahive.config import load_config, save_config +logger = logging.getLogger("mediahive.winmain") + BACKEND_HOST = "127.0.0.1" BACKEND_PORT = 8420 HEALTH_TIMEOUT = 2 # seconds @@ -669,6 +671,19 @@ def _icon_path() -> str | None: return str(ico) if ico.exists() else None +def _webview_start_kwargs() -> dict[str, str]: + """Return platform-specific pywebview startup kwargs.""" + # On macOS, force Qt backend so pywebview uses Chromium/WebEngine instead of WKWebView. + if sys.platform == "darwin": + return {"gui": "qt"} + return {} + + +def _selected_webview_backend() -> str: + """Return the configured pywebview GUI backend name for logging.""" + return _webview_start_kwargs().get("gui", "default") + + def _run_initial_setup() -> str | None: """Show a setup window, prompt for a folder, then close and return the path. @@ -691,7 +706,7 @@ def _run_initial_setup() -> str | None: chosen.append(result[0]) window.destroy() - webview.start(func=on_shown, icon=_icon_path()) + webview.start(func=on_shown, icon=_icon_path(), **_webview_start_kwargs()) return chosen[0] if chosen else None @@ -766,6 +781,7 @@ def winmain() -> None: raise RuntimeError(f"Backend did not become ready within {HEALTH_TIMEOUT}s") api = JsApi() + logger.info("Configured pywebview backend: %s", _selected_webview_backend()) window = webview.create_window( title="MediaHive", url=backend_url, @@ -778,11 +794,18 @@ def winmain() -> None: def on_shown() -> None: api._window = window + try: + user_agent = window.evaluate_js("navigator.userAgent") + if isinstance(user_agent, str): + logger.info("Embedded webview user agent: %s", user_agent) + except Exception as exc: + logger.warning("Could not read embedded user agent: %s", exc) + nonlocal poll_thread if poll_thread is None and _supports_gamepad_remote(): poll_thread = _start_gamepad_remote(poll_stop, mediaroot) - webview.start(func=on_shown, icon=_icon_path()) + webview.start(func=on_shown, icon=_icon_path(), **_webview_start_kwargs()) poll_stop.set() if poll_thread is not None: diff --git a/pyproject.toml b/pyproject.toml index 55d3827..5c189bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,10 @@ parse-torrent-title = { git = "https://github.com/platelminto/parse-torrent-titl [project.optional-dependencies] gui = [ - "pywebview>=6.2.1", + "pywebview>=6.2.1; platform_system != 'Darwin'", + "pywebview[qt5]>=6.2.1; platform_system == 'Darwin'", + "qtpy>=2.4.1; platform_system == 'Darwin'", + "PyQt5>=5.15.11; platform_system == 'Darwin'", "pythonnet>=3.1.0rc0; platform_system == 'Windows' and python_version >= '3.14'", "pyinstaller>=6.0", ] diff --git a/scripts/MediaHive.spec b/scripts/MediaHive.spec index 83c5551..c42f60b 100644 --- a/scripts/MediaHive.spec +++ b/scripts/MediaHive.spec @@ -36,41 +36,57 @@ if _icon_win.exists(): if _icon_mac.exists(): _datas.append((str(_icon_mac), "mediahive/assets")) +_hiddenimports = [ + # uvicorn dynamic imports + "uvicorn.logging", + "uvicorn.loops", + "uvicorn.loops.auto", + "uvicorn.loops.asyncio", + "uvicorn.protocols", + "uvicorn.protocols.http", + "uvicorn.protocols.http.auto", + "uvicorn.protocols.http.h11_impl", + "uvicorn.protocols.websockets", + "uvicorn.protocols.websockets.auto", + "uvicorn.protocols.websockets.websockets_impl", + "uvicorn.lifespan", + "uvicorn.lifespan.on", + # mediahive & hivescan modules imported at runtime + "mediahive.server", + "mediahive.hivescan.scanner", + "mediahive.hivescan.indexer", + "mediahive.hivescan.scanning", + "mediahive.hivescan.images", + "mediahive.hivescan.showreel", + "mediahive.hivescan.tmdb_client", + # async / ASGI internals + "anyio", + "anyio._backends._asyncio", + "starlette.routing", + # msgspec TOML write backend + "tomli_w", +] + +if sys.platform == "darwin": + _hiddenimports.extend( + [ + # pywebview Qt backend selected dynamically via webview.start(gui="qt") + "webview.platforms.qt", + "qtpy", + "PyQt5", + "PyQt5.QtCore", + "PyQt5.QtGui", + "PyQt5.QtWidgets", + "PyQt5.QtWebEngineWidgets", + ] + ) + a = Analysis( [mediahive.winmain.__file__], pathex=[], binaries=_binaries, datas=_datas, - hiddenimports=[ - # uvicorn dynamic imports - "uvicorn.logging", - "uvicorn.loops", - "uvicorn.loops.auto", - "uvicorn.loops.asyncio", - "uvicorn.protocols", - "uvicorn.protocols.http", - "uvicorn.protocols.http.auto", - "uvicorn.protocols.http.h11_impl", - "uvicorn.protocols.websockets", - "uvicorn.protocols.websockets.auto", - "uvicorn.protocols.websockets.websockets_impl", - "uvicorn.lifespan", - "uvicorn.lifespan.on", - # mediahive & hivescan modules imported at runtime - "mediahive.server", - "mediahive.hivescan.scanner", - "mediahive.hivescan.indexer", - "mediahive.hivescan.scanning", - "mediahive.hivescan.images", - "mediahive.hivescan.showreel", - "mediahive.hivescan.tmdb_client", - # async / ASGI internals - "anyio", - "anyio._backends._asyncio", - "starlette.routing", - # msgspec TOML write backend - "tomli_w", - ], + hiddenimports=_hiddenimports, hookspath=[], runtime_hooks=[], excludes=[],