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
+10 -3
View File
@@ -31,9 +31,10 @@ import (
// OrganizerService moves/renames files into library structures.
type OrganizerService struct {
cfg *config.Config
log *zap.Logger
repo *repository.Container
cfg *config.Config
log *zap.Logger
repo *repository.Container
probe *FFprobeService // optional; used for 洗版 resolution comparison
}
// NewOrganizerService is the constructor.
@@ -41,10 +42,16 @@ func NewOrganizerService(cfg *config.Config, log *zap.Logger, repo *repository.C
return &OrganizerService{cfg: cfg, log: log, repo: repo}
}
// SetProbe wires an FFprobe service so directory organize can compare real
// pixel dimensions when deciding whether to 洗版 (replace by higher resolution).
// Optional: when nil the organizer falls back to filename resolution tokens.
func (o *OrganizerService) SetProbe(p *FFprobeService) { o.probe = p }
// OrganizeResult reports what happened.
type OrganizeResult struct {
Organized int `json:"organized"`
Skipped int `json:"skipped"`
Replaced int `json:"replaced,omitempty"`
Errors []string `json:"errors,omitempty"`
}