fix: stabilize library paths and subscription matching

This commit is contained in:
ShukeBta
2026-06-28 12:15:10 +08:00
parent e4abb03ca5
commit a97dc35790
102 changed files with 4582 additions and 1519 deletions
@@ -120,7 +120,7 @@ func TestScanRootCloudLibraryCreatesAutoCategoryLibraries(t *testing.T) {
wantLibraries := map[string]string{
"cloud://openlist/电视剧/欧美剧/The Show/The.Show.S01E01.mkv": byDisplayDir["电视剧/欧美剧"].ID,
"cloud://openlist/电影/华语电影/Movie.2024.mkv": byDisplayDir["电影/华语电影"].ID,
"cloud://openlist/国漫/剑来/剑来.S01E01.mkv": byDisplayDir["动漫/国漫"].ID,
"cloud://openlist/动漫/国漫/剑来/剑来.S01E01.mkv": byDisplayDir["动漫/国漫"].ID,
}
for _, row := range rows {
if row.LibraryID != wantLibraries[row.Path] {
@@ -162,6 +162,203 @@ func TestScanRootCloudLibraryCreatesAutoCategoryLibraries(t *testing.T) {
}
}
func TestScanRootCloudAutoCategoryAppendsExistingLibraryRoot(t *testing.T) {
upstream := newOpenListAPIServer(t, func(path string, page, perPage int) ([]openListTestEntry, int) {
switch path {
case "/":
return []openListTestEntry{{Name: "电影", IsDir: true}}, 1
case "/电影":
return []openListTestEntry{{Name: "华语电影", IsDir: true}}, 1
case "/电影/华语电影":
return []openListTestEntry{{Name: "Movie.2024.mkv", Size: 202}}, 1
default:
t.Fatalf("unexpected openlist path %q", path)
return nil, 0
}
})
defer upstream.Close()
db := newServiceTestDB(t, &model.Library{}, &model.LibraryRoot{}, &model.Media{}, &model.Setting{}, &model.StorageConfig{})
repos := repository.New(db)
storage := newOpenListStorageForTest(t, repos, upstream.URL)
local := model.Library{Name: "华语电影", Path: "/media/电影/华语电影", Type: "movie", Enabled: true}
if err := repos.Library.CreateWithRoots(t.Context(), &local, []model.LibraryRoot{{
Name: "华语电影",
Path: local.Path,
Enabled: true,
}}); err != nil {
t.Fatal(err)
}
root := model.Library{Name: "OpenList", Path: "cloud://openlist", Type: "movie", Enabled: true}
if err := repos.Library.Create(t.Context(), &root); err != nil {
t.Fatal(err)
}
scanner := NewScannerService(&config.Config{}, zap.NewNop(), repos, NewHub(zap.NewNop()), nil, nil)
scanner.SetStorageConfig(storage)
res, err := scanner.ScanLibrary(t.Context(), root.ID)
if err != nil {
t.Fatalf("scan root cloud: %v", err)
}
if res.Added != 1 {
t.Fatalf("added = %d, want 1", res.Added)
}
libs, err := repos.Library.List(t.Context())
if err != nil {
t.Fatal(err)
}
for _, lib := range libs {
if CloudLibraryAutoCategory(lib) {
t.Fatalf("auto category should append to existing library, got extra library %#v", lib)
}
}
roots, err := repos.Library.ListRoots(t.Context(), local.ID)
if err != nil {
t.Fatal(err)
}
if len(roots) != 2 {
t.Fatalf("roots = %#v, want local root plus cloud root", roots)
}
cloudRoot := roots[1]
if cloudRoot.Name != "华语电影" || !CloudLibraryAutoCategory(model.Library{Path: cloudRoot.Path}) {
t.Fatalf("cloud root = %#v, want auto-category 华语电影 root", cloudRoot)
}
info, ok := ParseCloudLibraryMount(cloudRoot.Path)
if !ok || info.DisplayDir != "电影/华语电影" || info.ScanDir != "电影/华语电影" {
t.Fatalf("cloud root mount = %#v, want display/scan 电影/华语电影", info)
}
var media model.Media
if err := repos.DB.First(&media, "path = ?", "cloud://openlist/电影/华语电影/Movie.2024.mkv").Error; err != nil {
t.Fatal(err)
}
if media.LibraryID != local.ID || media.LibraryRootID != cloudRoot.ID {
t.Fatalf("media placement = library %s root %s, want %s/%s", media.LibraryID, media.LibraryRootID, local.ID, cloudRoot.ID)
}
}
func TestScanRootCloudAutoCategoryPreservesFlatScanDir(t *testing.T) {
upstream := newOpenListAPIServer(t, func(path string, page, perPage int) ([]openListTestEntry, int) {
switch path {
case "/":
return []openListTestEntry{{Name: "国漫", IsDir: true}}, 1
case "/国漫":
return []openListTestEntry{{Name: "剑来", IsDir: true}}, 1
case "/国漫/剑来":
return []openListTestEntry{{Name: "剑来.S01E01.mkv", Size: 303}}, 1
default:
t.Fatalf("unexpected openlist path %q", path)
return nil, 0
}
})
defer upstream.Close()
db := newServiceTestDB(t, &model.Library{}, &model.LibraryRoot{}, &model.Media{}, &model.Setting{}, &model.StorageConfig{})
repos := repository.New(db)
storage := newOpenListStorageForTest(t, repos, upstream.URL)
local := model.Library{Name: "国漫", Path: "/media/动漫/国漫", Type: "anime", Enabled: true}
if err := repos.Library.CreateWithRoots(t.Context(), &local, []model.LibraryRoot{{
Name: "国漫",
Path: local.Path,
Enabled: true,
}}); err != nil {
t.Fatal(err)
}
root := model.Library{Name: "OpenList", Path: "cloud://openlist", Type: "movie", Enabled: true}
if err := repos.Library.Create(t.Context(), &root); err != nil {
t.Fatal(err)
}
scanner := NewScannerService(&config.Config{}, zap.NewNop(), repos, NewHub(zap.NewNop()), nil, nil)
scanner.SetStorageConfig(storage)
if _, err := scanner.ScanLibrary(t.Context(), root.ID); err != nil {
t.Fatalf("scan root cloud: %v", err)
}
roots, err := repos.Library.ListRoots(t.Context(), local.ID)
if err != nil {
t.Fatal(err)
}
if len(roots) != 2 {
t.Fatalf("roots = %#v, want local root plus flat cloud root", roots)
}
cloudRoot := roots[1]
info, ok := ParseCloudLibraryMount(cloudRoot.Path)
if !ok || info.DisplayDir != "动漫/国漫" || info.ScanDir != "国漫" {
t.Fatalf("flat cloud root mount = %#v, want display 动漫/国漫 and scan 国漫", info)
}
res, err := scanner.ScanLibraryRoot(t.Context(), local.ID, cloudRoot.ID)
if err != nil {
t.Fatalf("scan flat cloud root: %v", err)
}
if res.Skipped != 1 && res.Updated != 1 {
t.Fatalf("flat cloud root rescan = %#v, want existing media refreshed/skipped", res)
}
}
func TestScanRootCloudAutoCategoryMigratesExistingAutoLibrary(t *testing.T) {
upstream := newOpenListAPIServer(t, func(path string, page, perPage int) ([]openListTestEntry, int) {
switch path {
case "/":
return []openListTestEntry{{Name: "电视剧", IsDir: true}}, 1
case "/电视剧":
return []openListTestEntry{{Name: "欧美剧", IsDir: true}}, 1
case "/电视剧/欧美剧":
return []openListTestEntry{{Name: "The Show", IsDir: true}}, 1
case "/电视剧/欧美剧/The Show":
return []openListTestEntry{{Name: "The.Show.S01E01.mkv", Size: 101}}, 1
default:
t.Fatalf("unexpected openlist path %q", path)
return nil, 0
}
})
defer upstream.Close()
db := newServiceTestDB(t, &model.Library{}, &model.LibraryRoot{}, &model.Media{}, &model.Setting{}, &model.StorageConfig{})
repos := repository.New(db)
storage := newOpenListStorageForTest(t, repos, upstream.URL)
local := model.Library{Name: "欧美剧", Path: "/media/电视剧/欧美剧", Type: "tv", Enabled: true}
if err := repos.Library.CreateWithRoots(t.Context(), &local, []model.LibraryRoot{{
Name: "欧美剧",
Path: local.Path,
Enabled: true,
}}); err != nil {
t.Fatal(err)
}
root := model.Library{Name: "OpenList", Path: "cloud://openlist", Type: "movie", Enabled: true}
oldAuto := model.Library{Name: "欧美剧", Path: BuildCloudAutoCategoryLibraryPath("openlist", "电视剧/欧美剧"), Type: "tv", Enabled: true}
for _, lib := range []*model.Library{&root, &oldAuto} {
if err := repos.Library.Create(t.Context(), lib); err != nil {
t.Fatal(err)
}
}
mediaPath := "cloud://openlist/电视剧/欧美剧/The Show/The.Show.S01E01.mkv"
if err := repos.DB.Create(&model.Media{LibraryID: oldAuto.ID, Title: "The Show", Path: mediaPath}).Error; err != nil {
t.Fatal(err)
}
scanner := NewScannerService(&config.Config{}, zap.NewNop(), repos, NewHub(zap.NewNop()), nil, nil)
scanner.SetStorageConfig(storage)
if _, err := scanner.ScanLibrary(t.Context(), root.ID); err != nil {
t.Fatalf("scan root cloud: %v", err)
}
if old, err := repos.Library.FindByID(t.Context(), oldAuto.ID); err != nil || old != nil {
t.Fatalf("old auto library = %#v, err=%v; want removed", old, err)
}
roots, err := repos.Library.ListRoots(t.Context(), local.ID)
if err != nil {
t.Fatal(err)
}
if len(roots) != 2 {
t.Fatalf("roots = %#v, want local root plus migrated cloud root", roots)
}
var media model.Media
if err := repos.DB.First(&media, "path = ?", mediaPath).Error; err != nil {
t.Fatal(err)
}
if media.LibraryID != local.ID || media.LibraryRootID != roots[1].ID {
t.Fatalf("migrated media placement = %s/%s, want %s/%s", media.LibraryID, media.LibraryRootID, local.ID, roots[1].ID)
}
}
func TestScanCloudLibraryListsChildDirectoriesConcurrently(t *testing.T) {
var active int32
var maxActive int32
@@ -242,3 +439,19 @@ func TestScanCloudLibraryListsChildDirectoriesConcurrently(t *testing.T) {
t.Fatalf("scan result = %#v, want visited=2 added=2", res)
}
}
func newOpenListStorageForTest(t *testing.T, repos *repository.Container, serverURL string) *StorageConfigService {
t.Helper()
log := zap.NewNop()
storage := NewStorageConfigService(log, repos, NewCryptoService("", log))
if _, err := storage.Save(t.Context(), StorageInput{
Type: "openlist",
Config: map[string]any{
"server": serverURL,
"token": "openlist-token",
},
}); err != nil {
t.Fatal(err)
}
return storage
}