From 8322808741c614e6c288224f970aff86ff37ee32 Mon Sep 17 00:00:00 2001 From: ShukeBta <272197458+ShukeBta@users.noreply.github.com> Date: Sat, 27 Jun 2026 03:30:20 +0800 Subject: [PATCH] split config loading helpers --- internal/config/config.go | 336 ----------------------------------- internal/config/defaults.go | 112 ++++++++++++ internal/config/normalize.go | 73 ++++++++ internal/config/types.go | 163 +++++++++++++++++ 4 files changed, 348 insertions(+), 336 deletions(-) create mode 100644 internal/config/defaults.go create mode 100644 internal/config/normalize.go create mode 100644 internal/config/types.go diff --git a/internal/config/config.go b/internal/config/config.go index 3dab0cf..bea13ec 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -8,8 +8,6 @@ package config import ( - "crypto/rand" - "encoding/hex" "fmt" "os" "path/filepath" @@ -21,175 +19,6 @@ import ( // EnvPrefix 是所有环境变量驱动的覆盖使用的前缀。 const EnvPrefix = "MEDIASTATION" -const ( - defaultDatabaseMaxOpenConns = 4 - defaultDatabaseMaxIdleConns = 2 - defaultLicenseServerURL = "https://mgosever.3jzs.com" - defaultLicensePublicKey = "MCowBQYDK2VwAyEABRXnXy+urjrbKit6Yu/HiezWgP0NdsZW3tsegJWRrtI=" -) - -// Config 是根配置聚合。 -type Config struct { - App AppConfig `mapstructure:"app"` - Database DatabaseConfig `mapstructure:"database"` - Secrets SecretsConfig `mapstructure:"secrets"` - Logging LoggingConfig `mapstructure:"logging"` - Cache CacheConfig `mapstructure:"cache"` - Search SearchConfig `mapstructure:"search"` - Media MediaConfig `mapstructure:"media"` - Transcoder TranscoderConfig `mapstructure:"transcoder"` - AI AIConfig `mapstructure:"ai"` - FlareSolverr FlareSolverrConfig `mapstructure:"flaresolverr"` - ApiConfig ApiConfigConfig `mapstructure:"api_config"` - Organizer OrganizerConfig `mapstructure:"organizer"` - License LicenseConfig `mapstructure:"license"` -} - -// ApiConfigConfig API 配置相关设置。 -type ApiConfigConfig struct { - // AutoEncrypt 是否自动加密敏感字段 - AutoEncrypt bool `mapstructure:"auto_encrypt"` - // DefaultTimeout 默认请求超时(秒) - DefaultTimeout int `mapstructure:"default_timeout"` -} - -// TranscoderConfig 控制 HLS / ffmpeg 后端。 -type TranscoderConfig struct { - Encoder string `mapstructure:"encoder"` // "" / nvenc / qsv / vaapi - Enabled bool `mapstructure:"enabled"` - HardwareAccel bool `mapstructure:"hardware_accel"` - Preset string `mapstructure:"preset"` - VideoBitrate string `mapstructure:"video_bitrate"` - MaxRate string `mapstructure:"max_rate"` - BufSize string `mapstructure:"buf_size"` - MaxHeight int `mapstructure:"max_height"` - SegmentSeconds int `mapstructure:"segment_seconds"` - Realtime bool `mapstructure:"realtime"` - Threads int `mapstructure:"threads"` - MaxConcurrent int `mapstructure:"max_concurrent"` - IdleTimeoutSeconds int `mapstructure:"idle_timeout_seconds"` -} - -// AppConfig 保存运行时应用参数。 -type AppConfig struct { - Port int `mapstructure:"port"` - Debug bool `mapstructure:"debug"` - Env string `mapstructure:"env"` - DataDir string `mapstructure:"data_dir"` - WebDir string `mapstructure:"web_dir"` - FFmpegPath string `mapstructure:"ffmpeg_path"` - FFprobePath string `mapstructure:"ffprobe_path"` - // FFprobeMaxConcurrent limits concurrent ffprobe/ffmpeg metadata probes. - // NAS devices can become unresponsive when a scan starts many probe - // processes at once, so the default is deliberately conservative. - FFprobeMaxConcurrent int `mapstructure:"ffprobe_max_concurrent"` - // CloudScanMaxConcurrent limits concurrent cloud directory list requests - // inside one mounted cloud library scan. - CloudScanMaxConcurrent int `mapstructure:"cloud_scan_max_concurrent"` - MaxCPUThreads int `mapstructure:"max_cpu_threads"` - VAAPIDevice string `mapstructure:"vaapi_device"` - CORSOrigins []string `mapstructure:"cors_origins"` - ServerURL string `mapstructure:"server_url"` -} - -// DatabaseConfig 配置 GORM 数据库。默认 auto: -// Docker Compose 主线会注入 PostgreSQL DSN;裸机/旧部署没有 DSN 时回退 SQLite。 -type DatabaseConfig struct { - Type string `mapstructure:"type"` - DBPath string `mapstructure:"db_path"` - DSN string `mapstructure:"dsn"` - WALMode bool `mapstructure:"wal_mode"` - BusyTimeout int `mapstructure:"busy_timeout"` - CacheSize int `mapstructure:"cache_size"` - MaxOpenConns int `mapstructure:"max_open_conns"` - MaxIdleConns int `mapstructure:"max_idle_conns"` -} - -// SecretsConfig 保存 JWT / 第三方 API 密钥(不要提交值)。 -type SecretsConfig struct { - JWTSecret string `mapstructure:"jwt_secret"` - TMDbAPIKey string `mapstructure:"tmdb_api_key"` - TMDbAPIProxy string `mapstructure:"tmdb_api_proxy"` - TMDbImageProxy string `mapstructure:"tmdb_image_proxy"` - BangumiToken string `mapstructure:"bangumi_access_token"` - TheTVDBAPIKey string `mapstructure:"thetvdb_api_key"` - FanartAPIKey string `mapstructure:"fanart_tv_api_key"` - DoubanCookie string `mapstructure:"douban_cookie"` - // 用于加密的密钥,如果为空则使用 JWTSecret - EncryptionKey string `mapstructure:"encryption_key"` -} - -// LoggingConfig 配置 Zap。 -type LoggingConfig struct { - Level string `mapstructure:"level"` - Format string `mapstructure:"format"` - OutputPath string `mapstructure:"output_path"` - EnableRotation bool `mapstructure:"enable_rotation"` - MaxSizeMB int `mapstructure:"max_size_mb"` - MaxAgeDays int `mapstructure:"max_age_days"` - MaxBackups int `mapstructure:"max_backups"` -} - -// CacheConfig 控制磁盘转码/刮削缓存。 -type CacheConfig struct { - CacheDir string `mapstructure:"cache_dir"` - MaxDiskUsageMB int `mapstructure:"max_disk_usage_mb"` - TTLHours int `mapstructure:"ttl_hours"` - AutoCleanup bool `mapstructure:"auto_cleanup"` - CleanupIntervalMin int `mapstructure:"cleanup_interval_min"` - RedisURL string `mapstructure:"redis_url"` - RedisPrefix string `mapstructure:"redis_prefix"` - MediaTTLSeconds int `mapstructure:"media_ttl_seconds"` -} - -type SearchConfig struct { - Backend string `mapstructure:"backend"` - OpenSearchURL string `mapstructure:"opensearch_url"` - Index string `mapstructure:"index"` - Username string `mapstructure:"username"` - Password string `mapstructure:"password"` -} - -// MediaConfig 保存默认库位置(用于引导库)。 -type MediaConfig struct { - MoviesDir string `mapstructure:"movies_dir"` - TVDir string `mapstructure:"tv_dir"` - AnimeDir string `mapstructure:"anime_dir"` -} - -// AIConfig 配置可选的 LLM 提供者。 -type AIConfig struct { - Enabled bool `mapstructure:"enabled"` - Provider string `mapstructure:"provider"` - APIBase string `mapstructure:"api_base"` - APIKey string `mapstructure:"api_key"` - Model string `mapstructure:"model"` - Timeout int `mapstructure:"timeout"` - MaxConcurrent int `mapstructure:"max_concurrent"` -} - -// LicenseConfig configures the optional MediaStationGo license server bridge. -type LicenseConfig struct { - ServerURL string `mapstructure:"server_url"` - HMACSecret string `mapstructure:"hmac_secret"` - PublicKey string `mapstructure:"public_key"` -} - -// OrganizerConfig 配置媒体文件智能分类整理。 -type OrganizerConfig struct { - SmartClassify bool `mapstructure:"smart_classify"` - AutoAfterDownload bool `mapstructure:"auto_after_download"` - Categories map[string]string `mapstructure:"categories"` -} - -// FlareSolverrConfig 配置 FlareSolverr 服务(用于绕过 Cloudflare/WAF)。 -type FlareSolverrConfig struct { - Enabled bool `mapstructure:"enabled"` - URL string `mapstructure:"url"` - Session string `mapstructure:"session"` - Timeout int `mapstructure:"timeout"` -} - // Load 从默认值 / 文件 / 环境读取配置。 // // 即使没有文件也始终返回可用的 Config。 @@ -236,171 +65,6 @@ func Load() (*Config, error) { return cfg, nil } -func setDefaults(v *viper.Viper) { - v.SetDefault("app.port", 8080) - v.SetDefault("app.debug", false) - v.SetDefault("app.env", "production") - v.SetDefault("app.data_dir", "./data") - v.SetDefault("app.web_dir", "./web/dist") - v.SetDefault("app.ffmpeg_path", "ffmpeg") - v.SetDefault("app.ffprobe_path", "ffprobe") - v.SetDefault("app.ffprobe_max_concurrent", 2) - v.SetDefault("app.cloud_scan_max_concurrent", 8) - v.SetDefault("app.max_cpu_threads", 2) - v.SetDefault("app.vaapi_device", "/dev/dri/renderD128") - v.SetDefault("app.cors_origins", []string{}) - v.SetDefault("app.server_url", "") - - v.SetDefault("database.type", "auto") - v.SetDefault("database.db_path", "./data/mediastation.db") - v.SetDefault("database.dsn", "") - v.SetDefault("database.wal_mode", true) - v.SetDefault("database.busy_timeout", 5000) - v.SetDefault("database.cache_size", -20000) - v.SetDefault("database.max_open_conns", defaultDatabaseMaxOpenConns) - v.SetDefault("database.max_idle_conns", defaultDatabaseMaxIdleConns) - - v.SetDefault("secrets.jwt_secret", "") - - v.SetDefault("logging.level", "warn") - v.SetDefault("logging.format", "console") - v.SetDefault("logging.enable_rotation", true) - v.SetDefault("logging.max_size_mb", 20) - v.SetDefault("logging.max_age_days", 30) - v.SetDefault("logging.max_backups", 10) - - v.SetDefault("cache.cache_dir", "./cache") - v.SetDefault("cache.cleanup_interval_min", 60) - v.SetDefault("cache.redis_url", "") - v.SetDefault("cache.redis_prefix", "mediastationgo") - v.SetDefault("cache.media_ttl_seconds", 15) - - v.SetDefault("search.backend", "") - v.SetDefault("search.opensearch_url", "") - v.SetDefault("search.index", "mediastation_media") - v.SetDefault("search.username", "") - v.SetDefault("search.password", "") - - v.SetDefault("ai.enabled", false) - v.SetDefault("ai.provider", "openai") - v.SetDefault("ai.api_base", "https://api.openai.com/v1") - v.SetDefault("ai.model", "gpt-4o-mini") - v.SetDefault("ai.timeout", 30) - v.SetDefault("ai.max_concurrent", 3) - - v.SetDefault("flaresolverr.enabled", false) - v.SetDefault("flaresolverr.url", "http://localhost:8191") - v.SetDefault("flaresolverr.session", "mediastation") - v.SetDefault("flaresolverr.timeout", 60) - - v.SetDefault("downloads.smart_classify", true) - v.SetDefault("organizer.smart_classify", true) - v.SetDefault("organizer.auto_after_download", false) - v.SetDefault("organize.scrape_after", true) - v.SetDefault("scrape.delay_min_ms", 250) - v.SetDefault("scrape.delay_max_ms", 500) - v.SetDefault("organizer.categories.chinese_movie", "华语电影") - v.SetDefault("organizer.categories.animation_movie", "动画电影") - v.SetDefault("organizer.categories.foreign_movie", "外语电影") - v.SetDefault("organizer.categories.euus_movie", "欧美电影") - v.SetDefault("organizer.categories.jk_movie", "日韩电影") - v.SetDefault("organizer.categories.domestic_tv", "国产剧") - v.SetDefault("organizer.categories.euus_tv", "欧美剧") - v.SetDefault("organizer.categories.jk_tv", "日韩剧") - v.SetDefault("organizer.categories.jp_anime", "日番") - v.SetDefault("organizer.categories.cn_anime", "国漫") - v.SetDefault("organizer.categories.euus_anime", "欧美动漫") - v.SetDefault("organizer.categories.variety", "综艺") - v.SetDefault("organizer.categories.documentary", "纪录片") - v.SetDefault("organizer.categories.children", "儿童") - v.SetDefault("organizer.categories.uncategorized_tv", "未分类") - - v.SetDefault("transcoder.encoder", "") - v.SetDefault("transcoder.enabled", true) - v.SetDefault("transcoder.hardware_accel", false) - v.SetDefault("transcoder.preset", "veryfast") - v.SetDefault("transcoder.video_bitrate", "1500k") - v.SetDefault("transcoder.max_rate", "1800k") - v.SetDefault("transcoder.buf_size", "3000k") - v.SetDefault("transcoder.max_height", 720) - v.SetDefault("transcoder.segment_seconds", 4) - v.SetDefault("transcoder.realtime", true) - v.SetDefault("transcoder.threads", 2) - v.SetDefault("transcoder.max_concurrent", 1) - v.SetDefault("transcoder.idle_timeout_seconds", 120) - - // API Config 默认设置 - v.SetDefault("api_config.auto_encrypt", true) - v.SetDefault("api_config.default_timeout", 30) - - v.SetDefault("license.server_url", defaultLicenseServerURL) - v.SetDefault("license.hmac_secret", "") - v.SetDefault("license.public_key", defaultLicensePublicKey) -} - -// normalize 填充派生默认值并自愈空的关键字段。 -func (c *Config) normalize() error { - if c.App.DataDir == "" { - c.App.DataDir = "./data" - } - if c.Database.DBPath == "" { - c.Database.DBPath = filepath.Join(c.App.DataDir, "mediastation.db") - } - if c.Database.Type == "" { - c.Database.Type = "auto" - } - if c.App.MaxCPUThreads < 1 { - c.App.MaxCPUThreads = 1 - } - if c.App.MaxCPUThreads > 8 { - c.App.MaxCPUThreads = 8 - } - if c.App.CloudScanMaxConcurrent < 1 { - c.App.CloudScanMaxConcurrent = 1 - } - if c.App.CloudScanMaxConcurrent > 16 { - c.App.CloudScanMaxConcurrent = 16 - } - if c.Database.MaxOpenConns <= 0 { - c.Database.MaxOpenConns = defaultDatabaseMaxOpenConns - } - if c.Database.MaxIdleConns <= 0 || c.Database.MaxIdleConns > c.Database.MaxOpenConns { - c.Database.MaxIdleConns = defaultDatabaseMaxIdleConns - if c.Database.MaxIdleConns > c.Database.MaxOpenConns { - c.Database.MaxIdleConns = c.Database.MaxOpenConns - } - } - if c.Cache.CacheDir == "" { - c.Cache.CacheDir = filepath.Join(c.App.DataDir, "cache") - } - if c.Cache.RedisPrefix == "" { - c.Cache.RedisPrefix = "mediastationgo" - } - if c.Cache.MediaTTLSeconds < 1 { - c.Cache.MediaTTLSeconds = 15 - } - c.Search.Backend = strings.ToLower(strings.TrimSpace(c.Search.Backend)) - if c.Search.Index == "" { - c.Search.Index = "mediastation_media" - } - if c.Secrets.JWTSecret == "" { - // 持久化自动生成的密钥以在操作员忘记配置时保持会话稳定。 - path := filepath.Join(c.App.DataDir, ".jwt_secret") - if data, err := os.ReadFile(path); err == nil && len(data) > 0 { // #nosec G304 -- path is fixed to .jwt_secret under configured DataDir. - c.Secrets.JWTSecret = strings.TrimSpace(string(data)) - } else { - buf := make([]byte, 32) - if _, err := rand.Read(buf); err != nil { - return fmt.Errorf("generate jwt secret: %w", err) - } - c.Secrets.JWTSecret = hex.EncodeToString(buf) - _ = os.MkdirAll(c.App.DataDir, 0o750) - _ = os.WriteFile(path, []byte(c.Secrets.JWTSecret), 0o600) - } - } - return nil -} - // asConfigFileNotFound 是 errors.As 的小辅助函数,避免在这个短文件中导入 errors。 func asConfigFileNotFound(err error, target *viper.ConfigFileNotFoundError) bool { if err == nil { diff --git a/internal/config/defaults.go b/internal/config/defaults.go new file mode 100644 index 0000000..53fb179 --- /dev/null +++ b/internal/config/defaults.go @@ -0,0 +1,112 @@ +package config + +import "github.com/spf13/viper" + +const ( + defaultDatabaseMaxOpenConns = 4 + defaultDatabaseMaxIdleConns = 2 + defaultLicenseServerURL = "https://mgosever.3jzs.com" + defaultLicensePublicKey = "MCowBQYDK2VwAyEABRXnXy+urjrbKit6Yu/HiezWgP0NdsZW3tsegJWRrtI=" +) + +func setDefaults(v *viper.Viper) { + v.SetDefault("app.port", 8080) + v.SetDefault("app.debug", false) + v.SetDefault("app.env", "production") + v.SetDefault("app.data_dir", "./data") + v.SetDefault("app.web_dir", "./web/dist") + v.SetDefault("app.ffmpeg_path", "ffmpeg") + v.SetDefault("app.ffprobe_path", "ffprobe") + v.SetDefault("app.ffprobe_max_concurrent", 2) + v.SetDefault("app.cloud_scan_max_concurrent", 8) + v.SetDefault("app.max_cpu_threads", 2) + v.SetDefault("app.vaapi_device", "/dev/dri/renderD128") + v.SetDefault("app.cors_origins", []string{}) + v.SetDefault("app.server_url", "") + + v.SetDefault("database.type", "auto") + v.SetDefault("database.db_path", "./data/mediastation.db") + v.SetDefault("database.dsn", "") + v.SetDefault("database.wal_mode", true) + v.SetDefault("database.busy_timeout", 5000) + v.SetDefault("database.cache_size", -20000) + v.SetDefault("database.max_open_conns", defaultDatabaseMaxOpenConns) + v.SetDefault("database.max_idle_conns", defaultDatabaseMaxIdleConns) + + v.SetDefault("secrets.jwt_secret", "") + + v.SetDefault("logging.level", "warn") + v.SetDefault("logging.format", "console") + v.SetDefault("logging.enable_rotation", true) + v.SetDefault("logging.max_size_mb", 20) + v.SetDefault("logging.max_age_days", 30) + v.SetDefault("logging.max_backups", 10) + + v.SetDefault("cache.cache_dir", "./cache") + v.SetDefault("cache.cleanup_interval_min", 60) + v.SetDefault("cache.redis_url", "") + v.SetDefault("cache.redis_prefix", "mediastationgo") + v.SetDefault("cache.media_ttl_seconds", 15) + + v.SetDefault("search.backend", "") + v.SetDefault("search.opensearch_url", "") + v.SetDefault("search.index", "mediastation_media") + v.SetDefault("search.username", "") + v.SetDefault("search.password", "") + + v.SetDefault("ai.enabled", false) + v.SetDefault("ai.provider", "openai") + v.SetDefault("ai.api_base", "https://api.openai.com/v1") + v.SetDefault("ai.model", "gpt-4o-mini") + v.SetDefault("ai.timeout", 30) + v.SetDefault("ai.max_concurrent", 3) + + v.SetDefault("flaresolverr.enabled", false) + v.SetDefault("flaresolverr.url", "http://localhost:8191") + v.SetDefault("flaresolverr.session", "mediastation") + v.SetDefault("flaresolverr.timeout", 60) + + v.SetDefault("downloads.smart_classify", true) + v.SetDefault("organizer.smart_classify", true) + v.SetDefault("organizer.auto_after_download", false) + v.SetDefault("organize.scrape_after", true) + v.SetDefault("scrape.delay_min_ms", 250) + v.SetDefault("scrape.delay_max_ms", 500) + v.SetDefault("organizer.categories.chinese_movie", "华语电影") + v.SetDefault("organizer.categories.animation_movie", "动画电影") + v.SetDefault("organizer.categories.foreign_movie", "外语电影") + v.SetDefault("organizer.categories.euus_movie", "欧美电影") + v.SetDefault("organizer.categories.jk_movie", "日韩电影") + v.SetDefault("organizer.categories.domestic_tv", "国产剧") + v.SetDefault("organizer.categories.euus_tv", "欧美剧") + v.SetDefault("organizer.categories.jk_tv", "日韩剧") + v.SetDefault("organizer.categories.jp_anime", "日番") + v.SetDefault("organizer.categories.cn_anime", "国漫") + v.SetDefault("organizer.categories.euus_anime", "欧美动漫") + v.SetDefault("organizer.categories.variety", "综艺") + v.SetDefault("organizer.categories.documentary", "纪录片") + v.SetDefault("organizer.categories.children", "儿童") + v.SetDefault("organizer.categories.uncategorized_tv", "未分类") + + v.SetDefault("transcoder.encoder", "") + v.SetDefault("transcoder.enabled", true) + v.SetDefault("transcoder.hardware_accel", false) + v.SetDefault("transcoder.preset", "veryfast") + v.SetDefault("transcoder.video_bitrate", "1500k") + v.SetDefault("transcoder.max_rate", "1800k") + v.SetDefault("transcoder.buf_size", "3000k") + v.SetDefault("transcoder.max_height", 720) + v.SetDefault("transcoder.segment_seconds", 4) + v.SetDefault("transcoder.realtime", true) + v.SetDefault("transcoder.threads", 2) + v.SetDefault("transcoder.max_concurrent", 1) + v.SetDefault("transcoder.idle_timeout_seconds", 120) + + // API Config 默认设置 + v.SetDefault("api_config.auto_encrypt", true) + v.SetDefault("api_config.default_timeout", 30) + + v.SetDefault("license.server_url", defaultLicenseServerURL) + v.SetDefault("license.hmac_secret", "") + v.SetDefault("license.public_key", defaultLicensePublicKey) +} diff --git a/internal/config/normalize.go b/internal/config/normalize.go new file mode 100644 index 0000000..8e42f0d --- /dev/null +++ b/internal/config/normalize.go @@ -0,0 +1,73 @@ +package config + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "os" + "path/filepath" + "strings" +) + +// normalize 填充派生默认值并自愈空的关键字段。 +func (c *Config) normalize() error { + if c.App.DataDir == "" { + c.App.DataDir = "./data" + } + if c.Database.DBPath == "" { + c.Database.DBPath = filepath.Join(c.App.DataDir, "mediastation.db") + } + if c.Database.Type == "" { + c.Database.Type = "auto" + } + if c.App.MaxCPUThreads < 1 { + c.App.MaxCPUThreads = 1 + } + if c.App.MaxCPUThreads > 8 { + c.App.MaxCPUThreads = 8 + } + if c.App.CloudScanMaxConcurrent < 1 { + c.App.CloudScanMaxConcurrent = 1 + } + if c.App.CloudScanMaxConcurrent > 16 { + c.App.CloudScanMaxConcurrent = 16 + } + if c.Database.MaxOpenConns <= 0 { + c.Database.MaxOpenConns = defaultDatabaseMaxOpenConns + } + if c.Database.MaxIdleConns <= 0 || c.Database.MaxIdleConns > c.Database.MaxOpenConns { + c.Database.MaxIdleConns = defaultDatabaseMaxIdleConns + if c.Database.MaxIdleConns > c.Database.MaxOpenConns { + c.Database.MaxIdleConns = c.Database.MaxOpenConns + } + } + if c.Cache.CacheDir == "" { + c.Cache.CacheDir = filepath.Join(c.App.DataDir, "cache") + } + if c.Cache.RedisPrefix == "" { + c.Cache.RedisPrefix = "mediastationgo" + } + if c.Cache.MediaTTLSeconds < 1 { + c.Cache.MediaTTLSeconds = 15 + } + c.Search.Backend = strings.ToLower(strings.TrimSpace(c.Search.Backend)) + if c.Search.Index == "" { + c.Search.Index = "mediastation_media" + } + if c.Secrets.JWTSecret == "" { + // 持久化自动生成的密钥以在操作员忘记配置时保持会话稳定。 + path := filepath.Join(c.App.DataDir, ".jwt_secret") + if data, err := os.ReadFile(path); err == nil && len(data) > 0 { // #nosec G304 -- path is fixed to .jwt_secret under configured DataDir. + c.Secrets.JWTSecret = strings.TrimSpace(string(data)) + } else { + buf := make([]byte, 32) + if _, err := rand.Read(buf); err != nil { + return fmt.Errorf("generate jwt secret: %w", err) + } + c.Secrets.JWTSecret = hex.EncodeToString(buf) + _ = os.MkdirAll(c.App.DataDir, 0o750) + _ = os.WriteFile(path, []byte(c.Secrets.JWTSecret), 0o600) + } + } + return nil +} diff --git a/internal/config/types.go b/internal/config/types.go new file mode 100644 index 0000000..6326ccd --- /dev/null +++ b/internal/config/types.go @@ -0,0 +1,163 @@ +package config + +// Config 是根配置聚合。 +type Config struct { + App AppConfig `mapstructure:"app"` + Database DatabaseConfig `mapstructure:"database"` + Secrets SecretsConfig `mapstructure:"secrets"` + Logging LoggingConfig `mapstructure:"logging"` + Cache CacheConfig `mapstructure:"cache"` + Search SearchConfig `mapstructure:"search"` + Media MediaConfig `mapstructure:"media"` + Transcoder TranscoderConfig `mapstructure:"transcoder"` + AI AIConfig `mapstructure:"ai"` + FlareSolverr FlareSolverrConfig `mapstructure:"flaresolverr"` + ApiConfig ApiConfigConfig `mapstructure:"api_config"` + Organizer OrganizerConfig `mapstructure:"organizer"` + License LicenseConfig `mapstructure:"license"` +} + +// ApiConfigConfig API 配置相关设置。 +type ApiConfigConfig struct { + // AutoEncrypt 是否自动加密敏感字段 + AutoEncrypt bool `mapstructure:"auto_encrypt"` + // DefaultTimeout 默认请求超时(秒) + DefaultTimeout int `mapstructure:"default_timeout"` +} + +// TranscoderConfig 控制 HLS / ffmpeg 后端。 +type TranscoderConfig struct { + Encoder string `mapstructure:"encoder"` // "" / nvenc / qsv / vaapi + Enabled bool `mapstructure:"enabled"` + HardwareAccel bool `mapstructure:"hardware_accel"` + Preset string `mapstructure:"preset"` + VideoBitrate string `mapstructure:"video_bitrate"` + MaxRate string `mapstructure:"max_rate"` + BufSize string `mapstructure:"buf_size"` + MaxHeight int `mapstructure:"max_height"` + SegmentSeconds int `mapstructure:"segment_seconds"` + Realtime bool `mapstructure:"realtime"` + Threads int `mapstructure:"threads"` + MaxConcurrent int `mapstructure:"max_concurrent"` + IdleTimeoutSeconds int `mapstructure:"idle_timeout_seconds"` +} + +// AppConfig 保存运行时应用参数。 +type AppConfig struct { + Port int `mapstructure:"port"` + Debug bool `mapstructure:"debug"` + Env string `mapstructure:"env"` + DataDir string `mapstructure:"data_dir"` + WebDir string `mapstructure:"web_dir"` + FFmpegPath string `mapstructure:"ffmpeg_path"` + FFprobePath string `mapstructure:"ffprobe_path"` + // FFprobeMaxConcurrent limits concurrent ffprobe/ffmpeg metadata probes. + // NAS devices can become unresponsive when a scan starts many probe + // processes at once, so the default is deliberately conservative. + FFprobeMaxConcurrent int `mapstructure:"ffprobe_max_concurrent"` + // CloudScanMaxConcurrent limits concurrent cloud directory list requests + // inside one mounted cloud library scan. + CloudScanMaxConcurrent int `mapstructure:"cloud_scan_max_concurrent"` + MaxCPUThreads int `mapstructure:"max_cpu_threads"` + VAAPIDevice string `mapstructure:"vaapi_device"` + CORSOrigins []string `mapstructure:"cors_origins"` + ServerURL string `mapstructure:"server_url"` +} + +// DatabaseConfig 配置 GORM 数据库。默认 auto: +// Docker Compose 主线会注入 PostgreSQL DSN;裸机/旧部署没有 DSN 时回退 SQLite。 +type DatabaseConfig struct { + Type string `mapstructure:"type"` + DBPath string `mapstructure:"db_path"` + DSN string `mapstructure:"dsn"` + WALMode bool `mapstructure:"wal_mode"` + BusyTimeout int `mapstructure:"busy_timeout"` + CacheSize int `mapstructure:"cache_size"` + MaxOpenConns int `mapstructure:"max_open_conns"` + MaxIdleConns int `mapstructure:"max_idle_conns"` +} + +// SecretsConfig 保存 JWT / 第三方 API 密钥(不要提交值)。 +type SecretsConfig struct { + JWTSecret string `mapstructure:"jwt_secret"` + TMDbAPIKey string `mapstructure:"tmdb_api_key"` + TMDbAPIProxy string `mapstructure:"tmdb_api_proxy"` + TMDbImageProxy string `mapstructure:"tmdb_image_proxy"` + BangumiToken string `mapstructure:"bangumi_access_token"` + TheTVDBAPIKey string `mapstructure:"thetvdb_api_key"` + FanartAPIKey string `mapstructure:"fanart_tv_api_key"` + DoubanCookie string `mapstructure:"douban_cookie"` + // 用于加密的密钥,如果为空则使用 JWTSecret + EncryptionKey string `mapstructure:"encryption_key"` +} + +// LoggingConfig 配置 Zap。 +type LoggingConfig struct { + Level string `mapstructure:"level"` + Format string `mapstructure:"format"` + OutputPath string `mapstructure:"output_path"` + EnableRotation bool `mapstructure:"enable_rotation"` + MaxSizeMB int `mapstructure:"max_size_mb"` + MaxAgeDays int `mapstructure:"max_age_days"` + MaxBackups int `mapstructure:"max_backups"` +} + +// CacheConfig 控制磁盘转码/刮削缓存。 +type CacheConfig struct { + CacheDir string `mapstructure:"cache_dir"` + MaxDiskUsageMB int `mapstructure:"max_disk_usage_mb"` + TTLHours int `mapstructure:"ttl_hours"` + AutoCleanup bool `mapstructure:"auto_cleanup"` + CleanupIntervalMin int `mapstructure:"cleanup_interval_min"` + RedisURL string `mapstructure:"redis_url"` + RedisPrefix string `mapstructure:"redis_prefix"` + MediaTTLSeconds int `mapstructure:"media_ttl_seconds"` +} + +type SearchConfig struct { + Backend string `mapstructure:"backend"` + OpenSearchURL string `mapstructure:"opensearch_url"` + Index string `mapstructure:"index"` + Username string `mapstructure:"username"` + Password string `mapstructure:"password"` +} + +// MediaConfig 保存默认库位置(用于引导库)。 +type MediaConfig struct { + MoviesDir string `mapstructure:"movies_dir"` + TVDir string `mapstructure:"tv_dir"` + AnimeDir string `mapstructure:"anime_dir"` +} + +// AIConfig 配置可选的 LLM 提供者。 +type AIConfig struct { + Enabled bool `mapstructure:"enabled"` + Provider string `mapstructure:"provider"` + APIBase string `mapstructure:"api_base"` + APIKey string `mapstructure:"api_key"` + Model string `mapstructure:"model"` + Timeout int `mapstructure:"timeout"` + MaxConcurrent int `mapstructure:"max_concurrent"` +} + +// LicenseConfig configures the optional MediaStationGo license server bridge. +type LicenseConfig struct { + ServerURL string `mapstructure:"server_url"` + HMACSecret string `mapstructure:"hmac_secret"` + PublicKey string `mapstructure:"public_key"` +} + +// OrganizerConfig 配置媒体文件智能分类整理。 +type OrganizerConfig struct { + SmartClassify bool `mapstructure:"smart_classify"` + AutoAfterDownload bool `mapstructure:"auto_after_download"` + Categories map[string]string `mapstructure:"categories"` +} + +// FlareSolverrConfig 配置 FlareSolverr 服务(用于绕过 Cloudflare/WAF)。 +type FlareSolverrConfig struct { + Enabled bool `mapstructure:"enabled"` + URL string `mapstructure:"url"` + Session string `mapstructure:"session"` + Timeout int `mapstructure:"timeout"` +}