9.9 KiB
Hivescan → FastAPI Server Conversion
Status: Backend complete. Frontend integration and mediahive bridge pending.
Architecture
Two separate FastAPI servers sharing the same codebase, launched independently:
- mediahive (port 8420) — frontend, media serving, play/open-folder actions. Not yet connected to scanner.
- hivescan server (port 8421) — scanning, TMDb lookups, cover downloads, showreel generation. Owns the in-memory index. Exposes WS for index state + updates.
Each has its own entry point in pyproject.toml:
hivescan = "hivescan.__main__:main"
mediahive = "mediahive.__main__:main"
The hivescan CLI accepts scan paths as positional args, with --host / --port / -o options. Configuration is passed to the server via HIVESCAN_PATHS and HIVESCAN_OUTPUT environment variables.
In-Memory Index (hivescan/index_store.py)
IndexStoreclass holds two plain dicts (movies: dict[str, dict],series: dict[str, dict]), keyed by itemid. Single source of truth — all mutations are synchronous in the asyncio event loop, no locks needed.index.jsonon disk is a recovery snapshot only. Written via a debounced background task (SNAPSHOT_DEBOUNCE = 5.0seconds). On startup,load_snapshot()populates dicts from disk to avoid full rescan; the scan then runs on top to pick up changes.- Upsert by item
id. Items from previous runs are preserved (offline drives). flush_snapshot()forces an immediate write (called on shutdown).- Snapshot format: version 5, includes
stats, sortedmoviesandserieslists,media_root,generated_attimestamp.
WebSocket Protocol
The hivescan server exposes GET /ws. On connect:
- Server calls
IndexStore.connect(ws)— accepts the socket, adds it to_clients: set[WebSocket], sends the full current state:{"type": "init", "data": {"movies": [...], "series": [...]}} - Live pushes on every mutation:
{"type": "upsert", "kind": "movie", "item": { ... }} {"type": "upsert", "kind": "series", "item": { ... }} {"type": "remove", "kind": "movie", "id": "abc123"} {"type": "task", "data": {"id": "scan-abc12345", "status": "running", "progress": 0.42, "detail": "The Matrix (1999)"}} - Task messages have
statusvalues:running,completed,cancelled,error. - Dead connections are cleaned up via
_safe_send()— failed sends cause the socket to be discarded.
HTTP Endpoints
| Method | Path | Description |
|---|---|---|
WS |
/ws |
Live index updates & task progress |
POST |
/api/scan |
Trigger a new scan (optional paths body override). Returns {"status": "already_running"} if a scan is in progress. |
GET |
/api/status |
{scanning, movies, series, showreel_queue} |
GET |
/api/index |
Full index as JSON (HTTP fallback for non-WS clients) |
CORS is enabled for all origins.
Scanning (asyncio, no threads)
hivescan/indexer.py:_process_moviesand_process_seriesare async generators._process_moviesyieldsTuple[dict, Optional[Tuple[str, Path, str]]]— the movie dict and an optional showreel task(video_path, media_folder, title)._process_seriesyieldsTuple[dict, List[Tuple[str, Path, int, int, str]]]— the series dict and a list of episode reel tasks(video_path, media_folder, season_num, episode_num, series_title).
- TMDb HTTP calls use
httpx.AsyncClient(lazy-initialised singleton). Disk cache stays synchronous (fast local I/O). - Cover/backdrop downloads use a separate
httpx.AsyncClientinimages.py. - File scanning (
Path.rglob,stat,Path.iterdir): runs synchronously in the event loop (fast enough). Glob expansion for scan paths happens at startup in the lifespan handler. - Each yielded item is upserted into the in-memory dict (sync) and broadcast to WS clients (non-blocking
asyncio.create_taskper client send). print()calls throughout replaced withlogging.getLogger().
Showreel Generation
- Background
_showreel_workertask started at server startup, runs for the lifetime of the server. - Uses an
asyncio.Queue— scan tasks push("movie", task_data, item_id)or("episode", task_data, item_id)tuples. - Processes one task at a time to avoid saturating CPU/GPU.
- Invokes ffmpeg/ffprobe via
asyncio.create_subprocess_execwithasyncio.wait_fortimeouts (replaced allsubprocess.runcalls). generate_showreel_imagesaccepts anon_progresscallback (replacedtqdmprogress bar parameter).generate_episode_reelno longer takes a progress bar parameter.- On completion: updates the relevant item in the in-memory dict (
showreel_imagesfor movies,reel_imagefor episodes) → re-upserts → WS broadcast. - Skips tasks where showreels/reels already exist on disk.
File Changes
Modified
| File | Changes |
|---|---|
indexer.py |
_process_movies / _process_series → async generators yielding (item_dict, showreel_tasks). _build_seasons_data → async. Removed generate_media_index() and _run_showreel_generation() (server orchestrates now). Removed json, datetime, tqdm imports. Added logging. print() → logger.info(). |
tmdb_client.py |
httpx.Client → httpx.AsyncClient (lazy singleton via _get_http_client()). All public functions async: tmdb_api_request, fetch_movie_details, fetch_series_details, fetch_season_details, fetch_movie_info, fetch_series_info, _search_movie_with_fallbacks, _search_series_with_fallbacks. time.sleep(1) → await asyncio.sleep(1). Disk cache functions unchanged. |
images.py |
urllib.request → httpx.AsyncClient (lazy singleton via _get_image_client()). All functions async: _download_image, download_cover_image, download_backdrop_image, download_season_poster. |
showreel.py |
subprocess.run → asyncio.create_subprocess_exec + asyncio.wait_for. All ffmpeg/ffprobe functions async: get_av1_encoder, detect_dovi_profile, is_hdr_video, detect_crop, get_video_duration, generate_showreel_images, generate_episode_reel. tqdm removed. print() → logger.debug/info/warning/error. Sync helper functions unchanged (get_expected_showreel_paths, get_expected_episode_reel_path, movie_showreels_exist, episode_reel_exists, get_bluray_uri, get_encoder_options, get_dovi_to_hdr10_filter). |
__main__.py |
Was CLI argparse with --no-showreels/--no-covers and direct scan execution. Now a server launcher: positional paths args, --host (default 0.0.0.0), --port (default 8421), -o/--output-dir. Sets HIVESCAN_PATHS/HIVESCAN_OUTPUT env vars and calls hivescan.server.run(). |
__init__.py |
Replaced generate_media_index export with IndexStore. Updated module docstring. |
pyproject.toml |
Removed tqdm>=4.67.3 from dependencies. Entry points unchanged. |
New
| File | Purpose |
|---|---|
hivescan/index_store.py |
IndexStore class (~200 lines): in-memory dict storage, upsert_movie/series, remove_movie/series, load_snapshot, flush_snapshot, debounced _write_snapshot, WS client management (connect, disconnect), _broadcast with _safe_send, broadcast_task, get_full_index. |
hivescan/server.py |
FastAPI app (~350 lines): lifespan handler (env config, glob expansion, store init, auto-scan), WS endpoint, HTTP endpoints (/api/scan, /api/status, /api/index), _run_scan orchestrator (iterates async generators, upserts, queues showreels, broadcasts progress), _showreel_worker (queue consumer), run() standalone entry. |
Unchanged
scanning.py, parsing.py, models.py, utils.py — all remain synchronous.
Plan: mediahive Integration
Data flow
hivescan (8421) ──WS──▶ mediahive (8420) ──WS──▶ browser
keeps its own
in-memory dict
mediahive keeps its own in-memory copy of the index (movies: dict, series: dict), populated from one of two sources:
- hivescan WS (preferred) — on startup, mediahive connects to
ws://localhost:8421/ws. Theinitmessage seeds the dicts, thenupsert/removemessages keep them in sync. Task messages are forwarded to browser clients as-is. - Disk fallback — if hivescan is not running (connection refused / WS drops and doesn't reconnect), load
index.jsonfrom disk once as a static snapshot. The frontend still works, just without live updates.
Auto-reconnect: if the WS drops, mediahive retries on a backoff. When it reconnects, it gets a fresh init and replaces its dicts entirely.
mediahive/server.py changes
- New WS client task started in
lifespan. Connects to hivescan, handles messages, updates in-memory dicts. On failure, loadsindex.jsonfallback. - New WS endpoint
GET /wsfor browser clients. On connect: send{"type": "init", "data": {movies, series}}from the local dicts. Forwardupsert,remove,taskmessages as they arrive from hivescan. - Existing
GET /api/index— return from in-memory dicts instead of readingindex.jsoneach time. Keeps working for non-WS clients. Remove thelinux_to_windows_pathconversion (paths are already relative). - Keep
play,open-folder,media/{path}endpoints unchanged.
Frontend changes
| File | Change |
|---|---|
composables/useWebSocket.ts (new) |
Connect to mediahive's /ws (same origin, no CORS). On init: replace reactive index. On upsert: merge item by id. On remove: delete by id. On task: update reactive task state. Auto-reconnect with backoff. |
types.ts |
Add WsMessage, TaskInfo types. |
App.vue |
Replace loadMediaIndex() fetch with the WS composable. Items appear incrementally as scan runs. Fall back to GET /api/index if WS never connects. |
Header.vue |
Show task indicator (spinner + progress text) when scan/showreel tasks are active. |
api.ts |
Remove loadMediaIndex(). Keep playMedia(), openFolder(), getCoverUrl(). |