fix: honor STRM playback mode for cloud clients

This commit is contained in:
ShukeBta
2026-06-13 00:17:39 +08:00
parent 5d942f2c42
commit ba43117a57
9 changed files with 344 additions and 29 deletions
+20
View File
@@ -573,6 +573,7 @@ func embyItemByIDHandler(svc *service.Container) gin.HandlerFunc {
embyError(c, http.StatusNotFound, "item not found")
return
}
embyAttachRequestTokenToPlaybackInfo(c, out)
c.JSON(http.StatusOK, out)
}
}
@@ -1064,6 +1065,18 @@ func embyEmptyItemsHandler(_ *service.Container) gin.HandlerFunc {
}
}
func embyServerDomainsHandler(_ *service.Container) gin.HandlerFunc {
return func(c *gin.Context) {
c.JSON(http.StatusOK, []any{})
}
}
func embyDanmuRawHandler(_ *service.Container) gin.HandlerFunc {
return func(c *gin.Context) {
c.Data(http.StatusOK, "text/plain; charset=utf-8", nil)
}
}
func embyBrandingConfigHandler(_ *service.Container) gin.HandlerFunc {
return func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
@@ -1119,6 +1132,10 @@ func registerEmbyRoutes(r *gin.Engine, jwtSecret string, svc *service.Container)
for _, path := range []string{"/System/Endpoint", "/system/endpoint"} {
grp.GET(path, embySystemEndpointHandler(svc))
}
for _, path := range []string{"/System/Ext/ServerDomains", "/system/ext/serverdomains"} {
grp.GET(path, embyServerDomainsHandler(svc))
grp.HEAD(path, embyServerDomainsHandler(svc))
}
for _, path := range []string{"/System/Configuration/Public", "/system/configuration/public"} {
grp.GET(path, embyPublicServerConfigurationHandler(svc))
grp.HEAD(path, embyPublicServerConfigurationHandler(svc))
@@ -1193,6 +1210,8 @@ func registerEmbyRoutes(r *gin.Engine, jwtSecret string, svc *service.Container)
auth.GET("/Shows/NextUp", embyEmptyItemsHandler(svc))
auth.GET("/Users/:userId/Shows/NextUp", embyEmptyItemsHandler(svc))
auth.GET("/MediaSegments/:id", embyEmptyItemsHandler(svc))
auth.GET("/Items/:id/Similar", embyEmptyItemsHandler(svc))
auth.GET("/api/danmu/:id/raw", embyDanmuRawHandler(svc))
auth.GET("/Items/:id/PlaybackInfo", embyPlaybackInfoHandler(svc))
auth.POST("/Items/:id/PlaybackInfo", embyPlaybackInfoHandler(svc))
@@ -1257,6 +1276,7 @@ func registerLowercaseEmbyAuthRoutes(auth *gin.RouterGroup, svc *service.Contain
auth.GET("/shows/nextup", embyEmptyItemsHandler(svc))
auth.GET("/users/:userId/shows/nextup", embyEmptyItemsHandler(svc))
auth.GET("/mediasegments/:id", embyEmptyItemsHandler(svc))
auth.GET("/items/:id/similar", embyEmptyItemsHandler(svc))
auth.GET("/items/:id/playbackinfo", embyPlaybackInfoHandler(svc))
auth.POST("/items/:id/playbackinfo", embyPlaybackInfoHandler(svc))
+52 -2
View File
@@ -179,6 +179,56 @@ func TestEmbyPublicSystemInfoLooksLikeModernEmbyServer(t *testing.T) {
}
}
func TestEmbyMobileCompatibilityRoutesAvoidPlaybackBlocking404s(t *testing.T) {
gin.SetMode(gin.TestMode)
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
t.Fatalf("open db: %v", err)
}
if err := db.AutoMigrate(&model.User{}); err != nil {
t.Fatalf("migrate: %v", err)
}
if err := db.Create(&model.User{
Base: model.Base{ID: "user-1"},
Username: "viewer",
PasswordHash: "hash",
Role: "admin",
IsActive: true,
}).Error; err != nil {
t.Fatalf("create user: %v", err)
}
cfg := &config.Config{}
cfg.Secrets.JWTSecret = "test-secret"
repos := repository.New(db)
router := gin.New()
registerEmbyRoutes(router, cfg.Secrets.JWTSecret, &service.Container{
Repo: repos,
Emby: service.NewEmbyService(cfg, zap.NewNop(), repos),
})
token := signedTestToken(t, cfg.Secrets.JWTSecret)
tests := []struct {
path string
auth bool
wantCode int
}{
{path: "/emby/System/Ext/ServerDomains", wantCode: http.StatusOK},
{path: "/emby/Items/msgo-series-demo/Similar", auth: true, wantCode: http.StatusOK},
{path: "/emby/api/danmu/media-demo/raw", auth: true, wantCode: http.StatusOK},
}
for _, tc := range tests {
req := httptest.NewRequest(http.MethodGet, tc.path, nil)
if tc.auth {
req.Header.Set("X-Emby-Token", token)
}
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != tc.wantCode {
t.Fatalf("%s status = %d body=%s", tc.path, w.Code, w.Body.String())
}
}
}
func TestEmbySenPlayerDiscoveryRoutesReturnProtocolResponses(t *testing.T) {
gin.SetMode(gin.TestMode)
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
@@ -645,14 +695,14 @@ func TestEmbyPlaybackInfoTokenizesCloudPath(t *testing.T) {
}
source := body["MediaSources"].([]any)[0].(map[string]any)
pathURL, _ := source["Path"].(string)
if !strings.HasPrefix(pathURL, "/Videos/cloud-1/stream") || !strings.Contains(pathURL, "api_key=") {
if !strings.HasPrefix(pathURL, "/api/stream/cloud-1") || !strings.Contains(pathURL, "api_key=") {
t.Fatalf("cloud Path should be tokenized stream URL, got %#v", source)
}
if strings.Contains(pathURL, "/api/cloud/play/") {
t.Fatalf("cloud Path should not expose naked cloud play URL: %#v", source)
}
directURL, _ := source["DirectStreamUrl"].(string)
if !strings.HasPrefix(directURL, "/Videos/cloud-1/stream.") || !strings.Contains(directURL, "api_key=") {
if !strings.HasPrefix(directURL, "/api/stream/cloud-1") || !strings.Contains(directURL, "api_key=") {
t.Fatalf("DirectStreamUrl should stay tokenized: %#v", source)
}
}