mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 03:06:38 +08:00
b0fe40142a
* Rebrand MMTL to MeBox across codebase and assets Rename the project display name, Go module path, environment variable prefix (MEBOX_*), Docker image references, and UI branding from MMTL/mmtl to MeBox/mebox. Replace logo assets with the new MeBox icon and keep legacy SQLite migration support for existing mmtl.db deployments. Co-authored-by: truewhile <truewhile@users.noreply.github.com> * Fix logo icons: use cube-only crop without truncated text Previous icon generation cropped too much of the source image, including partial MeBox wordmark text that was cut off in square icon containers. Regenerate logo-64/192/512, favicon, and SVG from cube-only region. Co-authored-by: truewhile <truewhile@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: truewhile <truewhile@users.noreply.github.com>
131 lines
4.4 KiB
Go
131 lines
4.4 KiB
Go
package service
|
|
|
|
import (
|
|
"errors"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/truewhile/MeBox/internal/model"
|
|
)
|
|
|
|
func TestOrganizeScanRootUsesActualOrganizedTarget(t *testing.T) {
|
|
target := filepath.Join(string(filepath.Separator), "media", "电影", "动画电影", "Big Buck Bunny (2008)", "Big Buck Bunny (2008).mp4")
|
|
res := &OrganizeResult{
|
|
DestPath: filepath.Join(string(filepath.Separator), "media"),
|
|
Items: []OrganizePreviewItem{{
|
|
Target: target,
|
|
Action: "organize",
|
|
}},
|
|
}
|
|
|
|
want := filepath.Dir(target)
|
|
if got := organizeScanRoot(res, ""); got != want {
|
|
t.Fatalf("organizeScanRoot() = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestOrganizeScanRootUsesCommonAffectedCategoryRoot(t *testing.T) {
|
|
root := filepath.Join(string(filepath.Separator), "media", "电影", "动画电影")
|
|
res := &OrganizeResult{
|
|
DestPath: filepath.Join(string(filepath.Separator), "media"),
|
|
Items: []OrganizePreviewItem{
|
|
{Target: filepath.Join(root, "Movie A (2026)", "Movie A (2026).mp4"), Action: "organize"},
|
|
{Target: filepath.Join(root, "Movie B (2026)", "Movie B (2026).mp4"), Action: "organize"},
|
|
},
|
|
}
|
|
|
|
if got := organizeScanRoot(res, ""); got != root {
|
|
t.Fatalf("organizeScanRoot() = %q, want %q", got, root)
|
|
}
|
|
}
|
|
|
|
func TestOrganizePipelineFailsWhenEveryOrganizeItemErrors(t *testing.T) {
|
|
root := t.TempDir()
|
|
src := filepath.Join(root, "downloads")
|
|
dest := filepath.Join(root, "media")
|
|
writeOrgFile(t, filepath.Join(src, "Dune 2021 1080p.mkv"), "movie")
|
|
|
|
origLinkFile := linkFile
|
|
linkFile = func(_, _ string) error {
|
|
return errors.New("cross-device link")
|
|
}
|
|
t.Cleanup(func() { linkFile = origLinkFile })
|
|
|
|
repos := newOrganizerTestRepo(t)
|
|
organizer := NewOrganizerService(nil, zap.NewNop(), repos)
|
|
tasks := NewTaskTrackerService(zap.NewNop(), nil)
|
|
pipeline := NewOrganizePipelineService(zap.NewNop(), repos, organizer, nil, tasks)
|
|
|
|
_, err := pipeline.Run(t.Context(), OrganizePipelineRequest{
|
|
Scope: OrganizeScopeDirectory,
|
|
Trigger: OrganizeTriggerScheduled,
|
|
SourcePath: src,
|
|
DestPath: dest,
|
|
TransferMode: string(TransferHardlink),
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected organize pipeline to fail when every item errors")
|
|
}
|
|
if !strings.Contains(err.Error(), "organize failed") || !strings.Contains(err.Error(), "hardlink failed") {
|
|
t.Fatalf("error = %q, want organize failure with hardlink detail", err.Error())
|
|
}
|
|
snap := tasks.Snapshot()
|
|
if len(snap.Recent) != 1 {
|
|
t.Fatalf("recent tasks = %d, want 1", len(snap.Recent))
|
|
}
|
|
task := snap.Recent[0]
|
|
if task.Status != TaskStatusFailed {
|
|
t.Fatalf("task status = %q, want failed", task.Status)
|
|
}
|
|
if !strings.Contains(task.Error, "hardlink failed") {
|
|
t.Fatalf("task error = %q, want hardlink detail", task.Error)
|
|
}
|
|
if len(task.Details) == 0 || !strings.Contains(task.Details[0], "cross-device link") {
|
|
t.Fatalf("task details = %#v, want transfer error detail", task.Details)
|
|
}
|
|
}
|
|
|
|
func TestOrganizePipelinePrefersCurrentMediaLibraryAfterMediaOrganize(t *testing.T) {
|
|
repos := newOrganizerTestRepo(t)
|
|
sourceLib := model.Library{Name: "待整理", Path: filepath.Join(t.TempDir(), "incoming"), Type: "tv", Enabled: true}
|
|
targetLib := model.Library{Name: "国产剧", Path: filepath.Join(t.TempDir(), "media", "电视剧", "国产剧"), Type: "tv", Enabled: true}
|
|
if err := repos.Library.Create(t.Context(), &sourceLib); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repos.Library.Create(t.Context(), &targetLib); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
media := model.Media{
|
|
LibraryID: targetLib.ID,
|
|
Title: "南部档案",
|
|
Path: filepath.Join(targetLib.Path, "南部档案", "Season 01", "南部档案 - S01E01.mkv"),
|
|
SeasonNum: 1,
|
|
EpisodeNum: 1,
|
|
ScrapeStatus: "matched",
|
|
}
|
|
if err := repos.Media.Upsert(t.Context(), &media); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
pipeline := NewOrganizePipelineService(zap.NewNop(), repos, nil, nil, nil)
|
|
got := pipeline.scanPreferredLibraryID(t.Context(), OrganizePipelineRequest{
|
|
Scope: OrganizeScopeMedia,
|
|
MediaID: media.ID,
|
|
})
|
|
if got != targetLib.ID {
|
|
t.Fatalf("preferred library = %q, want current media library %q", got, targetLib.ID)
|
|
}
|
|
|
|
got = pipeline.scanPreferredLibraryID(t.Context(), OrganizePipelineRequest{
|
|
Scope: OrganizeScopeMedia,
|
|
MediaID: media.ID,
|
|
PreferredLibraryID: sourceLib.ID,
|
|
})
|
|
if got != sourceLib.ID {
|
|
t.Fatalf("explicit preferred library = %q, want %q", got, sourceLib.ID)
|
|
}
|
|
}
|