fix(organize): 入库二级分类对齐MoviePilot结构 + 手动整理不再作为一级目录

- anime 顶层目录独立为「动漫」(动漫/{国漫,日番,儿童}),不再混入电视剧
- categoryPhysicalRootDir 把二级分类名映射到正确顶层:
  动画电影/华语电影/外语电影→电影; 国漫/日番/儿童→动漫;
  国产剧/日韩剧/欧美剧/纪录片/综艺/未分类→电视剧; 成人→成人
- 新增 staging 重定向:目标根落在「手动整理」等暂存目录时,上提到
  父级媒体根,媒体真正归入分类目录(如 媒体/电影/华语电影/片名),
  而非停留在手动整理下或与分类目录并列
- organizeLibraryRootForLayout 跳过 staging 名的库,避免回流暂存目录
- 同步更新陈旧测试期望(电视剧→动漫)+新增 staging 重定向测试
This commit is contained in:
ShukeBta
2026-06-18 19:32:59 +00:00
parent 0cec6606c1
commit b16f7d6663
6 changed files with 263 additions and 19 deletions
+51 -1
View File
@@ -127,7 +127,7 @@ func (o *OrganizerService) OrganizeDirectory(ctx context.Context, opts OrganizeO
if _, ok := ParseCloudLibraryMount(requestedDest); ok {
return nil, errors.New("organize destination must be a local writable media directory; enable cloud transfer in external storage when writing to cloud")
}
dest := resolveMappedDestinationPath(requestedDest)
dest := redirectOrganizeStagingRoot(resolveMappedDestinationPath(requestedDest))
if dest == "" || dest == "." {
return nil, errors.New("destination path required")
}
@@ -537,6 +537,9 @@ func (o *OrganizerService) lookupOrganizeMetadata(ctx context.Context, src, sour
} else if err != nil && o.log != nil {
o.log.Debug("organize read local metadata before rename failed", zap.String("path", src), zap.Error(err))
}
if match := o.lookupOrganizeAdultMetadata(ctx, src, mediaType, title); match != nil {
return match
}
if o == nil || o.scraper == nil || !o.scraper.AnyEnabled() {
return nil
}
@@ -605,6 +608,49 @@ func (o *OrganizerService) lookupOrganizeMetadata(ctx context.Context, src, sour
return nil
}
func (o *OrganizerService) lookupOrganizeAdultMetadata(ctx context.Context, src, mediaType, title string) *Match {
if o == nil || o.scraper == nil || o.scraper.adult == nil || !o.scraper.adult.Enabled() {
return nil
}
isAdult := normalizeOrganizeMediaType(mediaType) == "adult"
candidates := []string{src, filepath.Base(src), title}
outCodes := make([]string, 0, len(candidates))
seen := map[string]struct{}{}
for _, candidate := range candidates {
code := normalizeAdultCode(candidate)
if code == "" {
continue
}
if _, ok := seen[code]; ok {
continue
}
seen[code] = struct{}{}
outCodes = append(outCodes, code)
}
if !isAdult && len(outCodes) == 0 {
return nil
}
for _, code := range outCodes {
match, err := o.scraper.adult.Search(ctx, code)
if err != nil {
if o.log != nil {
o.log.Debug("organize adult metadata search failed", zap.String("source", src), zap.String("code", code), zap.Error(err))
}
continue
}
if match != nil && strings.TrimSpace(match.Title) != "" {
if o.log != nil {
o.log.Info("organize adult metadata matched before rename",
zap.String("source", src),
zap.String("code", code),
zap.String("title", match.Title))
}
return match
}
}
return nil
}
func organizeMetadataMatchTrusted(query string, sourceYear int, match *Match) bool {
if match == nil || strings.TrimSpace(match.Title) == "" {
return false
@@ -834,6 +880,10 @@ func (o *OrganizerService) organizeLibraryRootForLayout(ctx context.Context, des
if _, ok := ParseCloudLibraryMount(lib.Path); ok {
continue
}
if isOrganizeStagingDir(lib.Path) {
// "手动整理"等暂存库不作为入库目标,避免把媒体留在暂存目录里。
continue
}
if destRoot != "" && destRoot != "." && !pathWithin(lib.Path, destRoot) && !pathWithin(destRoot, lib.Path) {
continue
}