diff --git a/internal/service/emby_series.go b/internal/service/emby_series.go index ce652e5..7a7b15d 100644 --- a/internal/service/emby_series.go +++ b/internal/service/emby_series.go @@ -276,7 +276,9 @@ func (e *EmbyService) seasonsForSeries(series embySeriesGroup) []embySeasonGroup } season.Episodes = append(season.Episodes, episode) } - sort.Ints(order) + sort.SliceStable(order, func(i, j int) bool { + return embySeasonSortOrder(order[i]) < embySeasonSortOrder(order[j]) + }) out := make([]embySeasonGroup, 0, len(order)) for _, seasonNum := range order { out = append(out, *bySeason[seasonNum]) diff --git a/internal/service/emby_series_hierarchy_test.go b/internal/service/emby_series_hierarchy_test.go index 870a652..df58bb9 100644 --- a/internal/service/emby_series_hierarchy_test.go +++ b/internal/service/emby_series_hierarchy_test.go @@ -3,6 +3,7 @@ package service import ( "context" "fmt" + "sort" "testing" "time" @@ -272,6 +273,11 @@ func TestEmbySeparatesOVAAndOADFromSeasonOne(t *testing.T) { if len(seasonItems) != 3 { t.Fatalf("expected season 1, OVA and OAD separately, got %#v", seasonItems) } + for i, want := range []int{1, embySeasonOVA, embySeasonOAD} { + if seasonItems[i]["IndexNumber"] != want { + t.Fatalf("season order [%d] = %#v, want %d", i, seasonItems[i], want) + } + } seasonByIndex := make(map[int]map[string]any, len(seasonItems)) for _, season := range seasonItems { @@ -306,6 +312,35 @@ func TestEmbySeparatesOVAAndOADFromSeasonOne(t *testing.T) { } } +func TestEmbySeasonSortOrderMatchesWeb(t *testing.T) { + seasons := []int{ + embySeasonNCED, + embySeasonOAD, + 2, + embySeasonTheatrical, + 1, + embySeasonGenericSpecial, + embySeasonOVA, + } + sort.SliceStable(seasons, func(i, j int) bool { + return embySeasonSortOrder(seasons[i]) < embySeasonSortOrder(seasons[j]) + }) + want := []int{ + 1, + 2, + embySeasonGenericSpecial, + embySeasonTheatrical, + embySeasonOVA, + embySeasonOAD, + embySeasonNCED, + } + for i := range want { + if seasons[i] != want[i] { + t.Fatalf("season order = %#v, want %#v", seasons, want) + } + } +} + func TestEmbySeasonCandidatesCoverPersistedSpecialNumbers(t *testing.T) { for _, persisted := range []int{0, -1} { candidates := embySeasonCandidates(persisted) diff --git a/internal/service/emby_series_ids.go b/internal/service/emby_series_ids.go index 71b5846..f2be6d8 100644 --- a/internal/service/emby_series_ids.go +++ b/internal/service/emby_series_ids.go @@ -208,6 +208,42 @@ func seasonName(seasonNum int) string { return fmt.Sprintf("第 %d 季", seasonNum) } +// embySeasonSortOrder mirrors the web UI's seasonSortOrder: regular seasons +// first in ascending order, followed by specials in a stable category order. +func embySeasonSortOrder(seasonNum int) int { + if seasonNum > 0 { + return seasonNum + } + switch seasonNum { + case embySeasonGenericSpecial: + return 1000 + case embySeasonTheatrical: + return 1001 + case embySeasonOVA: + return 1002 + case embySeasonOAD: + return 1003 + case embySeasonOVD: + return 1004 + case embySeasonONA: + return 1005 + case embySeasonExtra: + return 1006 + case embySeasonBonus: + return 1007 + case embySeasonOmake: + return 1008 + case embySeasonPictureDrama: + return 1009 + case embySeasonNCOP: + return 1010 + case embySeasonNCED: + return 1011 + default: + return 2000 + } +} + func sortSeriesGroups(groups []embySeriesGroup, p ItemsParams) { switch primarySupportedEmbySort(p.SortBy, false) { case "sortname", "name":