diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-04 20:30:29 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-04 20:30:29 +0300 |
| commit | ab27849d725879c57e82c499faf56b34aa79be57 (patch) | |
| tree | 36ffc21a9513ef04b6a562fd4b7c766d1fa2f385 | |
| parent | 7f21e7296a28f7ae70827c5d6a75f4257444ea17 (diff) | |
removing the ploan plan
| -rw-r--r-- | PLAN.md | 456 |
1 files changed, 0 insertions, 456 deletions
diff --git a/PLAN.md b/PLAN.md deleted file mode 100644 index 130e134..0000000 --- a/PLAN.md +++ /dev/null @@ -1,456 +0,0 @@ -# KISS Go Media Player — Implementation Plan - -## Overview -A self-hosted, Kubernetes-deployable web media player written in Go (stdlib `net/http`) with a SQLite backend, vanilla JavaScript PWA frontend, and progressive offline capability. Designed for **KISS** (Keep It Stupidly Simple): minimal dependencies, zero frontend frameworks, fully interface-driven for testability. - -**Target test coverage:** >80% using dependency injection, hand-written mocks, and SQLite `:memory:` instances. - ---- - -## Architecture - -- **Backend:** Go 1.23, `net/http`, stdlib + `modernc.org/sqlite` -- **Frontend:** Vanilla JS, CSS Custom Properties, HTML5 `<video>`/`<audio>` custom overlay controls -- **Build:** Mage (`Magefile.go`) with `Build`, `Test`, `Install` targets -- **Deploy:** Multi-stage `Dockerfile`; K8s `Deployment` + `Service` + two PVCs (`/data`, `/media`) -- **Storage:** SQLite on `/data`; media library on `/media`; `ffmpeg`/`ffprobe` installed in runtime image - ---- - -## Project Structure - -``` -./ -├── cmd/player/ -│ └── main.go # Entrypoint: flags, config, wire dependencies, start server -├── internal/ -│ ├── version.go # const Version; printed via -version flag -│ ├── config.go # Env-based config with validation -│ ├── clock/ -│ │ ├── clock.go # Clock interface (Now() time.Time) -│ │ └── real.go # RealClock implementation -│ ├── model/ -│ │ └── media.go # Domain structs (zero external deps) -│ ├── repository/ -│ │ ├── repository.go # All repository interfaces -│ │ ├── sqlite.go # Concrete SQLite implementations -│ │ ├── sqlite_test.go # :memory: table-driven tests -│ │ └── mock.go # Hand-written fakes for service/handler tests -│ ├── scanner/ -│ │ ├── scanner.go # Recursive set + media scan -│ │ ├── scanner_test.go -│ │ └── fs.go # FS interface + OS implementation -│ ├── probe/ -│ │ ├── probe.go # ffprobe wrapper interface -│ │ ├── probe_test.go # Golden JSON fixtures -│ │ └── testdata/ -│ ├── thumb/ -│ │ ├── thumb.go # Thumbnail generator interface -│ │ └── thumb_test.go -│ ├── auth/ -│ │ ├── auth.go # Hasher interface, session manager -│ │ └── auth_test.go -│ ├── service/ -│ │ ├── media.go # MediaService (business layer) -│ │ ├── admin.go # AdminService (user/set CRUD) -│ │ ├── progress.go # Playback counter + resume logic -│ │ ├── progress_test.go # Heavy tests for 60s rule -│ │ ├── gc.go # Garbage collector for soft-deleted items -│ │ └── gc_test.go # Mock clock + repo tests -│ ├── api/ -│ │ ├── server.go # Route table, middleware wiring -│ │ ├── handlers.go # Thin HTTP adapters -│ │ ├── handlers_test.go # httptest + mocked services -│ │ └── middleware.go # Session auth, admin gate, bootstrap bypass -│ └── setassign/ -│ ├── assign.go # Permission helper logic -│ └── assign_test.go -├── web/ -│ ├── index.html # SPA shell (auth-gated) -│ ├── login.html # Login form -│ ├── bootstrap.html # One-time admin bootstrap form -│ ├── css/ -│ │ ├── base.css # Reset, layout utilities -│ │ ├── theme.css # Default dark palette (ALL var(--*) definitions) -│ │ ├── components.css # Buttons, cards, forms, modals -│ │ ├── player.css # Custom overlay, fullscreen progress-bar-visible -│ │ ├── layout.css # Responsive grid/flex -│ │ ├── login.css # Login page layout - │ │ └── themes/ # (empty; themes toggled via data-theme in theme.css) - -│ └── js/ -│ ├── app.js # Router, auth bootstrap, orchestration -│ ├── api.js # Fetch wrapper (credentials: include) -│ ├── keyboard.js # Global keydown listener (hjkl/arrows/p/f/Esc/r/s/3/Enter/Space)// -│ ├── selection.js # `.selected` index management (mouse + keyboard sync) -│ ├── player.js # Play/resume/switch/fullscreen logic -│ ├── favorites.js # Heart toggle -│ ├── progress.js # Periodic progress POST every 3s while playing -│ ├── shuffle.js # Toggle random ordering of current filtered view -│ ├── themes.js # Swap `<link>` href for dark/light mode toggle -│ ├── search.js # `/` key quick search bar (debounced) -│ ├── notes.js # Per-media notes modal (CRUD) -│ ├── admin.js # User CRUD, permission matrix, rescan trigger -│ └── pwa.js # Service Worker registration -├── Dockerfile # Multi-stage (builder + alpine w/ ffmpeg) -├── k8s/ -│ ├── deployment.yaml -│ ├── service.yaml -│ ├── pvc-db.yaml # /data -│ └── pvc-media.yaml # /media -├── Magefile.go -├── go.mod -├── go.sum # Generated -└── PLAN.md # This file -``` - ---- - -## Database Schema (SQLite) - -```sql -PRAGMA foreign_keys = ON; - -CREATE TABLE users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - is_admin INTEGER NOT NULL DEFAULT 0, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP -); - -CREATE TABLE sets ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - root_path TEXT UNIQUE NOT NULL, - cover_thumbnail_path TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP -); - -CREATE TABLE set_permissions ( - set_id INTEGER NOT NULL REFERENCES sets(id) ON DELETE CASCADE, - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - role TEXT CHECK(role IN ('owner','viewer')) NOT NULL DEFAULT 'viewer', - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (set_id, user_id) -); - -CREATE TABLE media ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - set_id INTEGER NOT NULL REFERENCES sets(id) ON DELETE CASCADE, - rel_path TEXT NOT NULL, - file_name TEXT NOT NULL, - abs_path TEXT NOT NULL, - type TEXT CHECK(type IN ('video','audio')) NOT NULL, - duration REAL, - codec TEXT, - resolution TEXT, - bitrate INTEGER, - file_size_bytes INTEGER, - thumbnail_path TEXT, - play_count INTEGER NOT NULL DEFAULT 0, - deleted_at DATETIME, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - UNIQUE(set_id, rel_path) -); - -CREATE TABLE tags ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT UNIQUE NOT NULL -); - -CREATE TABLE media_tags ( - media_id INTEGER NOT NULL REFERENCES media(id) ON DELETE CASCADE, - tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE, - PRIMARY KEY (media_id, tag_id) -); - -CREATE TABLE favorites ( - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - media_id INTEGER NOT NULL REFERENCES media(id) ON DELETE CASCADE, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (user_id, media_id) -); - -CREATE TABLE playback_progress ( - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - media_id INTEGER NOT NULL REFERENCES media(id) ON DELETE CASCADE, - position_seconds REAL NOT NULL, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (user_id, media_id) -); - -CREATE TABLE playback_accumulator ( - session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE, - media_id INTEGER NOT NULL REFERENCES media(id) ON DELETE CASCADE, - last_position REAL NOT NULL DEFAULT 0, - accumulated_seconds REAL NOT NULL DEFAULT 0, - counted INTEGER NOT NULL DEFAULT 0, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (session_id, media_id) -); - -CREATE TABLE sessions ( - id TEXT PRIMARY KEY, - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - expires_at DATETIME NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP -); - -CREATE TABLE shares ( - token TEXT PRIMARY KEY, - media_id INTEGER NOT NULL REFERENCES media(id) ON DELETE CASCADE, - created_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - expires_at DATETIME NOT NULL, - max_uses INTEGER, - used_count INTEGER NOT NULL DEFAULT 0 -); - -CREATE TABLE media_notes ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - media_id INTEGER NOT NULL REFERENCES media(id) ON DELETE CASCADE, - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - content TEXT NOT NULL DEFAULT '', - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - UNIQUE(media_id, user_id) -); - --- Indexes -CREATE INDEX idx_media_set_id ON media(set_id); -CREATE INDEX idx_media_rel_path ON media(set_id, rel_path); -CREATE INDEX idx_media_deleted_at ON media(deleted_at); -CREATE INDEX idx_media_type ON media(type); -CREATE INDEX idx_media_filename ON media(file_name); -CREATE INDEX idx_permissions_user ON set_permissions(user_id); -CREATE INDEX idx_permissions_set ON set_permissions(set_id); -CREATE INDEX idx_shares_expires ON shares(expires_at); -``` - ---- - -## API Routes - -| Method | Route | Auth | Description | -|--------|-------|------|-------------| -| `GET` | `/bootstrap.html` | public | One-time admin creation if `users` table empty | -| `POST` | `/api/bootstrap` | public | Submit first admin user (blocked if users exist) | -| `GET` | `/login.html` | public | Login form | -| `POST` | `/api/login` | public | Create session cookie | -| `POST` | `/api/logout` | session | Invalidate cookie + DB row | -| `GET` | `/api/sets` | session | List visible sets (admin = all) | -| `POST` | `/api/sets/:id/cover` | session (owner/admin) | Regenerate set cover thumbnail | -| `GET` | `/api/media` | session | Query: `set_id`, `type`, `search`, `tags`, `favorites`, `min_duration`, `max_duration`, `sort`, `limit`, `offset` | -| `GET` | `/api/media/:id` | session | Detail + metadata + favorite status + user's note | -| `GET` | `/api/media/:id/stream` | session | Serve original file with Range support | -| `GET` | `/api/media/:id/download` | session | `Content-Disposition: attachment` | -| `GET` | `/api/media/:id/thumbnail` | session | Serve thumbnail file | -| `POST` | `/api/media/:id/thumbnail` | session | Regenerate thumbnail at random offset | -| `POST` | `/api/media/:id/favorite` | session | Toggle favorite | -| `POST` | `/api/media/:id/tags` | session | Assign tag | -| `DELETE` | `/api/media/:id/tags/:tag` | session | Remove tag | -| `POST` | `/api/progress` | session | Update position + accumulate counter | -| `DELETE` | `/api/media/:id` | session (owner/admin) | Soft delete | -| `POST` | `/api/media/:id/restore` | session (owner/admin) | Restore from trash | -| `GET` | `/api/admin/trash` | admin | List soft-deleted items | -| `POST` | `/api/admin/rescan` | admin | Trigger filesystem rescan | -| `GET` | `/api/admin/users` | admin | List users | -| `POST` | `/api/admin/users` | admin | Create user | -| `DELETE` | `/api/admin/users/:id` | admin | Remove user (not self) | -| `GET` | `/api/admin/permissions` | admin | Set permissions matrix | -| `POST` | `/api/admin/permissions` | admin | Grant set access | -| `DELETE` | `/api/admin/permissions` | admin | Revoke set access | -| `POST` | `/api/sets/:id/upload` | session (owner/admin) | Upload file (multipart, max 100MB) | -| `POST` | `/api/media/:id/shares` | session | Create new share link (expires 14d default) | -| `GET` | `/api/media/:id/shares` | session | List active shares for media | -| `DELETE` | `/api/shares/:token` | session | Revoke share | -| `GET` | `/s/:token` | public | Share landing page with inline player | -| `GET` | `/s/:token/stream` | public | Stream shared file | -| `GET` | `/` | session | SPA shell `index.html` (or redirect) | -| `GET` | `/healthz` | public | Liveness probe (no DB) | -| `GET` | `/readyz` | public | Readiness probe (DB ping) | -| `GET` | `/api/media/:id/notes` | session | Get current user's note | -| `POST` | `/api/media/:id/notes` | session | Create or update note | -| `DELETE` | `/api/media/:id/notes` | session | Delete user's note | - ---- - -## Feature Details - -### 1. Sets and Scanner -- **Set** = immediate child directory of `MEDIA_ROOT`. -- Scanner walks each set **recursively**. -- `ffprobe` extracts duration, codec, resolution, bitrate. -- `os.Stat` gets `file_size_bytes`. -- `ffmpeg` generates thumbnail at ~10s (input-side seek `-ss`). -- Audio files render as **compact text rows** in UI (no thumbnail for single audio items). -- Sets display a **cover thumbnail** (`.cover.jpg` in set root) derived from a random video frame inside the set. - -### 2. Auth -- `golang.org/x/crypto/bcrypt` for password hashing. -- HTTP-only cookie sessions stored in `sessions` table. -- **Bootstrap**: If `users` is empty, all routes redirect to `/bootstrap.html` until first admin is created. - -### 3. Roles -- `set_permissions.role` = `owner` or `viewer`. -- **Owner/admin** can upload to that set. -- Admin sees all sets implicitly. - -### 4. Playback -- Custom `<video>`/`<audio>` overlay with play/pause, seek bar, volume, mute, time, fullscreen toggle. -- **Fullscreen** (`f` key) uses Fullscreen API on the **wrapper container** so overlay controls (including progress bar) remain visible. -- **Auto-next**: On playback end, play next item from the current filtered/sorted/shuffled result set. - -### 5. Playback Counter -- Client POSTs `position_seconds` every 3 seconds while playing. -- Server tracks `delta = new_pos - last_position`, clamped to `[0, 12]`. -- After cumulative `>= 60` seconds and `counted = false`, increments `media.play_count`. - -### 6. Keyboard Navigation -- `↑↓` or `kj` — navigate media list. -- `←→` or `hl` — switch sets/pages. -- `Enter` — open selected media. -- `p` or `Space` — play/pause/switch to selected. -- `f` — toggle fullscreen. -- `Esc` — exit fullscreen or deselect. -- `r` — toggle shuffle on current filtered result set (lost on refresh). -- `s` — generate share link for selected media. -- `/` — trigger universal search input. - -### 7. Selection -- `.selected` class highlights the focused media card. -- Keyboard focus and mouse click both update selection. -- Visually distinct from `.playing` (which shows a play indicator). - -### 8. Filtering -- **Query params**: `set_id`, `type`, `search` (filename/substring), `tags` (AND logic, comma-separated), `favorites=true`, `min_duration`, `max_duration` (seconds). -- **Sort**: `name`, `date`, `duration`, `play_count`, `random`. -- **Search (`/`)**: Debounced 300ms. Searches filename, rel_path, codec, resolution, tags, notes, sets. - -### 9. Upload -- `POST /api/sets/:id/upload` with `multipart/form-data`. -- Max file size: **100MB** (`MAX_UPLOAD_SIZE_MB=100`). -- If filename exists, append `(1)`, `(2)`, etc. -- After save, immediate `ffprobe` + `ffmpeg` thumbnail + insert into `media`. - -### 10. Share Links -- `POST /api/media/:id/shares` generates a new random token. -- Default expiration: **7 days** from now. -- Public routes `/s/:token` and `/s/:token/stream` bypass auth. -- Each time `s` is pressed, a **new share** is created (old ones remain valid until expiry). - -### 11. Soft Delete + GC -- `DELETE /api/media/:id` sets `deleted_at = NOW()`. -- Media hidden from normal views; shown in admin trash view. -- Admin/owner can restore before 7 days. -- Background goroutine (`time.Ticker`, default every 30 minutes) selects items where `deleted_at < NOW() - 7 days`. -- Physical file deleted via `os.Remove()`, then **hard DELETE** from DB row. - -### 12. Thumbnail Regeneration -- `POST /api/media/:id/thumbnail` or set cover regeneration. -- Random offset `rand.Float64() * duration`; `ffmpeg -ss` input-side seek. -- Overwrites existing file. - -### 13. Per-Media Notes -- Private **per-user** notes via `media_notes` table. -- UI: notes icon on media detail opens textarea modal. -- Auto-save or explicit save button. - -### 14. Theming + Dark Mode -- All colors defined in `css/theme.css` using `var(--*)`. -- No inline styles anywhere. -- Sun/moon toggle in header swaps `<link>` tag to `light.css`. -- Zero color literals in component styles. - -### 15. PWA -- `manifest.json` for installable app. -- Service Worker caches static assets for offline. -- App works as standalone install on mobile/desktop. - ---- - -## Testability Requirements - -- **Dependencies injected via constructors** (no package-level vars). -- **Interfaces at all boundaries**: DB, FS, `ffprobe`, `ffmpeg`, bcrypt, clock, sessions. -- **Heavy unit tests** with `:memory:` SQLite for repository layer. -- **Mock-based tests** for service layer using hand-written fakes. -- **HTTP tests** with `httptest` + mocked services for handlers. -- **Target:** >80% coverage via `go test ./... -race -coverprofile=cov.out`. - ---- - -## Kubernetes Deployment - -- **Image**: Multi-stage Dockerfile; final stage based on `alpine` with `apk add ffmpeg`. -- **Security**: non-root user (`65534:65534`), `runAsNonRoot: true`, `fsGroup: 65534`. -- **PVCs**: Separate volumes for `/data` (SQLite) and `/media` (library). -- **Probes**: `/healthz` (liveness, no DB), `/readyz` (readiness, DB ping). -- **Graceful shutdown**: Handle `SIGTERM`, close DB, finish active requests. - ---- - -## Environment Configuration - -| Variable | Default | Description | -|----------|---------|-------------| -| `PORT` | `8080` | HTTP listen port | -| `MEDIA_ROOT` | `./media` | Root path for set directories | -| `DB_PATH` | `data.db` | SQLite database file | -| `MAX_UPLOAD_SIZE_MB` | `100` | Max upload size per file | -| `SESSION_TIMEOUT_HOURS` | `24` | Cookie expiry | -| `GC_INTERVAL_MINUTES` | `30` | Garbage collector tick | -| `SHARE_DEFAULT_EXPIRY_DAYS` | `7` | Default share link lifetime | -| `LOG_LEVEL` | `info` | Log verbosity | -| `SECURE_COOKIES` | `true` | Set `Secure` flag on session cookies; disable for plain-HTTP local deployments | - ---- - -## Magefile Targets - -- `mage` (default) → `mage build` -- `mage build` -- `mage test` -- `mage install` -- `mage clean` -- `mage docker-build` -- `mage docker-push` - ---- - -## Completion Criteria - -All 31 features implemented, tested, and deployable: -1. Sets & Scanner -2. SQLite DB -3. Auth & Sessions -4. Admin Panel -5. Playback (with fullscreen overlay) -6. Download -7. Tagging -8. Playback Counter (>60s) -9. Favorites -10. Progress Tracking -11. Soft Delete -12. GC Worker -13. Thumbnail Regeneration -14. Filtering (set/type/filename/tags/favorites/duration/filesize) -15. File Size Display -16. PWA -17. Theming (CSS variables) -18. Dark Mode Toggle -19. Upload (500MB, owner/admin) -20. Share Links (s key, 7 days) -21. Shuffle (r key, filtered scope) -22. Keyboard Navigation -23. Selection Highlight -24. Auto-Next Playback -25. Admin Bootstrap (`bootstrap.html`) -26. Set Cover Thumbnail -27. Audio/Podcast/Audiobook Text Rows -28. Audiobooks (audio tag) -29. Per-Media Notes (private per-user) -30. Quick Search ( `/` key) -31. Local Mode & K8s Deployability - |
