Add a pluggable cloud-disk subsystem (internal/service/cloud) exposing remote
files as playable media via HTTP 302 redirects, so the host never transcodes
nor (by default) streams the bytes:
- quark (夸克网盘): cookie auth, directory listing + download_url resolution.
- cloud115 (115 网盘): cookie auth + QR-code login flow (token/poll/exchange);
pickcode → CDN URL resolved for 302 offload.
- StorageConfigService gains cloud types, Ping() probes, List/Resolve/Import.
- New endpoints: admin /cloud/:type/{list,import,qr/start,qr/poll} and authed
/cloud/play/:type (302 redirect, or reverse-proxy when the link needs auth
headers). Imported files become Media rows with STRMURL → the play endpoint.
- Frontend: 115网盘/夸克网盘 tabs with cookie input, 115 QR-code login, a cloud
file browser and one-click 302 import.
Providers are exercised against httptest mock servers (list/resolve/QR state
machine). Live login + playback require a real cloud account.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Add an admin toggle playback.direct_only that offloads all decoding to the
client. When enabled:
- Emby PlaybackInfo no longer advertises SupportsTranscoding nor a
TranscodingUrl, forcing Emby/Infuse/Yamby clients to direct play.
- HLS endpoints refuse (ErrTranscodeDisabled) so the host never spawns ffmpeg.
- Web player forces direct play, hides the HLS toggle, and surfaces a hint.
Exposed via /system/info (direct_play_only) and the Settings page.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Add a pluggable cloud-disk subsystem (internal/service/cloud) exposing remote
files as playable media via HTTP 302 redirects, so the host never transcodes
nor (by default) streams the bytes:
- quark (夸克网盘): cookie auth, directory listing + download_url resolution.
- cloud115 (115 网盘): cookie auth + QR-code login flow (token/poll/exchange);
pickcode → CDN URL resolved for 302 offload.
- StorageConfigService gains cloud types, Ping() probes, List/Resolve/Import.
- New endpoints: admin /cloud/:type/{list,import,qr/start,qr/poll} and authed
/cloud/play/:type (302 redirect, or reverse-proxy when the link needs auth
headers). Imported files become Media rows with STRMURL → the play endpoint.
- Frontend: 115网盘/夸克网盘 tabs with cookie input, 115 QR-code login, a cloud
file browser and one-click 302 import.
Providers are exercised against httptest mock servers (list/resolve/QR state
machine). Live login + playback require a real cloud account.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Add an admin toggle playback.direct_only that offloads all decoding to the
client. When enabled:
- Emby PlaybackInfo no longer advertises SupportsTranscoding nor a
TranscodingUrl, forcing Emby/Infuse/Yamby clients to direct play.
- HLS endpoints refuse (ErrTranscodeDisabled) so the host never spawns ffmpeg.
- Web player forces direct play, hides the HLS toggle, and surfaces a hint.
Exposed via /system/info (direct_play_only) and the Settings page.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- Add OrganizeDirectory service: walk an arbitrary source directory (download
dir / NAS direct-read path) and organize video files into the destination,
without requiring the source to be a registered library.
- Dedup: skip media already present in the destination (matched by scanned DB
identity title+year[/season+episode], robust to dir case/layout, plus a
filesystem folder fallback).
- 洗版 (resolution replacement): when the source resolution is higher than the
existing version, replace the lower-res file (+NFO sidecar +DB row). Prefers
scanned dimensions, then ffprobe, then filename token; never replaces on
unknown resolution.
- New endpoints: GET /admin/organize/sources (download/media dir candidates)
and POST /admin/organize/source.
- UI: ToolsPage adds a '整理来源目录(去重+洗版)' form so operators can pick the
download dir as the organize source.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Three regressions reported on third-party clients and the web UI:
- Third-party clients (Emby/Jellyfin) dropped login / could not play /
could not refresh the library, roughly hourly. The Emby
AuthenticateByName response returned the 60-minute access token, but
Emby clients have no refresh mechanism and reuse the AccessToken until
logout. Issue a long-lived (30d) token for the Emby compat layer via
AuthService.IssueEmbyToken so device sessions persist.
- Web could be thrown back to login under load: /auth/refresh was inside
the IP rate-limited /auth group, so multiple users/tabs behind one
reverse-proxy/NAT IP exhausted the budget and refresh failed -> logout.
Only login/register are rate-limited now (raised to 30/min for shared
IPs); refresh is excluded (already protected by a one-time refresh token).
- Posters/images stopped displaying on the web home and other pages
(refresh did not help). The SSRF/path hardening (a) blocked the image
proxy whenever a hostname *resolved* to a private IP, which happens
under GFW DNS poisoning of public CDNs like image.tmdb.org, and (b)
restricted local image reads to data/cache/movies/tv/anime dirs only,
dropping sidecar posters stored under arbitrary per-library roots to a
placeholder. isPrivateHost now only blocks literal private/loopback IPs
(real SSRF vectors) and ImageProxy also allows reads under configured
library roots.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Previously organize only exposed a single 'target dir' that was actually
the destination, conflating 源目录 (where files to organize live) with
目的地目录 (where organized files go). Add an explicit source directory:
- OrganizeOptions gains SourcePath; DestPath replaces the old TargetPath
(destination) for clarity. New organize.source_dir setting + source_path
request override; resolveSourceRoot falls back to library path.
- OrganizeLibraryWithOptions only organizes media located under the source
root, so operators can point at a specific download/staging folder and
organize into a distinct destination.
- Handler accepts source_path/dest_path (target_path kept as a deprecated
alias for the destination, backward compatible).
- Settings page splits into 整理源目录(待整理) + 整理目的地目录; Tools organize
panel exposes 源目录 + 目的地目录 inputs with clear copy.
Defaults are unchanged (source = destination = library path) so existing
setups behave identically. Adds a regression test that organize is scoped
to the source directory.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- Organizer: add move/copy/hardlink/symlink transfer modes (default move),
per-request target_path/transfer_mode overrides, and honor organize.target_dir
/ organize.transfer_mode settings.
- keep_seeding (default on): escalate move->hardlink (cross-device->copy) so the
qBittorrent source stays in place and continues seeding after organize.
- qBittorrent SetLocation + POST /downloads/relocate to migrate whole torrents
while keeping them seeding.
- Scanner: FileID (device:inode) hardlink dedup to avoid duplicate recognition
and double-counted storage; extract single-file ingest.
- Watcher: recursive watch + incremental per-file ingest/remove instead of full
re-scan; periodic full library scan now gated behind scan.periodic_enabled
(default off) to reduce disk wear.
- Telegram bot: handle callback_query in polling, declare allowed_updates in
webhook, answer callbacks.
- Frontend: settings for transfer mode / keep_seeding / periodic scan; organize
panel target dir + transfer mode overrides.
- Tests for transfer modes, organizer resolution, SetLocation, inode dedup,
incremental ingest/remove.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- createPlayProfileHandler: return 201 Created (not 200) on success
- create/update handlers: return 500 for infra errors, 400 only for
validation errors (using new ErrPlayProfileValidation sentinel)
- deletePlayProfileHandler: validate JSON body (was silently ignored)
- verifyPlayProfilePINHandler: validate JSON body (was silently ignored)
- verify handler catch-all: return 500 (not 400) for unexpected errors
- Service layer: wrap validation errors with ErrPlayProfileValidation
so handlers can distinguish client vs server errors
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- Add isPrivateHost() to block image proxy requests to loopback/private/
link-local IPs (SSRF mitigation)
- Add isAllowedLocalPath() to restrict local file reads to configured
data/cache/media directories only
- CORS middleware now takes debug flag; wildcard only when debug=true,
production omits headers (same-origin enforced)
- Add per-IP sliding-window rate limiter (10 req/min) on login/register
and Emby AuthenticateByName endpoints
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- Remove all license-related code (handler/service/repository/model)
License authorization is managed by separate server:
https://github.com/ShukeBta/MediaStationLicenseServer
- Fix compilation errors: model field alignment, method name fixes,
route conflicts, struct literal corrections (7 files)
- Add Chinese README.md as primary, English README_EN.md
- Update .gitignore: exclude .workbuddy/, editor backups
- Add new repository files: assistant, play_profile, storage_config
Backend additions:
- New GORM models: NotifyChannel, PlayProfile (with PIN hashing).
- NotifyChannelService: multi-channel CRUD + unified dispatcher
supporting Telegram / Bark / WeChat / Webhook with optional
per-channel event filtering.
- PlayProfileService: per-user 'viewing personas' with
content-rating gates, library scoping, PIN protection, and
player defaults (autoplay, skip-intro, audio/subtitle prefs).
- New endpoints:
GET /admin/notify/channels list
POST /admin/notify/channels create
PUT /admin/notify/channels/:id update
DELETE /admin/notify/channels/:id delete
POST /admin/notify/channels/:id/test send test
GET /play-profiles list (admin: ?all=true)
POST /play-profiles create
PUT /play-profiles/:id update
DELETE /play-profiles/:id delete
GET /media/recent home page rail
GET /media/stats library composition
GET /watch-history list
GET /watch-history/stats aggregate
GET /watch-history/continue continue-watching rail
DELETE /watch-history[?media_id=] clear
DELETE /watch-history/:id remove one
GET /discover/sections available rails
GET /discover/feed?sections=a,b multi-section TMDb fetch
GET /system/info name/version/runtime
GET /system/status uptime/cpu/mem/disk
GET /system/scheduler read-only scheduler view
GET /stats/overview counts only
GET /stats/trend daily plays
GET /stats/top-content most played
GET /stats/libraries per-library size
GET /stats/monitor live hardware
Frontend additions:
- New API helpers: notify_channels, play_profiles, history,
stats_extra, media_extra, discover_extra, system.
- ProfileManagementPage (/play-profiles): full CRUD form with
PIN management, library scoping, language preferences.
- NotifyChannelsPage (/notify-channels, admin): typed config
forms per channel type + per-channel test action.
- SettingsPage (/settings, admin): grouped key/value editor
covering General, Organize/Scrape, Adult, qBittorrent.
- Layout sidebar links and App.tsx routes wired for all three.
Port the complete site management system from the original MediaStation:
Model:
- model/model.go: Added Site table with fields for name, base_url,
site_type (nexusphp/gazelle/unit3d/mteam/custom_rss), auth_type
(cookie/api_key/authorization), cookie, api_key, auth_header,
user_agent, rss_url, timeout, priority, use_proxy, enabled,
login_status, downloader. Registered in AllModels().
Backend services:
- service/site.go: SiteService with CRUD (Create/List/FindByID/Update/
Delete), TestConnection (validates credentials via HTTP, updates
login_status in DB), and Search (cross-site fan-out that queries
every enabled site adapter and returns merged results sorted by
seeders descending).
- service/site_adapter.go: SiteAdapter interface + NewSiteAdapter
factory + 5 implementations:
* nexusPhpAdapter — regex HTML scraping of torrents.php
* gazelleAdapter — JSON API /ajax.php?action=browse
* unit3dAdapter — REST API /api/torrents/filter
* mteamAdapter — M-Team v3 POST /api/torrent/search with x-api-key
* rssAdapter — XML RSS/Atom feed parsing with keyword filter
Handler + routes:
- handler/sites.go: listSites, getSite, createSite, updateSite,
deleteSite, testSite, siteSearch handlers.
- handler/handler.go: 7 new routes under authed group:
GET/POST /sites, GET/PUT/DELETE /sites/:id,
POST /sites/:id/test, GET /sites/search.
Frontend:
- api/sites.ts: typed interfaces (Site, SiteSearchResult,
CreateSiteInput) and sitesAPI helper.
- pages/SitesPage.tsx: full CRUD UI with add-site form (site_type +
auth_type selectors, cookie/api_key/rss inputs), connection test
button with live status indicator, delete action.
- pages/SiteSearchPage.tsx: cross-site search with keyword input,
merged result table (site/title/size/seeders/leechers/free),
one-click download-to-qBittorrent button.
- App.tsx: lazy-loaded routes /sites and /site-search.
- Layout.tsx: sidebar links 站点管理 + 站点搜索 under 自动化 group.
Verified: go build + go vet + go test pass; tsc -b + vite build passes
(32 lazy chunks, main bundle 255 KB / 85 KB gzipped).
Complete the port of the remaining MediaStation features:
Backend services:
- service/backup.go: SQLite hot backup via VACUUM INTO, list, delete,
restore. Backup files stored under {data_dir}/backups/ with timestamps.
- service/notifier.go: Multi-channel push notifications (Telegram /
Bark / WeChat Server酱 / Webhook). Configuration via settings table
keys notify.telegram.*, notify.bark.*, etc.
- service/organizer.go: Auto-rename + move media files into library
directory structure. Movie: {Title} ({Year})/{Title} ({Year}).ext,
TV: {Title}/Season XX/{Title} - SxxExx.ext. Both per-media and
per-library batch endpoints.
- service/douban.go: Douban (豆瓣) metadata provider using the
unofficial subject_suggest API. Returns Chinese titles + posters.
Requires douban_cookie in secrets config.
Handlers:
- handler/backup.go: GET/POST/DELETE /admin/backups, POST restore.
- handler/organizer.go: POST /admin/media/:id/organize,
POST /admin/libraries/:id/organize.
- handler/notify.go: POST /admin/notify/test.
Frontend pages:
- WatchHistoryPage.tsx: full paginated history with progress bars,
resume buttons, poster thumbnails and timestamps.
- PosterWallPage.tsx: dense 8-column poster grid of all media
(matches the original MediaStation PosterWallView.vue).
Sidebar additions: 观看历史 + 海报墙 links in the main nav group.
Verified: go build, go vet, go test all pass; tsc -b && vite build
emits 30 lazy chunks; main bundle 254 KB / 85 KB gzipped.
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.
Bugs fixed:
- handler/media.go: createLibraryHandler and deleteLibraryHandler
spawned goroutines that called svc.Watcher.Refresh(c.Request.Context()).
Since the HTTP request returns immediately, the context is cancelled
before the watcher finishes. Use context.Background() so the refresh
always completes.
- handler/streaming.go: scrapeLibraryHandler had the same issue with
c.Copy().Request.Context(). Fixed to context.Background().
Smoke test improvements:
- Always create test media files (dummy or ffmpeg), always run scan and
search — only ffprobe-specific assertions (width=320) and HLS are
gated behind HAVE_FFMPEG.
- ID is now always set (from library media list), so history/favourites
/playlists tests never hit an unbound variable.
- NFO + recycle-bin assertions no longer gated behind HAVE_FFMPEG since
they work on any media row (even dummy files).
Verified: go build + go vet + go test all pass; 55/55 endpoint assertions
pass with 0 server 5xx; smoke test 26/26 PASS with and without ffmpeg.