fix emby cloud library merging

This commit is contained in:
ShukeBta
2026-06-27 17:58:37 +08:00
parent 1ee10caa5a
commit f386333b9c
3 changed files with 190 additions and 1 deletions
+47 -1
View File
@@ -231,5 +231,51 @@ func CloudLibraryMergeKey(lib model.Library) (string, bool) {
if name == "" {
return "", false
}
return strings.ToLower(strings.TrimSpace(lib.Type)) + "\x00" + name, true
typeKey := cloudLibraryMergeTypeKey(lib.Type)
return typeKey + "\x00" + cloudLibraryMergeNameKey(typeKey, name), true
}
func cloudLibraryMergeTypeKey(libraryType string) string {
switch strings.ToLower(strings.TrimSpace(libraryType)) {
case "tv", "anime", "variety":
return "tvshows"
default:
return strings.ToLower(strings.TrimSpace(libraryType))
}
}
func cloudLibraryMergeNameKey(typeKey, name string) string {
switch typeKey {
case "movie":
switch name {
case "国产电影", "大陆电影", "华语电影":
return "华语电影"
case "外语电影", "欧美电影", "日韩电影", "日本电影", "韩国电影":
return "外语电影"
case "纪录", "纪录片":
return "纪录片"
case "演唱会", "concert":
return "演唱会"
case "动画电影", "动漫电影":
return "动画电影"
}
case "tvshows":
switch name {
case "国产剧", "大陆剧", "华语剧", "国剧":
return "国产剧"
case "欧美剧", "美剧", "英剧":
return "欧美剧"
case "日韩剧", "日剧", "韩剧":
return "日韩剧"
case "国漫", "国产动漫", "国产动画":
return "国漫"
case "日番", "日漫", "番剧", "日本动漫", "日本动画":
return "日番"
case "欧美动漫", "欧美动画", "西方动画":
return "欧美动漫"
case "纪录", "纪录片":
return "纪录片"
}
}
return name
}
@@ -84,6 +84,61 @@ func TestFilterDisplayCloudLibrariesMergesCloudMountIntoExistingLibrary(t *testi
}
}
func TestFilterDisplayCloudLibrariesMergesEpisodicTypeAliases(t *testing.T) {
db := newServiceTestDB(t, &model.Library{}, &model.Media{})
repos := repository.New(db)
local := model.Library{Name: "国漫", Path: "/media/动漫/国漫", Type: "tv", Enabled: true}
cloud := model.Library{Name: "OpenList · 国漫", Path: BuildCloudLibraryPath("openlist", "/国漫", "/国漫"), Type: "anime", Enabled: true}
movie := model.Library{Name: "国漫", Path: BuildCloudLibraryPath("openlist", "/电影/国漫", "/电影/国漫"), Type: "movie", Enabled: true}
for _, lib := range []*model.Library{&local, &cloud, &movie} {
if err := repos.Library.Create(t.Context(), lib); err != nil {
t.Fatal(err)
}
}
filtered := FilterDisplayCloudLibraries(t.Context(), repos, []model.Library{local, cloud, movie})
if got := libraryNames(filtered); !slices.Equal(got, []string{"国漫", "国漫"}) {
t.Fatalf("filtered names = %#v, want local episodic plus separate movie library", got)
}
if filtered[0].ID != local.ID || filtered[1].ID != movie.ID {
t.Fatalf("filtered libraries = %#v, want anime cloud merged into local tv but movie kept", filtered)
}
merged := MergedLibraryIDs([]model.Library{local, cloud, movie}, local)
if !slices.Equal(merged, []string{local.ID, cloud.ID}) {
t.Fatalf("merged ids = %#v, want local tv + cloud anime only", merged)
}
}
func TestFilterDisplayCloudLibrariesMergesCategoryNameAliases(t *testing.T) {
db := newServiceTestDB(t, &model.Library{}, &model.Media{})
repos := repository.New(db)
foreignMovie := model.Library{Name: "外语电影", Path: "/media/电影/外语电影", Type: "movie", Enabled: true}
westernMovie := model.Library{Name: "OpenList · 欧美电影", Path: BuildCloudLibraryPath("openlist", "/欧美电影", "/欧美电影"), Type: "movie", Enabled: true}
eastAsianMovie := model.Library{Name: "OpenList · 日韩电影", Path: BuildCloudLibraryPath("openlist", "/日韩电影", "/日韩电影"), Type: "movie", Enabled: true}
jpAnime := model.Library{Name: "日番", Path: "/media/动漫/日番", Type: "tv", Enabled: true}
jpAnimeCloud := model.Library{Name: "OpenList · 日漫", Path: BuildCloudLibraryPath("openlist", "/日漫", "/日漫"), Type: "anime", Enabled: true}
for _, lib := range []*model.Library{&foreignMovie, &westernMovie, &eastAsianMovie, &jpAnime, &jpAnimeCloud} {
if err := repos.Library.Create(t.Context(), lib); err != nil {
t.Fatal(err)
}
}
filtered := FilterDisplayCloudLibraries(t.Context(), repos, []model.Library{foreignMovie, westernMovie, eastAsianMovie, jpAnime, jpAnimeCloud})
if got := libraryNames(filtered); !slices.Equal(got, []string{"外语电影", "日番"}) {
t.Fatalf("filtered names = %#v, want user-facing alias libraries only", got)
}
movieMerged := MergedLibraryIDs([]model.Library{foreignMovie, westernMovie, eastAsianMovie, jpAnime, jpAnimeCloud}, foreignMovie)
if !slices.Equal(movieMerged, []string{foreignMovie.ID, westernMovie.ID, eastAsianMovie.ID}) {
t.Fatalf("movie merged ids = %#v, want foreign movie aliases", movieMerged)
}
animeMerged := MergedLibraryIDs([]model.Library{foreignMovie, westernMovie, eastAsianMovie, jpAnime, jpAnimeCloud}, jpAnime)
if !slices.Equal(animeMerged, []string{jpAnime.ID, jpAnimeCloud.ID}) {
t.Fatalf("anime merged ids = %#v, want jp anime aliases", animeMerged)
}
}
func TestFilterDeprecatedNativeCloudLibrariesHidesPopulatedHistory(t *testing.T) {
db := newServiceTestDB(t, &model.Library{}, &model.Media{})
repos := repository.New(db)
@@ -0,0 +1,88 @@
package service
import (
"testing"
"github.com/ShukeBta/MediaStationGo/internal/model"
)
func TestEmbyViewsMergeEpisodicCloudLibrariesIntoUserLibrary(t *testing.T) {
svc := newTestEmbyService(t)
local := model.Library{Name: "国漫", Path: "/media/动漫/国漫", Type: "tv", Enabled: true}
cloud := model.Library{Name: "OpenList · 国漫", Path: BuildCloudLibraryPath("openlist", "/国漫", "/国漫"), Type: "anime", Enabled: true}
for _, lib := range []*model.Library{&local, &cloud} {
if err := svc.repo.Library.Create(t.Context(), lib); err != nil {
t.Fatalf("create library: %v", err)
}
}
if err := svc.repo.DB.Create(&model.Media{
Base: model.Base{ID: "cloud-show-1"},
LibraryID: cloud.ID,
Title: "云盘国漫",
Path: "cloud://openlist/国漫/云盘国漫/Season 01/云盘国漫.S01E01.mkv",
SeasonNum: 1,
EpisodeNum: 1,
}).Error; err != nil {
t.Fatalf("create media: %v", err)
}
views, err := svc.Views(t.Context(), "user-1")
if err != nil {
t.Fatalf("views: %v", err)
}
viewItems := views["Items"].([]map[string]any)
if len(viewItems) != 1 {
t.Fatalf("emby views = %#v, want one merged user-facing library", viewItems)
}
if viewItems[0]["Id"] != local.ID || viewItems[0]["Name"] != "国漫" {
t.Fatalf("merged view should use local library identity, got %#v", viewItems[0])
}
items, err := svc.Items(t.Context(), ItemsParams{ParentID: local.ID, Recursive: true, IncludeItemTypes: []string{"Episode"}, Limit: 50})
if err != nil {
t.Fatalf("items: %v", err)
}
episodes := items["Items"].([]map[string]any)
if len(episodes) != 1 || episodes[0]["Id"] != "cloud-show-1" {
t.Fatalf("merged local library should include cloud episodes, got %#v", episodes)
}
}
func TestEmbyViewsMergeCloudCategoryAliasesIntoUserLibrary(t *testing.T) {
svc := newTestEmbyService(t)
local := model.Library{Name: "日番", Path: "/media/动漫/日番", Type: "tv", Enabled: true}
cloud := model.Library{Name: "OpenList · 日漫", Path: BuildCloudLibraryPath("openlist", "/日漫", "/日漫"), Type: "anime", Enabled: true}
for _, lib := range []*model.Library{&local, &cloud} {
if err := svc.repo.Library.Create(t.Context(), lib); err != nil {
t.Fatalf("create library: %v", err)
}
}
if err := svc.repo.DB.Create(&model.Media{
Base: model.Base{ID: "cloud-anime-1"},
LibraryID: cloud.ID,
Title: "云盘日漫",
Path: "cloud://openlist/日漫/云盘日漫/Season 01/云盘日漫.S01E01.mkv",
SeasonNum: 1,
EpisodeNum: 1,
}).Error; err != nil {
t.Fatalf("create media: %v", err)
}
views, err := svc.Views(t.Context(), "user-1")
if err != nil {
t.Fatalf("views: %v", err)
}
viewItems := views["Items"].([]map[string]any)
if len(viewItems) != 1 || viewItems[0]["Id"] != local.ID || viewItems[0]["Name"] != "日番" {
t.Fatalf("emby views = %#v, want cloud alias merged into local 日番", viewItems)
}
items, err := svc.Items(t.Context(), ItemsParams{ParentID: local.ID, Recursive: true, IncludeItemTypes: []string{"Episode"}, Limit: 50})
if err != nil {
t.Fatalf("items: %v", err)
}
episodes := items["Items"].([]map[string]any)
if len(episodes) != 1 || episodes[0]["Id"] != "cloud-anime-1" {
t.Fatalf("merged local 日番 library should include 日漫 cloud episodes, got %#v", episodes)
}
}