4.4 KiB
4.4 KiB
Multi-Root Implementation Notes
Overview
MediaHive now supports multiple independent media roots. Each root is a filesystem directory with its own index, scanner, and WebSocket stream. The frontend merges per-root state into a single reactive view.
Architecture
Root Identity
- Root ID: friendly root name derived from configured path basename.
- Name/ID collision handling: suffixes
2,3, … are appended to keep each root ID unique. - Path normalization: lower-case Windows drive letter, strip trailing slashes, forward slashes only (
as_posix()).
Per-Root Runtime (RootContext)
Each active root gets an isolated RootContext managed by the Supervisor:
root_id,root_path— stable identifiersIndexStore— owns snapshot at<root>/.mediahive/index.jsonRootScanner— per-root scanning instance (replaced legacy global scanner)asyncio.Queue+ consumer task — bridges scanner events to WebSocketstatus:idle|loading|ready|scanning|error
Supervisor
- Holds
dict[str, RootContext]keyed byroot_id. replace_roots(new_roots)atomically swaps the active set:- Validate & canonicalize paths.
- Derive unique friendly
root_idfor each. - Prepare new
RootContexts (load snapshots). - Swap dict atomically.
- Stop removed contexts in background with bounded timeout.
- Exposes merged read helpers (
merged_index,all_statuses).
Item IDs
root_id is stored separately on each item.
Movie.iduses a slug built from the movie title and year, for examplespider-man-no-way-home-2021.Series.iduses a slug built from the series title, for examplelost.- Legacy snapshot migrations are handled by
scripts/indexmigr.py, not during app startup.
API
| Endpoint | Description |
|---|---|
GET /api/roots |
List all roots (name, path, root_id, status) |
PUT /api/roots |
Atomically replace full root map {name: path} |
WS /api/ws/{root_id} |
Per-root WebSocket (init/upsert/remove/task + status/task events) |
GET /api/media/{root_id}/{path:path} |
Serve media file scoped to root |
GET /api/assets/{root_id}/{asset_type}/{asset_path:path} |
Serve .mediahive/{asset_type} assets (movies, series, people) |
POST /api/play/{root_id} |
Play file within root |
POST /api/open-folder/{root_id} |
Open folder within root |
GET /api/meta/{root_id}/{meta_key} |
Per-root metadata (for example playback-state) |
POST /api/ui/pick-folder |
Native OS folder picker (returns path) |
Removed legacy endpoints:
/api/change-folder,/api/index,/api/scan,/api/status,/api/playback/resume-positions. No backwards compatibility is maintained.
macOS Startup Safety
The server must not touch the filesystem during startup, because macOS may show permission dialogs that block the event loop and prevent the HTTP server from accepting requests.
lifespan()creates a background task (_activate_all_roots()) and immediately yields.- All filesystem validation (
exists(),is_dir(),resolve()) runs in a thread pool viaasyncio.to_thread(). - CLI entry points (
__main__.py,winmain.py,hivescan/__main__.py) pass raw paths via theMEDIAHIVE_ROOTSenvironment variable; they do not validate paths before starting the server.
POSIX Path Enforcement
All stored and transmitted paths use forward slashes exclusively:
_normalize_path()always returns POSIX paths.- Config stores
p.as_posix(). - URLs use
/separators. Path(root_path) / relative_pathworks correctly on Windows becausePathaccepts POSIX separators.
Config Migration
- Old
media_folderstring is auto-migrated toroots: {basename: path}on load. rootsis persisted back to TOML config.
Scanner
- Legacy global module-level scanner API was removed from
hivescan/scanner.py. RootScanneris the only scanning interface.- Each
RootScannerowns its ownshowreel_queue,scan_task,rescan_worker_task, and_seen_mtimes.
Frontend
useMediaWebSocket.tsmanages one WebSocket per active root.App.vuemerges per-rootmovieMap/seriesMapinto a singlemediaIndex.Header.vueprovides add/remove root UI viaPUT /api/roots.- Playback URLs are root-qualified (
/api/media/{root_id}/...). - Metadata cache assets use typed root paths (
/api/assets/{root_id}/{asset_type}/...) rather than exposing.mediahivein URLs.