diff --git a/internal/handler/series.go b/internal/handler/series.go index c9ac3fe..314bc8e 100644 --- a/internal/handler/series.go +++ b/internal/handler/series.go @@ -89,7 +89,7 @@ func listLibrarySeriesHandler(svc *service.Container) gin.HandlerFunc { writeInternalOrCanceled(c, err) return } - cards = service.SortSeriesCards(cards, sortSpec.Field, sortSpec.Order, history) + cards = sortRemoteSeriesCards(cards, sortSpec, history) page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) size, _ := strconv.Atoi(c.DefaultQuery("page_size", "500")) if page < 1 { diff --git a/internal/handler/series_sort.go b/internal/handler/series_sort.go new file mode 100644 index 0000000..be5afa6 --- /dev/null +++ b/internal/handler/series_sort.go @@ -0,0 +1,23 @@ +package handler + +import ( + "slices" + "time" + + "github.com/truewhile/MeBox/internal/service" +) + +// sortRemoteSeriesCards keeps the DateLastContentAdded order returned by the +// remote Emby server when LastAddedAt is not exposed in the Series DTO. +func sortRemoteSeriesCards(cards []service.SeriesCard, spec service.MediaSortSpec, history map[string]time.Time) []service.SeriesCard { + if spec.Field != "updated_at" { + return service.SortSeriesCards(cards, spec.Field, spec.Order, history) + } + + out := append([]service.SeriesCard(nil), cards...) + // RemoteSeriesCards always fetches DateLastContentAdded in descending order. + if spec.Order == "asc" { + slices.Reverse(out) + } + return service.SortSeriesCards(out, spec.Field, spec.Order, history) +} diff --git a/internal/handler/series_sort_test.go b/internal/handler/series_sort_test.go new file mode 100644 index 0000000..c43aaaa --- /dev/null +++ b/internal/handler/series_sort_test.go @@ -0,0 +1,21 @@ +package handler + +import ( + "testing" + + "github.com/truewhile/MeBox/internal/service" +) + +func TestSortRemoteSeriesCardsUpdatedAtUsesRemoteOrder(t *testing.T) { + cards := []service.SeriesCard{{Key: "newest"}, {Key: "middle"}, {Key: "oldest"}} + + desc := sortRemoteSeriesCards(cards, service.MediaSortSpec{Field: "updated_at", Order: "desc"}, nil) + if desc[0].Key != "newest" || desc[1].Key != "middle" || desc[2].Key != "oldest" { + t.Fatalf("descending order = %q, %q, %q", desc[0].Key, desc[1].Key, desc[2].Key) + } + + asc := sortRemoteSeriesCards(cards, service.MediaSortSpec{Field: "updated_at", Order: "asc"}, nil) + if asc[0].Key != "oldest" || asc[1].Key != "middle" || asc[2].Key != "newest" { + t.Fatalf("ascending order = %q, %q, %q", asc[0].Key, asc[1].Key, asc[2].Key) + } +} diff --git a/internal/service/media_sort.go b/internal/service/media_sort.go index 6a10d27..7811adb 100644 --- a/internal/service/media_sort.go +++ b/internal/service/media_sort.go @@ -70,7 +70,9 @@ func SortSeriesCards(cards []SeriesCard, field, order string, history map[string } sort.SliceStable(out, func(i, j int) bool { cmp := compareSeriesByField(out[i], out[j], spec, history) - if cmp == 0 { + // updated_at 表示最后一集添加日期。远程卡片可能没有该字段,此时 + // 保持调用方给定的顺序,不要用 ID 再次打乱。 + if cmp == 0 && spec.Field != "updated_at" { cmp = compareID(out[i].Key, out[j].Key) } return cmp < 0 @@ -173,7 +175,7 @@ func seriesUpdatedTime(card SeriesCard) (time.Time, bool) { if card.LastAddedAt != nil && !card.LastAddedAt.IsZero() { return *card.LastAddedAt, true } - return mediaUpdatedTime(card.Rep) + return time.Time{}, false } func seriesLastPlayed(card SeriesCard, history map[string]time.Time) (time.Time, bool) { diff --git a/internal/service/media_sort_test.go b/internal/service/media_sort_test.go index 9cafa49..f45f51e 100644 --- a/internal/service/media_sort_test.go +++ b/internal/service/media_sort_test.go @@ -2,6 +2,7 @@ package service import ( "testing" + "time" "github.com/truewhile/MeBox/internal/model" ) @@ -40,3 +41,45 @@ func TestSortSeriesCardsTitleAsc(t *testing.T) { t.Fatalf("series order = %q, %q", got[0].Key, got[1].Key) } } + +func TestSortSeriesCardsUpdatedAtDoesNotFallbackToMediaDate(t *testing.T) { + olderMediaDate := time.Date(2023, time.January, 1, 0, 0, 0, 0, time.UTC) + newerMediaDate := time.Date(2025, time.January, 1, 0, 0, 0, 0, time.UTC) + cards := []SeriesCard{ + { + Key: "remote-first", + Rep: model.Media{Base: model.Base{ + ID: "remote-first", + CreatedAt: olderMediaDate, + UpdatedAt: olderMediaDate, + }}, + }, + { + Key: "remote-second", + Rep: model.Media{Base: model.Base{ + ID: "remote-second", + CreatedAt: newerMediaDate, + UpdatedAt: newerMediaDate, + }}, + }, + } + + got := SortSeriesCards(cards, "updated_at", "desc", nil) + if got[0].Key != "remote-first" || got[1].Key != "remote-second" { + t.Fatalf("series order = %q, %q, want remote-provided order", got[0].Key, got[1].Key) + } +} + +func TestSortSeriesCardsUpdatedAtSortsKnownLastEpisodeDates(t *testing.T) { + older := time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC) + newer := time.Date(2025, time.January, 1, 0, 0, 0, 0, time.UTC) + cards := []SeriesCard{ + {Key: "older", LastAddedAt: &older}, + {Key: "newer", LastAddedAt: &newer}, + } + + got := SortSeriesCards(cards, "updated_at", "desc", nil) + if got[0].Key != "newer" || got[1].Key != "older" { + t.Fatalf("series order = %q, %q", got[0].Key, got[1].Key) + } +} diff --git a/web/src/utils/groupSeries.ts b/web/src/utils/groupSeries.ts index 5d8a749..0711312 100644 --- a/web/src/utils/groupSeries.ts +++ b/web/src/utils/groupSeries.ts @@ -460,7 +460,7 @@ export function groupSeries(items: Media[] = []): SeriesCard[] { ? compactSeriesKey(externalKey) : getSeriesKey(m) - const mAddedAt = m.created_at || m.updated_at || '' + const mAddedAt = m.updated_at || m.created_at || '' const g = groups.get(key) if (!g) { groups.set(key, { key, rep: m, linkMedia: m, count: 1, last_added_at: mAddedAt }) diff --git a/web/src/utils/mediaSort.ts b/web/src/utils/mediaSort.ts index cb3b43e..9ea513a 100644 --- a/web/src/utils/mediaSort.ts +++ b/web/src/utils/mediaSort.ts @@ -114,7 +114,7 @@ export function sortMediaList( case 'created_at': return compareDates(a.created_at, b.created_at, order) || compareStrings(a.title, b.title, 'asc') case 'updated_at': - return compareDates(a.created_at || a.updated_at, b.created_at || b.updated_at, order) || compareStrings(a.title, b.title, 'asc') + return compareDates(a.updated_at || a.created_at, b.updated_at || b.created_at, order) || compareStrings(a.title, b.title, 'asc') case 'rating': case 'imdb_rating': { const cmp = compareNumbers(a.rating || 0, b.rating || 0, order)