Skip to content

Commit a7a5b70

Browse files
authored
fix: 避免非播放动作导致的 302 链接获取,以及兼容一些播放器会同时发起多个播放请求 (#80)
PlaybackInfo 接口通常会带一个 IsPlayback 参数,Emby 官方客户端的实现中,进入详情页触发的 PlaybackInfo 请求的 IsPlayback=false,而真正播放的时候的 PlaybackInfo 调用 IsPlayback=true,所以可以根据这个特征做过滤 另外观察到一些播放器会发起多个 IsPlayback=true 的请求,同时触发多个 stream 接口调用,所以在 stream 接口加了锁避免同一个 item 多次触发 302 链接获取
1 parent 4eb9763 commit a7a5b70

3 files changed

Lines changed: 97 additions & 8 deletions

File tree

internal/handler/emby.go

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,18 @@ import (
2020
"regexp"
2121
"strconv"
2222
"strings"
23+
"sync"
2324

2425
"github.com/gin-gonic/gin"
2526
)
2627

2728
// Emby服务器处理器
2829
type EmbyServerHandler struct {
29-
server *emby.EmbyServer // Emby 服务器
30-
routerRules []RegexpRouteRule // 正则路由规则
31-
proxy *httputil.ReverseProxy // 反向代理
32-
httpStrmHandler StrmHandlerFunc
30+
server *emby.EmbyServer // Emby 服务器
31+
routerRules []RegexpRouteRule // 正则路由规则
32+
proxy *httputil.ReverseProxy // 反向代理
33+
httpStrmHandler StrmHandlerFunc
34+
playbackInfoMutex sync.Map // 视频流处理并发控制,确保同一个 item ID 的重定向请求串行化,避免重复获取缓存
3335
}
3436

3537
// 初始化
@@ -119,6 +121,16 @@ func (*EmbyServerHandler) GetSubtitleCacheRegexp() *regexp.Regexp {
119121
// /Items/:itemId/PlaybackInfo
120122
// 强制将 HTTPStrm 设置为支持直链播放和转码、AlistStrm 设置为支持直链播放并且禁止转码
121123
func (embyServerHandler *EmbyServerHandler) ModifyPlaybackInfo(rw *http.Response) error {
124+
// 检查 IsPlayback 参数,如果为 false 则不做修改直接返回
125+
// 从响应的请求中获取参数,因为响应对象包含原始请求
126+
// 使用不区分大小写的方式获取查询参数
127+
isPlayback := getQueryValueCaseInsensitive(rw.Request.URL.Query(), "IsPlayback")
128+
logging.Debugf("IsPlayback 参数值: '%s' (请求 URL: %s)", isPlayback, rw.Request.URL.String())
129+
if strings.ToLower(isPlayback) == "false" {
130+
logging.Debug("IsPlayback=false,跳过 PlaybackInfo 修改")
131+
return nil
132+
}
133+
122134
defer rw.Body.Close()
123135
body, err := io.ReadAll(rw.Body)
124136
if err != nil {
@@ -235,6 +247,29 @@ func (embyServerHandler *EmbyServerHandler) VideosHandler(ctx *gin.Context) {
235247
// EmbyServer >= 4.9 ====> mediaSourceID = mediasource_31
236248
mediaSourceID := ctx.Query("mediasourceid")
237249

250+
// 从 URL 中提取 item ID(例如:/emby/videos/43609/stream 中的 43609)
251+
var itemID string
252+
if matches := constants.EmbyRegexp.Router.VideosHandler.FindStringSubmatch(orginalPath); len(matches) > 0 {
253+
parts := strings.Split(orginalPath, "/")
254+
for i, part := range parts {
255+
if part == "videos" && i+1 < len(parts) {
256+
itemID = parts[i+1]
257+
break
258+
}
259+
}
260+
}
261+
262+
// 并发控制:确保同一个 item ID 只有一个任务在运行
263+
// 将整个处理流程放在锁内,避免重复查询和重复获取重定向 URL
264+
var mu *sync.Mutex
265+
if itemID != "" {
266+
mutex, _ := embyServerHandler.playbackInfoMutex.LoadOrStore(itemID, &sync.Mutex{})
267+
mu = mutex.(*sync.Mutex)
268+
mu.Lock()
269+
defer mu.Unlock()
270+
logging.Debugf("开始处理 item %s 的 VideosHandler 请求", itemID)
271+
}
272+
238273
logging.Debugf("请求 ItemsServiceQueryItem:%s", mediaSourceID)
239274
itemResponse, err := embyServerHandler.server.ItemsServiceQueryItem(strings.Replace(mediaSourceID, "mediasource_", "", 1), 1, "Path,MediaSources") // 查询 item 需要去除前缀仅保留数字部分
240275
if err != nil {
@@ -257,6 +292,7 @@ func (embyServerHandler *EmbyServerHandler) VideosHandler(ctx *gin.Context) {
257292
switch strmFileType {
258293
case constants.HTTPStrm:
259294
if *mediasource.Protocol == emby.HTTP {
295+
// httpStrmHandler 内部有缓存机制,锁确保串行化访问
260296
ctx.Redirect(http.StatusFound, embyServerHandler.httpStrmHandler(*mediasource.Path, ctx.Request.UserAgent()))
261297
return
262298
}

internal/handler/jellyfin.go

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,18 @@ import (
2020
"regexp"
2121
"strconv"
2222
"strings"
23+
"sync"
2324

2425
"github.com/gin-gonic/gin"
2526
)
2627

2728
// Jellyfin 服务器处理器
2829
type JellyfinHandler struct {
29-
server *jellyfin.Jellyfin // Jellyfin 服务器
30-
routerRules []RegexpRouteRule // 正则路由规则
31-
proxy *httputil.ReverseProxy // 反向代理
32-
httpStrmHandler StrmHandlerFunc
30+
server *jellyfin.Jellyfin // Jellyfin 服务器
31+
routerRules []RegexpRouteRule // 正则路由规则
32+
proxy *httputil.ReverseProxy // 反向代理
33+
httpStrmHandler StrmHandlerFunc
34+
playbackInfoMutex sync.Map // 视频流处理并发控制,确保同一个 item ID 的重定向请求串行化,避免重复获取缓存
3335
}
3436

3537
func NewJellyfinHander(addr string, apiKey string) (*JellyfinHandler, error) {
@@ -101,6 +103,16 @@ func (JellyfinHandler) GetSubtitleCacheRegexp() *regexp.Regexp {
101103
// /Items/:itemId
102104
// 强制将 HTTPStrm 设置为支持直链播放和转码、AlistStrm 设置为支持直链播放并且禁止转码
103105
func (jellyfinHandler *JellyfinHandler) ModifyPlaybackInfo(rw *http.Response) error {
106+
// 检查 IsPlayback 参数,如果为 false 则不做修改直接返回
107+
// 从响应的请求中获取参数,因为响应对象包含原始请求
108+
// 使用不区分大小写的方式获取查询参数
109+
isPlayback := getQueryValueCaseInsensitive(rw.Request.URL.Query(), "IsPlayback")
110+
logging.Debugf("IsPlayback 参数值: '%s' (请求 URL: %s)", isPlayback, rw.Request.URL.String())
111+
if strings.ToLower(isPlayback) == "false" {
112+
logging.Debug("IsPlayback=false,跳过 PlaybackInfo 修改")
113+
return nil
114+
}
115+
104116
defer rw.Body.Close()
105117
data, err := io.ReadAll(rw.Body)
106118
if err != nil {
@@ -207,6 +219,30 @@ func (jellyfinHandler *JellyfinHandler) VideosHandler(ctx *gin.Context) {
207219
return
208220
}
209221

222+
// 从 URL 中提取 item ID(例如:/Videos/813a630bcf9c3f693a2ec8c498f868d2/stream 中的 813a630bcf9c3f693a2ec8c498f868d2)
223+
var itemID string
224+
path := ctx.Request.URL.Path
225+
if matches := constants.JellyfinRegexp.Router.VideosHandler.FindStringSubmatch(path); len(matches) > 0 {
226+
parts := strings.Split(path, "/")
227+
for i, part := range parts {
228+
if part == "Videos" && i+1 < len(parts) {
229+
itemID = parts[i+1]
230+
break
231+
}
232+
}
233+
}
234+
235+
// 并发控制:确保同一个 item ID 只有一个任务在运行
236+
// 将整个处理流程放在锁内,避免重复查询和重复获取重定向 URL
237+
var mu *sync.Mutex
238+
if itemID != "" {
239+
mutex, _ := jellyfinHandler.playbackInfoMutex.LoadOrStore(itemID, &sync.Mutex{})
240+
mu = mutex.(*sync.Mutex)
241+
mu.Lock()
242+
defer mu.Unlock()
243+
logging.Debugf("开始处理 item %s 的 VideosHandler 请求", itemID)
244+
}
245+
210246
mediaSourceID := ctx.Query("mediasourceid")
211247
logging.Debugf("请求 ItemsServiceQueryItem:%s", mediaSourceID)
212248
itemResponse, err := jellyfinHandler.server.ItemsServiceQueryItem(mediaSourceID, 1, "Path,MediaSources") // 查询 item 需要去除前缀仅保留数字部分
@@ -230,6 +266,7 @@ func (jellyfinHandler *JellyfinHandler) VideosHandler(ctx *gin.Context) {
230266
switch strmFileType {
231267
case constants.HTTPStrm:
232268
if *mediasource.Protocol == jellyfin.HTTP {
269+
// httpStrmHandler 内部有缓存机制,锁确保串行化访问
233270
ctx.Redirect(http.StatusFound, jellyfinHandler.httpStrmHandler(*mediasource.Path, ctx.Request.UserAgent()))
234271
return
235272
}

internal/handler/utils.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,22 @@ func responseModifyCreater(proxy *httputil.ReverseProxy, modifyResponseFN func(r
4040
}
4141
}
4242

43+
// 不区分大小写地获取查询参数值
44+
//
45+
// 从 url.Values 中查找指定键名的值,忽略大小写
46+
func getQueryValueCaseInsensitive(query url.Values, key string) string {
47+
keyLower := strings.ToLower(key)
48+
for k, v := range query {
49+
if strings.ToLower(k) == keyLower {
50+
if len(v) > 0 {
51+
return v[0]
52+
}
53+
return ""
54+
}
55+
}
56+
return ""
57+
}
58+
4359
// 根据 Strm 文件路径识别 Strm 文件类型
4460
//
4561
// 返回 Strm 文件类型和一个可选配置

0 commit comments

Comments
 (0)