diff --git a/internal/service/media_series.go b/internal/service/media_series.go index e6bbf11..30dc827 100644 --- a/internal/service/media_series.go +++ b/internal/service/media_series.go @@ -5,6 +5,7 @@ import ( "regexp" "sort" "strings" + "time" "github.com/ShukeBta/MediaStationGo/internal/model" ) @@ -16,6 +17,11 @@ type SeriesCard struct { Count int `json:"count"` } +type seriesCardGroup struct { + card SeriesCard + latest time.Time +} + func (s *MediaService) ListLibrarySeriesCards(ctx context.Context, libraryID string, visibility MediaVisibility) ([]SeriesCard, int64, error) { rows, _, err := s.listAllMediaVisible(ctx, libraryID, visibility) if err != nil { @@ -73,7 +79,7 @@ func groupMediaSeriesCards(items []model.Media) []SeriesCard { if len(items) == 0 { return nil } - cards := make([]SeriesCard, 0) + groups := make([]seriesCardGroup, 0) byKey := make(map[string]int, len(items)) for _, item := range items { key := mediaSeriesKey(item) @@ -81,7 +87,11 @@ func groupMediaSeriesCards(items []model.Media) []SeriesCard { continue } if idx, ok := byKey[key]; ok { - card := &cards[idx] + group := &groups[idx] + if latest := seriesMediaTime(item); latest.After(group.latest) { + group.latest = latest + } + card := &group.card card.Count++ if betterSeriesLinkMedia(item, card.LinkMedia) { card.LinkMedia = item @@ -99,12 +109,29 @@ func groupMediaSeriesCards(items []model.Media) []SeriesCard { } continue } - byKey[key] = len(cards) - cards = append(cards, SeriesCard{Key: key, Rep: item, LinkMedia: item, Count: 1}) + byKey[key] = len(groups) + groups = append(groups, seriesCardGroup{ + card: SeriesCard{Key: key, Rep: item, LinkMedia: item, Count: 1}, + latest: seriesMediaTime(item), + }) + } + sort.SliceStable(groups, func(i, j int) bool { + return groups[i].latest.After(groups[j].latest) + }) + cards := make([]SeriesCard, 0, len(groups)) + for _, group := range groups { + cards = append(cards, group.card) } return cards } +func seriesMediaTime(media model.Media) time.Time { + if media.UpdatedAt.After(media.CreatedAt) { + return media.UpdatedAt + } + return media.CreatedAt +} + func betterSeriesLinkMedia(candidate, current model.Media) bool { candidateScore := librarySpecificityScore(candidate) currentScore := librarySpecificityScore(current) diff --git a/internal/service/media_series_test.go b/internal/service/media_series_test.go index 2f7b0ba..f9a3238 100644 --- a/internal/service/media_series_test.go +++ b/internal/service/media_series_test.go @@ -2,6 +2,7 @@ package service import ( "testing" + "time" "github.com/ShukeBta/MediaStationGo/internal/model" ) @@ -28,6 +29,45 @@ func TestMediaSeriesKeyCollapsesNestedSpecialFolders(t *testing.T) { } } +func TestGroupMediaSeriesCardsSortsByLatestEpisodeTime(t *testing.T) { + now := time.Date(2026, 7, 2, 12, 0, 0, 0, time.UTC) + newerFirstEpisode := model.Media{ + Base: model.Base{CreatedAt: now.Add(-72 * time.Hour), UpdatedAt: now.Add(-72 * time.Hour)}, + LibraryID: "lib-tv", + Title: "更新合集", + Path: `F:\media\电视剧\国产剧\更新合集\Season 01\更新合集.S01E01.mkv`, + SeasonNum: 1, + EpisodeNum: 1, + } + newerLatestEpisode := model.Media{ + Base: model.Base{CreatedAt: now, UpdatedAt: now}, + LibraryID: "lib-tv", + Title: "更新合集", + Path: `F:\media\电视剧\国产剧\更新合集\Season 01\更新合集.S01E02.mkv`, + SeasonNum: 1, + EpisodeNum: 2, + } + olderSeries := model.Media{ + Base: model.Base{CreatedAt: now.Add(-24 * time.Hour), UpdatedAt: now.Add(-24 * time.Hour)}, + LibraryID: "lib-tv", + Title: "较早合集", + Path: `F:\media\电视剧\国产剧\较早合集\Season 01\较早合集.S01E01.mkv`, + SeasonNum: 1, + EpisodeNum: 1, + } + + cards := groupMediaSeriesCards([]model.Media{olderSeries, newerFirstEpisode, newerLatestEpisode}) + if len(cards) != 2 { + t.Fatalf("cards=%#v, want two series cards", cards) + } + if cards[0].Key != mediaSeriesKey(newerFirstEpisode) { + t.Fatalf("first card key=%q, want latest series key=%q", cards[0].Key, mediaSeriesKey(newerFirstEpisode)) + } + if cards[0].Count != 2 { + t.Fatalf("latest series count=%d, want 2", cards[0].Count) + } +} + func TestMediaSeriesKeyCollapsesSpecialTitleSuffix(t *testing.T) { main := model.Media{ LibraryID: "lib-tv", diff --git a/web/src/pages/LibrariesPage.tsx b/web/src/pages/LibrariesPage.tsx index 4903eea..1e6f4a1 100644 --- a/web/src/pages/LibrariesPage.tsx +++ b/web/src/pages/LibrariesPage.tsx @@ -7,7 +7,7 @@ import { LibrariesEmptyState, LibrariesHeader, } from './LibrariesPageSections' -import { latestLibraryCards, type LibraryPreview } from './librariesPageModel' +import { isSeriesLibraryType, latestLibraryCards, type LibraryPreview } from './librariesPageModel' export function LibrariesPage() { const [previews, setPreviews] = useState([]) @@ -38,6 +38,13 @@ export function LibrariesPage() { const libs = await libraryAPI.list() const rows = await Promise.all(libs.map(async (library) => { try { + if (isSeriesLibraryType(library.type)) { + const [seriesPage, mediaPage] = await Promise.all([ + libraryAPI.listSeries(library.id, 1, 10), + libraryAPI.listMedia(library.id, 1, 1, { groupVersions: false }), + ]) + return { library, items: [], total: mediaPage.total, cards: seriesPage.items ?? [] } satisfies LibraryPreview + } const page = await libraryAPI.listMedia(library.id, 1, 160, { groupVersions: false }) const cards = latestLibraryCards(page.items) return { library, items: page.items, total: page.total, cards } satisfies LibraryPreview diff --git a/web/src/pages/librariesPageModel.ts b/web/src/pages/librariesPageModel.ts index 063f030..e081597 100644 --- a/web/src/pages/librariesPageModel.ts +++ b/web/src/pages/librariesPageModel.ts @@ -8,6 +8,10 @@ export type LibraryPreview = { cards: SeriesCard[] } +export function isSeriesLibraryType(type?: string) { + return type === 'tv' || type === 'anime' || type === 'variety' +} + export function latestLibraryCards(items: Media[]): SeriesCard[] { return groupSeries(items) .sort((a, b) => mediaTime(b.rep) - mediaTime(a.rep) || artworkScore(b.rep) - artworkScore(a.rep))