- useMediaWebSocket: shallowRef mediaIndex/tasks/loading/error/connected
to eliminate Proxy overhead on the entire library data structure.
- useMediaWebSocket: replace per-task setTimeout leak with single 3s
sweep interval for completed task cleanup.
- App.vue: remove {deep:true} watcher on mediaIndex; shallow ref change
is sufficient to trigger search worker sync.
- useGamepadNavigation: rAF only when gamepads are connected; idle
fallback to 500ms setTimeout to stop permanent 60fps CPU drain.
- useKeyboardNavigation: deduplicate synced scroll rAF requests to
prevent overlapping animation frames.
- MediaDetail/SeriesFullView/CollageHero: pause, clear src, and load()
video elements on unmount and before ref replacement to release
decoder/memory resources.
- MediaDetail/SeriesFullView: clear all volume fade intervals on
unmount to stop interval timer leaks.
Replace the fragile per-row tail/maxVirtual state with a clean global-
metrics approach:
- Measure cardWidth, stride, paddingLeft, viewportWidth, rightDeadzone
once from the first synced row; use the same values for all rows.
- Per-row max scroll computed purely from math:
max(0, paddingLeft + (N-1)*stride - maxVisibleLeft)
where maxVisibleLeft = viewportWidth - cardWidth - rightDeadzone.
- Global virtual offset (syncedRowsCurrentOffset/targetOffset) is shared
across all rows; each row clamps independently in applySyncedRowScroll.
- Tail is a global constant (= rightDeadzone) applied to all rows.
- Fix scrolling-back bug: desiredOffset was stuck at currentOffset when
moving left; now always computed from anchor column position.
- Avoid full animation sweep on view entry: init current/target offset
from existing DOM scrollLeft on first use.
Also removes per-row --sync-row-max-virtual CSS var and simplifies
.media-row padding to use only --sync-row-tail.
- Tail is now computed and applied per-row instead of globally.
- Right-side-only tail padding so item positions don't shift when tail grows.
- Removed anchor-row clamping from effectiveAnchorOffset and targetOffset;
each row independently clamps the shared virtual offset.
- Rows that fit entirely on screen (maxOffset == 0) keep left alignment.
- Short rows retain their scrolled position when focus moves to longer rows.
Fixes erratic scrolling and zig-zag when navigating across rows of different
lengths in movie/series/search categories.
When clicking a search result, showDetail navigates to the detail page.
The route.fullPath watcher then clears searchQuery, which propagates to
Header's localSearch watcher, which emits @search='', which calls
updateSearchQuery(''). This in turn called clearSearch(), which did a
router.replace() to the category page because it didn't check whether
the current route actually had a search query.
The fix: in clearSearch(), if there's no search query in the current
URL, just clear reactive state and return without navigating. This
breaks the circular flow that was overwriting the detail-page
navigation.
Remove the separate Play and Info buttons from the CollageHero featured
item. All collage items (including the featured one) are now uniform
<a href> links that navigate to the detail page.
- CollageHero: remove hasResumePosition prop, play/info emits, and
associated play button logic
- App.vue: remove @play and @info bindings from CollageHero
- Delete unused HeroSection.vue component and its styles
MediaCard, MediaRow, CollageHero, HeroSection, and MediaDetail cast
cards now render as <a> tags with real hrefs. Modified clicks
(ctrl+click, middle-click) navigate natively, enabling open-in-new-tab
and link copying. Plain left-clicks continue through the existing
showDetail flow for side-effects like focus-state saving.
When typing in the search bar on a detail page, navigating to search
results would trigger restoreFocusForPage(), which focused the 'last
viewed' card and interrupted typing. Skip focus restoration whenever
the search input is currently the active element.
The clip-path slant on collage slices previously varied with the number
of posters (hard-coded 10% steps). Express the slant as a percentage
of each slice's own width so the horizontal offset always equals the
fixed 5% container overlap, giving a constant angle for any count >= 2.
Use SHParseDisplayName + SHOpenFolderAndSelectItems to open the parent folder and select files with special-character paths. Keep explorer.exe /n,/select as a fallback when the API path fails.
Uvicorn's asyncio_loop_factory returns SelectorEventLoop on Windows
whenever reload=True (use_subprocess=True). SelectorEventLoop does not
support asyncio subprocess APIs, causing NotImplementedError in the app.
Fix: pass loop='none' so uvicorn respects the ProactorEventLoop policy
we configure, instead of overriding it. Also suppress the Py3.14
DeprecationWarning on the policy class itself.
- Remove vite-plugin-pwa and all service worker code. The app no longer
registers a service worker; legacy SWs are unregistered on load.
- Move snapshot parsing (thousands of sync Path.exists() calls) into a
thread pool via asyncio.to_thread() so the health check responds
immediately on startup.
- Wrap aiopathlib AsyncPath.iterdir() (sync os.scandir under the hood)
in asyncio.to_thread() in scanner and scanning modules so directory
walks do not monopolize the event loop.
- Add root_id to Torrent type for per-torrent root tracking
- Merge movies: combine all torrents, keep best metadata, sort by quality
- Merge series: combine seasons/episodes/torrents across roots
- Torrents are quality-sorted (resolution, HDR, DV, Atmos) across all roots
- App.vue play/openFolder use torrent-specific root_id when available
Items with the same content hash from multiple roots are now shown as a
single card. When duplicates exist, the item with the best metadata
(info, cover, backdrop, showreels, torrent count) is displayed.
- Change header icon from folder to settings cog
- Replace roots dropdown panel with full-screen settings overlay
- Keep media roots as the only setting section for now
- Remove legacy endpoints: /api/change-folder, /api/index, /api/scan, /api/status,
/api/playback/resume-positions
- Remove legacy global scanner module-level API from hivescan/scanner.py
- Defer all filesystem validation to background task in server lifespan (macOS-safe)
- CLI and winmain pass raw paths via MEDIAHIVE_ROOTS; no pre-startup validation
- Enforce POSIX paths everywhere (as_posix(), no backslash leakage)
- Remove MEDIAHIVE_PATH and MEDIAHIVE_DEFER_INITIAL_ROOT env vars
- Update frontend api.ts to use per-root resume positions
- Update docs/API.md and docs/multi-index-plan.md
- Fix Python 2 style except clauses in hivescan/utils.py and scanning.py
The :global() pseudo-class in Vue scoped SFC styles was being mangled
by the Vue compiler, producing broken CSS that applied properties like
z-index and opacity directly to the <html> element and dropped the
intended component selectors entirely.
Replace the html[data-mouse-active] / html[data-pointer-visible] data
attribute approach with plain CSS classes (mouse-active, pointer-visible)
toggled on document.documentElement. In Vue scoped styles, prefixing a
selector with html.mouse-active or html:not(.mouse-active) compiles
correctly — the html part stays global while the component selector
receives the scoped attribute — avoiding :global() entirely.
This keeps all component styles inside <style scoped> and fixes the
layout breakage caused by the original commit.
Bug 1 (shutdown hang):
- Add _kill_proc() helper to force-kill ffmpeg subprocesses immediately
when a showreel generation task is cancelled or fails.
- Unlink the incomplete output file after killing the process.
- Applies to both generate_showreel_images() and generate_episode_reel().
Bug 2 (path inconsistency):
- Use .as_posix() instead of str() for all persisted and stored paths
across the codebase (config, env vars, index store, API responses,
torrent paths, playable files, cover images, episode reels, etc.).
- Ensures forward slashes are used exclusively even on Windows.
Files changed:
- mediahive/hivescan/showreel.py
- mediahive/server.py
- mediahive/hivescan/scanner.py
- mediahive/hivescan/scanning.py
- mediahive/hivescan/indexer.py
- mediahive/hivescan/images.py
- mediahive/hivescan/__main__.py
- Grid layout: poster/releases left, cast spanning center+right top, versions center, metadata right
- Cast gallery moved to wider center column with photo-card grid (2:3 aspect, text overlay)
- Full cast now stored in index (removed 10-item truncation)
- Cast gender field added to metadata model, TMDB client, indexer, and frontend types
- Gender-specific cast placeholder SVGs (male suit silhouette, female portrait)
- Synopsis text moved from left poster box into right metadata card
- Format badge backgrounds made opaque (res/qual/codec/audio solid colors)
- Releases list moved under poster image in left column
- Cast photo and character font sizes reduced for compact display
- mediahive/winmain.py: pywebview launcher with threaded uvicorn backend,
- scripts/winbuild.py: build script — downloads ffmpeg, runs PyInstaller, makes zip
- mediahive/config.py: TOML config persistence in %APPDATA%/mediahive/
- server.py: POST /api/change-folder switches media root
- showreel.py: suppress console windows for ffmpeg subprocesses on Windows
- protocol.py: add ChangeFolderRequest struct