Files
MeBox/internal/service/emby_compat_test.go
T

633 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 (
"context"
"testing"
"time"
"go.uber.org/zap"
"github.com/ShukeBta/MediaStationGo/internal/config"
"github.com/ShukeBta/MediaStationGo/internal/model"
"github.com/ShukeBta/MediaStationGo/internal/repository"
"github.com/ShukeBta/MediaStationGo/internal/service/cloud"
)
func TestEmbyItemsExposeSeriesSeasonEpisodeHierarchy(t *testing.T) {
svc := newTestEmbyService(t)
lib := model.Library{Name: "番剧", Path: `F:\downloads\日番`, Type: "anime", Enabled: true}
if err := svc.repo.Library.Create(t.Context(), &lib); err != nil {
t.Fatalf("create library: %v", err)
}
for _, media := range []model.Media{
{
Base: model.Base{ID: "ep-1"},
LibraryID: lib.ID,
Title: "间谍过家家",
OriginalName: "SPY×FAMILY",
EpisodeTitle: "第 1 集",
Path: `F:\downloads\日番\剧集\间谍过家家\Season 02\间谍过家家 - S02E01.mkv`,
PosterURL: `F:\poster.jpg`,
SeasonNum: 2,
EpisodeNum: 1,
},
{
Base: model.Base{ID: "ep-2"},
LibraryID: lib.ID,
Title: "间谍过家家",
OriginalName: "SPY×FAMILY",
EpisodeTitle: "第 2 集",
Path: `F:\downloads\日番\剧集\间谍过家家\Season 02\间谍过家家 - S02E02.mkv`,
PosterURL: `F:\poster.jpg`,
SeasonNum: 2,
EpisodeNum: 2,
},
} {
if err := svc.repo.DB.Create(&media).Error; err != nil {
t.Fatalf("create media: %v", err)
}
}
root, err := svc.Items(t.Context(), ItemsParams{ParentID: lib.ID, Limit: 50})
if err != nil {
t.Fatalf("library items: %v", err)
}
rootItems := root["Items"].([]map[string]any)
if len(rootItems) != 1 {
t.Fatalf("expected one series card, got %#v", rootItems)
}
seriesID := rootItems[0]["Id"].(string)
if rootItems[0]["Type"] != "Series" || rootItems[0]["IsFolder"] != true || rootItems[0]["Name"] != "间谍过家家" {
t.Fatalf("unexpected series payload: %#v", rootItems[0])
}
seasons, err := svc.Items(t.Context(), ItemsParams{ParentID: seriesID, Limit: 50})
if err != nil {
t.Fatalf("series items: %v", err)
}
seasonItems := seasons["Items"].([]map[string]any)
if len(seasonItems) != 1 || seasonItems[0]["Type"] != "Season" || seasonItems[0]["IndexNumber"] != 2 {
t.Fatalf("unexpected seasons: %#v", seasonItems)
}
episodes, err := svc.Items(t.Context(), ItemsParams{ParentID: seasonItems[0]["Id"].(string), IncludeItemTypes: []string{"Episode"}, Recursive: true, Limit: 50})
if err != nil {
t.Fatalf("season episodes: %v", err)
}
episodeItems := episodes["Items"].([]map[string]any)
if len(episodeItems) != 2 || episodeItems[0]["Type"] != "Episode" || episodeItems[0]["Name"] != "第 1 集" {
t.Fatalf("unexpected episodes: %#v", episodeItems)
}
if episodeItems[0]["SeriesId"] != seriesID || episodeItems[0]["ParentId"] != seasonItems[0]["Id"] {
t.Fatalf("episode hierarchy not linked: %#v", episodeItems[0])
}
latest, err := svc.LatestItems(t.Context(), "user-1", lib.ID, 10)
if err != nil {
t.Fatalf("latest items: %v", err)
}
if len(latest) != 1 || latest[0]["Type"] != "Series" {
t.Fatalf("latest should be grouped by series: %#v", latest)
}
playback, err := svc.PlaybackInfo(t.Context(), seriesID, "user-1")
if err != nil {
t.Fatalf("series playback fallback: %v", err)
}
sources := playback["MediaSources"].([]map[string]any)
if sources[0]["Id"] != "ep-1" {
t.Fatalf("series playback should fall back to first episode: %#v", sources)
}
if sources[0]["DirectStreamUrl"] != "/Videos/ep-1/stream.mkv" {
t.Fatalf("playback should use Emby-compatible stream URL: %#v", sources[0])
}
}
func TestEmbyItemsKeepSpecialsInSeasonZero(t *testing.T) {
svc := newTestEmbyService(t)
lib := model.Library{Name: "番剧", Path: `F:\downloads\日番`, Type: "anime", Enabled: true}
if err := svc.repo.Library.Create(t.Context(), &lib); err != nil {
t.Fatalf("create library: %v", err)
}
media := model.Media{
Base: model.Base{ID: "sp-1"},
LibraryID: lib.ID,
Title: "间谍过家家",
Path: `F:\downloads\日番\间谍过家家\Specials\间谍过家家 - S00E01.mkv`,
PosterURL: `F:\episode-still.jpg`,
SeasonNum: 0,
EpisodeNum: 1,
}
if err := svc.repo.DB.Create(&media).Error; err != nil {
t.Fatalf("create media: %v", err)
}
root, err := svc.Items(t.Context(), ItemsParams{ParentID: lib.ID, Limit: 50})
if err != nil {
t.Fatalf("library items: %v", err)
}
rootItems := root["Items"].([]map[string]any)
if len(rootItems) != 1 || rootItems[0]["Type"] != "Series" {
t.Fatalf("expected one series card, got %#v", rootItems)
}
seasons, err := svc.Items(t.Context(), ItemsParams{ParentID: rootItems[0]["Id"].(string), Limit: 50})
if err != nil {
t.Fatalf("series seasons: %v", err)
}
seasonItems := seasons["Items"].([]map[string]any)
if len(seasonItems) != 1 || seasonItems[0]["Type"] != "Season" || seasonItems[0]["IndexNumber"] != 0 || seasonItems[0]["Name"] != "特别篇" {
t.Fatalf("specials should be exposed as season zero: %#v", seasonItems)
}
episodes, err := svc.Items(t.Context(), ItemsParams{ParentID: seasonItems[0]["Id"].(string), IncludeItemTypes: []string{"Episode"}, Recursive: true, Limit: 50})
if err != nil {
t.Fatalf("special episodes: %v", err)
}
episodeItems := episodes["Items"].([]map[string]any)
if len(episodeItems) != 1 {
t.Fatalf("expected one special episode, got %#v", episodeItems)
}
if episodeItems[0]["ParentIndexNumber"] != 0 || episodeItems[0]["SeasonId"] != seasonItems[0]["Id"] || episodeItems[0]["ParentId"] != seasonItems[0]["Id"] {
t.Fatalf("special episode linked to wrong season: %#v season=%#v", episodeItems[0], seasonItems[0])
}
if tags, ok := episodeItems[0]["ImageTags"].(map[string]string); !ok || tags["Primary"] != "sp-1" {
t.Fatalf("episode still should be exposed as Primary image: %#v", episodeItems[0]["ImageTags"])
}
}
func TestEmbyEpisodeStillIsPrimaryImageNotArt(t *testing.T) {
svc := newTestEmbyService(t)
lib := model.Library{Name: "剧集", Path: `/media/tv`, Type: "tv", Enabled: true}
if err := svc.repo.Library.Create(t.Context(), &lib); err != nil {
t.Fatalf("create library: %v", err)
}
media := model.Media{
Base: model.Base{ID: "ep-still"},
LibraryID: lib.ID,
Title: "间谍过家家",
Path: `/media/tv/间谍过家家/Season 02/间谍过家家 - S02E01.mkv`,
PosterURL: `https://image.example/show-poster.jpg`,
BackdropURL: `https://image.example/episode-still.jpg`,
SeasonNum: 2,
EpisodeNum: 1,
}
if err := svc.repo.DB.Create(&media).Error; err != nil {
t.Fatalf("create media: %v", err)
}
item := svc.itemPayload(t.Context(), &media, false, 0)
if tags, ok := item["ImageTags"].(map[string]string); !ok || tags["Primary"] != "ep-still" {
t.Fatalf("episode should expose a primary image tag: %#v", item["ImageTags"])
}
if tags, ok := item["BackdropImageTags"].([]string); !ok || len(tags) != 0 {
t.Fatalf("episode still must not be exposed as art/backdrop: %#v", item["BackdropImageTags"])
}
primary, err := svc.ImageURL(t.Context(), "ep-still", "Primary")
if err != nil {
t.Fatalf("primary image url: %v", err)
}
if primary != media.BackdropURL {
t.Fatalf("episode Primary image = %q, want still %q", primary, media.BackdropURL)
}
art, err := svc.ImageURL(t.Context(), "ep-still", "Art")
if err != nil {
t.Fatalf("art image url: %v", err)
}
if art == media.BackdropURL {
t.Fatalf("episode still must not be returned as Art image")
}
}
func TestEmbyLatestItemsIncludesMergedCloudMovieLibrary(t *testing.T) {
svc := newTestEmbyService(t)
local := model.Library{Name: "国产电影", Path: `/media/国产电影`, Type: "movie", Enabled: true}
cloud := model.Library{Name: "OpenList · 国产电影", Path: BuildCloudLibraryPath("openlist", "/国产电影", "/国产电影"), Type: "movie", Enabled: true}
for _, lib := range []*model.Library{&local, &cloud} {
if err := svc.repo.Library.Create(t.Context(), lib); err != nil {
t.Fatalf("create library: %v", err)
}
}
for _, media := range []model.Media{
{
Base: model.Base{ID: "local-movie", CreatedAt: time.Now().Add(-time.Minute)},
LibraryID: local.ID,
Title: "本地版本",
Path: `/media/国产电影/local.mkv`,
},
{
Base: model.Base{ID: "cloud-movie", CreatedAt: time.Now()},
LibraryID: cloud.ID,
Title: "云盘版本",
Path: `cloud://openlist/国产电影/cloud.mkv`,
},
} {
if err := svc.repo.DB.Create(&media).Error; err != nil {
t.Fatalf("create media: %v", err)
}
}
latest, err := svc.LatestItems(t.Context(), "user-1", local.ID, 10)
if err != nil {
t.Fatalf("latest items: %v", err)
}
if len(latest) != 2 {
t.Fatalf("latest items = %#v, want local and merged cloud media", latest)
}
if latest[0]["Id"] != "cloud-movie" || latest[1]["Id"] != "local-movie" {
t.Fatalf("latest order/items = %#v, want cloud then local", latest)
}
}
func TestEmbyVirtualSeriesArtworkUsesListCache(t *testing.T) {
svc := newTestEmbyService(t)
lib := model.Library{Name: "番剧", Path: `/media/anime`, Type: "anime", Enabled: true}
if err := svc.repo.Library.Create(t.Context(), &lib); err != nil {
t.Fatalf("create library: %v", err)
}
media := model.Media{
Base: model.Base{ID: "ep-1"},
LibraryID: lib.ID,
Title: "剑来",
Path: `/media/anime/剑来/Season 01/剑来 - S01E01.mkv`,
PosterURL: `/poster.jpg`,
BackdropURL: `/backdrop.jpg`,
SeasonNum: 1,
EpisodeNum: 1,
}
if err := svc.repo.DB.Create(&media).Error; err != nil {
t.Fatalf("create media: %v", err)
}
root, err := svc.Items(t.Context(), ItemsParams{ParentID: lib.ID, Limit: 50})
if err != nil {
t.Fatalf("library items: %v", err)
}
items := root["Items"].([]map[string]any)
seriesID := items[0]["Id"].(string)
cancelled, cancel := context.WithCancel(t.Context())
cancel()
poster, err := svc.ImageURL(cancelled, seriesID, "Primary")
if err != nil {
t.Fatalf("image url from cache: %v", err)
}
if poster != "/poster.jpg" {
t.Fatalf("poster = %q, want cached poster", poster)
}
backdrop, err := svc.ImageURL(cancelled, seriesID, "Backdrop")
if err != nil {
t.Fatalf("backdrop url from cache: %v", err)
}
if backdrop != "/backdrop.jpg" {
t.Fatalf("backdrop = %q, want cached backdrop", backdrop)
}
}
func TestEmbyCloudAnimeUsesSeriesNameFromChineseSeasonFolder(t *testing.T) {
svc := newTestEmbyService(t)
lib := model.Library{Name: "OpenList · 国漫", Path: `cloud://openlist/国漫`, Type: "anime", Enabled: true}
if err := svc.repo.Library.Create(t.Context(), &lib); err != nil {
t.Fatalf("create library: %v", err)
}
for _, media := range []model.Media{
{
Base: model.Base{ID: "cloud-ep-1"},
LibraryID: lib.ID,
Title: "04",
Path: `cloud://openlist/国漫/剑来/第二季/04.mkv`,
SeasonNum: 2,
EpisodeNum: 4,
},
{
Base: model.Base{ID: "cloud-ep-2"},
LibraryID: lib.ID,
Title: "05",
Path: `cloud://openlist/国漫/剑来/第二季/05.mkv`,
SeasonNum: 2,
EpisodeNum: 5,
},
} {
if err := svc.repo.DB.Create(&media).Error; err != nil {
t.Fatalf("create media: %v", err)
}
}
root, err := svc.Items(t.Context(), ItemsParams{ParentID: lib.ID, Limit: 50})
if err != nil {
t.Fatalf("library items: %v", err)
}
items := root["Items"].([]map[string]any)
if len(items) != 1 || items[0]["Type"] != "Series" || items[0]["Name"] != "剑来" {
t.Fatalf("cloud anime should be grouped as one series named 剑来, got %#v", items)
}
}
func TestEmbyMovieLibrarySeasonNumbersStayMovies(t *testing.T) {
svc := newTestEmbyService(t)
lib := model.Library{Name: "动画电影", Path: `/media/movies/animation`, Type: "Movie", Enabled: true}
if err := svc.repo.Library.Create(t.Context(), &lib); err != nil {
t.Fatalf("create library: %v", err)
}
media := model.Media{
Base: model.Base{ID: "movie-with-episode-numbers"},
LibraryID: lib.ID,
Title: "Movie Mistaken S01E01",
Path: `/media/movies/animation/Movie.Mistaken.S01E01.mkv`,
PosterURL: `/poster.jpg`,
SeasonNum: 1,
EpisodeNum: 1,
}
if err := svc.repo.DB.Create(&media).Error; err != nil {
t.Fatalf("create media: %v", err)
}
out, err := svc.Items(t.Context(), ItemsParams{ParentID: lib.ID, IncludeItemTypes: []string{"Movie"}, Limit: 50})
if err != nil {
t.Fatalf("items: %v", err)
}
items := out["Items"].([]map[string]any)
if len(items) != 1 {
t.Fatalf("movie library item filtered out by season numbers: %#v", out)
}
if items[0]["Type"] != "Movie" || items[0]["ParentId"] != lib.ID {
t.Fatalf("movie library item should stay Movie, got %#v", items[0])
}
tags := items[0]["ImageTags"].(map[string]string)
if tags["Primary"] == "" {
t.Fatalf("movie poster should expose Primary image tag: %#v", items[0])
}
episodes, err := svc.Items(t.Context(), ItemsParams{ParentID: lib.ID, IncludeItemTypes: []string{"Episode"}, Limit: 50})
if err != nil {
t.Fatalf("episode query: %v", err)
}
if len(episodes["Items"].([]map[string]any)) != 0 {
t.Fatalf("movie library should not expose movies as episodes, got %#v", episodes)
}
item, err := svc.Item(t.Context(), media.ID, "user-1")
if err != nil {
t.Fatalf("item: %v", err)
}
if item["Type"] != "Movie" || item["ParentId"] != lib.ID {
t.Fatalf("direct item should stay Movie, got %#v", item)
}
rootMovies, err := svc.Items(t.Context(), ItemsParams{IncludeItemTypes: []string{"Movie"}, Recursive: true, Limit: 50})
if err != nil {
t.Fatalf("root movie query: %v", err)
}
rootItems := rootMovies["Items"].([]map[string]any)
if len(rootItems) != 1 || rootItems[0]["Id"] != media.ID || rootItems[0]["Type"] != "Movie" {
t.Fatalf("root movie query should include movie-library item despite season numbers, got %#v", rootItems)
}
rootEpisodes, err := svc.Items(t.Context(), ItemsParams{IncludeItemTypes: []string{"Episode"}, Recursive: true, Limit: 50})
if err != nil {
t.Fatalf("root episode query: %v", err)
}
if len(rootEpisodes["Items"].([]map[string]any)) != 0 {
t.Fatalf("root episode query should not expose movie-library item, got %#v", rootEpisodes)
}
}
func TestEmbyMovieLibraryFiltersMisplacedSeriesPaths(t *testing.T) {
svc := newTestEmbyService(t)
lib := model.Library{Name: "电影", Path: `/media/movies`, Type: "movie", Enabled: true}
if err := svc.repo.Library.Create(t.Context(), &lib); err != nil {
t.Fatalf("create library: %v", err)
}
show := model.Media{
Base: model.Base{ID: "misplaced-show"},
LibraryID: lib.ID,
Title: "错放剧集",
Path: `/media/movies/国产剧/错放剧集/Season 01/错放剧集 - S01E01.mkv`,
SeasonNum: 1,
EpisodeNum: 1,
}
movie := model.Media{
Base: model.Base{ID: "movie"},
LibraryID: lib.ID,
Title: "普通电影",
Path: `/media/movies/普通电影.2026.mkv`,
}
if err := svc.repo.DB.Create(&show).Error; err != nil {
t.Fatalf("create show: %v", err)
}
if err := svc.repo.DB.Create(&movie).Error; err != nil {
t.Fatalf("create movie: %v", err)
}
out, err := svc.Items(t.Context(), ItemsParams{ParentID: lib.ID, IncludeItemTypes: []string{"Movie"}, Limit: 50})
if err != nil {
t.Fatalf("items: %v", err)
}
items := out["Items"].([]map[string]any)
if len(items) != 1 || items[0]["Id"] != movie.ID {
t.Fatalf("movie library should filter misplaced series paths, got %#v", items)
}
item, err := svc.Item(t.Context(), show.ID, "user-1")
if err != nil {
t.Fatalf("direct item: %v", err)
}
if item["Type"] != "Episode" {
t.Fatalf("misplaced series path should be typed as Episode directly, got %#v", item)
}
}
// TestEmbyMovieLibraryGroupsEpisodicContentIntoSeries 验证方案 B: 电影类型库里
// 混入的「剧集结构」内容(如整合成剧集的剧场版/合集动画, 路径含 Season/SxxE)
// 在常规浏览(未指定 IncludeItemTypes)时应聚成 Series 卡片, 而不是以散装单集
// (Episode)漏出; 同库里真正的电影仍按 Movie 显示。两类并存于同一电影库视图。
func TestEmbyMovieLibraryGroupsEpisodicContentIntoSeries(t *testing.T) {
svc := newTestEmbyService(t)
lib := model.Library{Name: "动画电影", Path: `/media/动画电影`, Type: "movie", Enabled: true}
if err := svc.repo.Library.Create(t.Context(), &lib); err != nil {
t.Fatalf("create library: %v", err)
}
base := time.Now()
// 剧集结构内容: 同一部「高达剧场版」的两集, 单集 tmdb 各不相同(模拟 NFO 单集 id 污染)。
rows := []model.Media{
{
Base: model.Base{ID: "gundam-e13", CreatedAt: base.Add(2 * time.Minute)},
LibraryID: lib.ID,
Title: "高达剧场版",
Path: `/media/动画电影/高达剧场版/Season 01/高达剧场版 - S01E13.mkv`,
PosterURL: `/poster.jpg`,
TMDbID: 4375419,
SeasonNum: 1,
EpisodeNum: 13,
},
{
Base: model.Base{ID: "gundam-e14", CreatedAt: base.Add(3 * time.Minute)},
LibraryID: lib.ID,
Title: "高达剧场版",
Path: `/media/动画电影/高达剧场版/Season 01/高达剧场版 - S01E14.mkv`,
PosterURL: `/poster.jpg`,
TMDbID: 4375461,
SeasonNum: 1,
EpisodeNum: 14,
},
// 真正的电影。
{
Base: model.Base{ID: "real-movie", CreatedAt: base.Add(1 * time.Minute)},
LibraryID: lib.ID,
Title: "普通动画电影",
Path: `/media/动画电影/普通动画电影.2024.mkv`,
PosterURL: `/poster.jpg`,
},
}
for i := range rows {
if err := svc.repo.DB.Create(&rows[i]).Error; err != nil {
t.Fatalf("create media: %v", err)
}
}
out, err := svc.Items(t.Context(), ItemsParams{ParentID: lib.ID, Limit: 50})
if err != nil {
t.Fatalf("items: %v", err)
}
items := out["Items"].([]map[string]any)
var seriesCount, movieCount, episodeCount int
var seriesPayload map[string]any
for _, item := range items {
switch item["Type"] {
case "Series":
seriesCount++
seriesPayload = item
case "Movie":
movieCount++
case "Episode":
episodeCount++
}
}
if episodeCount != 0 {
t.Fatalf("movie library must not leak flat episodes, got %d episode items: %#v", episodeCount, items)
}
if seriesCount != 1 {
t.Fatalf("episodic content should collapse into exactly one Series card, got %d: %#v", seriesCount, items)
}
if movieCount != 1 {
t.Fatalf("real movie should stay as one Movie item, got %d: %#v", movieCount, items)
}
// 两集 tmdb 不同, 但按路径剧名聚成同一 Series, 集数应为 2。
if got := seriesPayload["RecursiveItemCount"]; got != 2 {
t.Fatalf("series should contain both episodes despite differing tmdb ids, got RecursiveItemCount=%v", got)
}
// Series 卡片可下钻: 解析其 season → episodes。
seriesID, _ := seriesPayload["Id"].(string)
if seriesID == "" {
t.Fatalf("series payload missing Id: %#v", seriesPayload)
}
drill, err := svc.Items(t.Context(), ItemsParams{ParentID: seriesID, Recursive: true, Limit: 50})
if err != nil {
t.Fatalf("series drill-down: %v", err)
}
drillItems := drill["Items"].([]map[string]any)
if len(drillItems) != 2 {
t.Fatalf("series drill-down should list both episodes, got %#v", drillItems)
}
}
func TestEmbyMergedLocalCloudMovieVersionsShareMediaSources(t *testing.T) {
svc := newTestEmbyService(t)
local := model.Library{Name: "国产电影", Path: `/media/国产电影`, Type: "movie", Enabled: true}
cloud := model.Library{Name: "OpenList · 国产电影", Path: BuildCloudLibraryPath("openlist", "/国产电影", "/国产电影"), Type: "movie", Enabled: true}
for _, lib := range []*model.Library{&local, &cloud} {
if err := svc.repo.Library.Create(t.Context(), lib); err != nil {
t.Fatalf("create library: %v", err)
}
}
for _, media := range []model.Media{
{
Base: model.Base{ID: "local-version", CreatedAt: time.Now()},
LibraryID: local.ID,
Title: "流浪地球",
Year: 2019,
Path: `/media/国产电影/流浪地球.2019.1080p.mkv`,
Container: "mkv",
Width: 1920,
},
{
Base: model.Base{ID: "cloud-version", CreatedAt: time.Now().Add(time.Minute)},
LibraryID: cloud.ID,
Title: "流浪地球",
Year: 2019,
Path: `cloud://openlist/国产电影/流浪地球.2019.2160p.mkv`,
Container: "mkv",
STRMURL: "https://example.invalid/cloud",
Width: 3840,
},
} {
if err := svc.repo.DB.Create(&media).Error; err != nil {
t.Fatalf("create media: %v", err)
}
}
items, err := svc.Items(t.Context(), ItemsParams{ParentID: local.ID, IncludeItemTypes: []string{"Movie"}, Recursive: true, Limit: 10})
if err != nil {
t.Fatalf("items: %v", err)
}
rows := items["Items"].([]map[string]any)
if len(rows) != 1 {
t.Fatalf("merged local/cloud versions should show as one item, got %#v", rows)
}
if rows[0]["Id"] != "local-version" {
t.Fatalf("local media should be the representative item, got %#v", rows[0])
}
sources := rows[0]["MediaSources"].([]map[string]any)
if len(sources) != 2 {
t.Fatalf("merged item should expose two media sources, got %#v", sources)
}
playback, err := svc.PlaybackInfo(t.Context(), "local-version", "user-1")
if err != nil {
t.Fatalf("playback: %v", err)
}
playSources := playback["MediaSources"].([]map[string]any)
if len(playSources) != 2 {
t.Fatalf("playback should expose local and cloud versions, got %#v", playSources)
}
}
func newTestEmbyService(t *testing.T) *EmbyService {
t.Helper()
db := newServiceTestDB(t, &model.Library{}, &model.Series{}, &model.Media{}, &model.Favorite{}, &model.PlaybackHistory{}, &model.User{}, &model.Setting{})
// 内存库 + 异步探测协程:限制为单连接,避免连接池新建连接时
// 拿到一个空白的 :memory: 实例(no such table)。
if sqlDB, err := db.DB(); err == nil {
sqlDB.SetMaxOpenConns(1)
}
repos := repository.New(db)
return NewEmbyService(&config.Config{}, zap.NewNop(), repos)
}
type fakeCloudPlaybackResolver struct {
link *cloud.DirectLink
typ string
ref string
ua string
}
func (f *fakeCloudPlaybackResolver) CloudResolve(_ context.Context, typ, fileRef, clientUA string) (*cloud.DirectLink, error) {
f.typ = typ
f.ref = fileRef
f.ua = clientUA
return f.link, nil
}
type fakeCloudPlaybackProber struct {
probe *ProbeResult
rawURL string
headers map[string]string
}
func (f *fakeCloudPlaybackProber) ProbeHTTP(_ context.Context, rawURL string, headers map[string]string) (*ProbeResult, error) {
f.rawURL = rawURL
f.headers = headers
return f.probe, nil
}