- 逆向 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:备份→重写→校验→失败自动回滚,同步视频代理段配置
89 lines
2.7 KiB
Go
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
|
|
}
|