Skip to content

Commit d6e0779

Browse files
authored
feat: add cache for HTTPStrm (#62)
Problem Emby calls /Videos/{id}/stream three times for each HTTPStrm playback. MediaWarp recalculates the redirect on every request (HEAD chain, signed URL refresh, etc.). Upstream providers (PikPak, Aliyun, …) may return different URLs or single-use tokens per resolution. Results: noticeable latency (up to ~40% on public links) and risk of inconsistent redirects when providers invalidate tokenized URLs mid-flow. Solution Added an HTTPStrm-specific in-memory cache keyed by MediaSourceId. Reuse the first resolved URL for the subsequent two Emby requests until the entry expires. New configuration options: HTTPStrm.CacheEnable (bool) — enable/disable the cache. HTTPStrm.CacheTTL (duration, default 1m) — control entry lifetime. Integrated the cache into both Emby and Jellyfin handlers right before final redirect logic. Validated settings during config load (time.ParseDuration, fallback to 1m); works alongside FinalURL. Thread-safe map with automatic eviction of expired entries. Impact When resolving private/intranet URLs to public ones, the average access time drops by ≈30% because the second and third requests hit the cache. Providers issuing single-use tokens no longer see three back-to-back resolutions, reducing divergent redirects or premature expirations. Users can switch the cache off to keep the original behavior if needed.
1 parent 4f3fdbb commit d6e0779

File tree

7 files changed

+172
-78
lines changed

7 files changed

+172
-78
lines changed

config/config.yaml.example

Lines changed: 75 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,76 @@
1-
Port: 9000 # MideWarp 监听端口
2-
3-
MediaServer: # 媒体服务器相关设置
4-
Type: Emby # 媒体服务器类型(可选选项:Emby、Jellyfin)
5-
ADDR: http://localhost:8096 # 媒体服务器地址
6-
AUTH: 2eaxxxxxxxxxa8 # 媒体服务器认证方式
7-
8-
Logger: # 日志设定
9-
AccessLogger: # 访问日志设定
10-
Console: True # 是否将访问日志文件输出到终端中
11-
File: False # 是否将访问日志文件记录到文件中
12-
ServiceLogger: # 服务日志设定
13-
Console: True # 是否将服务日志文件输出到终端中
14-
File: True # 是否将服务日志文件记录到文件中
15-
16-
Web: # Web 页面修改相关设置
17-
Enable: False # 总开关
18-
Custom: False # 是否加载自定义静态资源
19-
Index: False # 是否从 custom 目录读取 index.html 文件
20-
Head: | # 是否添加自定义字段到 index.html 的头部中
21-
<script src="/MediaWarp/custom/emby-front-end-mod/actor-plus.js"></script>
22-
<script src="/MediaWarp/custom/emby-front-end-mod/emby-swiper.js"></script>
23-
<script src="/MediaWarp/custom/emby-front-end-mod/emby-tab.js"></script>
24-
<script src="/MediaWarp/custom/emby-front-end-mod/fanart-show.js"></script>
25-
<script src="/MediaWarp/custom/emby-front-end-mod/playbackRate.js"></script>
26-
27-
Robots: | # 自定义 robots.txt,若为空表示不修改
28-
User-agent: *
29-
Disallow: /
30-
31-
Crx: False # crx 美化(Emby:https://github.com/Nolovenodie/emby-crx;Jellyfin:https://github.com/newday-life/jellyfin-crx)
32-
ActorPlus: True # 过滤没有头像的演员和制作人员
33-
FanartShow: False # 显示同人图(fanart 图)
34-
ExternalPlayerUrl: False # 是否开启外置播放器(仅 Emby)
35-
Danmaku: False # Web 弹幕(Emby:https://github.com/9channel/dd-danmaku;Jellyfin:https://github.com/Izumiko/jellyfin-danmaku)
36-
VideoTogether: False # 共同观影,详情见 https://videotogether.github.io/
37-
38-
ClientFilter: # 客户端过滤器
39-
Enable: False # 是否启用客户端过滤器
40-
Mode: BlackList # WhileList / BlackList # 黑白名单模式
41-
ClientList: # 名单列表
42-
- Fileball
43-
- Infuse
44-
45-
HTTPStrm: # HTTPStrm 相关配置(Strm 文件内容是 标准 HTTP URL)
46-
Enable: True # 是否开启 HttpStrm 重定向
47-
TransCode: False # False:强制关闭转码 True:保持原有转码设置
48-
FinalURL: True # 对 URL 进行重定向判断,找到非重定向地址再重定向给客户端,减少客户端重定向次数(适用于 Strm 内容是局域网地址但是想要在公网之中播放)
49-
PrefixList: # EmbyServer 中 Strm 文件的前缀(符合该前缀的 Strm 文件且被正确识别为 HTTP 协议都会路由到该规则下)
50-
- /media/strm/http
51-
- /media/strm/https
52-
53-
AlistStrm: # AlistStrm 相关配置(Strm 文件内容是 Alist 上文件的路径,目前仅支持适配 Alist V3)
54-
Enable: True # 是否启用 AlistStrm 重定向
55-
TransCode: True # False:强制关闭转码 True:保持原有转码设置
56-
RawURL: False # Fasle:响应 Alist 服务器的直链(要求客户端可以访问到 Alist) True:直接响应 Alist 上游的真实链接(alist api 中的 raw_url 属性)
57-
List: # Alist 服务关配置列表
58-
- ADDR: http://192.168.1.100:5244 # Alist 服务器地址
59-
Username: admin # Alist 服务器账号
60-
Password: adminadmin # Alist 服务器密码
61-
PrefixList: # EmbyServer 中 Strm 文件的前缀(符合该前缀的 Strm 文件都会路由到该规则下)
62-
- /media/strm/MyAlist # 同一个 Alist 可以有多个前缀规则
63-
- /mnt/cd2/strm
64-
- ADDR: https://xiaoya.com # 可以填写多个配置
65-
Token: xxxxxxx # Token 优先级高于 Username 和 Password
66-
PrefixList:
67-
- /media/strm
68-
69-
Subtitle: # 字体相关设置(仅 Emby 支持)
70-
Enable: True # 启用
71-
SRT2ASS: True # SRT 字幕转 ASS 字幕
72-
ASSStyle: # SRT 字幕转 ASS 字幕使用的样式
73-
- "Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding"
1+
Port: 9000 # MideWarp 监听端口
2+
3+
MediaServer: # 媒体服务器相关设置
4+
Type: Emby # 媒体服务器类型(可选选项:Emby、Jellyfin)
5+
ADDR: http://localhost:8096 # 媒体服务器地址
6+
AUTH: 2eaxxxxxxxxxa8 # 媒体服务器认证方式
7+
8+
Logger: # 日志设定
9+
AccessLogger: # 访问日志设定
10+
Console: True # 是否将访问日志文件输出到终端中
11+
File: False # 是否将访问日志文件记录到文件中
12+
ServiceLogger: # 服务日志设定
13+
Console: True # 是否将服务日志文件输出到终端中
14+
File: True # 是否将服务日志文件记录到文件中
15+
16+
Web: # Web 页面修改相关设置
17+
Enable: False # 总开关
18+
Custom: False # 是否加载自定义静态资源
19+
Index: False # 是否从 custom 目录读取 index.html 文件
20+
Head: | # 是否添加自定义字段到 index.html 的头部中
21+
<script src="/MediaWarp/custom/emby-front-end-mod/actor-plus.js"></script>
22+
<script src="/MediaWarp/custom/emby-front-end-mod/emby-swiper.js"></script>
23+
<script src="/MediaWarp/custom/emby-front-end-mod/emby-tab.js"></script>
24+
<script src="/MediaWarp/custom/emby-front-end-mod/fanart-show.js"></script>
25+
<script src="/MediaWarp/custom/emby-front-end-mod/playbackRate.js"></script>
26+
27+
Robots: | # 自定义 robots.txt,若为空表示不修改
28+
User-agent: *
29+
Disallow: /
30+
31+
Crx: False # crx 美化(Emby:https://github.com/Nolovenodie/emby-crx;Jellyfin:https://github.com/newday-life/jellyfin-crx)
32+
ActorPlus: True # 过滤没有头像的演员和制作人员
33+
FanartShow: False # 显示同人图(fanart 图)
34+
ExternalPlayerUrl: False # 是否开启外置播放器(仅 Emby)
35+
Danmaku: False # Web 弹幕(Emby:https://github.com/9channel/dd-danmaku;Jellyfin:https://github.com/Izumiko/jellyfin-danmaku)
36+
VideoTogether: False # 共同观影,详情见 https://videotogether.github.io/
37+
38+
ClientFilter: # 客户端过滤器
39+
Enable: False # 是否启用客户端过滤器
40+
Mode: BlackList # WhileList / BlackList # 黑白名单模式
41+
ClientList: # 名单列表
42+
- Fileball
43+
- Infuse
44+
45+
HTTPStrm: # HTTPStrm 相关配置(Strm 文件内容是 标准 HTTP URL)
46+
Enable: True # 是否开启 HttpStrm 重定向
47+
TransCode: False # False:强制关闭转码 True:保持原有转码设置
48+
FinalURL: True # 对 URL 进行重定向判断,找到非重定向地址再重定向给客户端,减少客户端重定向次数(适用于 Strm 内容是局域网地址但是想要在公网之中播放)
49+
CacheEnable: True # 是否启用 HTTPStrm 重定向内存缓存
50+
CacheTTL: 1m # 重定向缓存有效期(time.ParseDuration 格式)
51+
PrefixList: # EmbyServer 中 Strm 文件的前缀(符合该前缀的 Strm 文件且被正确识别为 HTTP 协议都会路由到该规则下)
52+
- /media/strm/http
53+
- /media/strm/https
54+
55+
AlistStrm: # AlistStrm 相关配置(Strm 文件内容是 Alist 上文件的路径,目前仅支持适配 Alist V3)
56+
Enable: True # 是否启用 AlistStrm 重定向
57+
TransCode: True # False:强制关闭转码 True:保持原有转码设置
58+
RawURL: False # Fasle:响应 Alist 服务器的直链(要求客户端可以访问到 Alist) True:直接响应 Alist 上游的真实链接(alist api 中的 raw_url 属性)
59+
List: # Alist 服务关配置列表
60+
- ADDR: http://192.168.1.100:5244 # Alist 服务器地址
61+
Username: admin # Alist 服务器账号
62+
Password: adminadmin # Alist 服务器密码
63+
PrefixList: # EmbyServer 中 Strm 文件的前缀(符合该前缀的 Strm 文件都会路由到该规则下)
64+
- /media/strm/MyAlist # 同一个 Alist 可以有多个前缀规则
65+
- /mnt/cd2/strm
66+
- ADDR: https://xiaoya.com # 可以填写多个配置
67+
Token: xxxxxxx # Token 优先级高于 Username 和 Password
68+
PrefixList:
69+
- /media/strm
70+
71+
Subtitle: # 字体相关设置(仅 Emby 支持)
72+
Enable: True # 启用
73+
SRT2ASS: True # SRT 字幕转 ASS 字幕
74+
ASSStyle: # SRT 字幕转 ASS 字幕使用的样式
75+
- "Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding"
7476
- "Style: Default,楷体,20,&H03FFFFFF,&H00FFFFFF,&H00000000,&H02000000,-1,0,0,0,100,100,0,0,1,1,0,2,10,10,10,1"

internal/cache/httpstrm/cache.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package httpstrm
2+
3+
import (
4+
"sync"
5+
"time"
6+
)
7+
8+
type entry struct {
9+
url string
10+
expiresAt time.Time
11+
}
12+
13+
type Cache struct {
14+
mu sync.RWMutex
15+
items map[string]entry
16+
}
17+
18+
func New() *Cache {
19+
return &Cache{items: make(map[string]entry)}
20+
}
21+
22+
func (c *Cache) Get(key string) (string, bool) {
23+
c.mu.RLock()
24+
value, ok := c.items[key]
25+
c.mu.RUnlock()
26+
if !ok {
27+
return "", false
28+
}
29+
if time.Now().After(value.expiresAt) {
30+
c.mu.Lock()
31+
delete(c.items, key)
32+
c.mu.Unlock()
33+
return "", false
34+
}
35+
return value.url, true
36+
}
37+
38+
func (c *Cache) Set(key, url string, ttl time.Duration) {
39+
if ttl <= 0 {
40+
c.Delete(key)
41+
return
42+
}
43+
c.mu.Lock()
44+
c.items[key] = entry{url: url, expiresAt: time.Now().Add(ttl)}
45+
c.mu.Unlock()
46+
}
47+
48+
func (c *Cache) Delete(key string) {
49+
c.mu.Lock()
50+
delete(c.items, key)
51+
c.mu.Unlock()
52+
}

internal/config/config.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,16 @@ func loadConfig(path string) error {
124124
if err := viper.UnmarshalKey("HTTPStrm", &HTTPStrm); err != nil {
125125
return fmt.Errorf("HTTPStrmSetting 解析失败:%v", err)
126126
}
127+
if ttlStr := viper.GetString("HTTPStrm.CacheTTL"); ttlStr != "" {
128+
duration, err := time.ParseDuration(ttlStr)
129+
if err != nil {
130+
return fmt.Errorf("HTTPStrm.CacheTTL 解析失败:%v", err)
131+
}
132+
HTTPStrm.CacheTTL = duration
133+
}
134+
if HTTPStrm.CacheTTL <= 0 {
135+
HTTPStrm.CacheTTL = time.Minute
136+
}
127137
if err := viper.UnmarshalKey("AlistStrm", &AlistStrm); err != nil {
128138
return fmt.Errorf("AlistStrmSetting 解析失败:%v", err)
129139
}

internal/config/type.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package config
22

3-
import "MediaWarp/constants"
3+
import (
4+
"MediaWarp/constants"
5+
"time"
6+
)
47

58
// 程序版本信息
69
type VersionInfo struct {
@@ -55,10 +58,12 @@ type ClientFilterSetting struct {
5558

5659
// HTTPStrm播放设置
5760
type HTTPStrmSetting struct {
58-
Enable bool
59-
TransCode bool // false->强制关闭转码 true->保持原有转码设置
60-
FinalURL bool // 对 URL 进行重定向判断,找到非重定向地址再重定向给客户端,减少客户端重定向次数
61-
PrefixList []string
61+
Enable bool
62+
TransCode bool // false->强制关闭转码 true->保持原有转码设置
63+
FinalURL bool // 对 URL 进行重定向判断,找到非重定向地址再重定向给客户端,减少客户端重定向次数
64+
CacheEnable bool
65+
CacheTTL time.Duration
66+
PrefixList []string
6267
}
6368

6469
// AlistStrm具体设置

internal/handler/emby.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,13 @@ func (embyServerHandler *EmbyServerHandler) VideosHandler(ctx *gin.Context) {
238238
switch strmFileType {
239239
case constants.HTTPStrm:
240240
if *mediasource.Protocol == emby.HTTP {
241+
if config.HTTPStrm.CacheEnable {
242+
if cachedURL, ok := httpStrmRedirectCache.Get(mediaSourceID); ok {
243+
logging.Info("HTTPStrm 重定向至:", cachedURL)
244+
ctx.Redirect(http.StatusFound, cachedURL)
245+
return
246+
}
247+
}
241248
redirectURL := *mediasource.Path
242249
if config.HTTPStrm.FinalURL {
243250
logging.Debug("HTTPStrm 启用获取最终 URL,开始尝试获取最终 URL")
@@ -249,6 +256,9 @@ func (embyServerHandler *EmbyServerHandler) VideosHandler(ctx *gin.Context) {
249256
} else {
250257
logging.Debug("HTTPStrm 未启用获取最终 URL,直接使用原始 URL")
251258
}
259+
if config.HTTPStrm.CacheEnable && config.HTTPStrm.CacheTTL > 0 {
260+
httpStrmRedirectCache.Set(mediaSourceID, redirectURL, config.HTTPStrm.CacheTTL)
261+
}
252262
logging.Info("HTTPStrm 重定向至:", redirectURL)
253263
ctx.Redirect(http.StatusFound, redirectURL)
254264
}

internal/handler/httpstrm_cache.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package handler
2+
3+
import "MediaWarp/internal/cache/httpstrm"
4+
5+
var httpStrmRedirectCache = httpstrm.New()

internal/handler/jellyfin.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,13 @@ func (jellyfinHandler *JellyfinHandler) VideosHandler(ctx *gin.Context) {
210210
switch strmFileType {
211211
case constants.HTTPStrm:
212212
if *mediasource.Protocol == jellyfin.HTTP {
213+
if config.HTTPStrm.CacheEnable {
214+
if cachedURL, ok := httpStrmRedirectCache.Get(mediaSourceID); ok {
215+
logging.Info("HTTPStrm 重定向至:", cachedURL)
216+
ctx.Redirect(http.StatusFound, cachedURL)
217+
return
218+
}
219+
}
213220
redirectURL := *mediasource.Path
214221
if config.HTTPStrm.FinalURL {
215222
logging.Debug("HTTPStrm 启用获取最终 URL,开始尝试获取最终 URL")
@@ -221,6 +228,9 @@ func (jellyfinHandler *JellyfinHandler) VideosHandler(ctx *gin.Context) {
221228
} else {
222229
logging.Debug("HTTPStrm 未启用获取最终 URL,直接使用原始 URL")
223230
}
231+
if config.HTTPStrm.CacheEnable && config.HTTPStrm.CacheTTL > 0 {
232+
httpStrmRedirectCache.Set(mediaSourceID, redirectURL, config.HTTPStrm.CacheTTL)
233+
}
224234
logging.Info("HTTPStrm 重定向至:", redirectURL)
225235
ctx.Redirect(http.StatusFound, redirectURL)
226236
}

0 commit comments

Comments
 (0)