From eeed9f3ef78411eaffdf6bd5438acfa757803164 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Thu, 21 May 2026 04:06:44 +0000 Subject: [PATCH] Implement Safari video startup fixes and range streaming support --- README.md | 12 +- docs/API.md | 4 +- docs/development.md | 32 +--- frontend/src/App.vue | 9 +- frontend/src/api.ts | 76 ++++++++- frontend/src/components/CollageHero.vue | 54 ++++--- frontend/src/components/MediaDetail.vue | 178 +++++++++++++++------ frontend/src/components/SeriesFullView.vue | 43 +++-- frontend/src/types.ts | 3 + mediahive/assets/mediahive.icns | Bin 0 -> 1188703 bytes mediahive/hivescan/indexer.py | 82 ++++++---- mediahive/hivescan/scanner.py | 26 ++- mediahive/hivescan/showreel.py | 172 ++++++++++++++++---- mediahive/index_store.py | 61 +++++++ mediahive/models/data.py | 5 +- mediahive/server.py | 131 ++++++++++++--- mediahive/winmain.py | 162 +++++++++++++------ scripts/MediaHive.spec | 54 +++++-- scripts/release.py | 69 ++++---- scripts/winbuild.py | 176 +++++++++++++++++--- 20 files changed, 1015 insertions(+), 334 deletions(-) create mode 100644 mediahive/assets/mediahive.icns diff --git a/README.md b/README.md index 56f44fd..9ce8827 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ Netflix style browsing of your local media archive. Supports keyboard, mouse and - Scans your chosen media folder for all movies and series that can be found - Produces preview video clips and downloads metadata - Search on cast and character names, not just titles -- Hand off playback to your preferred Windows player -- Implement gamepad controls for MPC-BE which does not otherwise support that +- Hand off playback to your preferred system player +- Implement gamepad controls for MPC-BE on Windows (where needed) Extract the ZIP in some place and run MediaHive.exe to start the app. Currently we have no installer, but you can pin to start/taskbar for easier access. On the first startup the app asks for your media folder, that can later be changed by clicking in-app folder icon. @@ -28,11 +28,13 @@ MediaHive is designed to work with a mouse, keyboard, or gamepad. | Keyboard | Arrow keys move focus, `Enter` activates the focused item, `Escape` goes back, and `/` jumps to search. | | Gamepad | D-pad or left stick moves focus, `A` selects or plays, and `B` goes back. | -## Recommended Player: MPC-BE +## Recommended Players -For the best playback quality, install [MPC-BE](https://github.com/Aleksoid1978/MPC-BE/releases) and set it as default player so that MediaHive opens video files with it. The MPC Video Renderer is better than other players, having Dolby Vision and other things you may need supported out of the box. +- Windows: [MPC-BE](https://github.com/Aleksoid1978/MPC-BE/releases) +- macOS: [IINA](https://iina.io/) +- Linux: your distro's preferred default media player -For gamepad control, in MPC-BE Options, enable Web Interface, listen on port 13579. MediaHive automatically connects to that port on localhost. +MediaHive opens files with the OS default app. On Windows, if MPC-BE is your default player and Web Interface is enabled on port `13579`, MediaHive can send gamepad commands to MPC-BE. On macOS and Linux, MediaHive relies on native player controls. - `A` toggles play and pause. - `B` closes the player. diff --git a/docs/API.md b/docs/API.md index 64f9ca7..1809f9e 100644 --- a/docs/API.md +++ b/docs/API.md @@ -15,6 +15,7 @@ MediaHive exposes a small local API used by the desktop app and frontend. | `POST` | `/api/scan` | Triggers a new scan if the scanner is active. | | `POST` | `/api/play` | Opens a media file with the system player. | | `POST` | `/api/open-folder` | Opens a folder in the system file explorer, or selects a file in its parent folder. | +| `GET` | `/api/player/status` | Returns whether remote player control is currently available. | | `GET` | `/api/mpcbe/status` | Reports whether MPC-BE's local web interface is reachable. | | `GET` | `/api/media/{file_path:path}` | Serves files from the active media root. | | `WS` | `/api/ws` | Streams live index updates and task progress events. | @@ -24,4 +25,5 @@ MediaHive exposes a small local API used by the desktop app and frontend. - `POST /api/change-folder` validates the new folder, saves it to config, and switches the in-memory scanner asynchronously. - `POST /api/play` and `POST /api/open-folder` expect JSON request bodies matching the frontend calls. - `GET /api/media/{file_path:path}` is constrained to the current media root. -- `GET /api/mpcbe/status` only checks the local MPC-BE web interface. +- `GET /api/player/status` returns `{ "remote": true|false }`. +- `GET /api/mpcbe/status` returns `false` on non-Windows platforms. diff --git a/docs/development.md b/docs/development.md index ecb9754..553a852 100644 --- a/docs/development.md +++ b/docs/development.md @@ -43,34 +43,6 @@ This launches the same pywebview-based desktop flow used by the Windows build. ## Notes - The selected media folder is scanned continuously by the backend. -- The Windows desktop app remembers the chosen folder between launches. +- The desktop app remembers the chosen folder between launches. - HTTP and WebSocket endpoints are documented in [API.md](API.md). -- MPC-BE integration details live in [mpc-be.md](mpc-be.md). -*** Add File: c:\mediahive\docs\API.md -# API - -MediaHive exposes a small local API used by the desktop app and frontend. - -## Endpoints - -| Method | Path | Purpose | -| --- | --- | --- | -| `GET` | `/api/health` | Lightweight health check. | -| `GET` | `/api/config` | Returns the currently selected media folder. | -| `POST` | `/api/change-folder` | Persists and switches the active media folder without restarting the app. | -| `GET` | `/api/index` | Returns the current in-memory media index. | -| `GET` | `/api/playback/resume-positions` | Returns saved resume positions by media path. | -| `GET` | `/api/status` | Returns scanner and library status information. | -| `POST` | `/api/scan` | Triggers a new scan if the scanner is active. | -| `POST` | `/api/play` | Opens a media file with the system player. | -| `POST` | `/api/open-folder` | Opens a folder in the system file explorer, or selects a file in its parent folder. | -| `GET` | `/api/mpcbe/status` | Reports whether MPC-BE's local web interface is reachable. | -| `GET` | `/api/media/{file_path:path}` | Serves files from the active media root. | -| `WS` | `/api/ws` | Streams live index updates and task progress events. | - -## Notes - -- `POST /api/change-folder` validates the new folder, saves it to config, and switches the in-memory scanner asynchronously. -- `POST /api/play` and `POST /api/open-folder` expect JSON request bodies matching the frontend calls. -- `GET /api/media/{file_path:path}` is constrained to the current media root. -- `GET /api/mpcbe/status` only checks the local MPC-BE web interface. +- MPC-BE integration details (Windows only) live in [mpc-be.md](mpc-be.md). diff --git a/frontend/src/App.vue b/frontend/src/App.vue index da6e5a2..b639910 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -517,6 +517,7 @@ function movieToMediaItem(movie: Movie): MediaItem { year: movie.year, cover_path: movie.cover_path, showreel_images: movie.showreel_images, + showreel_source_sets: movie.showreel_source_sets, type: 'movies', resolution: resolution, data: movie, @@ -526,10 +527,15 @@ function movieToMediaItem(movie: Movie): MediaItem { function seriesToMediaItem(series: Series): MediaItem { // For series, collect reel images from all episodes const reelImages: string[] = []; + const reelSourceSets: string[][] = []; for (const season of series.seasons || []) { for (const episode of season.episodes || []) { - if (episode.reel_image) { + if (episode.reel_sources && episode.reel_sources.length > 0) { + reelImages.push(episode.reel_sources[0]); + reelSourceSets.push(episode.reel_sources); + } else if (episode.reel_image) { reelImages.push(episode.reel_image); + reelSourceSets.push([episode.reel_image]); } } } @@ -540,6 +546,7 @@ function seriesToMediaItem(series: Series): MediaItem { year: null, cover_path: series.cover_path, showreel_images: reelImages.length > 0 ? reelImages : null, + showreel_source_sets: reelSourceSets.length > 0 ? reelSourceSets : null, type: 'series', data: series, }; diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 002fb72..a07e3b8 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -1,5 +1,9 @@ import type { MediaIndex } from './types'; +export interface PlayerStatus { + remote: boolean; +} + export function normalizeMediaPath(input: string): string { return input .replace(/\\/g, '/') @@ -7,6 +11,65 @@ export function normalizeMediaPath(input: string): string { .replace(/^\/+/, ''); } +export function isVideoPath(path: string | null | undefined): boolean { + return Boolean(path && /\.(webm|mp4|mkv|avi|mov)$/i.test(path)); +} + +export interface VideoSourceAttributes { + type: string; + codecs: string; +} + +export function isSafariBrowser(): boolean { + if (typeof navigator === 'undefined') { + return false; + } + + const ua = navigator.userAgent; + return /Safari/i.test(ua) && !/Chrome|Chromium|CriOS|Edg|OPR|FxiOS/i.test(ua); +} + +export function getVideoPreviewUrl(url: string): string { + if (!url || !isSafariBrowser()) { + return url; + } + + if (url.includes('#')) { + return url; + } + + // Safari often needs a tiny time offset to paint the first frame before playback. + return `${url}#t=0.001`; +} + +export function getVideoSourceAttributes(path: string | null | undefined): VideoSourceAttributes { + if (!path) { + return { type: 'video/mp4; codecs="hvc1"', codecs: 'hvc1' }; + } + + if (/\.webm$/i.test(path)) { + return { type: 'video/webm; codecs="av01"', codecs: 'av01' }; + } + + if (/\.mp4$/i.test(path) || /\.m4v$/i.test(path)) { + return { type: 'video/mp4; codecs="hvc1"', codecs: 'hvc1' }; + } + + if (/\.mov$/i.test(path)) { + return { type: 'video/quicktime; codecs="hvc1"', codecs: 'hvc1' }; + } + + if (/\.avi$/i.test(path)) { + return { type: 'video/x-msvideo', codecs: '' }; + } + + if (/\.mkv$/i.test(path)) { + return { type: 'video/x-matroska', codecs: '' }; + } + + return { type: 'video/mp4; codecs="hvc1"', codecs: 'hvc1' }; +} + /** * Load the media index from the server */ @@ -40,7 +103,7 @@ export async function playMedia(filePath: string): Promise { } /** - * Open a folder in Windows Explorer + * Open a folder in the system file manager */ export async function openFolder(folderPath: string): Promise { const normalizedPath = normalizeMediaPath(folderPath); @@ -60,6 +123,17 @@ export async function openFolder(folderPath: string): Promise { } } +/** + * Return player integration capabilities for the current OS. + */ +export async function getPlayerStatus(): Promise { + const response = await fetch('/api/player/status'); + if (!response.ok) { + throw new Error(`Failed to load player status: ${response.statusText}`); + } + return response.json(); +} + /** * Return whether MPC-BE local web control is currently reachable. */ diff --git a/frontend/src/components/CollageHero.vue b/frontend/src/components/CollageHero.vue index 5e296f8..d2a79f1 100644 --- a/frontend/src/components/CollageHero.vue +++ b/frontend/src/components/CollageHero.vue @@ -32,13 +32,21 @@ class="collage-media" />