115 deprecated the plain web /files/download endpoint (ordinary cookies no
longer get file_url), breaking 302 playback with 'no file_url'. Switch
Resolve() to the current proapi.115.com/app/chrome/downurl endpoint, which
uses 115's m115 (RSA+XOR) request/response encryption (vendored from the
MIT-licensed SheltonZhu/115driver).
115 CDN links are bound to the User-Agent used to request them, so resolve
with the playback client's own UA (plumbed from the play handler) — the host
can then issue a pure 302 the client fetches directly, preserving true offload.
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 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>