From a02583ee7d47c9fe233893b173aac59814025e7e Mon Sep 17 00:00:00 2001 From: truewhile <62226914+truewhile@users.noreply.github.com> Date: Wed, 2 Sep 2026 19:00:01 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/service/emby_remote_web.go | 21 ++++-- internal/service/media_versions.go | 26 ++++++++ internal/service/media_versions_test.go | 46 +++++++++++++ web/src/components/LayoutHeaderSections.tsx | 13 +++- web/src/pages/MediaDetailPage.tsx | 17 +++++ web/src/pages/MediaDetailPageSections.tsx | 17 +++-- web/src/pages/useMediaDetailPageState.ts | 74 ++++++++++++++++++++- 7 files changed, 199 insertions(+), 15 deletions(-) diff --git a/internal/service/emby_remote_web.go b/internal/service/emby_remote_web.go index d1334da..27f1584 100644 --- a/internal/service/emby_remote_web.go +++ b/internal/service/emby_remote_web.go @@ -301,11 +301,22 @@ func (r *EmbyRemoteService) MapRemoteItemToMedia(ctx context.Context, mount *mod media.SeasonNum = 0 media.EpisodeNum = 0 } - if mount != nil && strings.TrimSpace(mount.RemoteViewID) != "" { - libID := EncodeEmbyRemoteID(mount.ID, mount.RemoteViewID) - media.DisplayLibraryID = libID - media.LibraryID = libID - } + if mount != nil && strings.TrimSpace(mount.RemoteViewID) != "" { + libID := EncodeEmbyRemoteID(mount.ID, mount.RemoteViewID) + media.DisplayLibraryID = libID + media.LibraryID = libID + libName := strings.TrimSpace(mount.Name) + if libName == "" { + libName = strings.TrimSpace(mount.RemoteViewName) + } + if libName == "" && acct != nil { + libName = acct.Name + } else if acct != nil && acct.Name != "" && !strings.Contains(libName, acct.Name) { + libName = acct.Name + " · " + libName + } + media.LibraryName = libName + media.DisplayLibraryName = libName + } return media } diff --git a/internal/service/media_versions.go b/internal/service/media_versions.go index 92997ac..aea4764 100644 --- a/internal/service/media_versions.go +++ b/internal/service/media_versions.go @@ -120,6 +120,11 @@ func GroupMediaVersions(items []model.Media) []MediaItem { } func mediaVersionGroupKey(m model.Media) string { + // 远程 Emby 挂载条目保持独立,不与其它远程条目或本地条目折叠合并。 + if IsEmbyRemoteID(m.ID) { + return fmt.Sprintf("embyremote:%s", m.ID) + } + if m.SeasonNum > 0 || m.EpisodeNum > 0 { switch { case m.TMDbID > 0: @@ -146,14 +151,32 @@ func mediaVersionGroupKey(m model.Media) string { fmt.Sprintf("%d:%d", m.SeasonNum, m.EpisodeNum), }, "|") } + + libKey := strings.ToLower(strings.TrimSpace(m.LibraryID)) + if libKey == "" { + libKey = strings.ToLower(strings.TrimSpace(m.DisplayLibraryID)) + } + switch { case m.TMDbID > 0: + if libKey != "" { + return fmt.Sprintf("movie:%s:tmdb:%d", libKey, m.TMDbID) + } return fmt.Sprintf("tmdb:%d", m.TMDbID) case m.BangumiID > 0: + if libKey != "" { + return fmt.Sprintf("movie:%s:bangumi:%d", libKey, m.BangumiID) + } return fmt.Sprintf("bangumi:%d", m.BangumiID) case strings.TrimSpace(m.DoubanID) != "": + if libKey != "" { + return fmt.Sprintf("movie:%s:douban:%s", libKey, strings.ToLower(strings.TrimSpace(m.DoubanID))) + } return "douban:" + strings.ToLower(strings.TrimSpace(m.DoubanID)) case strings.TrimSpace(m.TheTVDBID) != "": + if libKey != "" { + return fmt.Sprintf("movie:%s:thetvdb:%s", libKey, strings.ToLower(strings.TrimSpace(m.TheTVDBID))) + } return "thetvdb:" + strings.ToLower(strings.TrimSpace(m.TheTVDBID)) } title := firstNonEmpty(m.OriginalName, m.Title) @@ -173,6 +196,9 @@ func mediaVersionGroupKey(m model.Media) string { if year <= 0 { _, year = CleanQuery(m.Path) } + if libKey != "" { + return fmt.Sprintf("movie:%s:%s:%d", libKey, title, year) + } return fmt.Sprintf("movie:%s:%d", title, year) } diff --git a/internal/service/media_versions_test.go b/internal/service/media_versions_test.go index d5bc044..f93175d 100644 --- a/internal/service/media_versions_test.go +++ b/internal/service/media_versions_test.go @@ -218,3 +218,49 @@ func TestSearchMediaVisiblePageGroupedPaginatesAfterVersionGrouping(t *testing.T t.Fatalf("primary version = %q, want %q", page[0].Media.Path, rows[0].Path) } } + +func TestGroupMediaVersionsDoesNotMergeDifferentRemoteSeries(t *testing.T) { + // 模拟来自两个不同挂载库(如 00 新番连载 与 2018 动漫)的同名剧集《碧蓝之海》 + season1 := model.Media{ + Base: model.Base{ID: EncodeEmbyRemoteID("mount-1", "156019")}, + LibraryID: EncodeEmbyRemoteID("mount-1", "view-1"), + Title: "碧蓝之海", + Year: 2018, + LibraryName: "2018 动漫", + } + season2 := model.Media{ + Base: model.Base{ID: EncodeEmbyRemoteID("mount-2", "156030")}, + LibraryID: EncodeEmbyRemoteID("mount-2", "view-2"), + Title: "碧蓝之海", + Year: 2024, + LibraryName: "00 新番连载", + } + + grouped := groupMediaVersions([]model.Media{season1, season2}) + if len(grouped) != 2 { + t.Fatalf("expected 2 separate groups for different remote series/libraries, got %d: %#v", len(grouped), grouped) + } + if grouped[0].Title != "碧蓝之海" || grouped[1].Title != "碧蓝之海" { + t.Fatalf("expected both titles to be '碧蓝之海'") + } +} + +func TestGroupMediaVersionsDoesNotMergeAcrossDifferentLibrariesForMovies(t *testing.T) { + movieLib1 := model.Media{ + Base: model.Base{ID: "m-1"}, + LibraryID: "lib-1", + Title: "碧蓝之海", + Year: 2018, + } + movieLib2 := model.Media{ + Base: model.Base{ID: "m-2"}, + LibraryID: "lib-2", + Title: "碧蓝之海", + Year: 2018, + } + + grouped := groupMediaVersions([]model.Media{movieLib1, movieLib2}) + if len(grouped) != 2 { + t.Fatalf("expected 2 separate groups for movies in different libraries, got %d", len(grouped)) + } +} diff --git a/web/src/components/LayoutHeaderSections.tsx b/web/src/components/LayoutHeaderSections.tsx index e900e24..7f69151 100644 --- a/web/src/components/LayoutHeaderSections.tsx +++ b/web/src/components/LayoutHeaderSections.tsx @@ -239,9 +239,16 @@ function LayoutHeaderSearch() {
- {item.title} -
++ {item.title} +
+ {(item.display_library_name || item.library_name) && ( + + {item.display_library_name || item.library_name} + + )} +