- 逆向 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:备份→重写→校验→失败自动回滚,同步视频代理段配置
319 lines
10 KiB
Go
319 lines
10 KiB
Go
package config
|
||
|
||
import (
|
||
"fmt"
|
||
"log"
|
||
"os"
|
||
"sync"
|
||
"sync/atomic"
|
||
"time"
|
||
|
||
"github.com/spf13/viper"
|
||
)
|
||
|
||
// Config 应用配置结构
|
||
type Config struct {
|
||
Server ServerConfig `mapstructure:"server"`
|
||
Fanqie FanqieConfig `mapstructure:"fanqie"`
|
||
Cache CacheConfig `mapstructure:"cache"`
|
||
DeviceKeyPool DeviceKeyPoolConfig `mapstructure:"device_key_pool"`
|
||
}
|
||
|
||
// ServerConfig 服务器配置
|
||
type ServerConfig struct {
|
||
Port string `mapstructure:"port"`
|
||
Host string `mapstructure:"host"`
|
||
AccessKey string `mapstructure:"access_key"`
|
||
ProtectAll bool `mapstructure:"protect_all"` // 业务接口也要求访问密钥(公网部署建议开启)
|
||
RateLimitQPS float64 `mapstructure:"rate_limit_qps"` // 每 IP 每秒请求上限,0=不限制
|
||
RequestLog RequestLogConfig `mapstructure:"request_log"` // 请求日志(只记元数据与错误摘要)
|
||
}
|
||
|
||
// RequestLogConfig 请求日志配置
|
||
type RequestLogConfig struct {
|
||
Enabled bool `mapstructure:"enabled"` // 是否记录业务接口请求元数据
|
||
RetentionDays int `mapstructure:"retention_days"` // 日志保留天数,<=0 视为 7
|
||
}
|
||
|
||
// FanqieConfig 番茄小说配置
|
||
type FanqieConfig struct {
|
||
Algorithm string `mapstructure:"algorithm"`
|
||
Signature string `mapstructure:"signature"` // 签名方法: sign2 / sign4
|
||
Content ContentConfig `mapstructure:"content"`
|
||
Manga MangaConfig `mapstructure:"manga"`
|
||
Video VideoConfig `mapstructure:"video"`
|
||
}
|
||
|
||
// ContentConfig 内容处理配置
|
||
type ContentConfig struct {
|
||
RemoveImages bool `mapstructure:"remove_images"`
|
||
RemoveImageDesc bool `mapstructure:"remove_image_desc"`
|
||
RemoveBlankLines bool `mapstructure:"remove_blank_lines"`
|
||
RemoveHTMLTags bool `mapstructure:"remove_html_tags"`
|
||
RemoveChapterTitle bool `mapstructure:"remove_chapter_title"`
|
||
AddParagraphIndent bool `mapstructure:"add_paragraph_indent"`
|
||
DoubleNewline bool `mapstructure:"double_newline"` // 段落间使用双换行
|
||
EnableReview bool `mapstructure:"enable_review"` // 是否启用段评功能
|
||
}
|
||
|
||
// MangaConfig 漫画处理配置
|
||
type MangaConfig struct {
|
||
OutputMode string `mapstructure:"output_mode"` // 输出模式: data_uri / base64 / url
|
||
}
|
||
|
||
// VideoConfig 视频处理配置
|
||
type VideoConfig struct {
|
||
OutputMode string `mapstructure:"output_mode"` // 输出模式: url(原始CDN链接,内容加密)/ proxy(本地解密代理,可直接播放)
|
||
Quality string `mapstructure:"quality"` // 默认清晰度: max(最高档)/ 360p/480p/... 或档位 key(video_1..N),留空返回全部
|
||
CacheEnabled bool `mapstructure:"cache_enabled"` // 解密后视频的磁盘缓存
|
||
CachePath string `mapstructure:"cache_path"` // 缓存目录
|
||
}
|
||
|
||
// CacheConfig 缓存配置
|
||
type CacheConfig struct {
|
||
Enabled bool `mapstructure:"enabled"`
|
||
Backend string `mapstructure:"backend"` // 存储后端: sqlite | postgres
|
||
SQLitePath string `mapstructure:"sqlite_path"`
|
||
Postgres PostgresConfig `mapstructure:"postgres"`
|
||
Redis RedisConfig `mapstructure:"redis"`
|
||
CleanupEnabled bool `mapstructure:"cleanup_enabled"` // 是否启用缓存清理
|
||
CleanupSeconds int `mapstructure:"cleanup_seconds"` // 缓存清理时间(秒)
|
||
}
|
||
|
||
// PostgresConfig PostgreSQL 连接配置
|
||
type PostgresConfig struct {
|
||
Host string `mapstructure:"host"`
|
||
Port int `mapstructure:"port"`
|
||
User string `mapstructure:"user"`
|
||
Password string `mapstructure:"password"`
|
||
Database string `mapstructure:"database"`
|
||
SSLMode string `mapstructure:"sslmode"` // disable | prefer | require
|
||
}
|
||
|
||
// RedisConfig Redis 缓存配置(启用后正文缓存与音频代理缓存走 Redis,统计数据与设备池仍在 SQL 库)
|
||
type RedisConfig struct {
|
||
Enabled bool `mapstructure:"enabled"`
|
||
Host string `mapstructure:"host"`
|
||
Port int `mapstructure:"port"`
|
||
Password string `mapstructure:"password"`
|
||
DB int `mapstructure:"db"`
|
||
}
|
||
|
||
// DeviceKeyPoolConfig 设备密钥池配置
|
||
type DeviceKeyPoolConfig struct {
|
||
Enabled bool `mapstructure:"enabled"` // 是否启用设备密钥池
|
||
InitialSize int `mapstructure:"initial_size"` // 初始化设备数量
|
||
MinSize int `mapstructure:"min_size"` // 最小设备数量
|
||
MaxSize int `mapstructure:"max_size"` // 最大设备数量
|
||
}
|
||
|
||
// ConfigManager 配置管理器
|
||
type ConfigManager struct {
|
||
mu sync.RWMutex
|
||
configPath string
|
||
callbacks []func(*Config)
|
||
}
|
||
|
||
var (
|
||
// appConfig 全局配置实例(原子指针,热更新替换时读取侧无数据竞争)
|
||
appConfig atomic.Pointer[Config]
|
||
manager *ConfigManager
|
||
)
|
||
|
||
// GetAppConfig 获取当前配置,未加载时返回 nil
|
||
func GetAppConfig() *Config {
|
||
return appConfig.Load()
|
||
}
|
||
|
||
// NewConfigManager 创建配置管理器
|
||
func NewConfigManager() *ConfigManager {
|
||
return &ConfigManager{
|
||
callbacks: make([]func(*Config), 0),
|
||
}
|
||
}
|
||
|
||
// ApplyTimezone 按 TZ 环境变量/.env 设置进程时区。
|
||
// 必须在任何时间显示之前调用(time.Local 首次使用后不再重读 TZ)。
|
||
// 环境变量已设 TZ 时以环境变量为准(.env 不覆盖已有环境变量)。
|
||
func ApplyTimezone() {
|
||
loadDotenv("./.env")
|
||
tz := os.Getenv("TZ")
|
||
if tz == "" {
|
||
return
|
||
}
|
||
if _, err := time.LoadLocation(tz); err != nil {
|
||
log.Printf("[config/WARN] 无效时区 TZ=%s,沿用系统时区: %v", tz, err)
|
||
return // 不写入 TZ:无效值会让 time.Local 落到 UTC
|
||
}
|
||
os.Setenv("TZ", tz)
|
||
}
|
||
|
||
// LoadConfig 加载配置文件
|
||
func LoadConfig(configPath string) error {
|
||
// 初始化管理器
|
||
if manager == nil {
|
||
manager = NewConfigManager()
|
||
manager.configPath = configPath
|
||
}
|
||
|
||
// 加载 .env(若存在),敏感配置可由环境变量覆盖
|
||
loadDotenv("./.env")
|
||
|
||
viper.SetConfigFile(configPath)
|
||
viper.SetConfigType("yaml")
|
||
|
||
// 设置默认值
|
||
setDefaults()
|
||
|
||
if err := viper.ReadInConfig(); err != nil {
|
||
return fmt.Errorf("读取配置文件失败: %w", err)
|
||
}
|
||
|
||
newConfig := &Config{}
|
||
if err := viper.Unmarshal(newConfig); err != nil {
|
||
return fmt.Errorf("解析配置文件失败: %w", err)
|
||
}
|
||
applyEnvOverrides(newConfig)
|
||
|
||
// 更新配置
|
||
appConfig.Store(newConfig)
|
||
|
||
return nil
|
||
}
|
||
|
||
// setDefaults 设置默认值
|
||
func setDefaults() {
|
||
// 服务器配置
|
||
viper.SetDefault("server.port", "40180")
|
||
viper.SetDefault("server.host", "0.0.0.0")
|
||
viper.SetDefault("server.request_log.enabled", false)
|
||
viper.SetDefault("server.request_log.retention_days", 7)
|
||
|
||
// 番茄小说配置
|
||
viper.SetDefault("fanqie.algorithm", "gorgon")
|
||
viper.SetDefault("fanqie.signature", "sign2")
|
||
|
||
// 内容处理配置
|
||
viper.SetDefault("fanqie.content.remove_images", false)
|
||
viper.SetDefault("fanqie.content.remove_image_desc", false)
|
||
viper.SetDefault("fanqie.content.remove_blank_lines", true)
|
||
viper.SetDefault("fanqie.content.remove_html_tags", false)
|
||
viper.SetDefault("fanqie.content.remove_chapter_title", false)
|
||
viper.SetDefault("fanqie.content.add_paragraph_indent", false)
|
||
viper.SetDefault("fanqie.content.double_newline", true)
|
||
viper.SetDefault("fanqie.content.enable_review", true)
|
||
|
||
// 漫画处理配置
|
||
viper.SetDefault("fanqie.manga.output_mode", "url")
|
||
|
||
// 视频处理配置
|
||
viper.SetDefault("fanqie.video.output_mode", "proxy")
|
||
viper.SetDefault("fanqie.video.quality", "max")
|
||
viper.SetDefault("fanqie.video.cache_enabled", true)
|
||
viper.SetDefault("fanqie.video.cache_path", "./video_cache")
|
||
|
||
// 缓存配置
|
||
viper.SetDefault("cache.enabled", true)
|
||
viper.SetDefault("cache.backend", "sqlite")
|
||
viper.SetDefault("cache.sqlite_path", "./cache.db")
|
||
viper.SetDefault("cache.cleanup_enabled", false)
|
||
viper.SetDefault("cache.cleanup_seconds", 86400)
|
||
|
||
// PostgreSQL 配置
|
||
viper.SetDefault("cache.postgres.host", "127.0.0.1")
|
||
viper.SetDefault("cache.postgres.port", 5432)
|
||
viper.SetDefault("cache.postgres.user", "postgres")
|
||
viper.SetDefault("cache.postgres.password", "")
|
||
viper.SetDefault("cache.postgres.database", "fanqie")
|
||
viper.SetDefault("cache.postgres.sslmode", "disable")
|
||
|
||
// Redis 配置
|
||
viper.SetDefault("cache.redis.enabled", false)
|
||
viper.SetDefault("cache.redis.host", "127.0.0.1")
|
||
viper.SetDefault("cache.redis.port", 6379)
|
||
viper.SetDefault("cache.redis.password", "")
|
||
viper.SetDefault("cache.redis.db", 0)
|
||
|
||
// 设备密钥池配置
|
||
viper.SetDefault("device_key_pool.enabled", true)
|
||
viper.SetDefault("device_key_pool.initial_size", 3)
|
||
viper.SetDefault("device_key_pool.min_size", 2)
|
||
viper.SetDefault("device_key_pool.max_size", 10)
|
||
}
|
||
|
||
// GetConfig 获取配置实例
|
||
func GetConfig() *Config {
|
||
if cfg := appConfig.Load(); cfg != nil {
|
||
return cfg
|
||
}
|
||
log.Fatal("配置未初始化,请先调用 LoadConfig")
|
||
return nil
|
||
}
|
||
|
||
// ReloadConfig 重新加载配置文件
|
||
func ReloadConfig() error {
|
||
if manager == nil {
|
||
return fmt.Errorf("配置管理器未初始化")
|
||
}
|
||
|
||
manager.mu.RLock()
|
||
configPath := manager.configPath
|
||
manager.mu.RUnlock()
|
||
|
||
if configPath == "" {
|
||
return fmt.Errorf("配置文件路径为空")
|
||
}
|
||
|
||
// 重新读取配置文件
|
||
viper.SetConfigFile(configPath)
|
||
if err := viper.ReadInConfig(); err != nil {
|
||
return fmt.Errorf("重新读取配置文件失败: %w", err)
|
||
}
|
||
|
||
newConfig := &Config{}
|
||
if err := viper.Unmarshal(newConfig); err != nil {
|
||
return fmt.Errorf("重新解析配置文件失败: %w", err)
|
||
}
|
||
applyEnvOverrides(newConfig)
|
||
|
||
// 更新配置
|
||
manager.mu.Lock()
|
||
var oldAlgorithm string
|
||
if old := appConfig.Load(); old != nil {
|
||
oldAlgorithm = old.Fanqie.Algorithm
|
||
}
|
||
appConfig.Store(newConfig)
|
||
callbacks := make([]func(*Config), len(manager.callbacks))
|
||
copy(callbacks, manager.callbacks)
|
||
manager.mu.Unlock()
|
||
|
||
// 通知配置更新
|
||
for _, callback := range callbacks {
|
||
callback(newConfig)
|
||
}
|
||
|
||
log.Printf("配置重新加载成功: %s", configPath)
|
||
if oldAlgorithm != newConfig.Fanqie.Algorithm {
|
||
log.Printf("算法已切换: %s -> %s", oldAlgorithm, newConfig.Fanqie.Algorithm)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// RegisterConfigCallback 注册配置更新回调
|
||
func RegisterConfigCallback(callback func(*Config)) {
|
||
if manager == nil {
|
||
log.Printf("警告: 配置管理器未初始化,无法注册回调")
|
||
return
|
||
}
|
||
|
||
manager.mu.Lock()
|
||
defer manager.mu.Unlock()
|
||
manager.callbacks = append(manager.callbacks, callback)
|
||
}
|
||
|
||
// GetConfigManager 获取配置管理器实例
|
||
func GetConfigManager() *ConfigManager {
|
||
return manager
|
||
}
|
||
|