mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-14 09:06:37 +08:00
158 lines
5.4 KiB
Go
158 lines
5.4 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/truewhile/MeBox/internal/config"
|
|
"github.com/truewhile/MeBox/internal/model"
|
|
"github.com/truewhile/MeBox/internal/repository"
|
|
)
|
|
|
|
// newDanmakuRemoteResolverTest 起一个假的远程 Emby(账号 + 挂载齐备),返回接好
|
|
// 弹幕解析器的 builder。
|
|
func newDanmakuRemoteResolverTest(t *testing.T, handler http.Handler) *serviceContainerBuilder {
|
|
t.Helper()
|
|
server := httptest.NewServer(handler)
|
|
t.Cleanup(server.Close)
|
|
|
|
db := newServiceTestDB(t, &model.StrmAccount{}, &model.EmbyMount{})
|
|
repos := repository.New(db)
|
|
cfg := &config.Config{}
|
|
remote := NewEmbyRemoteService(cfg, zap.NewNop(), repos, NewCryptoService("", zap.NewNop()))
|
|
remote.SetRuntimeCache(NewRuntimeCacheService(cfg, zap.NewNop()))
|
|
|
|
rawConfig, _ := json.Marshal(map[string]string{
|
|
"url": server.URL,
|
|
"api_key": "test-api-key",
|
|
"remote_user_id": "uid-1",
|
|
})
|
|
if err := repos.StrmAccount.Create(t.Context(), &model.StrmAccount{
|
|
Base: model.Base{ID: "acct-1"},
|
|
Name: "远程 Emby",
|
|
Provider: model.StrmProviderEmbyRemote,
|
|
Config: string(rawConfig),
|
|
Enabled: true,
|
|
}); err != nil {
|
|
t.Fatalf("create account: %v", err)
|
|
}
|
|
if err := repos.EmbyMount.Create(t.Context(), &model.EmbyMount{
|
|
Base: model.Base{ID: "mount-1"},
|
|
AccountID: "acct-1",
|
|
RemoteViewID: "view-1",
|
|
RemoteViewName: "新番连载",
|
|
CollectionType: "tvshows",
|
|
Enabled: true,
|
|
}); err != nil {
|
|
t.Fatalf("create mount: %v", err)
|
|
}
|
|
return &serviceContainerBuilder{
|
|
cfg: cfg,
|
|
log: zap.NewNop(),
|
|
repos: repos,
|
|
c: &Container{Cfg: cfg, Log: zap.NewNop(), Repo: repos, EmbyRemote: remote},
|
|
}
|
|
}
|
|
|
|
func danmakuResolverTestEpisode(id string) map[string]any {
|
|
return map[string]any{
|
|
"Id": id,
|
|
"Name": "第 1 集",
|
|
"Type": "Episode",
|
|
"Path": "/media/anime/PSYREN/" + id + ".mkv",
|
|
"MediaSources": []any{
|
|
map[string]any{"Id": id + "-src", "Container": "mkv", "Size": int64(1 << 20)},
|
|
},
|
|
}
|
|
}
|
|
|
|
// 目录类条目(整部剧/季)自身没有媒体源,拿它的 ID 去请求远程 /Videos/{id}/stream 只会
|
|
// 回 500,哈希层于是每次都白白跳过。这条用例保证弹幕解析器跟随 /api/stream 的规则先
|
|
// 落到第一集:哈希算的是真正要播的那条流。
|
|
func TestResolveDanmakuRemoteMediaUsesPlayableChildForContainer(t *testing.T) {
|
|
var requested []string
|
|
b := newDanmakuRemoteResolverTest(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
requested = append(requested, r.URL.Path)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
switch r.URL.Path {
|
|
case "/emby/Users/uid-1/Items/series-100":
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"Id": "series-100", "Name": "PSYREN", "Type": "Series", "IsFolder": true,
|
|
})
|
|
case "/emby/Users/uid-1/Items":
|
|
if got := r.URL.Query().Get("ParentId"); got != "series-100" {
|
|
t.Errorf("ParentId = %q, want series-100", got)
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"Items": []map[string]any{
|
|
{"Id": "season-1", "Type": "Season", "IndexNumber": 1},
|
|
danmakuResolverTestEpisode("ep-1"),
|
|
},
|
|
})
|
|
case "/emby/Users/uid-1/Items/ep-1":
|
|
_ = json.NewEncoder(w).Encode(danmakuResolverTestEpisode("ep-1"))
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
|
|
media, streamURL, err := b.resolveDanmakuRemoteMedia(
|
|
context.Background(), EncodeEmbyRemoteID("mount-1", "series-100"))
|
|
if err != nil {
|
|
t.Fatalf("resolve failed: %v", err)
|
|
}
|
|
if media == nil {
|
|
t.Fatal("resolve returned no media metadata")
|
|
}
|
|
if !strings.Contains(streamURL, "/Videos/ep-1/stream") {
|
|
t.Fatalf("stream url = %q, want the playable child ep-1", streamURL)
|
|
}
|
|
if strings.Contains(streamURL, "series-100") {
|
|
t.Fatalf("stream url = %q, must not point at the container id (remote Emby answers 500)", streamURL)
|
|
}
|
|
if !strings.Contains(strings.Join(requested, " "), "/Items/ep-1") {
|
|
t.Fatalf("metadata must come from the playable child, requests = %v", requested)
|
|
}
|
|
}
|
|
|
|
// 本身可播的条目(单集/电影)不能被改写:直接取它自己的流地址。
|
|
func TestResolveDanmakuRemoteMediaKeepsPlayableItem(t *testing.T) {
|
|
b := newDanmakuRemoteResolverTest(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if r.URL.Path != "/emby/Users/uid-1/Items/ep-2" {
|
|
t.Errorf("unexpected upstream path %q", r.URL.Path)
|
|
}
|
|
_ = json.NewEncoder(w).Encode(danmakuResolverTestEpisode("ep-2"))
|
|
}))
|
|
|
|
media, streamURL, err := b.resolveDanmakuRemoteMedia(
|
|
context.Background(), EncodeEmbyRemoteID("mount-1", "ep-2"))
|
|
if err != nil {
|
|
t.Fatalf("resolve failed: %v", err)
|
|
}
|
|
if media == nil {
|
|
t.Fatal("resolve returned no media metadata")
|
|
}
|
|
if !strings.Contains(streamURL, "/Videos/ep-2/stream") {
|
|
t.Fatalf("stream url = %q, want /Videos/ep-2/stream", streamURL)
|
|
}
|
|
}
|
|
|
|
// 不是远程伪装 ID 时直接报错,不去猜一个可以拉取的地址。
|
|
func TestResolveDanmakuRemoteMediaRejectsPlainID(t *testing.T) {
|
|
b := newDanmakuRemoteResolverTest(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
t.Error("plain local ids must not reach the remote Emby")
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}))
|
|
|
|
if _, _, err := b.resolveDanmakuRemoteMedia(context.Background(), "local-media-1"); err == nil {
|
|
t.Fatal("expected an error for a non remote-emby id")
|
|
}
|
|
}
|