Files
MeBox/internal/service/organizer_source_test.go
T
soldosluka857 8f0c6d2324 feat(organize): separate source dir from destination dir (从源目录整理到目的地目录)
Previously organize only exposed a single 'target dir' that was actually
the destination, conflating 源目录 (where files to organize live) with
目的地目录 (where organized files go). Add an explicit source directory:

- OrganizeOptions gains SourcePath; DestPath replaces the old TargetPath
  (destination) for clarity. New organize.source_dir setting + source_path
  request override; resolveSourceRoot falls back to library path.
- OrganizeLibraryWithOptions only organizes media located under the source
  root, so operators can point at a specific download/staging folder and
  organize into a distinct destination.
- Handler accepts source_path/dest_path (target_path kept as a deprecated
  alias for the destination, backward compatible).
- Settings page splits into 整理源目录(待整理) + 整理目的地目录; Tools organize
  panel exposes 源目录 + 目的地目录 inputs with clear copy.

Defaults are unchanged (source = destination = library path) so existing
setups behave identically. Adds a regression test that organize is scoped
to the source directory.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-05-30 13:02:36 +08:00

86 lines
2.8 KiB
Go

package service
import (
"os"
"path/filepath"
"strings"
"testing"
"go.uber.org/zap"
"github.com/ShukeBta/MediaStationGo/internal/config"
"github.com/ShukeBta/MediaStationGo/internal/model"
)
// TestOrganizeLibraryScopesToSourceDir verifies organize reads from the
// source directory (源目录) and writes to the destination directory (目的地目录):
// only media located under SourcePath are organized; media outside it are
// left untouched.
func TestOrganizeLibraryScopesToSourceDir(t *testing.T) {
root := t.TempDir()
srcDir := filepath.Join(root, "downloads")
otherDir := filepath.Join(root, "elsewhere")
dest := filepath.Join(root, "library")
for _, d := range []string{srcDir, otherDir} {
if err := os.MkdirAll(d, 0o755); err != nil {
t.Fatal(err)
}
}
inSource := filepath.Join(srcDir, "In Source.mkv")
outside := filepath.Join(otherDir, "Outside.mkv")
if err := os.WriteFile(inSource, []byte("a"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(outside, []byte("b"), 0o644); err != nil {
t.Fatal(err)
}
repos := newOrganizerTestRepo(t)
lib := model.Library{Name: "Movies", Path: root, Type: "movie", Enabled: true}
if err := repos.Library.Create(t.Context(), &lib); err != nil {
t.Fatal(err)
}
mIn := model.Media{LibraryID: lib.ID, Title: "In Source", Path: inSource, Year: 2020, Container: "mkv", ScrapeStatus: "matched"}
mOut := model.Media{LibraryID: lib.ID, Title: "Outside", Path: outside, Year: 2021, Container: "mkv", ScrapeStatus: "matched"}
if err := repos.Media.Upsert(t.Context(), &mIn); err != nil {
t.Fatal(err)
}
if err := repos.Media.Upsert(t.Context(), &mOut); err != nil {
t.Fatal(err)
}
org := NewOrganizerService(&config.Config{}, zap.NewNop(), repos)
res, err := org.OrganizeLibraryWithOptions(t.Context(), lib.ID, OrganizeOptions{
SourcePath: srcDir,
DestPath: dest,
TransferMode: TransferCopy,
})
if err != nil {
t.Fatalf("organize: %v", err)
}
if res.Organized != 1 {
t.Fatalf("expected exactly 1 organized (only the in-source media), got %d (skipped %d)", res.Organized, res.Skipped)
}
// The in-source media should now live under the destination dir.
gotIn, err := repos.Media.FindByID(t.Context(), mIn.ID)
if err != nil || gotIn == nil {
t.Fatalf("reload in-source media: %v", err)
}
if !strings.HasPrefix(gotIn.Path, dest) {
t.Fatalf("in-source media should be organized under dest %q, got %q", dest, gotIn.Path)
}
// The outside media must be left untouched (not moved into dest).
gotOut, err := repos.Media.FindByID(t.Context(), mOut.ID)
if err != nil || gotOut == nil {
t.Fatalf("reload outside media: %v", err)
}
if gotOut.Path != outside {
t.Fatalf("outside media must not be organized; path changed to %q", gotOut.Path)
}
if _, err := os.Stat(outside); err != nil {
t.Fatalf("outside source file must remain in place: %v", err)
}
}