fix: classify download organize output

This commit is contained in:
ShukeBta
2026-06-08 10:06:55 +08:00
parent cf6c58d26e
commit ab9e5d6a8f
2 changed files with 131 additions and 4 deletions
+101 -4
View File
@@ -99,7 +99,7 @@ func (o *OrganizerService) OrganizeDirectory(ctx context.Context, opts OrganizeO
if _, ok := videoExtensions[ext]; !ok {
return nil
}
if err := o.organizeSourceFile(ctx, path, dest, mode, res); err != nil {
if err := o.organizeSourceFile(ctx, path, source, dest, mode, res); err != nil {
res.Errors = append(res.Errors, fmt.Sprintf("%s: %s", filepath.Base(path), err.Error()))
}
return nil
@@ -118,9 +118,14 @@ func (o *OrganizerService) OrganizeDirectory(ctx context.Context, opts OrganizeO
return res, nil
}
type organizeDirectoryLayout struct {
MediaType string
Category string
}
// organizeSourceFile organizes a single video file from the source directory
// into destRoot, applying dedup + 洗版.
func (o *OrganizerService) organizeSourceFile(ctx context.Context, src, destRoot string, mode TransferMode, res *OrganizeResult) error {
func (o *OrganizerService) organizeSourceFile(ctx context.Context, src, sourceRoot, destRoot string, mode TransferMode, res *OrganizeResult) error {
ext := filepath.Ext(src)
title, year := CleanQuery(src)
if title == "" {
@@ -134,12 +139,18 @@ func (o *OrganizerService) organizeSourceFile(ctx context.Context, src, destRoot
title = "Unknown"
}
season, episode := ParseEpisode(src)
layout := o.inferOrganizeDirectoryLayout(src, sourceRoot)
layoutRoot := destRoot
if layout.Category != "" {
root := o.organizeRoot(destRoot, layout.MediaType, layout.Category)
layoutRoot = categoryRoot(root, sanitizeFilename(layout.Category))
}
var destDir, dst, episodeTag string
if season > 0 || episode > 0 {
// TV/动漫/综艺等剧集:{destRoot}/{Title}/Season XX/{Title} - SxxExx.ext
episodeTag = fmt.Sprintf("S%02dE%02d", season, episode)
destDir = filepath.Join(destRoot, title, fmt.Sprintf("Season %02d", season))
destDir = filepath.Join(layoutRoot, title, fmt.Sprintf("Season %02d", season))
dst = filepath.Join(destDir, fmt.Sprintf("%s - %s%s", title, episodeTag, ext))
} else {
// 电影:{destRoot}/{Title} ({Year})/{Title} ({Year}).ext
@@ -147,7 +158,7 @@ func (o *OrganizerService) organizeSourceFile(ctx context.Context, src, destRoot
if year > 0 {
folder = fmt.Sprintf("%s (%d)", title, year)
}
destDir = filepath.Join(destRoot, folder)
destDir = filepath.Join(layoutRoot, folder)
dst = filepath.Join(destDir, folder+ext)
}
@@ -208,6 +219,92 @@ func (o *OrganizerService) organizeSourceFile(ctx context.Context, src, destRoot
return nil
}
func (o *OrganizerService) inferOrganizeDirectoryLayout(src, sourceRoot string) organizeDirectoryLayout {
for _, name := range organizeDirectoryCategoryCandidates(src, sourceRoot) {
if mediaType, category := o.mediaTypeForDirectoryCategory(name); mediaType != "" && category != "" {
return organizeDirectoryLayout{MediaType: mediaType, Category: category}
}
}
return organizeDirectoryLayout{}
}
func organizeDirectoryCategoryCandidates(src, sourceRoot string) []string {
var out []string
seen := map[string]struct{}{}
add := func(value string) {
value = strings.TrimSpace(value)
if value == "" || value == "." || value == string(os.PathSeparator) {
return
}
key := strings.ToLower(value)
if _, ok := seen[key]; ok {
return
}
seen[key] = struct{}{}
out = append(out, value)
}
cleanSourceRoot := filepath.Clean(sourceRoot)
add(filepath.Base(cleanSourceRoot))
rel, err := filepath.Rel(cleanSourceRoot, filepath.Clean(src))
if err != nil || rel == "." || strings.HasPrefix(rel, "..") {
return out
}
dir := filepath.Dir(rel)
if dir == "." {
return out
}
for _, part := range strings.Split(dir, string(os.PathSeparator)) {
add(part)
}
return out
}
func (o *OrganizerService) mediaTypeForDirectoryCategory(name string) (string, string) {
key := strings.ToLower(strings.TrimSpace(name))
if key == "" {
return "", ""
}
if hit, ok := o.directoryCategoryTypes()[key]; ok {
return hit.MediaType, hit.Category
}
return "", ""
}
func (o *OrganizerService) directoryCategoryTypes() map[string]organizeDirectoryLayout {
categories := o.categoryMap()
out := map[string]organizeDirectoryLayout{}
add := func(category, mediaType string) {
category = strings.TrimSpace(category)
if category == "" {
return
}
out[strings.ToLower(category)] = organizeDirectoryLayout{
MediaType: mediaType,
Category: category,
}
}
addConfigured := func(key, fallback, mediaType string) {
add(fallback, mediaType)
add(categoryName(categories, key, fallback), mediaType)
}
addConfigured("animation_movie", "动画电影", "movie")
addConfigured("chinese_movie", "华语电影", "movie")
addConfigured("jk_movie", "日韩电影", "movie")
addConfigured("euus_movie", "欧美电影", "movie")
addConfigured("foreign_movie", "外语电影", "movie")
addConfigured("domestic_tv", "国产剧", "tv")
addConfigured("euus_tv", "欧美剧", "tv")
addConfigured("jk_tv", "日韩剧", "tv")
addConfigured("cn_anime", "国漫", "anime")
addConfigured("jp_anime", "日番", "anime")
addConfigured("variety", "综艺", "variety")
addConfigured("documentary", "纪录片", "tv")
addConfigured("children", "儿童", "tv")
addConfigured("uncategorized_tv", "未分类", "tv")
return out
}
// existingVersionPaths returns existing destination files that represent the
// same media, combining two strategies and de-duplicating by path:
//
@@ -209,3 +209,33 @@ func TestOrganizeDirectoryTVEpisodeDedup(t *testing.T) {
t.Fatalf("expected E02 organized at %q: %v", e02, err)
}
}
func TestOrganizeDirectoryUsesDownloadCategoryLayout(t *testing.T) {
root := t.TempDir()
src := filepath.Join(root, "downloads")
dest := filepath.Join(root, "media")
writeOrgFile(t, filepath.Join(src, "国产剧", "狂飙.S01E01.2023.1080p.WEB-DL.mkv"), "kuangbiao-e01")
writeOrgFile(t, filepath.Join(src, "华语电影", "流浪地球2.2023.2160p.WEB-DL.H265.mkv"), "wandering-earth-2")
org := NewOrganizerService(&config.Config{}, zap.NewNop(), newOrganizerTestRepo(t))
res, err := org.OrganizeDirectory(t.Context(), OrganizeOptions{
SourcePath: src,
DestPath: dest,
TransferMode: TransferCopy,
})
if err != nil {
t.Fatalf("organize directory: %v", err)
}
if res.Organized != 2 || res.Replaced != 0 || res.Skipped != 0 {
t.Fatalf("expected organized=2 replaced=0 skipped=0, got %+v", res)
}
tv := filepath.Join(dest, "电视剧", "国产剧", "狂飙", "Season 01", "狂飙 - S01E01.mkv")
if _, err := os.Stat(tv); err != nil {
t.Fatalf("expected TV episode organized at %q: %v", tv, err)
}
movie := filepath.Join(dest, "电影", "华语电影", "流浪地球2 (2023)", "流浪地球2 (2023).mkv")
if _, err := os.Stat(movie); err != nil {
t.Fatalf("expected movie organized at %q: %v", movie, err)
}
}