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:
ShukeBta
2026-06-12 04:31:34 +00:00
parent a13aba1d9f
commit 91df7521ce
16 changed files with 530 additions and 72 deletions
+39 -8
View File
@@ -71,6 +71,8 @@ type ScannerService struct {
cloudMediaProbeMu sync.Mutex
cloudMediaProbing map[string]struct{}
cloudMediaProbeBackoff map[string]time.Time
cloudMediaProbeWarnMu sync.Mutex
cloudMediaProbeLastWarn time.Time
}
// NewScannerService is the constructor.
@@ -269,6 +271,10 @@ const maxCloudMediaProbeQueuePerScan = 32
const cloudMediaProbeFailureBackoff = 6 * time.Hour
// cloudMediaProbeQueueFullBackoff 是探测队列饱和时给单个文件挂的短退避,
// 防止后续扫描轮次对同一批文件反复尝试入队。
const cloudMediaProbeQueueFullBackoff = 30 * time.Minute
// CloudScanStatus is the operator-facing state for long-running cloud scans.
type CloudScanStatus struct {
LibraryID string `json:"library_id"`
@@ -359,9 +365,29 @@ func (s *ScannerService) queueCloudMediaProbe(typ, ref, path string) bool {
default:
s.cloudMediaProbeMu.Lock()
delete(s.cloudMediaProbing, path)
// 队列满说明探测工人已饱和;给该文件挂一个短退避,避免下一轮
// 扫描立刻重复尝试同一批文件。
if s.cloudMediaProbeBackoff == nil {
s.cloudMediaProbeBackoff = make(map[string]time.Time)
}
s.cloudMediaProbeBackoff[path] = time.Now().Add(cloudMediaProbeQueueFullBackoff)
s.cloudMediaProbeMu.Unlock()
if s.log != nil {
s.log.Warn("cloud media probe queue full", zap.String("provider", typ), zap.String("path", path))
// 限速告警:队列满在大库扫描中是常态而非异常,逐条 WARN 会
// 在几小时内产生数万行日志(真实环境出现过 41165 条)。
now := time.Now()
s.cloudMediaProbeWarnMu.Lock()
shouldWarn := now.Sub(s.cloudMediaProbeLastWarn) >= time.Minute
if shouldWarn {
s.cloudMediaProbeLastWarn = now
}
s.cloudMediaProbeWarnMu.Unlock()
if shouldWarn {
s.log.Warn("cloud media probe queue full; deferring remaining probes (logged at most once per minute)",
zap.String("provider", typ), zap.String("path", path))
} else {
s.log.Debug("cloud media probe queue full", zap.String("provider", typ), zap.String("path", path))
}
}
return false
}
@@ -372,14 +398,13 @@ func (s *ScannerService) queueCloudMediaProbeWithBudget(typ, ref, path string, b
if *budget <= 0 {
return false
}
}
if !s.queueCloudMediaProbe(typ, ref, path) {
return false
}
if budget != nil {
// 预算按「尝试」扣减而不是按「成功入队」扣减。否则当探测队列被
// 其他扫描填满时,本次扫描会对剩下的每一个文件都尝试入队并各打
// 一条日志——真实环境里曾因此产生过 4 万多条 "queue full" WARN,
// 这本身就是一笔可观的 CPU/磁盘开销。
*budget--
}
return true
return s.queueCloudMediaProbe(typ, ref, path)
}
func (s *ScannerService) beginCloudScan(ctx context.Context, lib *model.Library, mount CloudMountInfo) (context.Context, func(*ScanResult, error), error) {
@@ -917,7 +942,13 @@ func (s *ScannerService) scanCloudLibrary(ctx context.Context, lib *model.Librar
displayPath := joinCloudDisplayPath(displayDir, entry.Name)
path := cloudMediaPath(typ, displayPath)
localMeta := s.cloudFileMetadata(ctx, typ, displayPath, entry.Name, sidecars, dirMeta, librarySupportsSeasons(lib))
s.cacheCloudMetadataArtworkNow(ctx, localMeta)
// 每个文件的海报/背景图改走后台预取队列。此前这里是同步
// CloudResolve+下载(每张最多 20s 超时),几千个文件的云盘库
// 扫描会变成持续数小时的串行下载,把 CPU/带宽长期吃满。
if localMeta != nil {
s.queueCloudArtworkPrefetch(localMeta.PosterURL)
s.queueCloudArtworkPrefetch(localMeta.BackdropURL)
}
candidate := cloudCandidate{
ref: ref,
name: entry.Name,