feat(organize): organize arbitrary source dir (e.g. downloads) with dedup + 洗版

- 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>
This commit is contained in:
soldosluka857
2026-05-30 08:00:10 +00:00
committed by Shuke
parent 7cc59f095c
commit d03f84e78d
8 changed files with 724 additions and 4 deletions
+25
View File
@@ -12,6 +12,15 @@ export interface OrganizeOverrides {
transfer_mode?: string
}
// OrganizeSource is a selectable organize source directory (e.g. the download
// directory) surfaced so operators can organize an arbitrary directory and not
// only registered libraries.
export interface OrganizeSource {
label: string
path: string
kind: string
}
export const toolsAPI = {
organizeMedia: (mediaID: string, opts?: OrganizeOverrides) =>
api
@@ -26,6 +35,22 @@ export const toolsAPI = {
)
.then((r) => r.data),
// organizeSources lists selectable source directories (download/media dir).
organizeSources: () =>
api
.get<{ sources: OrganizeSource[] }>('/admin/organize/sources')
.then((r) => r.data.sources ?? []),
// organizeDirectory organizes an arbitrary source directory (e.g. downloads)
// into the destination with dedup + 洗版 (resolution replacement).
organizeDirectory: (opts: OrganizeOverrides) =>
api
.post<{ organized: number; skipped: number; replaced?: number; errors?: string[] }>(
'/admin/organize/source',
opts,
)
.then((r) => r.data),
notifyTest: (title: string, body: string) =>
api
.post<{ message: string }>('/admin/notify/test', { title, body })