refactor: remove legacy single-root APIs, defer filesystem I/O, enforce POSIX paths
- 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
This commit is contained in:
+15
-13
@@ -1,29 +1,31 @@
|
||||
# API
|
||||
|
||||
MediaHive exposes a small local API used by the desktop app and frontend.
|
||||
All media paths are scoped to a **root**, identified by a stable `root_id`.
|
||||
|
||||
## 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/config` | Returns the current root configuration. |
|
||||
| `GET` | `/api/roots` | List all active roots with status. |
|
||||
| `PUT` | `/api/roots` | Atomically replace the full root set. |
|
||||
| `GET` | `/api/roots/{root_id}/index` | Returns the media index for one root. |
|
||||
| `GET` | `/api/roots/{root_id}/status` | Returns scanner and library status for one root. |
|
||||
| `POST` | `/api/roots/{root_id}/scan` | Triggers a new scan for one root. |
|
||||
| `POST` | `/api/roots/{root_id}/play` | Opens a media file with the system player. |
|
||||
| `POST` | `/api/roots/{root_id}/open-folder` | Opens a folder in the system file explorer. |
|
||||
| `GET` | `/api/roots/{root_id}/playback/resume-positions` | Returns saved resume positions for one root. |
|
||||
| `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. |
|
||||
| `GET` | `/api/media/{root_id}/{file_path:path}` | Serves files from the specified root. |
|
||||
| `WS` | `/api/roots/{root_id}/ws` | Streams live index updates and task progress for one root. |
|
||||
|
||||
## 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.
|
||||
- `PUT /api/roots` accepts `{ "roots": { "name": "/absolute/path", ... } }`, validates paths, and atomically swaps the active set.
|
||||
- `POST /api/roots/{root_id}/play` and `POST /api/roots/{root_id}/open-folder` expect JSON request bodies with `file_path` / `folder_path` relative to the root.
|
||||
- `GET /api/media/{root_id}/{file_path:path}` is constrained to the specified root; path traversal outside the root is rejected.
|
||||
- `GET /api/player/status` returns `{ "remote": true|false }`.
|
||||
- `GET /api/mpcbe/status` returns `false` on non-Windows platforms.
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
# 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**: first 12 hex chars of SHA-256 of the *normalized* absolute path.
|
||||
- **Normalization**: resolve symlinks, lower-case Windows drive letter, strip trailing slashes, forward slashes only (`as_posix()`).
|
||||
- **Name**: derived from path basename; collisions resolved with `2`, `3`, … suffix.
|
||||
|
||||
### Per-Root Runtime (`RootContext`)
|
||||
|
||||
Each active root gets an isolated `RootContext` managed by the `Supervisor`:
|
||||
|
||||
- `root_id`, `root_path` — stable identifiers
|
||||
- `IndexStore` — owns snapshot at `<root>/.mediahive/index.json`
|
||||
- `RootScanner` — per-root scanning instance (replaced legacy global scanner)
|
||||
- `asyncio.Queue` + consumer task — bridges scanner events to WebSocket
|
||||
- `status`: `idle` | `loading` | `ready` | `scanning` | `error`
|
||||
|
||||
### Supervisor
|
||||
|
||||
- Holds `dict[str, RootContext]` keyed by `root_id`.
|
||||
- `replace_roots(new_roots)` atomically swaps the active set:
|
||||
1. Validate & canonicalize paths.
|
||||
2. Compute `root_id` for each.
|
||||
3. Prepare new `RootContext`s (load snapshots).
|
||||
4. Swap dict atomically.
|
||||
5. Stop removed contexts in background with bounded timeout.
|
||||
- Exposes merged read helpers (`merged_index`, `all_statuses`).
|
||||
|
||||
### Item IDs
|
||||
|
||||
Every `Movie.id` and `Series.id` is namespaced with its `root_id`:
|
||||
- Format: `{root_id}:{content_hash}`
|
||||
- Old snapshots are auto-migrated on load: IDs lacking the prefix get it prepended.
|
||||
|
||||
## API
|
||||
|
||||
| Endpoint | Description |
|
||||
|----------|-------------|
|
||||
| `GET /api/roots` | List all roots (name, path, root_id, status) |
|
||||
| `PUT /api/roots` | Atomically replace full root map `{name: path}` |
|
||||
| `GET /api/roots/{root_id}/index` | Full index for one root |
|
||||
| `GET /api/roots/{root_id}/status` | Per-root scanning/loading/error state |
|
||||
| `POST /api/roots/{root_id}/scan` | Trigger scan for one root |
|
||||
| `WS /api/roots/{root_id}/ws` | Per-root WebSocket (init/upsert/remove/task) |
|
||||
| `GET /api/media/{root_id}/{path:path}` | Serve media file scoped to root |
|
||||
| `POST /api/roots/{root_id}/play` | Play file within root |
|
||||
| `POST /api/roots/{root_id}/open-folder` | Open folder within root |
|
||||
| `GET /api/roots/{root_id}/playback/resume-positions` | Per-root resume positions |
|
||||
| `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 via `asyncio.to_thread()`.
|
||||
- CLI entry points (`__main__.py`, `winmain.py`, `hivescan/__main__.py`) pass raw paths via the `MEDIAHIVE_ROOTS` environment 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_path` works correctly on Windows because `Path` accepts POSIX separators.
|
||||
|
||||
## Config Migration
|
||||
|
||||
- Old `media_folder` string is auto-migrated to `roots: {basename: path}` on load.
|
||||
- `roots` is persisted back to TOML config.
|
||||
|
||||
## Scanner
|
||||
|
||||
- Legacy global module-level scanner API was removed from `hivescan/scanner.py`.
|
||||
- `RootScanner` is the only scanning interface.
|
||||
- Each `RootScanner` owns its own `showreel_queue`, `scan_task`, `rescan_worker_task`, and `_seen_mtimes`.
|
||||
|
||||
## Frontend
|
||||
|
||||
- `useMediaWebSocket.ts` manages one WebSocket per active root.
|
||||
- `App.vue` merges per-root `movieMap`/`seriesMap` into a single `mediaIndex`.
|
||||
- `Header.vue` provides add/remove root UI via `PUT /api/roots`.
|
||||
- All media URLs are root-qualified (`/api/media/{root_id}/...`).
|
||||
Reference in New Issue
Block a user