mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-06 13:26:38 +08:00
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>
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user