mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
123 lines
4.1 KiB
Go
123 lines
4.1 KiB
Go
package handler
|
||
|
||
import (
|
||
"net/http"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
|
||
"github.com/truewhile/MeBox/internal/service"
|
||
)
|
||
|
||
// Emby 发现类接口的 handler:NextUp / Similar / Genres。
|
||
//
|
||
// 这三个接口此前返回空列表,导致第三方客户端首页「接下来播放」、详情页
|
||
// 「相似推荐」、按类型浏览全部为空白。它们必须始终返回 200 + 合法信封,
|
||
// 因为客户端在首页刷新时会并发请求,任何 4xx/5xx 都会被判定为服务端异常。
|
||
|
||
func embyNextUpHandler(svc *service.Container) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
userID := embyScopedUserID(c)
|
||
if userID == "" {
|
||
c.JSON(http.StatusOK, embyEmptyItemsPayload())
|
||
return
|
||
}
|
||
limit, _ := strconv.Atoi(embyFirstNonEmptyString(firstQueryValue(c, "Limit", "limit"), ""))
|
||
// YamBy / Emby 进剧集详情会带 SeriesId 请求「本剧下一集」。
|
||
// 忽略该参数会把全站 NextUp 第一条塞进详情页「继续播放」。
|
||
// 注意:不要把普通 ParentId(媒体库)当成 SeriesId,否则首页 NextUp 会被滤空。
|
||
seriesID := firstQueryValue(c, "SeriesId", "seriesId", "seriesid")
|
||
if seriesID == "" {
|
||
if parentID := firstQueryValue(c, "ParentId", "parentId", "parentid"); parentID != "" {
|
||
if strings.HasPrefix(parentID, "msgo-series-") || service.IsEmbyRemoteID(parentID) {
|
||
seriesID = parentID
|
||
}
|
||
}
|
||
}
|
||
out, err := svc.Emby.NextUp(c.Request.Context(), userID, seriesID, limit)
|
||
if err != nil {
|
||
c.JSON(http.StatusOK, embyEmptyItemsPayload())
|
||
return
|
||
}
|
||
embyAttachRequestTokenToMediaSources(c, out)
|
||
c.JSON(http.StatusOK, out)
|
||
}
|
||
}
|
||
|
||
// embyShowNextUpHandler 处理 /Shows/{id}/NextUp:把路径上的剧集 ID 当作 SeriesId。
|
||
func embyShowNextUpHandler(svc *service.Container) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
userID := embyScopedUserID(c)
|
||
if userID == "" {
|
||
c.JSON(http.StatusOK, embyEmptyItemsPayload())
|
||
return
|
||
}
|
||
seriesID := strings.TrimSpace(c.Param("id"))
|
||
if seriesID == "" || strings.EqualFold(seriesID, "NextUp") {
|
||
c.JSON(http.StatusOK, embyEmptyItemsPayload())
|
||
return
|
||
}
|
||
limit, _ := strconv.Atoi(embyFirstNonEmptyString(firstQueryValue(c, "Limit", "limit"), ""))
|
||
out, err := svc.Emby.NextUp(c.Request.Context(), userID, seriesID, limit)
|
||
if err != nil {
|
||
c.JSON(http.StatusOK, embyEmptyItemsPayload())
|
||
return
|
||
}
|
||
embyAttachRequestTokenToMediaSources(c, out)
|
||
c.JSON(http.StatusOK, out)
|
||
}
|
||
}
|
||
|
||
func embySimilarHandler(svc *service.Container) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
mediaID := strings.TrimSpace(c.Param("id"))
|
||
if mediaID == "" {
|
||
c.JSON(http.StatusOK, embyEmptyItemsPayload())
|
||
return
|
||
}
|
||
limit, _ := strconv.Atoi(embyFirstNonEmptyString(firstQueryValue(c, "Limit", "limit"), ""))
|
||
out, err := svc.Emby.SimilarItems(c.Request.Context(), mediaID, embyEffectiveUserID(c), limit)
|
||
if err != nil {
|
||
c.JSON(http.StatusOK, embyEmptyItemsPayload())
|
||
return
|
||
}
|
||
embyAttachRequestTokenToMediaSources(c, out)
|
||
c.JSON(http.StatusOK, out)
|
||
}
|
||
}
|
||
|
||
func embyGenresHandler(svc *service.Container) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
parentID := firstQueryValue(c, "ParentId", "parentId", "parentid")
|
||
out, err := svc.Emby.Genres(c.Request.Context(), embyEffectiveUserID(c), parentID)
|
||
if err != nil {
|
||
c.JSON(http.StatusOK, embyEmptyItemsPayload())
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, out)
|
||
}
|
||
}
|
||
|
||
// embyScopedUserID 解析「按用户请求」的 Emby 接口的生效用户。
|
||
//
|
||
// 路由上带 :userId 时(/Users/{uid}/Shows/NextUp),只允许查询自己:客户端
|
||
// 偶尔会带着别人的 id 请求,直接采信等于开放他人观看历史的读取。管理员同样
|
||
// 按自己处理,避免出现一条无人使用的越权路径。
|
||
func embyScopedUserID(c *gin.Context) string {
|
||
caller := embyEffectiveUserID(c)
|
||
requested := strings.TrimSpace(c.Param("userId"))
|
||
if requested == "" {
|
||
return caller
|
||
}
|
||
if requested == caller {
|
||
return caller
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// embyEmptyItemsPayload 与 embyEmptyItemsHandler 保持同一形状。
|
||
func embyEmptyItemsPayload() gin.H {
|
||
return gin.H{"Items": []any{}, "TotalRecordCount": 0}
|
||
}
|