mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-08 22:36:37 +08:00
fix: normalize organize category roots
This commit is contained in:
@@ -107,7 +107,7 @@ func (o *OrganizerService) OrganizeLibraryWithOptions(ctx context.Context, libra
|
|||||||
return nil, errors.New("organize source must be a local directory; cloud libraries should be managed from external storage scan/mount")
|
return nil, errors.New("organize source must be a local directory; cloud libraries should be managed from external storage scan/mount")
|
||||||
}
|
}
|
||||||
// 目的地目录:已位于该根下的文件视为已整理;受 dest_path 覆盖与设置影响。
|
// 目的地目录:已位于该根下的文件视为已整理;受 dest_path 覆盖与设置影响。
|
||||||
baseRoot := redirectOrganizeStagingRoot(o.resolveBaseRoot(ctx, lib, opts.DestPath))
|
baseRoot := normalizeOrganizeDestinationRoot(o.resolveBaseRoot(ctx, lib, opts.DestPath))
|
||||||
if _, ok := ParseCloudLibraryMount(baseRoot); ok {
|
if _, ok := ParseCloudLibraryMount(baseRoot); ok {
|
||||||
return nil, errors.New("organize destination must be a local writable media directory; enable cloud transfer in external storage when writing to cloud")
|
return nil, errors.New("organize destination must be a local writable media directory; enable cloud transfer in external storage when writing to cloud")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ func (o *OrganizerService) OrganizeDirectory(ctx context.Context, opts OrganizeO
|
|||||||
if _, ok := ParseCloudLibraryMount(requestedDest); ok {
|
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")
|
return nil, errors.New("organize destination must be a local writable media directory; enable cloud transfer in external storage when writing to cloud")
|
||||||
}
|
}
|
||||||
dest := redirectOrganizeStagingRoot(resolveMappedDestinationPath(requestedDest))
|
dest := normalizeOrganizeDestinationRoot(resolveMappedDestinationPath(requestedDest))
|
||||||
if dest == "" || dest == "." {
|
if dest == "" || dest == "." {
|
||||||
return nil, errors.New("destination path required")
|
return nil, errors.New("destination path required")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,49 @@ func TestOrganizeDirectoryUsesExplicitCategoryLibraryRoot(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOrganizeDirectoryTreatsCategoryDestAsCollectionRoot(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
src := filepath.Join(root, "downloads", "Some.Show.S01E01.2026.1080p.mkv")
|
||||||
|
dest := filepath.Join(root, "media")
|
||||||
|
writeOrgFile(t, src, "episode")
|
||||||
|
|
||||||
|
repos := newOrganizerTestRepo(t)
|
||||||
|
currentLib := model.Library{Name: "欧美剧", Path: filepath.Join(dest, "电视剧", "欧美剧"), Type: "tv", Enabled: true}
|
||||||
|
if err := repos.Library.Create(t.Context(), ¤tLib); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
org := NewOrganizerService(&config.Config{}, zap.NewNop(), repos)
|
||||||
|
res, err := org.OrganizeDirectory(t.Context(), OrganizeOptions{
|
||||||
|
SourcePath: src,
|
||||||
|
DestPath: currentLib.Path,
|
||||||
|
MediaType: "tv",
|
||||||
|
MediaCategory: "国产剧",
|
||||||
|
TransferMode: TransferCopy,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("organize category-root dest: %v", err)
|
||||||
|
}
|
||||||
|
if res.Organized != 1 || len(res.Items) != 1 {
|
||||||
|
t.Fatalf("result = %+v, want one organized item", res)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantRoot := filepath.Join(dest, "电视剧", "国产剧")
|
||||||
|
want := filepath.Join(wantRoot, "Some Show", "Season 01", "Some Show - S01E01.mkv")
|
||||||
|
if _, err := os.Stat(want); err != nil {
|
||||||
|
t.Fatalf("expected sibling category target at %q: %v; items=%#v", want, err, res.Items)
|
||||||
|
}
|
||||||
|
nested := filepath.Join(currentLib.Path, "国产剧", "Some Show", "Season 01", "Some Show - S01E01.mkv")
|
||||||
|
if _, err := os.Stat(nested); !os.IsNotExist(err) {
|
||||||
|
t.Fatalf("must not nest corrected category under current library, stat err=%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var created model.Library
|
||||||
|
if err := repos.DB.Where("path = ?", wantRoot).First(&created).Error; err != nil {
|
||||||
|
t.Fatalf("corrected category library should be auto-created: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestOrganizeDirectoryCreatesMissingCategoryLibraryForVisibility(t *testing.T) {
|
func TestOrganizeDirectoryCreatesMissingCategoryLibraryForVisibility(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
srcRoot := filepath.Join(root, "downloads")
|
srcRoot := filepath.Join(root, "downloads")
|
||||||
|
|||||||
@@ -161,24 +161,26 @@ func pathHasDirectChild(path, root, child string) bool {
|
|||||||
return strings.EqualFold(parts[0], child)
|
return strings.EqualFold(parts[0], child)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OrganizerService) ensureOrganizeLibraryForRoot(ctx context.Context, root, mediaType, category string) {
|
func (o *OrganizerService) ensureOrganizeLibraryForRoot(ctx context.Context, root, mediaType, category string) (model.Library, bool) {
|
||||||
if o == nil || o.repo == nil || o.repo.Library == nil {
|
if o == nil || o.repo == nil || o.repo.Library == nil {
|
||||||
return
|
return model.Library{}, false
|
||||||
}
|
}
|
||||||
root = filepath.Clean(strings.TrimSpace(root))
|
root = filepath.Clean(strings.TrimSpace(root))
|
||||||
if root == "" || root == "." {
|
if root == "" || root == "." {
|
||||||
return
|
return model.Library{}, false
|
||||||
}
|
}
|
||||||
if _, ok := ParseCloudLibraryMount(root); ok {
|
if _, ok := ParseCloudLibraryMount(root); ok {
|
||||||
return
|
return model.Library{}, false
|
||||||
}
|
}
|
||||||
libraries, err := o.repo.Library.List(ctx)
|
libraries, err := o.repo.Library.List(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if o.log != nil {
|
if o.log != nil {
|
||||||
o.log.Debug("list libraries before organize auto-create failed", zap.Error(err))
|
o.log.Debug("list libraries before organize auto-create failed", zap.Error(err))
|
||||||
}
|
}
|
||||||
return
|
return model.Library{}, false
|
||||||
}
|
}
|
||||||
|
var containingLibrary model.Library
|
||||||
|
hasContainingLibrary := false
|
||||||
for _, lib := range libraries {
|
for _, lib := range libraries {
|
||||||
if !lib.Enabled || strings.TrimSpace(lib.Path) == "" {
|
if !lib.Enabled || strings.TrimSpace(lib.Path) == "" {
|
||||||
continue
|
continue
|
||||||
@@ -186,9 +188,17 @@ func (o *OrganizerService) ensureOrganizeLibraryForRoot(ctx context.Context, roo
|
|||||||
if _, ok := ParseCloudLibraryMount(lib.Path); ok {
|
if _, ok := ParseCloudLibraryMount(lib.Path); ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if pathWithin(root, lib.Path) {
|
lib.Path = filepath.Clean(lib.Path)
|
||||||
return
|
if strings.EqualFold(lib.Path, root) {
|
||||||
|
return lib, true
|
||||||
}
|
}
|
||||||
|
if pathWithin(root, lib.Path) {
|
||||||
|
containingLibrary = lib
|
||||||
|
hasContainingLibrary = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(category) == "" && hasContainingLibrary {
|
||||||
|
return containingLibrary, true
|
||||||
}
|
}
|
||||||
name := strings.TrimSpace(category)
|
name := strings.TrimSpace(category)
|
||||||
if name == "" {
|
if name == "" {
|
||||||
@@ -211,7 +221,7 @@ func (o *OrganizerService) ensureOrganizeLibraryForRoot(ctx context.Context, roo
|
|||||||
zap.String("name", lib.Name),
|
zap.String("name", lib.Name),
|
||||||
zap.Error(err))
|
zap.Error(err))
|
||||||
}
|
}
|
||||||
return
|
return model.Library{}, false
|
||||||
}
|
}
|
||||||
if o.log != nil {
|
if o.log != nil {
|
||||||
o.log.Info("organize auto-created missing library",
|
o.log.Info("organize auto-created missing library",
|
||||||
@@ -219,6 +229,7 @@ func (o *OrganizerService) ensureOrganizeLibraryForRoot(ctx context.Context, roo
|
|||||||
zap.String("type", lib.Type),
|
zap.String("type", lib.Type),
|
||||||
zap.String("name", lib.Name))
|
zap.String("name", lib.Name))
|
||||||
}
|
}
|
||||||
|
return lib, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func organizeLibraryModelType(mediaType string) string {
|
func organizeLibraryModelType(mediaType string) string {
|
||||||
|
|||||||
@@ -210,7 +210,9 @@ func (o *OrganizerService) resolveOrganizeSourceLayoutRoot(ctx context.Context,
|
|||||||
layoutRoot = categoryRoot(layoutRoot, sanitizeFilename(layout.Category))
|
layoutRoot = categoryRoot(layoutRoot, sanitizeFilename(layout.Category))
|
||||||
}
|
}
|
||||||
if !matchedLibrary && !req.DryRun {
|
if !matchedLibrary && !req.DryRun {
|
||||||
o.ensureOrganizeLibraryForRoot(ctx, layoutRoot, layout.MediaType, layout.Category)
|
if targetLibrary, ok := o.ensureOrganizeLibraryForRoot(ctx, layoutRoot, layout.MediaType, layout.Category); ok {
|
||||||
|
targetLibraryID = targetLibrary.ID
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return layoutRoot, targetLibraryID
|
return layoutRoot, targetLibraryID
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ func (o *OrganizerService) resolveOrganizeMediaRequest(ctx context.Context, medi
|
|||||||
if _, ok := ParseCloudLibraryMount(lib.Path); ok {
|
if _, ok := ParseCloudLibraryMount(lib.Path); ok {
|
||||||
return organizeMediaRequest{}, errors.New("local organize cannot use cloud libraries directly; use external storage scan/mount for cloud media or enable cloud transfer to write to cloud")
|
return organizeMediaRequest{}, errors.New("local organize cannot use cloud libraries directly; use external storage scan/mount for cloud media or enable cloud transfer to write to cloud")
|
||||||
}
|
}
|
||||||
baseRoot := redirectOrganizeStagingRoot(o.resolveBaseRoot(ctx, lib, opts.DestPath))
|
baseRoot := normalizeOrganizeDestinationRoot(o.resolveBaseRoot(ctx, lib, opts.DestPath))
|
||||||
if _, ok := ParseCloudLibraryMount(baseRoot); ok {
|
if _, ok := ParseCloudLibraryMount(baseRoot); ok {
|
||||||
return organizeMediaRequest{}, errors.New("organize destination must be a local writable media directory; enable cloud transfer in external storage when writing to cloud")
|
return organizeMediaRequest{}, errors.New("organize destination must be a local writable media directory; enable cloud transfer in external storage when writing to cloud")
|
||||||
}
|
}
|
||||||
@@ -101,7 +101,9 @@ func (o *OrganizerService) buildOrganizeMediaDestination(ctx context.Context, re
|
|||||||
root = categoryRoot(root, category)
|
root = categoryRoot(root, category)
|
||||||
}
|
}
|
||||||
if !matchedLibrary && !req.dryRun {
|
if !matchedLibrary && !req.dryRun {
|
||||||
o.ensureOrganizeLibraryForRoot(ctx, root, mediaType, category)
|
if targetLibrary, ok := o.ensureOrganizeLibraryForRoot(ctx, root, mediaType, category); ok {
|
||||||
|
targetLibraryID = targetLibrary.ID
|
||||||
|
}
|
||||||
}
|
}
|
||||||
target, err := o.buildOrganizeTargetPath(ctx, organizeTargetInput{
|
target, err := o.buildOrganizeTargetPath(ctx, organizeTargetInput{
|
||||||
Root: root,
|
Root: root,
|
||||||
|
|||||||
@@ -160,6 +160,14 @@ func redirectOrganizeStagingRoot(root string) string {
|
|||||||
return cleaned
|
return cleaned
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeOrganizeDestinationRoot(root string) string {
|
||||||
|
cleaned := redirectOrganizeStagingRoot(root)
|
||||||
|
if collectionRoot := organizeMediaCollectionRoot(cleaned); collectionRoot != "" {
|
||||||
|
return collectionRoot
|
||||||
|
}
|
||||||
|
return cleaned
|
||||||
|
}
|
||||||
|
|
||||||
func pathAlreadyEndsWith(path, suffix string) bool {
|
func pathAlreadyEndsWith(path, suffix string) bool {
|
||||||
base := strings.TrimSpace(filepath.Base(filepath.Clean(path)))
|
base := strings.TrimSpace(filepath.Base(filepath.Clean(path)))
|
||||||
return strings.EqualFold(base, suffix)
|
return strings.EqualFold(base, suffix)
|
||||||
|
|||||||
@@ -111,10 +111,17 @@ func (o *OrganizerService) reclassifyScannedMedia(ctx context.Context, media mod
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
baseRoot := redirectOrganizeStagingRoot(o.resolveBaseRoot(ctx, &lib, ""))
|
baseRoot := normalizeOrganizeDestinationRoot(o.resolveBaseRoot(ctx, &lib, ""))
|
||||||
targetLibrary, matched := o.organizeLibraryForLayout(ctx, baseRoot, mediaType, category)
|
targetLibrary, matched := o.organizeLibraryForLayout(ctx, baseRoot, mediaType, category)
|
||||||
if !matched || strings.TrimSpace(targetLibrary.ID) == "" || strings.TrimSpace(targetLibrary.Path) == "" {
|
if !matched || strings.TrimSpace(targetLibrary.ID) == "" || strings.TrimSpace(targetLibrary.Path) == "" {
|
||||||
return false, nil
|
targetRoot := categoryRoot(o.organizeRoot(baseRoot, mediaType, category), category)
|
||||||
|
if dryRun {
|
||||||
|
targetLibrary = model.Library{Path: targetRoot}
|
||||||
|
} else if created, ok := o.ensureOrganizeLibraryForRoot(ctx, targetRoot, mediaType, category); ok {
|
||||||
|
targetLibrary = created
|
||||||
|
} else {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if strings.EqualFold(targetLibrary.ID, lib.ID) && pathWithin(media.Path, targetLibrary.Path) {
|
if strings.EqualFold(targetLibrary.ID, lib.ID) && pathWithin(media.Path, targetLibrary.Path) {
|
||||||
return false, nil
|
return false, nil
|
||||||
|
|||||||
@@ -297,6 +297,64 @@ func TestReclassifyMisclassifiedMediaMovesScannedAnimeToPhysicalAnimeLibrary(t *
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestReclassifyMisclassifiedMediaCreatesMissingTargetCategoryLibrary(t *testing.T) {
|
||||||
|
repos := newOrganizerTestRepo(t)
|
||||||
|
cfg := &config.Config{}
|
||||||
|
cfg.Organizer.SmartClassify = true
|
||||||
|
|
||||||
|
root := t.TempDir()
|
||||||
|
dest := filepath.Join(root, "media")
|
||||||
|
euusLib := model.Library{Name: "欧美剧", Path: filepath.Join(dest, "电视剧", "欧美剧"), Type: "tv", Enabled: true}
|
||||||
|
if err := repos.Library.Create(t.Context(), &euusLib); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wrongPath := filepath.Join(euusLib.Path, "Blades Of The Guardians", "Season 2", "Blades Of The Guardians - S02E01.mkv")
|
||||||
|
writeOrgFile(t, wrongPath, "episode")
|
||||||
|
if err := repos.DB.Create(&model.Media{
|
||||||
|
LibraryID: euusLib.ID,
|
||||||
|
Title: "镖人",
|
||||||
|
OriginalName: "Blades Of The Guardians",
|
||||||
|
Path: wrongPath,
|
||||||
|
SeasonNum: 2,
|
||||||
|
EpisodeNum: 1,
|
||||||
|
TMDbID: 107463,
|
||||||
|
Languages: "zh",
|
||||||
|
Countries: "CN",
|
||||||
|
Genres: "动画,动作冒险",
|
||||||
|
ScrapeStatus: "matched",
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
organizer := NewOrganizerService(cfg, zap.NewNop(), repos)
|
||||||
|
res, err := organizer.ReclassifyMisclassifiedMedia(t.Context(), MediaCategoryReclassifyOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("reclassify media: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
targetRoot := filepath.Join(dest, "动漫", "国漫")
|
||||||
|
want := filepath.Join(targetRoot, "镖人", "Season 02", "镖人 - S02E01.mkv")
|
||||||
|
if res.Reclassified != 1 {
|
||||||
|
t.Fatalf("reclassified = %d, want 1; items=%#v errors=%#v", res.Reclassified, res.Items, res.Errors)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(want); err != nil {
|
||||||
|
t.Fatalf("created category target missing at %q: %v", want, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var created model.Library
|
||||||
|
if err := repos.DB.Where("path = ?", targetRoot).First(&created).Error; err != nil {
|
||||||
|
t.Fatalf("missing auto-created anime library: %v", err)
|
||||||
|
}
|
||||||
|
var got model.Media
|
||||||
|
if err := repos.DB.First(&got, "path = ?", want).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got.LibraryID != created.ID {
|
||||||
|
t.Fatalf("library_id = %q, want auto-created library %q", got.LibraryID, created.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestReclassifyMisclassifiedMediaMovesWesternAnimationToWesternAnimeLibrary(t *testing.T) {
|
func TestReclassifyMisclassifiedMediaMovesWesternAnimationToWesternAnimeLibrary(t *testing.T) {
|
||||||
repos := newOrganizerTestRepo(t)
|
repos := newOrganizerTestRepo(t)
|
||||||
cfg := &config.Config{}
|
cfg := &config.Config{}
|
||||||
|
|||||||
@@ -207,6 +207,73 @@ func TestOrganizeMediaDoesNotRepeatCategoryWhenLibraryIsCategoryRoot(t *testing.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOrganizeMediaTreatsCategoryLibraryAsCollectionRoot(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
dest := filepath.Join(root, "media")
|
||||||
|
libraryRoot := filepath.Join(dest, "电视剧", "欧美剧")
|
||||||
|
sourceDir := filepath.Join(root, "downloads")
|
||||||
|
if err := os.MkdirAll(sourceDir, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
source := filepath.Join(sourceDir, "Some Show S01E02.mkv")
|
||||||
|
if err := os.WriteFile(source, []byte("episode"), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
db := newServiceTestDB(t, &model.Library{}, &model.Media{}, &model.Setting{})
|
||||||
|
repos := repository.New(db)
|
||||||
|
lib := model.Library{Name: "欧美剧", Path: libraryRoot, Type: "tv", Enabled: true}
|
||||||
|
if err := repos.Library.Create(t.Context(), &lib); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
media := model.Media{
|
||||||
|
LibraryID: lib.ID,
|
||||||
|
Title: "Some Show",
|
||||||
|
Path: source,
|
||||||
|
Container: "mkv",
|
||||||
|
SeasonNum: 1,
|
||||||
|
EpisodeNum: 2,
|
||||||
|
ScrapeStatus: "matched",
|
||||||
|
}
|
||||||
|
if err := repos.Media.Upsert(t.Context(), &media); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
organizer := NewOrganizerService(&config.Config{}, zap.NewNop(), repos)
|
||||||
|
dst, err := organizer.OrganizeMediaWithOptions(t.Context(), media.ID, OrganizeOptions{
|
||||||
|
MediaType: "tv",
|
||||||
|
MediaCategory: "国产剧",
|
||||||
|
TransferMode: TransferCopy,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
targetRoot := filepath.Join(dest, "电视剧", "国产剧")
|
||||||
|
want := filepath.Join(targetRoot, "Some Show", "Season 01", "Some Show - S01E02.mkv")
|
||||||
|
if dst != want {
|
||||||
|
t.Fatalf("dst = %q, want %q", dst, want)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(want); err != nil {
|
||||||
|
t.Fatalf("organized file missing: %v", err)
|
||||||
|
}
|
||||||
|
nested := filepath.Join(libraryRoot, "国产剧", "Some Show", "Season 01", "Some Show - S01E02.mkv")
|
||||||
|
if _, err := os.Stat(nested); !os.IsNotExist(err) {
|
||||||
|
t.Fatalf("must not nest corrected category under current library, stat err=%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var created model.Library
|
||||||
|
if err := db.Where("path = ?", targetRoot).First(&created).Error; err != nil {
|
||||||
|
t.Fatalf("corrected category library should be auto-created: %v", err)
|
||||||
|
}
|
||||||
|
var refreshed model.Media
|
||||||
|
if err := db.First(&refreshed, "id = ?", media.ID).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if refreshed.LibraryID != created.ID {
|
||||||
|
t.Fatalf("library_id = %q, want auto-created library %q", refreshed.LibraryID, created.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestOrganizeLibrarySkipsFilesAlreadyInsideLibrary(t *testing.T) {
|
func TestOrganizeLibrarySkipsFilesAlreadyInsideLibrary(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
libraryRoot := filepath.Join(root, "media", "电视剧", "国产剧")
|
libraryRoot := filepath.Join(root, "media", "电视剧", "国产剧")
|
||||||
|
|||||||
Reference in New Issue
Block a user