From f386333b9cd56d7e3bef347145fc21d3fe895d11 Mon Sep 17 00:00:00 2001 From: ShukeBta <272197458+ShukeBta@users.noreply.github.com> Date: Sat, 27 Jun 2026 17:58:37 +0800 Subject: [PATCH] fix emby cloud library merging --- internal/service/cloud_mount_filter.go | 48 +++++++++- internal/service/cloud_mount_filter_test.go | 55 ++++++++++++ .../service/emby_cloud_library_merge_test.go | 88 +++++++++++++++++++ 3 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 internal/service/emby_cloud_library_merge_test.go diff --git a/internal/service/cloud_mount_filter.go b/internal/service/cloud_mount_filter.go index 0f0159b..4657441 100644 --- a/internal/service/cloud_mount_filter.go +++ b/internal/service/cloud_mount_filter.go @@ -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 } diff --git a/internal/service/cloud_mount_filter_test.go b/internal/service/cloud_mount_filter_test.go index 4855cbb..e9a48a5 100644 --- a/internal/service/cloud_mount_filter_test.go +++ b/internal/service/cloud_mount_filter_test.go @@ -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) diff --git a/internal/service/emby_cloud_library_merge_test.go b/internal/service/emby_cloud_library_merge_test.go new file mode 100644 index 0000000..745f5fd --- /dev/null +++ b/internal/service/emby_cloud_library_merge_test.go @@ -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) + } +}