Files
fanqie-go/internal/fqapi/store_redis.go
T
chenxin 49d0a371fb ✨ feat(video): 新增视频 CENC 解密与流式代理播放
- 逆向 SpadeA(黑桃A) PlayAuth 混淆,从 spade_a 还原 AES-128 内容密钥
- 实现 cenc-aes-ctr MP4 解密(senc 逐样本 CTR + stsd entry 修复),实测可播放
- 新增 /video/proxy/{key} 代理:稳定 key + 磁盘缓存(二次命中提速 130x)+
  http.ServeContent 支持 Range/206/seek 流式播放
- /video 接口新增 output(url/proxy,默认 proxy) 与 quality(max/720p/档位key) 参数,
  可由 config fanqie.video.* 配置默认值;无匹配清晰度时明确报错
- 代理映射复用 audio_cache 存储,抽取 loadMediaProxyEntry 共用;挂接 7 天缓存清理
- deploy.sh 新增 --sync-nginx:备份→重写→校验→失败自动回滚,同步视频代理段配置
2026-10-10 12:58:25 +08:00

89 lines
2.7 KiB
Go

package fqapi
import (
"context"
"encoding/json"
"fmt"
"time"
)
// audioProxyEntry 音频代理缓存条目(Redis 版)
type audioProxyEntry struct {
OriginalURL string `json:"original_url"`
DeviceID string `json:"device_id"`
Headers map[string]string `json:"headers"`
}
const (
redisCacheKeyPrefix = "fq:cache:"
redisAudioKeyPrefix = "fq:audio:"
// audioCacheTTL 音频代理缓存有效期(匹配 CDN 链接有效期)
audioCacheTTL = 2 * time.Hour
)
// redisGetCache 从 Redis 获取正文缓存
func redisGetCache(key string) string {
// 读操作同样加 3s 超时,防止远程 Redis 网络抖动拖垮请求(与写路径一致)
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
val, err := redisCache.Get(ctx, redisCacheKeyPrefix+key).Result()
if err != nil {
return "" // miss 或错误都视为未命中
}
var cacheData CacheData
if err := json.Unmarshal([]byte(val), &cacheData); err != nil {
cacheLogger.Error("解析Redis缓存数据失败: " + err.Error())
return ""
}
cacheLogger.Debug("Redis缓存命中: " + key)
if time.Now().Unix() < int64(cacheData.Expi) {
return cacheData.Data
}
return ""
}
// redisUpdateCache 写入 Redis 正文缓存(TTL 与过期时间一致)
func redisUpdateCache(key string, value string, expi int) error {
cacheData := CacheData{
Data: value,
Expi: float64(time.Now().Unix() + int64(expi)),
}
jsonData, err := json.Marshal(cacheData)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
return redisCache.Set(ctx, redisCacheKeyPrefix+key, jsonData, time.Duration(expi)*time.Second).Err()
}
// saveAudioProxyEntry 将音频代理信息存入 Redis
func saveAudioProxyEntry(key string, entry *audioProxyEntry) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
jsonData, _ := json.Marshal(entry)
if err := redisCache.Set(ctx, redisAudioKeyPrefix+key, jsonData, audioCacheTTL).Err(); err != nil {
audioLogger.Error("保存音频缓存到Redis失败: " + err.Error())
}
}
// loadAudioProxyEntry 从 Redis 读取音频代理信息,未命中或过期返回错误
func loadAudioProxyEntry(key string) (*audioProxyEntry, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
val, err := redisCache.Get(ctx, redisAudioKeyPrefix+key).Result()
if err != nil {
return nil, fmt.Errorf("媒体信息不存在或已过期,请重新调用 /audio 或 /video 接口")
}
var entry audioProxyEntry
if err := json.Unmarshal([]byte(val), &entry); err != nil {
return nil, fmt.Errorf("解析音频缓存失败: %w", err)
}
return &entry, nil
}