Audit-driven port from the original Python MediaStation. Eight major
subsystems that were absent from the Go rewrite are now in place,
each with its own service, handler, frontend page and smoke-test
assertions.
Backend services
- service/crypto.go: AES-256-GCM encrypt/decrypt for at-rest secrets
keyed off the JWT secret. Legacy plaintext rows pass through
unchanged for smooth upgrades. Unit-tested.
- service/api_config.go: third-party provider config (TMDb, Bangumi,
TheTVDB, Fanart, Douban, OpenAI). Seeds defaults on first run.
Encrypts api_key on write, returns masked 'abc1****wxyz' projection.
- service/duplicate.go: sparse-sample MD5 (head + middle + tail, 1MiB
each, plus file-size suffix) duplicate finder. Picks 'best' primary
(matched > size > id) and marks others is_duplicate=true.
- service/filemanager.go: server-side allow-listed file browser used
by the library-path picker. Strict path-traversal protection.
- service/dlna.go: real SSDP M-SEARCH discovery + AVTransport
SetAVTransportURI/Play SOAP cast. 30 s discovery cache.
- service/scheduler.go: 3 recurring background jobs (library_scan
60min, transcode_cleanup 24h, recycle_purge 24h with 30-day
cutoff). Status + run-now endpoints.
- service/cache_cleanup.go: walkAndPrune helper used by scheduler.
- service/storage.go: DB-only disk-usage breakdown by library and by
container format.
- service/emby_compat.go: read-only Emby/Jellyfin shim
(System/Info, Users, Users/x/Views, Items, PlaybackInfo) so Infuse
/ VidHub / Kodi can browse MediaStationGo libraries.
Model updates
- Media: new strm_url (302 redirect target), file_hash, is_duplicate,
duplicate_of fields.
- APIConfig: new table for encrypted provider secrets.
- AutoMigrate registers APIConfig.
Stream layer
- StreamService.ServeFile now redirects 302 to strm_url when set so
WebDAV / Alist / S3 / HTTP direct links work transparently.
Handlers + routes
- Authed: GET /files, GET /storage, GET /dlna/devices, POST /dlna/cast,
PUT/DELETE /media/:id/strm, POST /strm/import,
POST /duplicates/{scan,unmark}.
- Admin: GET/PUT/DELETE /admin/api-configs/:provider,
GET /admin/scheduler, POST /admin/scheduler/:name/run.
- New /emby/* group: System/Info, Users, Users/:userId/Views,
Users/:userId/Items, Items/:id/PlaybackInfo (auth-required).
Frontend pages (lazy-loaded, 7 new chunks)
- DlnaPage: device list + media picker + cast button.
- FileManagerPage: root selector + breadcrumb + sortable listing.
- APIConfigsPage: per-provider card with masked-key editor.
- StoragePage: usage tiles + per-library bars + per-container grid.
- DuplicatesPage: scan form + grouped report with primary highlight.
- SchedulerPage: live job table with run-now button (5s refresh).
- Sidebar reorganised: 自动化 group adds DLNA, 管理 group adds
存储 / 文件浏览 / 重复文件 / 定时任务 / API 配置.
Smoke test additions (all admin-only)
- api-configs seeded with 6 providers
- api-config encrypted in db (sqlite3 enc:v1: prefix check)
- storage breakdown
- file browser lists library root + rejects /etc (path traversal)
- dlna devices endpoint
- scheduler exposes 3 jobs + run library_scan
- emby /System/Info + /Users/{x}/Views
- strm set + stream 302 + strm clear
- duplicate scan
Verified: go build, go vet, go test (incl. new TestCrypto* suite + the
existing TestParseEpisode/TestCleanQuery/TestSrtToVTT/TestStripASSTags/
TestBuildFFmpegArgs); tsc -b && vite build emits 28 route chunks plus
the deferred hls chunk; main bundle 253 KB / 85 KB gzipped; smoke test
PASS=42 / FAIL=0.
Backend
- service/ffprobe.go: thin ffprobe wrapper, parses duration / resolution /
codecs into a typed ProbeResult. 30s per-file timeout.
- service/tmdb.go: minimal TMDb provider (search/movie). Disabled when no
api key; supports tmdb_api_proxy / tmdb_image_proxy overrides for users
behind a firewall.
- service/scraper.go: filename cleaner (handles bracketed tags, scene
noise tokens, year extraction), per-row + per-library enrichment with a
4 RPS throttle and WS hub progress events. Unit-tested.
- service/scanner.go: now invokes ffprobe per file and kicks the TMDb
scraper in the background once a library scan finishes.
- service/transcoder.go: per-media ffmpeg HLS job manager; outputs
index.m3u8 + seg_NNNNN.ts under cache/hls/<id>; cancels jobs on
shutdown; publishes 'transcode' WS events.
- service/stream.go: serves HLS playlist (with 30s wait-for-ready) and
.ts segments with path-traversal protection. Adds Probe() helper used
by the admin 'reprobe' button.
- service/image_proxy.go: cached, host-allow-listed reverse proxy for
TMDb / Bangumi / Douban / Fanart / TheTVDB images so the SPA never
hits a CORS or GFW issue.
- service/playback.go: history upsert, favourites toggle, playlist CRUD
+ ordered items. RecentHistory joins with model.Media in one extra
query so the home page can render a 'Continue Watching' row.
- handler/streaming.go + handler/playback.go: REST endpoints for HLS,
image proxy, scrape (one + library), reprobe, history, favourites,
playlists.
- handler/handler.go: registers /api/hls/:id/{index.m3u8,:seg}, /api/img,
/api/history, /api/favourites/:id, /api/playlists/* with proper
auth/admin guards.
Frontend
- api/client.ts: imageURL() helper; hlsURL() endpoint; reuses the JWT in
a query parameter for <video src> and <img src>.
- api/playback.ts: typed helpers for history, favourites, playlists.
- components/MediaCard.tsx: optional 'progress' prop renders a thin
bottom progress bar, used by the new Continue Watching row.
- pages/HomePage.tsx: two rows (Continue Watching + Recently Added);
falls back to the empty-state hint when both are empty.
- pages/PlayerPage.tsx: hls.js (lazy-imported) with auto-fallback to
direct play; ?mode=hls|direct query toggle; resume position written
every 10s while playing.
- pages/MediaDetailPage.tsx: heart toggle + admin 'rescrape' / 'reprobe'
buttons + dedicated 'HLS 转码播放' CTA.
- pages/FavouritesPage.tsx, PlaylistsPage.tsx, PlaylistDetailPage.tsx:
new screens.
- components/Layout.tsx + App.tsx: sidebar links for Favourites and
Playlists; routes are now lazily code-split via React.lazy + Suspense
so the initial bundle stays at ~243 KB / 82 KB gzipped (hls.js is
fetched only on first HLS playback).
Verified: go build, go vet, go test (incl. CleanQuery cases) all pass;
frontend tsc -b && vite build emits 9 route chunks plus a deferred hls
chunk.