Replace _platform_zip_suffix() and its overrides with a _Platform
NamedTuple (tag/rid/dist_dir/icon/main_exe/setup_ext) plus
setup_artifact_name(). create_zip() is Windows-only in practice, so it
is now create_portable_zip() with a fixed -win64-portable.zip name
instead of a generic name patched by string replace.
MediaHive-*-full.nupkg sorted between the capitalized Setup/AppImage
user downloads on the Gitea release page. Renamed to mediahive-* they
group with the wheel/sdist below the downloads; feed manifests
(RELEASES, releases.*.json, assets.*.json) are patched to match.
System installs land in /Applications where Velopack's UpdateMac may not
be able to replace the .app during auto-update. Restricting the
distribution domains to currentUserHome skips the Destination Select
page and needs no admin rights; the postinstall script's sudo prefixes
are dropped since it already runs as the installing user.
Collapsing <domains> to localSystem did not remove the Destination
Select page (Installer shows it regardless) and only removed the
~/Applications option. Restore Velopack's default of both domains.
appauthor=False/opinion=False avoids mediahive\mediahive\Cache style
paths on Windows. fetch_dotnet now probes all known dotnet locations and
selects the highest Microsoft.NETCore.App major, requiring the version
vpk's TFM targets (10) — PATH on the Windows CI runner resolves to a
runtime-only .NET 8 while scoop holds the SDK 10.
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.
Config and logs now live in local (non-roaming) app data on Windows
(%LOCALAPPDATA%\mediahive) instead of roaming. Existing files in the
old roaming location are left untouched; no migration.
Per-build downloads of the dotnet runtime (~80 MB) were slow and flaky
(IncompleteRead killed a CI run). The .NET SDK is now a build-host
prerequisite; vpk and ffmpeg download once into a user-level cache
(~/.cache/mediahive-build, %LOCALAPPDATA%\mediahive-build) that
survives the per-run build dir.
Tells the user to allow the permission prompts macOS may show during
install and first launch. License page remains absent; Destination
Select was already stripped, leaving Introduction, Install, Summary.
Collapse the Velopack-generated distribution.xml to a single install
domain so macOS Installer skips Destination Select, leaving Apple's
minimum pages (Introduction, Install, Summary). Welcome/license/readme/
conclusion pages are already absent (no --inst* options passed).
- macOS: .pkg installer replaces the DMG; Linux: .AppImage replaces the ZIP
- winmain runs velopack.App() first (proper hook handling) and checks for
updates in the background; downloads are applied on next launch
- release.py uploads the vpk update feed (releases.<channel>.json, nupkgs)
so GiteaSource finds updates on the latest release
- guibuild.py bootstraps a .NET runtime into build/dotnet when the runner
host lacks one
Velopack runs the app with --veloapp-install/-updated/-uninstall during
those operations; without fast-exit handling the full GUI booted
mid-install (also clobbering config via the first-run setup flow) and
the installer reported hook errors. The post-install launch has no hook
args and still starts the app normally.
vpk runs on the machine's modern .NET runtime (WiX 3.11's .NET Framework
shim fails under the SYSTEM account) and produces a per-user Setup.exe
that needs no runtime on end-user machines.
Files extracted from a downloaded ZIP carry a Zone.Identifier stream and the
.NET Framework CLR refuses to load such assemblies, so pythonnet failed with
'Failed to resolve Python.Runtime.Loader.Initialize'. MSI-installed files have
no MOTW; the winmain strip fixes the ZIP distribution.
pywebview[qt] no longer depends on PyQtWebEngine, so fresh CI builds
produced a macOS app without its Chromium engine that crashed on launch.
Package MediaHive.app as a compressed DMG instead of zipping the raw
onedir folder; release.py accepts .dmg artifacts.
Build the GUI app on macos/windows/linux host runners on v* tag pushes.
All platform jobs converge on one Gitea release; release.py now reuses an
existing release, skips duplicate assets, and supports --no-dist so only
the linux job uploads the wheel/sdist.
- Fix merged playback-state dropping season/episode, which broke series
resume restore entirely.
- Track watch progress per episode (episodes map keyed S..E.., with done
markers on completion) while keeping one continue point per series as
the last watched episode, used for spoiler protection and season
selection. MPC-BE tracker resumes from each episode's own position.
- Assumed-playback fallback for any player and server-only mode: a launch
starts a session and the guessed position (base + elapsed, capped at
runtime) is written when frontend activity resumes; the real MPC-BE
tracker overrides guesses via timestamps. Frontend reports input
activity (throttled) via POST /api/activity.
- Ignore watches under 5 minutes in both trackers.
- Episode tiles show a small quadrant-circle watch indicator; near end
counts as fully watched, no data shows nothing.
- Fix episode audio ownership: single audioOwnerKey shared by mouse hover
and keyboard/gamepad focus, no longer clobbered by playback sync;
idle fade can be re-armed by continued activity.
Replace the 3D ring jukebox with a flat curtain browser: selected season
faces the screen as top of the left stack, side cards rotate about their
outer edge and recede into the screen, each side evenly spaced with pitch
adapting to its card count so cards never leave the stage.
- Mount episode videos only after the season switch settles, then
preload=auto + autoplay (avoids mid-animation media setup jank)
- Start videos immediately (no stagger) once an episode is pointed at;
keep staggered starts while navigating the season selector
- Keyboard/gamepad entry into the season row lands on the selected
season via data-nav-entry-col instead of the x-closest card
- Black episode tiles; hidden (ahead-of-cursor) tiles get a sheen and
vignette veil that cross-fades on reveal/conceal
- Nudge season info panel 8px left for balanced gaps
Render only the selected season's episodes; switch via a perspective-arc
poster carousel with floating season info, deferred video loading with
still-image fade-in, and spoiler fade for episodes ahead of the cursor.
uvicorn's reloader is unusable on Windows for this app: it restarts the
server child via CTRL_C_EVENT, which is never delivered to a plain spawn
child (no console process group of its own), so the child kept running
old code and the reloader blocked in process.join() after the first
reload — the scanner included. And when a child does restart, uvicorn
hands it parent-bound sockets that ProactorEventLoop cannot register
with IOCP (WinError 87 on accept), while the selector loop would break
asyncio subprocesses (ffmpeg/ffprobe showreel generation).
In dev mode on Windows, mediahive now runs a small supervisor instead:
it watches the package with watchfiles and respawns a fresh child
process (which binds its own sockets) on every change, and terminates
the child on Ctrl-C. POSIX keeps uvicorn's native reload.
Shutdown: RootScanner.stop() no longer does a final best-effort state
save — those to_thread writes ran on the asyncio default executor, whose
non-daemon threads blocked interpreter exit, so log lines appeared after
the shell prompt returned. All scanner state is already persisted after
each completed scan and when the showreel queue drains. Shutdown-time
log lines (scan cancelled, showreel worker stopping) move to debug.
The directory worker registered video files with
'node["changed"] = node["changed"] or await _register_candidate(...)',
so once one file in a directory was found changed, the rest were never
registered: they fell out of known_paths, were pruned from the persisted
seen-mtimes at finalize, and were rediscovered as new on every scan.
That rotating ~10-item subset was reprocessed (metadata, TMDb lookups,
logs) every 30 seconds forever.
Also fix the self-sustaining reprocess loop for items that produce no
index entries (no playable file, no parseable episodes, non-media):
they never appear in the index, so the DB-aware mtime gating alone could
never skip them. They are now recorded in a persisted empty_mtimes map
(scan-state.json) and skipped while their mtime is unchanged.
Episode/playable/blu-ray finder caches are now cleared at the start of
each scan: an empty result cached before a download finished (or during
a transient network-mount error) previously stuck for the process
lifetime and reported "no episodes found" for series that have episodes.
Logging: a no-change scan now emits a single line with item count and
duration; per-item "up to date" chatter moved to debug; change scans
report the new/changed count plus per-item reason lines (stored vs
current mtime, index/empty-marker membership), finalize prune/commit
traces, and scan-state load/save confirmations.
Add AGENTS.md noting the project's Python 3.14 baseline (PEP 758
unparenthesized except) so tooling does not flag it as a syntax error.
Skip ensure-visible and synced-row animations on focus restore, and
snapshot/restore panel scrollTop plus synced-row scrollLeft around
detail navigation so the browse page comes back pixel-identical.
dir_state now records only subtrees with >= _MIN_TRACKED_DIRS (8)
directories that yielded no items of interest; hot or tiny trees fall
back to default every-pass scheduling with no bookkeeping. Per-directory
classification already works at any depth, so e.g. torrents/Music is
pruned at its own root while torrents/Movies stays hot; backed-off
subtrees are still re-examined when their (exponentially growing, 1h
cap) timer expires.
- RootScanner takes indexed_paths (store torrent keys); a candidate is
skipped only when its mtime is unchanged AND it is present in the index,
so an emptied/partial index self-heals on the next pass (non-indexable
OTHER candidates still skip on mtime alone)
- discovery: a directory that cannot be listed (transient SMB failure)
now carries its previously known paths into the Sync set instead of
dropping them, and an unreadable media root fails the whole scan —
previously both produced a partial/empty known_paths that made
sync_torrent_paths delete items that still exist on disk
- worker: broad per-directory exception guard (carry known paths, stay
hot, keep other workers running); node completion hardened against
double-decrement; backoff state not updated from cancelled walks
- scanner.stop: await cancelled tasks concurrently under one 2s budget
and persist scan/reel/probe state concurrently (was sequential)
- supervisor.shutdown: stop root contexts concurrently (per-root scanner
waits and network-mount snapshot flushes no longer add up)
- winmain: retry log-file rotation while a previous instance still holds
mediahive.log (Windows denies rename of an open file — the old instance
dying slowly made new instances crash with a PermissionError); after 2s
show a 'waiting for previous instance' window, retry up to 15s, then
fall back to a per-pid log file
- winmain: gui_main() wraps winmain() and renders unhandled exceptions as
a TraceRite HTML page in a pywebview window instead of PyInstaller's
plain-text fatal-error dialog; __main__ GUI mode uses it too
- scanner: replace serial DFS with 16-worker priority work-queue, one
scandir round trip per directory (DirEntry mtimes), per-subtree
exponential backoff (persisted in scan-state.json v2), leaf candidates
gated on max(dir, newest video file) mtime so growing downloads and
lazy NTFS dir mtimes are handled
- spec: collect_data_files('tracerite') so style.css/script.js ship
- guibuild: tolerate non-UTF-8 console when printing status glyphs
- ruff format pass on indexer.py/index_store.py (hook requirement)
The wheel hook was a local addition (76cd022) that rebuilt the frontend
on every wheel build. Only the sdist build should invoke node; wheel
builds (including wheel-from-sdist) package the prebuilt frontend-build.
Restores scripts/fastapi-vue/buildhook.py to its pristine upstream state.
Bugs fixed:
- Partial rescans no longer replace whole index entries: Upsert events now
carry the scanned torrent paths and IndexStore merges partial rebuilds,
so touching one season no longer drops the others from listings.
- Deleted torrents are now detected: discovery reports the full candidate
set via a new Sync event after each completed scan, and the store prunes
file entries/episodes/seasons/items whose torrents vanished.
- Seen mtimes are committed only after a scan completes successfully, so
cancelled/failed scans retry their items.
- Showreel worker no longer rebroadcasts stale whole items; it sends narrow
MovieShowreel/EpisodeReel events that update only reel fields.
- Permanently failing reel generations (e.g. DoVi/libplacebo on GPU-less
machines) and short-video re-queueing are no longer retried every scan:
reel outcomes persist in reel-state.json with exponential backoff.
Optimizations:
- Persist scan state (scan-state.json) and ffmpeg probe results
(probe-cache.json, keyed by mtime+size, failures included) under
.mediahive/, written only when changed. A warm restart over an unchanged
library drops from ~57 s + 467 queued reel tasks to ~0.2 s with an empty
queue (measured on a 163-item root).
- Bounded parallelism (semaphores + gather) for cast-profile downloads,
TMDb title lookups, and season-detail fetches.
- Incremental TMDb-id index bookkeeping instead of rebuilding per upsert;
removed dead trigger_scan.
See docs/scanning-review.md for the full findings/fixes/measurements report.
- Observe the video element itself instead of el.closest('.episode-tile'):
ref callbacks can fire before ancestors are attached, so no tile was
ever observed and the visibility allowlist blocked all playback.
- Use a blocklist for off-screen culling (play until reported off-screen)
so observer lag/failure degrades to the old always-on behavior.
- Default to the first season playing on page view again.
- Keep preload="auto" so tiles show a first-frame preview picture;
playback gating already prevents GPU use from inactive videos.
- Stop videos staggered (same 500ms step as startup) on idle stop,
season switch and scroll culling; same for movie page showreels.
- Add useIdlePreviewPlayback composable: stops preview videos after 30s
without input and restarts them staggered on activity; also stops
while the tab is hidden.
- SeriesFullView: no episode videos play until a season is browsed
(keyboard/gamepad focus or mouse hover); only near-viewport tiles of
the active season play (IntersectionObserver); inactive seasons no
longer preload video data; sync is incremental so playing tiles are
not rewound on scroll.
- MediaDetail: header showreel videos use cancellable stagger timers,
pause when scrolled out of view, and honor the idle stop.
- Images: lazy/async decoding on series page posters; content-visibility:
auto on media cards to skip rendering off-screen row items.