From 83ffe8544aa7845810049083d1376f7c7d01cd2c Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Tue, 5 May 2026 21:26:46 +0300 Subject: Expand README and split documentation into docs/ directory --- docs/admin.md | 50 +++++++++++++++++++++++++++ docs/api.md | 84 ++++++++++++++++++++++++++++++++++++++++++++++ docs/configuration.md | 19 +++++++++++ docs/keyboard-shortcuts.md | 34 +++++++++++++++++++ docs/podcasts.md | 31 +++++++++++++++++ docs/quick-start.md | 56 +++++++++++++++++++++++++++++++ docs/theming.md | 53 +++++++++++++++++++++++++++++ 7 files changed, 327 insertions(+) create mode 100644 docs/admin.md create mode 100644 docs/api.md create mode 100644 docs/configuration.md create mode 100644 docs/keyboard-shortcuts.md create mode 100644 docs/podcasts.md create mode 100644 docs/quick-start.md create mode 100644 docs/theming.md (limited to 'docs') diff --git a/docs/admin.md b/docs/admin.md new file mode 100644 index 0000000..fcc0cdc --- /dev/null +++ b/docs/admin.md @@ -0,0 +1,50 @@ +Admin Guide +=========== + +Admin endpoints are gated by `RequireAdmin` middleware (checks `users.is_admin`). The admin panel is opened via the "Admin" button in the SPA header (shown only when the current user is an admin). + +### Bootstrap + +On first visit (no users exist), you are redirected to `/bootstrap.html` to create the initial admin account. + +### Creating Users + +1. Open the admin panel. +2. Enter username, password, and check "Is admin" if desired. +3. Submit — the frontend calls `POST /api/admin/users`. +4. Admins cannot delete themselves via `DELETE /api/admin/users/:id`. + +### Managing Set Permissions + +- `GET /api/admin/permissions` — list permissions matrix +- `POST /api/admin/permissions` — grant access to a set (`body: { set_id, user_id, role: "owner" | "viewer" }`) +- `DELETE /api/admin/permissions` — revoke access (`body: { set_id, user_id }`) + +Roles: +- `owner` — can upload to the set, soft-delete / restore media, regenerate thumbnails +- `viewer` — can browse and play media in the set + +Admins implicitly see all sets without explicit permission rows. + +### Rescanning the Library + +Click **Rescan** in the admin panel, or call: + +```bash +curl -X POST -b session= http:///api/admin/rescan +``` + +This triggers `FSScanner.Scan()`, which: +1. Scans immediate subdirectories of `MEDIA_ROOT` as **sets** +2. Recursively walks each set for supported media files +3. Probes new files with `ffprobe` +4. Generates thumbnails for video files +5. Inserts new records into the `media` table + +### Managing Trash + +- `GET /api/admin/trash` — list soft-deleted media +- `DELETE /api/media/{id}` — soft-delete a media item +- `POST /api/media/{id}/restore` — restore a soft-deleted item + +Soft-deleted media remains on disk until garbage collection removes it (see `GC_INTERVAL_MINUTES`). \ No newline at end of file diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..0132300 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,84 @@ +API Reference +============= + +### Public + +| Method | Path | Description | +|--------|------|-------------| +| `POST` | `/api/bootstrap` | Create the first admin account | +| `POST` | `/api/login` | Login | +| `GET` | `/healthz` | Liveness probe (no DB) | +| `GET` | `/readyz` | Readiness probe (DB ping) | +| `GET` | `/s/{token}` | View a shared media item | +| `GET` | `/s/{token}/stream` | Stream shared media | +| `GET` | `/s/{token}/thumbnail` | Thumbnail for shared media | +| `GET` | `/s/{token}/download` | Download shared media | + +### Session-required + +| Method | Path | Description | +|--------|------|-------------| +| `POST` | `/api/logout` | Logout | +| `GET` | `/api/sets` | List sets | +| `GET` | `/api/sets/{id}/browse` | Browse a set (folder navigation) | +| `GET` | `/api/sets/{id}/cover` | Get set cover image | +| `POST` | `/api/sets/{id}/cover` | Update set cover image | +| `POST` | `/api/sets/{id}/upload` | Upload file to set | +| `GET` | `/api/media` | List/search media (with filters) | +| `GET` | `/api/media/{id}` | Get media details | +| `GET` | `/api/media/{id}/stream` | Stream media (range support) | +| `GET` | `/api/media/{id}/download` | Download original file | +| `GET` | `/api/media/{id}/thumbnail` | Get thumbnail | +| `POST` | `/api/media/{id}/thumbnail` | Regenerate thumbnail | +| `POST` | `/api/media/{id}/favorite` | Toggle favorite | +| `POST` | `/api/media/{id}/tags` | Add tag | +| `DELETE` | `/api/media/{id}/tags/{tag}` | Remove tag | +| `POST` | `/api/media/{id}/shares` | Create share link | +| `GET` | `/api/media/{id}/shares` | List shares for a media item | +| `GET` | `/api/media/{id}/notes` | Get note | +| `POST` | `/api/media/{id}/notes` | Upsert note | +| `DELETE` | `/api/media/{id}/notes` | Delete note | +| `POST` | `/api/progress` | Save playback progress | +| `DELETE` | `/api/media/{id}` | Soft-delete media | +| `POST` | `/api/media/{id}/restore` | Restore soft-deleted media | +| `GET` | `/api/shares` | List my shares | +| `DELETE` | `/api/shares/{token}` | Revoke share | +| `GET` | `/api/podcasts` | List podcasts | +| `GET` | `/api/podcasts/{id}/episodes` | List episodes | +| `POST` | `/api/podcasts/episodes/{id}/download` | Download episode to server | +| `POST` | `/api/podcasts/episodes/{id}/complete` | Toggle episode listened | + +### Admin-only + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/admin/users` | List users | +| `POST` | `/api/admin/users` | Create user | +| `DELETE` | `/api/admin/users/{id}` | Delete user | +| `GET` | `/api/admin/permissions` | List permissions | +| `POST` | `/api/admin/permissions` | Grant set permission | +| `DELETE` | `/api/admin/permissions` | Revoke set permission | +| `POST` | `/api/admin/rescan` | Trigger library rescan | +| `GET` | `/api/admin/scan-progress` | Get scan progress | +| `GET` | `/api/admin/trash` | List soft-deleted media | +| `POST` | `/api/podcasts` | Subscribe to podcast feed | + +### Query parameters for `GET /api/media` + +| Parameter | Description | +|-----------|-------------| +| `search` | Plain text search on file name | +| `set_id` | Filter by a single set ID | +| `set_ids` | Filter by comma-separated set IDs | +| `type` | Filter by media type (e.g. `video`, `audio`, `image`) | +| `favorites` | `true` or `1` to show favorites only | +| `tags` | Comma-separated tag names | +| `min_duration` | Minimum duration in minutes | +| `max_duration` | Maximum duration in minutes | +| `filesize_min` | Minimum file size in MB | +| `filesize_max` | Maximum file size in MB | +| `sort` | Sort order (e.g. `random`) | +| `limit` | Page size | +| `offset` | Page offset | +| `folder` | Filter to a specific folder path | +| `parent` | Filter to items within a parent folder | \ No newline at end of file diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..60d6ad6 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,19 @@ +Configuration +============= + +All settings are environment variables. Unset variables use defaults. + +| Variable | Default | Validation | Description | +|----------|---------|------------|-------------| +| `PORT` | `8080` | 0–65535 | HTTP listen port (0 = ephemeral, used in tests) | +| `MEDIA_ROOT` | `./media` | — | Root path for media set directories | +| `DB_PATH` | `data.db` | — | SQLite database file path | +| `MAX_UPLOAD_SIZE_MB` | `100` | ≥ 1 | Max upload size per file (MB) | +| `SESSION_TIMEOUT_HOURS` | `24` | ≥ 1 | Cookie / session expiry | +| `GC_INTERVAL_MINUTES` | `30` | ≥ 1 | Garbage collector tick interval | +| `SHARE_DEFAULT_EXPIRY_DAYS` | `7` | ≥ 1 | Default share link lifetime | +| `PODCAST_CHECK_INTERVAL_MINUTES` | `60` | ≥ 1 | Podcast feed refresh interval | +| `LOG_LEVEL` | `info` | `debug` / `info` / `warn` / `error` | Log verbosity | +| `SECURE_COOKIES` | `true` | `true` / `false` | Set `Secure` flag on session cookies; set to `false` for plain-HTTP local deployments | + +**Important:** The K8s `Deployment` overrides `DB_PATH` to `/data/media.db` and `MEDIA_ROOT` to `/media` so the PVC mounts are used. Do not rely on the local defaults in a container. \ No newline at end of file diff --git a/docs/keyboard-shortcuts.md b/docs/keyboard-shortcuts.md new file mode 100644 index 0000000..cbf0cca --- /dev/null +++ b/docs/keyboard-shortcuts.md @@ -0,0 +1,34 @@ +Keyboard Shortcuts +================== + +Global shortcuts are registered in `web/js/keyboard.js`. They are **disabled** while the user is focused on an `INPUT`, `TEXTAREA`, or `contentEditable` element (except `Escape` to blur). + +| Key | Action | +|-----|--------| +| `↑` / `↓` | Navigate media list (up / down) | +| `k` / `j` | Navigate media list (up / down) | +| `←` / `→` | Seek or switch pages | +| `h` / `l` | Seek or switch pages | +| `Enter` | Open selected media (navigate to detail) | +| `Space` / `p` | Play / pause / switch to selected item | +| `N` / `P` | Next / previous track | +| `f` | Toggle fullscreen on the player wrapper | +| `Esc` | Exit fullscreen, or deselect current item | +| `r` | Toggle shuffle on the current filtered result set | +| `s` | Toggle sets sidebar | +| `S` | Generate a share link for the selected media | +| `/` | Focus the quick search bar (debounced) | +| `n` | Open notes modal for the selected media | +| `i` | Show / hide media info | +| `C` | Minimize player | +| `d` | Detach / reattach player | +| `D` | Download selected media | +| `+` / `−` | Zoom in / out (image viewer) | +| `Shift+S` | Toggle slideshow (images) | +| `L` | My Shares | +| `Backspace` | Go up one folder | +| `?` | Show / hide help | + +Search syntax: plain text searches file name. Modifiers: `min:30`, `max:55`, `tag:a,b`, `like:1`, `type:video`, `sort:random`, `minsize:10`, `maxsize:500`. + +My Shares modal: `↑` / `↓` or `k` / `j` navigate, `Enter` copies, `Delete` revokes, `Esc` closes. \ No newline at end of file diff --git a/docs/podcasts.md b/docs/podcasts.md new file mode 100644 index 0000000..6d03601 --- /dev/null +++ b/docs/podcasts.md @@ -0,0 +1,31 @@ +Podcast Support +=============== + +Podcasts are **special sets** (`sets.is_podcast = 1`). They reuse set permissions, browsing, and cover images, while adding feed management and episode tracking. + +### Subscribing + +Admin opens the **Podcasts** button in the admin panel (or calls `POST /api/podcasts`): +- Submit an RSS/Atom feed URL and optional folder name. +- Server creates a set folder, parses the feed, downloads the cover image, and inserts episodes into `podcast_episodes`. + +### Episode Management + +Episodes are stored in `podcast_episodes` and rendered in the browse grid for podcast sets: +- **Undownloaded** episodes show a **Download to server** button (calls `POST /api/podcasts/episodes/{id}/download`). +- **Downloaded** episodes become regular `media` rows and appear as normal media cards. +- Users can mark episodes as listened/unlistened via the checkmark button. + +### Background Feed Checker + +A background goroutine (`CheckFeeds`) refreshes feeds every hour (configurable via `PODCAST_CHECK_INTERVAL_MINUTES`). It uses conditional GET (`If-None-Match`, `If-Modified-Since`) to avoid re-downloading unchanged feeds. + +### API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/podcasts` | List podcast sets | +| `POST` | `/api/podcasts` | Subscribe to a new feed (admin) | +| `GET` | `/api/podcasts/{id}/episodes` | List episodes with status | +| `POST` | `/api/podcasts/episodes/{id}/download` | Server-side download | +| `POST` | `/api/podcasts/episodes/{id}/complete` | Toggle completion | \ No newline at end of file diff --git a/docs/quick-start.md b/docs/quick-start.md new file mode 100644 index 0000000..d38304d --- /dev/null +++ b/docs/quick-start.md @@ -0,0 +1,56 @@ +Quick Start +=========== + +### Running from source + +```bash +go build -o player ./cmd/player +./player +``` + +Open `http://localhost:8080` — on first visit you'll be redirected to the bootstrap page to create an admin account. + +### Running with Docker + +```bash +docker build -t player:latest . +docker run -p 8080:8080 \ + -v player-data:/data \ + -v /path/to/media:/media \ + -e MEDIA_ROOT=/media \ + -e DB_PATH=/data/media.db \ + player:latest +``` + +### Deploying to Kubernetes + +```bash +kubectl apply -f k8s/ +``` + +This creates a Deployment (non-root, probes included), ClusterIP Service, PVCs for `/data` and `/media`, and an optional Secret. + +The `Deployment` overrides two settings for K8s: +- `DB_PATH=/data/media.db` +- `MEDIA_ROOT=/media` + +Probes: +- **Liveness:** `GET /healthz` (no DB dependency) +- **Readiness:** `GET /readyz` (DB ping) + +Security: +- `runAsNonRoot: true` +- `runAsUser: 65534` / `runAsGroup: 65534` +- `allowPrivilegeEscalation: false` +- `readOnlyRootFilesystem: true` + +### Mage targets + +| Target | Description | +|--------|-------------| +| `mage build` | Compile the binary | +| `mage test` | Run `go test ./...` | +| `mage install` | Build and copy binary to `$GOPATH/bin` | +| `mage clean` | Remove build artifacts | +| `mage docker-build` | Build Docker image as `player:latest` | +| `mage docker-push` | Push `player:latest` to registry | \ No newline at end of file diff --git a/docs/theming.md b/docs/theming.md new file mode 100644 index 0000000..245a801 --- /dev/null +++ b/docs/theming.md @@ -0,0 +1,53 @@ +Theming +======= + +All colors live in `web/css/theme.css` as CSS Custom Properties on `:root`. + +### Current Implementation + +`themes.js` swaps the active theme by setting `document.documentElement.setAttribute('data-theme', ...)` and saves the preference to `localStorage`. Override blocks in `theme.css` handle the light variant: + +```css +/* Default (dark) — defined on :root */ +:root { + --bg-body: #0f1117; + --text-primary: #e6e8ef; + --accent: #5e9eff; + ... +} + +/* Light theme overrides */ +[data-theme="light"] { + --bg-body: #f4f5f8; + --text-primary: #12131a; + --accent: #2b6cb0; + ... +} +``` + +### Adding a New Theme + +Option A — inline override (recommended for small additions): + +1. Open `web/css/theme.css`. +2. Append a new attribute selector after the light block, e.g.: + +```css +[data-theme="solarized"] { + --bg-body: #002b36; + --text-primary: #839496; + --accent: #268bd2; + ... +} +``` + +3. Wire the toggle in `web/js/themes.js` (or expose a selector UI in `index.html`) to call `apply('solarized')`. + +Option B — separate file (if you prefer a stylesheet swap): + +1. Create `web/css/themes/.css` containing `:root { ... }` overrides. +2. Dynamically create or swap a `` in `themes.js` instead of using `data-theme`. + +**Rules:** +- No color literals in component styles — everything must go through `var(--*)`. +- Do not add inline styles in HTML or JS. \ No newline at end of file -- cgit v1.2.3