mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 03:26:37 +08:00
0eb3f104f4
- 新增 emby_remote 提供方:通过 STRM 账号体系配置远程 Emby 地址/用户名/密码
- EmbyService 聚合远程媒体库 Views/Items/详情/最近添加/全局搜索,远程数据不落库
- 条目 ID 统一伪装为 embyremote~{account}~{id},请求按账号路由回远程
- 播放支持账号级 proxy_play 配置:默认客户端直连远程;开启后由 MMTL 反向代理流/字幕
- 图片/元数据全部实时透传远程,播放状态(已看/收藏)透传回远程 Emby
- cloud 包新增 emby Provider(Ping/List/Resolve),账号测试/目录浏览自动生效
- 前端 STRM 页面支持 Emby 远程挂载账号(地址+凭据+代理开关)
136 lines
3.9 KiB
Go
136 lines
3.9 KiB
Go
package cloud
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// fakeEmbyServer 记录请求,按路径返回远程 Emby 风格响应。
|
|
func fakeEmbyServer(t *testing.T) *httptest.Server {
|
|
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case r.Method == http.MethodPost && r.URL.Path == "/emby/Users/AuthenticateByName":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"AccessToken":"remote-token","User":{"Id":"user-9"}}`))
|
|
case r.URL.Path == "/emby/System/Info":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"ServerName":"RemoteEmby"}`))
|
|
case r.URL.Path == "/emby/Users/user-9/Items" && r.URL.Query().Get("ParentId") == "":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"Items":[{"Id":"view-1","Name":"Movies","Type":"CollectionFolder","IsFolder":true}]}`))
|
|
case r.URL.Path == "/emby/Users/user-9/Items":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"Items":[{"Id":"movie-1","Name":"Avatar","Type":"Movie","IsFolder":false}]}`))
|
|
case strings.Contains(r.URL.Path, "/emby/Videos/movie-1/stream"):
|
|
w.Header().Set("Content-Type", "video/mp4")
|
|
_, _ = w.Write([]byte("fake-video-bytes"))
|
|
default:
|
|
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
|
}
|
|
}))
|
|
}
|
|
|
|
func TestEmbyProviderPingAuthenticatesAndGetsToken(t *testing.T) {
|
|
srv := fakeEmbyServer(t)
|
|
defer srv.Close()
|
|
|
|
p, err := New(TypeEmbyRemote, map[string]any{
|
|
"url": srv.URL,
|
|
"username": "alice",
|
|
"password": "secret",
|
|
}, srv.Client())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := p.Ping(context.Background()); err != nil {
|
|
t.Fatalf("ping: %v", err)
|
|
}
|
|
// 认证成功后 token 被记住,第二次 Ping 不应再走登录。
|
|
if err := p.Ping(context.Background()); err != nil {
|
|
t.Fatalf("ping 2: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestEmbyProviderListViewsAndChildren(t *testing.T) {
|
|
srv := fakeEmbyServer(t)
|
|
defer srv.Close()
|
|
|
|
p, err := New(TypeEmbyRemote, map[string]any{
|
|
"url": srv.URL,
|
|
"api_key": "fixed-token",
|
|
"remote_user_id": "user-9",
|
|
}, srv.Client())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
root, err := p.List(context.Background(), "")
|
|
if err != nil {
|
|
t.Fatalf("list root: %v", err)
|
|
}
|
|
if len(root) != 1 || root[0].Name != "Movies" || !root[0].IsDir {
|
|
t.Fatalf("root listing = %+v", root)
|
|
}
|
|
children, err := p.List(context.Background(), "view-1")
|
|
if err != nil {
|
|
t.Fatalf("list children: %v", err)
|
|
}
|
|
if len(children) != 1 || children[0].Name != "Avatar" || children[0].ID != "movie-1" {
|
|
t.Fatalf("children = %+v", children)
|
|
}
|
|
}
|
|
|
|
func TestEmbyProviderResolveDirectURLByDefault(t *testing.T) {
|
|
srv := fakeEmbyServer(t)
|
|
defer srv.Close()
|
|
|
|
p, err := New(TypeEmbyRemote, map[string]any{
|
|
"url": srv.URL,
|
|
"api_key": "fixed-token",
|
|
"remote_user_id": "user-9",
|
|
}, srv.Client())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
link, err := p.Resolve(context.Background(), "movie-1")
|
|
if err != nil {
|
|
t.Fatalf("resolve: %v", err)
|
|
}
|
|
if !strings.Contains(link.URL, "/emby/Videos/movie-1/stream") {
|
|
t.Fatalf("url = %q", link.URL)
|
|
}
|
|
if !strings.Contains(link.URL, "api_key=fixed-token") {
|
|
t.Fatalf("url missing api_key: %q", link.URL)
|
|
}
|
|
// 默认不代理播放流量。
|
|
if link.Proxy {
|
|
t.Fatal("emby remote must not proxy by default")
|
|
}
|
|
}
|
|
|
|
func TestEmbyProviderResolveProxyWhenConfigured(t *testing.T) {
|
|
srv := fakeEmbyServer(t)
|
|
defer srv.Close()
|
|
|
|
p, err := New(TypeEmbyRemote, map[string]any{
|
|
"url": srv.URL,
|
|
"api_key": "fixed-token",
|
|
"remote_user_id": "user-9",
|
|
"proxy_play": "true",
|
|
}, srv.Client())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
link, err := p.Resolve(context.Background(), "movie-1")
|
|
if err != nil {
|
|
t.Fatalf("resolve: %v", err)
|
|
}
|
|
if !link.Proxy {
|
|
t.Fatal("proxy_play=true must mark link as proxied")
|
|
}
|
|
if link.URL == "" {
|
|
t.Fatal("proxy link must still carry the remote URL")
|
|
}
|
|
} |