Files
MeBox/internal/service/media_series_test.go
T

640 lines
22 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"fmt"
"testing"
"time"
"go.uber.org/zap"
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
)
func TestListRecentSeriesCardsCountsAllEpisodesInSeries(t *testing.T) {
db := newServiceTestDB(t, &model.Library{}, &model.Media{})
repos := repository.New(db)
lib := model.Library{Name: "国漫", Path: "/media/anime", Type: "anime", Enabled: true}
if err := repos.Library.Create(t.Context(), &lib); err != nil {
t.Fatal(err)
}
now := time.Date(2026, 7, 2, 12, 0, 0, 0, time.UTC)
rows := make([]model.Media, 0, 40)
for i := 1; i <= 40; i++ {
created := now.Add(-48 * time.Hour)
if i > 23 {
created = now.Add(time.Duration(i) * time.Minute)
}
rows = append(rows, model.Media{
Base: model.Base{ID: fmt.Sprintf("recent-ep-%02d", i), CreatedAt: created, UpdatedAt: created},
LibraryID: lib.ID,
Title: "史上最强炼体老祖",
Path: fmt.Sprintf("/media/anime/国漫/史上最强炼体老祖/Season 01/史上最强炼体老祖.S01E%02d.mkv", i),
SeasonNum: 1,
EpisodeNum: i,
})
}
if err := repos.DB.Create(&rows).Error; err != nil {
t.Fatal(err)
}
svc := NewMediaService(&config.Config{}, zap.NewNop(), repos)
cards, err := svc.ListRecentSeriesCards(t.Context(), 24, MediaVisibility{IncludeNSFW: true})
if err != nil {
t.Fatal(err)
}
if len(cards) != 1 {
t.Fatalf("recent cards = %#v, want one series card", cards)
}
if cards[0].Count != 40 {
t.Fatalf("recent series count = %d, want full 40 episodes", cards[0].Count)
}
expectedLastAdded := now.Add(40 * time.Minute)
if cards[0].LastAddedAt == nil || !cards[0].LastAddedAt.Equal(expectedLastAdded) {
t.Fatalf("recent series LastAddedAt = %v, want %v", cards[0].LastAddedAt, expectedLastAdded)
}
}
func TestMediaSeriesKeyCollapsesNestedSpecialFolders(t *testing.T) {
main := model.Media{
LibraryID: "lib-tv",
Path: `cloud://openlist/动漫/国漫/示例剧/Season 01/示例剧.S01E01.mkv`,
SeasonNum: 1,
EpisodeNum: 1,
}
special := model.Media{
LibraryID: "lib-tv",
Path: `cloud://openlist/动漫/国漫/示例剧/Extras/Season 01/示例剧.SP01.mkv`,
}
if got, want := mediaSeriesKey(special), mediaSeriesKey(main); got != want {
t.Fatalf("special key=%q, want main key=%q", got, want)
}
cards := groupMediaSeriesCards([]model.Media{main, special})
if len(cards) != 1 || cards[0].Count != 2 {
t.Fatalf("cards=%#v, want one merged series card with two items", cards)
}
}
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",
Path: `cloud://openlist/电视剧/欧美剧/Example Show/Season 01/Example.Show.S01E01.mkv`,
SeasonNum: 1,
EpisodeNum: 1,
}
special := model.Media{
LibraryID: "lib-tv",
Path: `cloud://openlist/电视剧/欧美剧/Example Show Specials/Example.Show.Special.01.mkv`,
SeasonNum: 0,
EpisodeNum: 1,
}
chineseSpecial := model.Media{
LibraryID: "lib-tv",
Path: `cloud://openlist/动漫/国漫/示例剧 特别篇/示例剧.SP01.mkv`,
SeasonNum: 0,
EpisodeNum: 1,
}
chineseMain := model.Media{
LibraryID: "lib-tv",
Path: `cloud://openlist/动漫/国漫/示例剧/Season 01/示例剧.S01E01.mkv`,
SeasonNum: 1,
EpisodeNum: 1,
}
if got, want := mediaSeriesKey(special), mediaSeriesKey(main); got != want {
t.Fatalf("english special key=%q, want main key=%q", got, want)
}
if got, want := mediaSeriesKey(chineseSpecial), mediaSeriesKey(chineseMain); got != want {
t.Fatalf("chinese special key=%q, want main key=%q", got, want)
}
}
func TestMediaSeriesKeyCollapsesSeasonZeroAndSpecialAliases(t *testing.T) {
main := model.Media{
LibraryID: "lib-anime",
Path: `cloud://openlist/动漫/日番/宝可梦 (1997) {tmdb-60572}/Season 1/宝可梦.S01E01.mkv`,
SeasonNum: 1,
EpisodeNum: 1,
}
seasonZero := model.Media{
LibraryID: "lib-anime",
Path: `cloud://openlist/动漫/日番/宝可梦 (1997) {tmdb-60572}/Season 0/宝可梦.S00E34.mkv`,
SeasonNum: 0,
EpisodeNum: 34,
}
specialEpisode := model.Media{
LibraryID: "lib-anime",
Path: `cloud://openlist/动漫/日番/宝可梦 Special Episode/宝可梦.SP01.mkv`,
SeasonNum: 0,
EpisodeNum: 1,
}
extraEpisode := model.Media{
LibraryID: "lib-anime",
Path: `cloud://openlist/动漫/日番/宝可梦 番外篇/宝可梦.SP02.mkv`,
SeasonNum: 0,
EpisodeNum: 2,
}
want := mediaSeriesKey(main)
for name, item := range map[string]model.Media{
"season zero": seasonZero,
"special episode": specialEpisode,
"番外篇": extraEpisode,
} {
if got := mediaSeriesKey(item); got != want {
t.Fatalf("%s key=%q, want main key=%q", name, got, want)
}
}
}
func TestMediaSeriesKeyCollapsesNumberedSpecialSuffixes(t *testing.T) {
main := model.Media{
LibraryID: "lib-tv",
Path: `F:\media\电视剧\欧美剧\Example Show\Season 01\Example Show - S01E01.mkv`,
SeasonNum: 1,
EpisodeNum: 1,
}
chineseMain := model.Media{
LibraryID: "lib-tv",
Path: `F:\media\电视剧\欧美剧\示例剧\Season 01\示例剧.S01E01.mkv`,
SeasonNum: 1,
EpisodeNum: 1,
}
cases := map[string]struct {
item model.Media
want model.Media
}{
"sp number": {
item: model.Media{
LibraryID: "lib-tv",
Path: `F:\media\电视剧\欧美剧\Example Show SP01\Example Show.SP01.mkv`,
SeasonNum: 0,
EpisodeNum: 1,
},
want: main,
},
"ova number": {
item: model.Media{
LibraryID: "lib-tv",
Path: `F:\media\电视剧\欧美剧\Example Show OVA 1\Example Show.OVA.1.mkv`,
SeasonNum: 0,
EpisodeNum: 1,
},
want: main,
},
"season zero episode": {
item: model.Media{
LibraryID: "lib-tv",
Path: `F:\media\电视剧\欧美剧\Example Show S00E01\Example Show.S00E01.mkv`,
SeasonNum: 0,
EpisodeNum: 1,
},
want: main,
},
"wrapped special": {
item: model.Media{
LibraryID: "lib-tv",
Path: `F:\media\电视剧\欧美剧\Example Show [Special]\Example Show.Special.mkv`,
SeasonNum: 0,
EpisodeNum: 1,
},
want: main,
},
"chinese numbered special": {
item: model.Media{
LibraryID: "lib-tv",
Path: `F:\media\电视剧\欧美剧\示例剧 特别篇 第1集\示例剧.SP01.mkv`,
SeasonNum: 0,
EpisodeNum: 1,
},
want: chineseMain,
},
}
for name, tt := range cases {
want := mediaSeriesKey(tt.want)
if got := mediaSeriesKey(tt.item); got != want {
t.Fatalf("%s key=%q, want main key=%q", name, got, want)
}
}
}
func TestMediaSeriesKeyCleansReleaseNoiseFolders(t *testing.T) {
clean := model.Media{
LibraryID: "lib-variety",
Path: `F:\media\电视剧\综艺\Hntv Spring Festival Gala S01e (2026)\Season 1\Hntv Spring Festival Gala S01e - S01E202.ts`,
SeasonNum: 1,
EpisodeNum: 202,
}
dirty := model.Media{
LibraryID: "lib-variety",
Path: `F:\media\电视剧\综艺\Hntv Spring Festival Gala Fps Hlg Qhstudio S01e (2026)\Season 1\Hntv Spring Festival Gala Fps Hlg Qhstudio S01e - S01E202.ts`,
SeasonNum: 1,
EpisodeNum: 202,
}
if got, want := mediaSeriesKey(dirty), mediaSeriesKey(clean); got != want {
t.Fatalf("dirty folder key=%q, want clean folder key=%q", got, want)
}
noisyRelease := model.Media{
LibraryID: "lib-tv",
Path: `F:\media\电视剧\欧美剧\Motherhood Of Taihang Aac2 Mweb\Season 1\Motherhood Of Taihang Aac2 Mweb - S01E01-Aac2.Mweb.mkv`,
SeasonNum: 1,
EpisodeNum: 1,
}
cleanRelease := model.Media{
LibraryID: "lib-tv",
Path: `F:\media\电视剧\欧美剧\Motherhood Of Taihang\Season 1\Motherhood Of Taihang - S01E01.mkv`,
SeasonNum: 1,
EpisodeNum: 1,
}
if got, want := mediaSeriesKey(noisyRelease), mediaSeriesKey(cleanRelease); got != want {
t.Fatalf("release-noise folder key=%q, want clean key=%q", got, want)
}
}
func TestMediaSeriesKeyTreatsDomesticTelevisionFolderAsSeries(t *testing.T) {
main := model.Media{
LibraryID: "lib-domestic-tv",
Path: `/media/国产电视剧/人世间 (2022) [TMDBID-156568]/人世间.S01E01.mkv`,
SeasonNum: 1,
EpisodeNum: 1,
TMDbID: 156568,
}
weakEpisode := model.Media{
LibraryID: "lib-domestic-tv",
Path: `/media/国产电视剧/人世间 (2022) [TMDBID-156568]/人世间.S01E02.mkv`,
// Some local/cloud scans may miss S/E at first while local NFO or
// scraper metadata already carries an episode-level TMDb id.
TMDbID: 4375419,
}
folderRecord := model.Media{
LibraryID: "lib-domestic-tv",
Path: `/media/国产电视剧/人世间 (2022) [TMDBID-156568]`,
Title: "人世间",
TMDbID: 156568,
}
if got, want := mediaSeriesKey(weakEpisode), mediaSeriesKey(main); got != want {
t.Fatalf("domestic television folder key=%q, want main key=%q", got, want)
}
if got, want := mediaSeriesKey(folderRecord), mediaSeriesKey(main); got != want {
t.Fatalf("domestic television folder record key=%q, want main key=%q", got, want)
}
cards := groupMediaSeriesCards([]model.Media{main, weakEpisode, folderRecord})
if len(cards) != 1 || cards[0].Count != 3 {
t.Fatalf("cards=%#v, want one merged series card with three items", cards)
}
}
func TestMediaSeriesKeyUsesSeriesDirectoryExternalID(t *testing.T) {
episodeIDOnly := model.Media{
LibraryID: "lib-domestic-tv",
Path: `/media/电视剧/国产剧/人世间 (2022)/Season 01/人世间.S01E03.{tmdb-7129826}.mkv`,
SeasonNum: 1,
EpisodeNum: 3,
TMDbID: 7129826,
}
cleanFolder := model.Media{
LibraryID: "lib-domestic-tv",
Path: `/media/电视剧/国产剧/人世间 (2022)/Season 01/人世间.S01E04.mkv`,
SeasonNum: 1,
EpisodeNum: 4,
TMDbID: 156568,
}
if got, want := mediaSeriesKey(episodeIDOnly), mediaSeriesKey(cleanFolder); got != want {
t.Fatalf("episode filename tmdb id should not split clean folder key=%q, want %q", got, want)
}
}
func TestGroupMediaSeriesCardsMergesPollutedEpisodeFoldersBySharedShowID(t *testing.T) {
items := []model.Media{
{
LibraryID: "lib-variety",
Title: "脱口秀和Ta的朋友们",
Path: `F:\media\电视剧\综艺\脱口秀和Ta的朋友们 第01期\Season 01\show.S01E01.mkv`,
SeasonNum: 1,
EpisodeNum: 1,
TMDbID: 260001,
ScrapeStatus: "matched",
},
{
LibraryID: "lib-variety",
Title: "脱口秀和Ta的朋友们",
Path: `F:\media\电视剧\综艺\脱口秀和Ta的朋友们 第02期\Season 01\show.S01E02.mkv`,
SeasonNum: 1,
EpisodeNum: 2,
TMDbID: 260001,
ScrapeStatus: "matched",
},
}
cards := groupMediaSeriesCards(items)
if len(cards) != 1 || cards[0].Count != 2 {
t.Fatalf("cards=%#v, want one show card with two episodes", cards)
}
}
func TestGroupMediaSeriesCardsKeepsMixedTitlesInSameEpisodicDirectoryTogether(t *testing.T) {
items := []model.Media{
{
Base: model.Base{ID: "main-1"},
LibraryID: "anime",
Title: "住在拔作岛上的我应该如何是好?",
Path: `/media/影视库/动漫/拔作岛/[64bitsub][Nukitashi][01][AVC_2×FLAC].mkv.strm`,
SeasonNum: 1,
EpisodeNum: 1,
ScrapeStatus: "matched",
},
{
Base: model.Base{ID: "main-2"},
LibraryID: "anime",
Title: "住在拔作岛上的我应该如何是好?",
Path: `/media/影视库/动漫/拔作岛/[64bitsub][Nukitashi][02][AVC_2×FLAC].mkv.strm`,
SeasonNum: 1,
EpisodeNum: 2,
ScrapeStatus: "matched",
},
{
Base: model.Base{ID: "special-1"},
LibraryID: "anime",
Title: "nukitashi",
Path: `/media/影视库/动漫/拔作岛/[64bitsub][Nukitashi][CM_01][AVC_FLAC].mkv.strm`,
ScrapeStatus: "matched",
},
{
Base: model.Base{ID: "special-2"},
LibraryID: "anime",
Title: "nukitashi",
Path: `/media/影视库/动漫/拔作岛/[64bitsub][Nukitashi][PV_01][AVC_FLAC].mkv.strm`,
ScrapeStatus: "matched",
},
}
cards := groupMediaSeriesCards(items)
if len(cards) != 1 {
t.Fatalf("cards=%#v, want main episodes and specials in the same directory folded into one card", cards)
}
if cards[0].Count != 4 {
t.Fatalf("series count=%d, want 4 items", cards[0].Count)
}
}
func TestGroupMediaSeriesCardsKeepsMovieVersionsAsOneMovie(t *testing.T) {
items := []model.Media{
{
Base: model.Base{ID: "movie-copy-a"},
LibraryID: "foreign-movies",
Title: "杀的就是你",
Path: `F:\media\电影\外语电影\They Will Kill You (2026)\movie-a.mkv`,
TMDbID: 1292695,
},
{
Base: model.Base{ID: "movie-copy-b"},
LibraryID: "western-movies",
Title: "杀的就是你",
Path: `F:\media\电影\欧美电影\They Will Kill You (2026)\movie-b.mkv`,
TMDbID: 1292695,
},
}
cards := groupMediaSeriesCards(items)
if len(cards) != 1 {
t.Fatalf("cards=%#v, want duplicate movie locations folded into one card", cards)
}
if cards[0].Count != 1 {
t.Fatalf("movie card count=%d, want 1 so versions are not shown as episodes", cards[0].Count)
}
}
func TestGroupMediaSeriesCardsKeepsTheatricalMovieWithTVSeries(t *testing.T) {
episode := model.Media{
Base: model.Base{ID: "episode"},
LibraryID: "anime",
Title: "摇曳露营△",
Path: `/media/动漫/摇曳露营△ (2018)/Season 01/摇曳露营△.S01E01.mkv`,
PosterURL: "https://image.tmdb.org/t/p/w500/episode.jpg",
SeasonNum: 1,
EpisodeNum: 1,
}
theatrical := model.Media{
Base: model.Base{ID: "theatrical"},
LibraryID: "anime",
Title: "摇曳露营△ 剧场版",
Path: `/media/动漫/摇曳露营△ (2018)/摇曳露营△ 剧场版 (2022)/Eiga.Yurukyan.2022.Bluray.mkv`,
PosterURL: "/media/动漫/摇曳露营△ (2018)/剧场版/poster.jpg",
BackdropURL: "/media/动漫/摇曳露营△ (2018)/剧场版/background.jpg",
Overview: "剧场版简介",
TMDbID: 566466,
}
cards := groupMediaSeriesCards([]model.Media{episode, theatrical})
if len(cards) != 1 {
t.Fatalf("cards=%#v, want theatrical movie retained with TV series", cards)
}
if cards[0].Count != 2 {
t.Fatalf("series card count=%d, want TV episode plus theatrical movie", cards[0].Count)
}
if cards[0].Rep.ID != episode.ID {
t.Fatalf("series representative=%q, want TV episode %q", cards[0].Rep.ID, episode.ID)
}
if cards[0].Rep.Title != episode.Title {
t.Fatalf("series representative title=%q, want %q", cards[0].Rep.Title, episode.Title)
}
}
func TestGroupMediaSeriesCardsDoesNotCollideMovieAndTVExternalIDs(t *testing.T) {
movie := model.Media{
Base: model.Base{ID: "movie"},
LibraryID: "movies",
Title: "同号电影",
Path: `/media/电影/同号电影 (2026)/movie.mkv`,
TMDbID: 12345,
}
episode := model.Media{
Base: model.Base{ID: "episode"},
LibraryID: "tv",
Title: "同号剧集",
Path: `/media/tv/同号剧集/episode.mkv`,
SeasonNum: 1,
EpisodeNum: 1,
TMDbID: 12345,
}
cards := groupMediaSeriesCards([]model.Media{movie, episode})
if len(cards) != 2 {
t.Fatalf("cards=%#v, want movie and TV item kept separate despite equal numeric TMDb IDs", cards)
}
}
func TestMediaSeriesKeyDoesNotUseGenericLibraryFolderAsSeriesTitle(t *testing.T) {
items := []model.Media{
{LibraryID: "tv", Path: `/media/电视剧/Alpha Show.S01E01.mkv`, SeasonNum: 1, EpisodeNum: 1},
{LibraryID: "tv", Path: `/media/电视剧/Beta Show.S01E01.mkv`, SeasonNum: 1, EpisodeNum: 1},
}
cards := groupMediaSeriesCards(items)
if len(cards) != 2 {
t.Fatalf("cards=%#v, want two independent series from a flat library root", cards)
}
}
func TestGroupMediaSeriesCardsBridgesReleaseFoldersByMatchedSeriesTitle(t *testing.T) {
items := []model.Media{
{LibraryID: "tv", Title: "同一部剧", ScrapeStatus: "matched", Path: `/media/电视剧/同一部剧 版本甲/Season 01/ep1.mkv`, SeasonNum: 1, EpisodeNum: 1},
{LibraryID: "tv", Title: "同一部剧", ScrapeStatus: "matched", Path: `/media/电视剧/同一部剧 版本乙/Season 01/ep2.mkv`, SeasonNum: 1, EpisodeNum: 2},
}
cards := groupMediaSeriesCards(items)
if len(cards) != 1 || cards[0].Count != 2 {
t.Fatalf("cards=%#v, want one series bridged by matched title", cards)
}
}
func TestGroupMediaSeriesCardsKeepsIndependentMoviesSeparateInSharedSubdirectory(t *testing.T) {
// 同一分类子目录下存放多部不同标题的独立电影,不应被强制折叠成 1 部
items := []model.Media{
{LibraryID: "movies", Title: "老师2024偷窥篇", Path: `/media/小姐姐/国产/nana/老师2024偷窥篇.strm`},
{LibraryID: "movies", Title: "紫光灯下的肉体诱惑", Path: `/media/小姐姐/国产/nana/紫光灯下的肉体诱惑.strm`},
{LibraryID: "movies", Title: "修洗衣机", Path: `/media/小姐姐/国产/nana/修洗衣机.strm`},
}
cards := groupMediaSeriesCards(items)
if len(cards) != 3 {
t.Fatalf("got %d cards, want 3 independent movie cards", len(cards))
}
// 但同一部电影的 CD1 和 CD2 仍应正确折叠为 1 部
cdItems := []model.Media{
{LibraryID: "movies", Title: "cd1", Path: `/media/电影/指环王 (2001)/cd1.mkv`},
{LibraryID: "movies", Title: "cd2", Path: `/media/电影/指环王 (2001)/cd2.mkv`},
}
cdCards := groupMediaSeriesCards(cdItems)
if len(cdCards) != 1 {
t.Fatalf("got %d cards for cd1/cd2, want 1 folded movie card", len(cdCards))
}
}
func TestListMediaEpisodesKeepsIndependentMoviesSeparate(t *testing.T) {
db := newServiceTestDB(t, &model.Library{}, &model.Media{})
repos := repository.New(db)
lib := model.Library{Base: model.Base{ID: "lib-movies"}, Name: "电影", Type: "movies", Enabled: true}
if err := repos.DB.Create(&lib).Error; err != nil {
t.Fatal(err)
}
m1 := model.Media{
Base: model.Base{ID: "m1"},
LibraryID: lib.ID,
Title: "老师2024偷窥篇",
Path: `/media/小姐姐/国产/nana/老师2024偷窥篇.strm`,
}
m2 := model.Media{
Base: model.Base{ID: "m2"},
LibraryID: lib.ID,
Title: "紫光灯下的肉体诱惑",
Path: `/media/小姐姐/国产/nana/紫光灯下的肉体诱惑.strm`,
}
m3 := model.Media{
Base: model.Base{ID: "m3"},
LibraryID: lib.ID,
Title: "修洗衣机",
Path: `/media/小姐姐/国产/nana/修洗衣机.strm`,
}
if err := repos.DB.Create(&[]model.Media{m1, m2, m3}).Error; err != nil {
t.Fatal(err)
}
svc := NewMediaService(&config.Config{}, zap.NewNop(), repos)
eps, err := svc.ListMediaEpisodes(t.Context(), "m1", MediaVisibility{IncludeNSFW: true})
if err != nil {
t.Fatal(err)
}
if len(eps) != 1 || eps[0].ID != "m1" {
t.Fatalf("ListMediaEpisodes got %#v, want exactly m1", eps)
}
}
func TestListLibrarySeriesCardsCachesPrecomputedCards(t *testing.T) {
db := newServiceTestDB(t, &model.Library{}, &model.Media{})
repos := repository.New(db)
lib := model.Library{Name: "动画", Path: "/media/anime", Type: "anime", Enabled: true}
if err := repos.Library.Create(t.Context(), &lib); err != nil {
t.Fatal(err)
}
rows := []model.Media{
{LibraryID: lib.ID, Title: "示例动画", Path: "/media/anime/示例动画/S01E01.mkv", SeasonNum: 1, EpisodeNum: 1},
{LibraryID: lib.ID, Title: "示例动画", Path: "/media/anime/示例动画/S01E02.mkv", SeasonNum: 1, EpisodeNum: 2},
}
if err := repos.DB.Create(&rows).Error; err != nil {
t.Fatal(err)
}
svc := NewMediaService(&config.Config{}, zap.NewNop(), repos).
SetRuntimeCache(NewRuntimeCacheService(&config.Config{}, zap.NewNop()))
visibility := MediaVisibility{IncludeNSFW: true}
cards, total, err := svc.ListLibrarySeriesCards(t.Context(), lib.ID, visibility)
if err != nil {
t.Fatal(err)
}
if total != 1 || len(cards) != 1 || cards[0].Count != 2 {
t.Fatalf("cold series cards = %#v, total = %d; want one two-episode card", cards, total)
}
cachedObj, ok := svc.cache.GetObject(svc.libraryRowsCacheKey(lib.ID, visibility))
if !ok {
t.Fatal("expected full library rows and precomputed cards to be cached")
}
cached, ok := cachedObj.(*libraryRowsCacheValue)
if !ok || cached.Cards == nil || len(cached.Cards) != 1 {
t.Fatalf("cached rows value = %#v, want one precomputed card", cachedObj)
}
cards, total, err = svc.ListLibrarySeriesCards(t.Context(), lib.ID, visibility)
if err != nil {
t.Fatal(err)
}
if total != 1 || len(cards) != 1 || cards[0].Count != 2 {
t.Fatalf("warm series cards = %#v, total = %d; want cached result", cards, total)
}
}