package handler import ( "errors" "net/http" "net/url" "strings" "github.com/gin-gonic/gin" "github.com/ShukeBta/MediaStationGo/internal/service" ) func embyPlaybackInfoHandler(svc *service.Container) gin.HandlerFunc { return func(c *gin.Context) { uid := c.Param("userId") if uid == "" { uid = embyUserID(c) } out, err := svc.Emby.PlaybackInfo(c.Request.Context(), c.Param("id"), uid) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if out == nil { embyError(c, http.StatusNotFound, "not found") return } embyAttachRequestTokenToMediaSources(c, out) c.JSON(http.StatusOK, out) } } func embyAttachRequestTokenToMediaSources(c *gin.Context, out any) { token := embyRequestToken(c) if token == "" || out == nil { return } embyAttachTokenToMediaSourcesValue(out, token) } func embyAttachTokenToMediaSourcesValue(value any, token string) { switch typed := value.(type) { case map[string]any: embyAttachTokenToMediaSourcesMap(typed, token) case gin.H: embyAttachTokenToMediaSourcesMap(map[string]any(typed), token) case []map[string]any: for _, item := range typed { embyAttachTokenToMediaSourcesMap(item, token) } case []any: for _, item := range typed { embyAttachTokenToMediaSourcesValue(item, token) } } } func embyAttachTokenToMediaSourcesMap(out map[string]any, token string) { if out == nil { return } if sources, ok := out["MediaSources"].([]map[string]any); ok { embyAttachTokenToMediaSources(sources, token) } else if sources, ok := out["MediaSources"].([]any); ok { for _, source := range sources { if sourceMap, ok := source.(map[string]any); ok { embyAttachTokenToMediaSources([]map[string]any{sourceMap}, token) } } } if items, ok := out["Items"]; ok { embyAttachTokenToMediaSourcesValue(items, token) } } func embyAttachTokenToMediaSources(sources []map[string]any, token string) { for _, source := range sources { for _, key := range []string{"DirectStreamUrl", "TranscodingUrl"} { raw, ok := source[key].(string) if !ok { continue } source[key] = embyAppendAPIKey(raw, token) } } } func embyRequestToken(c *gin.Context) string { if c == nil { return "" } for _, key := range []string{"api_key", "apiKey", "ApiKey", "token", "X-Emby-Token", "X-MediaBrowser-Token"} { if value := strings.TrimSpace(c.Query(key)); value != "" { return value } } for _, header := range []string{"X-Emby-Token", "X-MediaBrowser-Token"} { if value := strings.TrimSpace(c.GetHeader(header)); value != "" { return value } } for _, header := range []string{"Authorization", "X-Emby-Authorization", "X-MediaBrowser-Authorization"} { if token := embyTokenFromAuthHeader(c.GetHeader(header)); token != "" { return token } } return "" } func embyTokenFromAuthHeader(value string) string { value = strings.TrimSpace(value) if value == "" { return "" } for _, prefix := range []string{"Bearer ", "Emby "} { if strings.HasPrefix(value, prefix) { return strings.TrimSpace(strings.TrimPrefix(value, prefix)) } } for _, part := range strings.Split(value, ",") { part = strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(part), "MediaBrowser ")) if !strings.HasPrefix(part, "Token=") { continue } token := strings.TrimSpace(strings.TrimPrefix(part, "Token=")) return strings.Trim(token, `"`) } if strings.Contains(value, "Token=") { return "" } return value } func embyAppendAPIKey(raw, token string) string { raw = strings.TrimSpace(raw) token = strings.TrimSpace(token) if raw == "" || token == "" { return raw } if strings.HasPrefix(raw, "//") { return raw } u, err := url.Parse(raw) if err != nil || u.IsAbs() { return raw } q := u.Query() if q.Get("api_key") == "" && q.Get("apiKey") == "" && q.Get("token") == "" { q.Set("api_key", token) u.RawQuery = q.Encode() } return u.String() } // embyVideoStreamHandler 是 GET /Videos/{id}/stream 的入口, // 直接代理到我们的 /api/stream/{id}(同一个 ServeFile)。 func embyVideoStreamHandler(svc *service.Container, cloudMode string) gin.HandlerFunc { return func(c *gin.Context) { uid := embyUserID(c) item, err := svc.Emby.Item(c.Request.Context(), c.Param("id"), uid) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if item == nil { c.Status(http.StatusNotFound) return } if embyShouldRedirectVideoStreamToSTRM(c, svc, c.Param("id"), cloudMode) { target := "/api/stream/" + url.PathEscape(strings.TrimSpace(c.Param("id"))) if token := embyPlaybackRedirectToken(c, svc); token != "" { target = embyAppendAPIKey(target, token) } setRedirectNoStoreHeaders(c) c.Redirect(http.StatusFound, absoluteRequestURL(c, target)) return } // 直接调用 Stream service 写入 response。 // 此前这里把所有错误一律吞成 404:云盘 Cookie 过期、直链解析失败、 // STRM 播放被关闭……在第三方播放器上全部表现为「404 不存在」, // 无法排查。现在区分:行不存在→404;云盘播放不可用/上游故障→502+原因。 err = svc.Stream.ServeFileWithCloudMode(c.Writer, c.Request, c.Param("id"), cloudMode) switch { case err == nil: case errors.Is(err, service.ErrMediaNotFound): c.Status(http.StatusNotFound) case errors.Is(err, service.ErrCloudPlaybackDisabled): if !c.Writer.Written() { c.JSON(http.StatusConflict, gin.H{"error": err.Error()}) } default: if !c.Writer.Written() { c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) } } } } func embyPlaybackRedirectToken(c *gin.Context, svc *service.Container) string { if token := embyRequestToken(c); token != "" { return token } if c == nil || svc == nil || svc.Auth == nil || svc.Repo == nil || svc.Repo.User == nil { return "" } uid := embyUserID(c) if uid == "" { return "" } u, err := svc.Repo.User.FindByID(c.Request.Context(), uid) if err != nil || u == nil { return "" } token, err := svc.Auth.IssueEmbyToken(u) if err != nil { return "" } return token } func embyShouldRedirectVideoStreamToSTRM(c *gin.Context, svc *service.Container, mediaID, cloudMode string) bool { if c == nil || svc == nil || svc.Repo == nil || svc.Repo.Media == nil || cloudMode != service.CloudPlaybackModeRedirectProxy { return false } settings := service.CloudPlaybackSettings(c.Request.Context(), svc.Repo) if settings.PreferredMode != service.CloudPlaybackModeSTRM || !settings.STRMEnabled { return false } m, err := svc.Repo.Media.FindByID(c.Request.Context(), mediaID) if err != nil || m == nil { return false } return strings.TrimSpace(m.STRMURL) != "" } func embyVideoHLSPlaylistHandler(svc *service.Container) gin.HandlerFunc { return func(c *gin.Context) { uid := embyUserID(c) item, err := svc.Emby.Item(c.Request.Context(), c.Param("id"), uid) if err != nil || item == nil || svc.Stream == nil { c.Status(http.StatusNotFound) return } err = svc.Stream.ServeHLSPlaylist(c.Writer, c.Request, c.Param("id")) if errors.Is(err, service.ErrTranscodeDisabled) { c.JSON(http.StatusConflict, gin.H{"error": "transcode disabled"}) return } if errors.Is(err, service.ErrTranscodeBusy) { c.JSON(http.StatusTooManyRequests, gin.H{"error": "transcode busy"}) return } if err != nil { c.Status(http.StatusNotFound) } } } func embyVideoHLSSegmentHandler(svc *service.Container) gin.HandlerFunc { return func(c *gin.Context) { uid := embyUserID(c) item, err := svc.Emby.Item(c.Request.Context(), c.Param("id"), uid) if err != nil || item == nil || svc.Stream == nil { c.Status(http.StatusNotFound) return } if err := svc.Stream.ServeHLSSegment(c.Writer, c.Request, c.Param("id"), c.Param("seg")); err != nil { c.Status(http.StatusNotFound) } } }