mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-11 15:56:37 +08:00
fix: 资源占用/登录稳定性/QB整理入库/第三方播放404 综合修复
资源占用(Docker 部署 CPU/内存长期居高): - 云盘探测预算改为按尝试扣减,杜绝队列满时对每个文件反复入队 并刷出数万条 WARN(实测日志 41165 条) - 探测队列满时给文件挂 30 分钟退避 + 告警限速为每分钟一条 - 扫描时每个文件的海报/背景图由同步下载(单张最长 20s)改为 后台预取队列,云盘大库扫描不再串行拉图数小时 - PlaybackInfo 的云盘 ffprobe 探测改异步(原同步最长 8s, 既拖慢起播又放大云盘流量),带单飞去重 - 访问日志跳过 /api/health 与静态资源;logging.level/format 配置真正生效(此前是死配置) 登录稳定性(经常登录报错): - refresh token 未及时落库期间,刷新请求可识别「待落库令牌」, 不再把用户踢回登录页;轮换/登出后取消后台补写,防止旧令牌复活 QB 下载整理入库: - 新增 download.path_mappings 设置:自定义下载器→本程序路径映射 (每行 客户端路径=本地路径),并复用 compose 环境变量映射规则 - 应用重启后补整理最近 24h 内完成的种子(此前重启即永久漏掉) - 下载客户端初始化失败仍注册并惰性重连(容器启动顺序免疫) - 硬链接跨文件系统(EXDEV)自动降级为复制,保种语义不变 第三方播放器 404: - 播放处理器不再把所有错误吞成 404:媒体不存在→404, 云盘解析失败/STRM 关闭→502+原因 - 存库的云盘播放 URL 规范化为相对路径,免疫扫描时固化的旧 host - 云盘媒体 SupportsDirectPlay=false,强制走带鉴权的 DirectStream
This commit is contained in:
@@ -69,6 +69,9 @@ type EmbyService struct {
|
||||
|
||||
visibilityMu sync.RWMutex
|
||||
visibilityCache map[string]embyVisibilityCacheEntry
|
||||
|
||||
cloudProbeMu sync.Mutex
|
||||
cloudProbeInFlight map[string]struct{}
|
||||
}
|
||||
|
||||
type cloudPlaybackResolver interface {
|
||||
@@ -1476,6 +1479,13 @@ func (e *EmbyService) PlaybackInfo(ctx context.Context, mediaID, userID string)
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ensureCloudTrackMetadata 在后台补齐云盘媒体的轨道元数据。
|
||||
//
|
||||
// 注意必须是异步的:此前这里在 PlaybackInfo 请求路径上同步执行
|
||||
// CloudResolve + ffprobe(HTTP)(最长 8 秒),既把第三方播放器的起播时间
|
||||
// 拖长到秒级,又让每一次点开详情/起播都可能触发一次云盘数据下载,是
|
||||
// Docker 部署下 CPU/带宽长期居高的来源之一。探测结果落库后,下一次
|
||||
// 请求自然能读到完整元数据。
|
||||
func (e *EmbyService) ensureCloudTrackMetadata(ctx context.Context, m *model.Media) {
|
||||
if e == nil || m == nil || e.storage == nil || e.probe == nil || !mediaTrackMetadataMissing(m) {
|
||||
return
|
||||
@@ -1484,30 +1494,48 @@ func (e *EmbyService) ensureCloudTrackMetadata(ctx context.Context, m *model.Med
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
probeCtx, cancel := context.WithTimeout(ctx, 8*time.Second)
|
||||
defer cancel()
|
||||
link, err := e.storage.CloudResolve(probeCtx, typ, ref, "")
|
||||
if err != nil {
|
||||
if e.log != nil {
|
||||
e.log.Debug("resolve cloud media for playback probe failed", zap.String("media_id", m.ID), zap.Error(err))
|
||||
mediaID := m.ID
|
||||
e.cloudProbeMu.Lock()
|
||||
if e.cloudProbeInFlight == nil {
|
||||
e.cloudProbeInFlight = make(map[string]struct{})
|
||||
}
|
||||
if _, busy := e.cloudProbeInFlight[mediaID]; busy {
|
||||
e.cloudProbeMu.Unlock()
|
||||
return
|
||||
}
|
||||
e.cloudProbeInFlight[mediaID] = struct{}{}
|
||||
e.cloudProbeMu.Unlock()
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
e.cloudProbeMu.Lock()
|
||||
delete(e.cloudProbeInFlight, mediaID)
|
||||
e.cloudProbeMu.Unlock()
|
||||
}()
|
||||
probeCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
link, err := e.storage.CloudResolve(probeCtx, typ, ref, "")
|
||||
if err != nil {
|
||||
if e.log != nil {
|
||||
e.log.Debug("resolve cloud media for playback probe failed", zap.String("media_id", mediaID), zap.Error(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
probe, err := e.probe.ProbeHTTP(probeCtx, link.URL, link.Headers)
|
||||
if err != nil {
|
||||
if e.log != nil {
|
||||
e.log.Debug("playback cloud ffprobe failed", zap.String("media_id", m.ID), zap.Error(err))
|
||||
probe, err := e.probe.ProbeHTTP(probeCtx, link.URL, link.Headers)
|
||||
if err != nil {
|
||||
if e.log != nil {
|
||||
e.log.Debug("playback cloud ffprobe failed", zap.String("media_id", mediaID), zap.Error(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
updates := probeResultUpdates(probe)
|
||||
if len(updates) == 0 {
|
||||
return
|
||||
}
|
||||
if err := e.repo.DB.WithContext(ctx).Model(&model.Media{}).Where("id = ?", m.ID).Updates(updates).Error; err != nil && e.log != nil {
|
||||
e.log.Debug("persist playback cloud probe failed", zap.String("media_id", m.ID), zap.Error(err))
|
||||
}
|
||||
applyProbeResultToMediaValue(m, probe)
|
||||
updates := probeResultUpdates(probe)
|
||||
if len(updates) == 0 {
|
||||
return
|
||||
}
|
||||
if err := e.repo.DB.WithContext(probeCtx).Model(&model.Media{}).Where("id = ?", mediaID).Updates(updates).Error; err != nil && e.log != nil {
|
||||
e.log.Debug("persist playback cloud probe failed", zap.String("media_id", mediaID), zap.Error(err))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func mediaTrackMetadataMissing(m *model.Media) bool {
|
||||
@@ -1630,11 +1658,16 @@ func (e *EmbyService) mediaSource(m *model.Media, asEmbedded, directOnly bool) m
|
||||
"RequiresClosing": false,
|
||||
"ReadAtNativeFramerate": false,
|
||||
"SupportsTranscoding": !directOnly,
|
||||
"SupportsDirectStream": true,
|
||||
"SupportsDirectPlay": true,
|
||||
"SupportsProbing": true,
|
||||
"RunTimeTicks": int64(m.DurationSec) * 10_000_000,
|
||||
"MediaStreams": e.mediaStreams(m),
|
||||
// 云盘媒体禁用 DirectPlay:DirectPlay 语义是「客户端直接访问
|
||||
// Path」,而云盘媒体的 Path 是不带鉴权 token 的内部 /api/cloud/play
|
||||
// 路径,Infuse/VidHub 等播放器直接请求会得到 401/404。强制它们走
|
||||
// DirectStream(/Videos/{id}/stream?api_key=...),由服务端校验后
|
||||
// 302 到云盘直链。
|
||||
"SupportsDirectStream": true,
|
||||
"SupportsDirectPlay": !isCloud,
|
||||
"SupportsProbing": true,
|
||||
"RunTimeTicks": int64(m.DurationSec) * 10_000_000,
|
||||
"MediaStreams": e.mediaStreams(m),
|
||||
}
|
||||
if !asEmbedded {
|
||||
src["DirectStreamUrl"] = embyDirectStreamURL(m.ID, container)
|
||||
|
||||
Reference in New Issue
Block a user