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>
The DLNA device list endpoint could return {"devices":null} in two cases:
the SSDP-failure path returned a nil slice, and the cached path copied an
empty cache via append([]DLNADevice(nil), cache...) which also yields nil.
The web UI reads res.devices.length, so a null devices field crashed the
DLNA page to a white screen. Return non-nil slices in both paths and guard
the frontend with a null coalesce. Adds a regression test.
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>