Skip to content

Commit f4cb788

Browse files
committed
feat(EmbyServerHandler、JellyfinHandler): 添加 HTTPStrm 最终 URL 获取功能,减少客户端重定向次数
1 parent 6afabca commit f4cb788

5 files changed

Lines changed: 102 additions & 4 deletions

File tree

config/config.yaml.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ ClientFilter: # 客户端过滤器
4141
HTTPStrm: # HTTPStrm 相关配置(Strm 文件内容是 标准 HTTP URL)
4242
Enable: True # 是否开启 HttpStrm 重定向
4343
TransCode: False # False:强制关闭转码 True:保持原有转码设置
44+
FinalURL: True # 对 URL 进行重定向判断,找到非重定向地址再重定向给客户端,减少客户端重定向次数(适用于 Strm 内容是局域网地址但是想要在公网之中播放)
4445
PrefixList: # EmbyServer 中 Strm 文件的前缀(符合该前缀的 Strm 文件且被正确识别为 HTTP 协议都会路由到该规则下)
4546
- /media/strm/http
4647
- /media/strm/https

internal/config/type.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ type ClientFilterSetting struct {
5656
type HTTPStrmSetting struct {
5757
Enable bool
5858
TransCode bool // false->强制关闭转码 true->保持原有转码设置
59+
FinalURL bool // 对 URL 进行重定向判断,找到非重定向地址再重定向给客户端,减少客户端重定向次数
5960
PrefixList []string
6061
}
6162

internal/handler/emby.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,8 +238,19 @@ func (embyServerHandler *EmbyServerHandler) VideosHandler(ctx *gin.Context) {
238238
switch strmFileType {
239239
case constants.HTTPStrm:
240240
if *mediasource.Protocol == emby.HTTP {
241-
logging.Info("HTTPStrm 重定向至:", *mediasource.Path)
242-
ctx.Redirect(http.StatusFound, *mediasource.Path)
241+
redirectURL := *mediasource.Path
242+
if config.HTTPStrm.FinalURL {
243+
logging.Debug("HTTPStrm 启用获取最终 URL,开始尝试获取最终 URL")
244+
if finalURL, err := getFinalURL(redirectURL); err != nil {
245+
logging.Warning("获取最终 URL 失败,使用原始 URL:", err)
246+
} else {
247+
redirectURL = finalURL
248+
}
249+
} else {
250+
logging.Debug("HTTPStrm 未启用获取最终 URL,直接使用原始 URL")
251+
}
252+
logging.Info("HTTPStrm 重定向至:", redirectURL)
253+
ctx.Redirect(http.StatusFound, redirectURL)
243254
}
244255
return
245256
case constants.AlistStrm: // 无需判断 *mediasource.Container 是否以Strm结尾,当 AlistStrm 存储的位置有对应的文件时,*mediasource.Container 会被设置为文件后缀

internal/handler/jellyfin.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,19 @@ func (jellyfinHandler *JellyfinHandler) VideosHandler(ctx *gin.Context) {
210210
switch strmFileType {
211211
case constants.HTTPStrm:
212212
if *mediasource.Protocol == jellyfin.HTTP {
213-
logging.Infof("HTTPStrm 重定向至:%s", *mediasource.Path)
214-
ctx.Redirect(http.StatusFound, *mediasource.Path)
213+
redirectURL := *mediasource.Path
214+
if config.HTTPStrm.FinalURL {
215+
logging.Debug("HTTPStrm 启用获取最终 URL,开始尝试获取最终 URL")
216+
if finalURL, err := getFinalURL(redirectURL); err != nil {
217+
logging.Warning("获取最终 URL 失败,使用原始 URL:", err)
218+
} else {
219+
redirectURL = finalURL
220+
}
221+
} else {
222+
logging.Debug("HTTPStrm 未启用获取最终 URL,直接使用原始 URL")
223+
}
224+
logging.Info("HTTPStrm 重定向至:", redirectURL)
225+
ctx.Redirect(http.StatusFound, redirectURL)
215226
}
216227
return
217228
case constants.AlistStrm: // 无需判断 *mediasource.Container 是否以Strm结尾,当 AlistStrm 存储的位置有对应的文件时,*mediasource.Container 会被设置为文件后缀

internal/handler/utils.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@ import (
66
"MediaWarp/internal/logging"
77
"bytes"
88
"compress/gzip"
9+
"errors"
910
"fmt"
1011
"io"
1112
"net/http"
1213
"net/http/httputil"
14+
"net/url"
1315
"strconv"
1416
"strings"
17+
"time"
1518

1619
"github.com/andybalholm/brotli"
1720
"github.com/gin-gonic/gin"
@@ -135,3 +138,74 @@ func updateBody(rw *http.Response, content []byte) error {
135138

136139
return nil
137140
}
141+
142+
const (
143+
MaxRedirectAttempts = 10 // 最大重定向次数限制
144+
RedirectTimeout = 10 * time.Second // 最大超时时间
145+
146+
)
147+
148+
var (
149+
ErrInvalidLocationHeader = errors.New("重定向 Location 头无效")
150+
ErrMaxRedirectsExceeded = fmt.Errorf("超过最大重定向次数限制(%d)", MaxRedirectAttempts)
151+
)
152+
153+
// 获取URL的最终目标地址(自动跟踪重定向)
154+
func getFinalURL(rawURL string) (string, error) {
155+
156+
parsedURL, err := url.Parse(rawURL) // 验证并解析输入URL
157+
if err != nil {
158+
return "", fmt.Errorf("非法 URL: %w", err)
159+
}
160+
if parsedURL.Scheme == "" {
161+
return "", fmt.Errorf("URL 缺少协议头: %s", parsedURL)
162+
}
163+
164+
// 创建自定义HTTP客户端配置
165+
client := &http.Client{
166+
Timeout: RedirectTimeout,
167+
CheckRedirect: func(req *http.Request, via []*http.Request) error {
168+
// 禁止自动重定向,以便手动处理
169+
return http.ErrUseLastResponse
170+
},
171+
}
172+
173+
currentURL := parsedURL.String()
174+
visited := make(map[string]struct{}, MaxRedirectAttempts)
175+
redirectChain := make([]string, 0, MaxRedirectAttempts+1)
176+
177+
// 跟踪重定向链
178+
for i := 0; i <= MaxRedirectAttempts; i++ {
179+
// 检测循环重定向
180+
if _, exists := visited[currentURL]; exists {
181+
return "", fmt.Errorf("检测到循环重定向,重定向链: %s", strings.Join(redirectChain, " -> "))
182+
}
183+
visited[currentURL] = struct{}{}
184+
redirectChain = append(redirectChain, currentURL)
185+
186+
// 创建HEAD请求(更高效,只获取头部信息)
187+
resp, err := client.Head(currentURL)
188+
if err != nil {
189+
return "", fmt.Errorf("发送 HTTP 请求失败:%w", err)
190+
}
191+
defer resp.Body.Close()
192+
193+
// 检查是否需要重定向 (3xx 状态码)
194+
if resp.StatusCode >= http.StatusMultipleChoices && resp.StatusCode < http.StatusBadRequest {
195+
location, err := resp.Location()
196+
if err != nil {
197+
return "", ErrInvalidLocationHeader
198+
}
199+
200+
// 处理相对路径重定向
201+
currentURL = location.String()
202+
continue
203+
}
204+
205+
// 返回最终的非重定向URL
206+
logging.Debug("重定向链:", strings.Join(redirectChain, " -> "))
207+
return resp.Request.URL.String(), nil
208+
}
209+
210+
return "", ErrMaxRedirectsExceeded
211+
}

0 commit comments

Comments
 (0)