630 lines
20 KiB
Go
630 lines
20 KiB
Go
/*
|
|
* Copyright (c) 2025-2026, Антон Аксенов
|
|
* This file is part of iptvc project
|
|
* MIT License: https://git.axenov.dev/IPTV/iptvc/src/branch/master/LICENSE
|
|
*/
|
|
|
|
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"math/rand"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/joho/godotenv"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// Константы значений по умолчанию для временных параметров.
|
|
// Внешний интерфейс (YAML, env, CLI) задаёт их в секундах,
|
|
// внутреннее представление — в миллисекундах.
|
|
const (
|
|
defaultTimeoutSeconds = 10
|
|
defaultAllCooldownSeconds = 1800 // 30 минут
|
|
defaultOneCooldownSeconds = 2
|
|
defaultChannelsCooldownSeconds = 0
|
|
defaultCacheTtlSeconds = 30
|
|
)
|
|
|
|
// Config описывает конфигурацию
|
|
type Config struct {
|
|
App AppConfig `yaml:"app"`
|
|
Server ServerConfig `yaml:"server"`
|
|
Site SiteConfig `yaml:"site"`
|
|
Check CheckConfig `yaml:"check"`
|
|
Cache CacheConfig `yaml:"cache"`
|
|
}
|
|
|
|
// AppConfig описывает общие настройки приложения
|
|
type AppConfig struct {
|
|
Timezone string `yaml:"timezone"`
|
|
Debug bool `yaml:"debug"`
|
|
LogLevel string `yaml:"log_level"`
|
|
Playlists string `yaml:"playlists"`
|
|
Tags string `yaml:"tags"`
|
|
}
|
|
|
|
// ServerConfig описывает конфигурацию веб-сервера
|
|
type ServerConfig struct {
|
|
Port uint `yaml:"port"`
|
|
Host string `yaml:"host"`
|
|
}
|
|
|
|
// Link описывает ссылку в навигации или подвале сайта.
|
|
// Если заданы Children, рендерится как выпадающее меню.
|
|
type Link struct {
|
|
Title string `yaml:"title"`
|
|
Url string `yaml:"url"`
|
|
Icon string `yaml:"icon"`
|
|
Children []Link `yaml:"children"`
|
|
}
|
|
|
|
// HeaderConfig описывает настройки шапки сайта
|
|
type HeaderConfig struct {
|
|
Title string `yaml:"title"`
|
|
Navigation []Link `yaml:"navigation"`
|
|
}
|
|
|
|
// SiteConfig описывает настройки сайта
|
|
type SiteConfig struct {
|
|
BaseUrl string `yaml:"base-url"`
|
|
RepoUrl string `yaml:"repo-url"`
|
|
PageSize uint `yaml:"page-size"`
|
|
Favicon string `yaml:"favicon"`
|
|
Header HeaderConfig `yaml:"header"`
|
|
FooterLinks []Link `yaml:"footer-links"`
|
|
}
|
|
|
|
// CacheConfig описывает конфигурацию подключения к keydb.
|
|
// Ttl задаётся в секундах.
|
|
type CacheConfig struct {
|
|
Enabled bool `yaml:"enabled"`
|
|
Host string `yaml:"host"`
|
|
Port uint `yaml:"port"`
|
|
Username string `yaml:"username"`
|
|
Password string `yaml:"password"`
|
|
Db uint `yaml:"db"`
|
|
Ttl uint `yaml:"ttl"`
|
|
IsActive bool `yaml:"-"`
|
|
}
|
|
|
|
// CheckConfig описывает параметры проверки плейлистов и каналов
|
|
type CheckConfig struct {
|
|
StartOnServe bool `yaml:"start-on-serve"`
|
|
Playlists CheckPlaylistsConfig `yaml:"playlists"`
|
|
Channels CheckChannelsConfig `yaml:"channels"`
|
|
}
|
|
|
|
// DurationSeconds — тип для временных интервалов, задаваемых в секундах,
|
|
// но хранящихся внутри в миллисекундах.
|
|
type DurationSeconds int
|
|
|
|
// UnmarshalYAML декодирует значение из секунд в миллисекунды.
|
|
func (d *DurationSeconds) UnmarshalYAML(value *yaml.Node) error {
|
|
var v float64
|
|
if err := value.Decode(&v); err != nil {
|
|
return err
|
|
}
|
|
*d = DurationSeconds(v * 1000)
|
|
return nil
|
|
}
|
|
|
|
// CheckPlaylistsConfig — параметры проверки плейлистов.
|
|
// Значения Timeout, AllCooldown, OneCooldown задаются в секундах
|
|
// в YAML/CLI/env, но хранятся в миллисекундах для использования в коде.
|
|
type CheckPlaylistsConfig struct {
|
|
UserAgent UserAgents `yaml:"user-agent"`
|
|
Timeout DurationSeconds `yaml:"timeout"` // ms internally
|
|
AllCooldown IntRange `yaml:"all-cooldown"` // ms internally
|
|
OneCooldown IntRange `yaml:"one-cooldown"` // ms internally
|
|
MaxRoutines int `yaml:"max-routines"` // count
|
|
PerRoutine int `yaml:"per-routine"` // count
|
|
}
|
|
|
|
// CheckChannelsConfig — параметры проверки каналов.
|
|
// Значения Timeout и Cooldown задаются в секундах
|
|
// в YAML/CLI/env, но хранятся в миллисекундах для использования в коде.
|
|
type CheckChannelsConfig struct {
|
|
UserAgent UserAgents `yaml:"user-agent"`
|
|
Timeout DurationSeconds `yaml:"timeout"` // ms internally
|
|
ByteRange int `yaml:"byte-range"` // bytes
|
|
Cooldown IntRange `yaml:"cooldown"` // ms internally
|
|
MaxRoutines int `yaml:"max-routines"` // count
|
|
PerRoutine int `yaml:"per-routine"` // count
|
|
}
|
|
|
|
// IntRange — значение, которое может быть скаляром или [min, max].
|
|
// Если скаляр: Min == Max. Если [min, max]: Min <= Max.
|
|
type IntRange struct {
|
|
Min int
|
|
Max int
|
|
}
|
|
|
|
// UnmarshalYAML реализует кастомный разбор IntRange из YAML.
|
|
// В YAML значение задаётся в секундах, результат сохраняется в миллисекундах.
|
|
// Поддерживает скаляр (int/float) или массив из двух значений [min, max].
|
|
func (r *IntRange) UnmarshalYAML(value *yaml.Node) error {
|
|
decodeNode := func(n *yaml.Node) (int, error) {
|
|
var f float64
|
|
if err := n.Decode(&f); err != nil {
|
|
return 0, err
|
|
}
|
|
return int(f * 1000), nil
|
|
}
|
|
|
|
if value.Kind == yaml.ScalarNode {
|
|
v, err := decodeNode(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.Min = v
|
|
r.Max = v
|
|
return nil
|
|
}
|
|
if value.Kind == yaml.SequenceNode {
|
|
if len(value.Content) == 0 {
|
|
r.Min, r.Max = 0, 0
|
|
return nil
|
|
}
|
|
min, err := decodeNode(value.Content[0])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.Min = min
|
|
if len(value.Content) >= 2 {
|
|
max, err := decodeNode(value.Content[1])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.Max = max
|
|
} else {
|
|
r.Max = min
|
|
}
|
|
return nil
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Value возвращает значение: если Min == Max — константу, иначе случайное в [Min, Max].
|
|
func (r IntRange) Value() int {
|
|
if r.Min == r.Max {
|
|
return r.Min
|
|
}
|
|
return r.Min + rand.Intn(r.Max-r.Min+1)
|
|
}
|
|
|
|
// UserAgents — список User-Agent строк. Может быть задан как скаляр или массив.
|
|
type UserAgents []string
|
|
|
|
// UnmarshalYAML реализует кастомный разбор UserAgents из YAML.
|
|
func (u *UserAgents) UnmarshalYAML(value *yaml.Node) error {
|
|
if value.Kind == yaml.ScalarNode {
|
|
var s string
|
|
if err := value.Decode(&s); err != nil {
|
|
return err
|
|
}
|
|
*u = []string{s}
|
|
return nil
|
|
}
|
|
if value.Kind == yaml.SequenceNode {
|
|
var arr []string
|
|
if err := value.Decode(&arr); err != nil {
|
|
return err
|
|
}
|
|
*u = arr
|
|
return nil
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Pick возвращает случайный User-Agent или пустую строку если список пуст.
|
|
func (u UserAgents) Pick() string {
|
|
if len(u) == 0 {
|
|
return ""
|
|
}
|
|
if len(u) == 1 {
|
|
return u[0]
|
|
}
|
|
return u[rand.Intn(len(u))]
|
|
}
|
|
|
|
// defaults возвращает конфигурацию со значениями по умолчанию.
|
|
// Временные параметры хранятся в миллисекундах.
|
|
func defaults() *Config {
|
|
return &Config{
|
|
App: AppConfig{
|
|
Timezone: "GMT",
|
|
Debug: false,
|
|
LogLevel: "info",
|
|
Playlists: "./playlists.ini",
|
|
Tags: "./channels.json",
|
|
},
|
|
Server: ServerConfig{
|
|
Port: 8800,
|
|
Host: "",
|
|
},
|
|
Site: SiteConfig{
|
|
BaseUrl: "http://localhost:8800",
|
|
PageSize: 0,
|
|
RepoUrl: "https://git.axenov.dev/IPTV",
|
|
Header: HeaderConfig{
|
|
Title: "IPTV Checker",
|
|
Navigation: []Link{
|
|
{Title: "Помощь", Icon: "help-circle-outline", Children: []Link{
|
|
{Title: "Документация", Url: "https://m3u.su/docs", Icon: "document-text-outline"},
|
|
{Title: "Исходники", Url: "https://git.axenov.dev/IPTV", Icon: "code-slash-outline"},
|
|
{Title: "@iptv_aggregator", Url: "https://t.me/iptv_aggregator", Icon: "paper-plane-outline"},
|
|
}},
|
|
},
|
|
},
|
|
FooterLinks: []Link{
|
|
{Title: "Исходники", Url: "https://git.axenov.dev/IPTV", Icon: "code-slash-outline"},
|
|
{Title: "axenov.dev", Url: "https://axenov.dev", Icon: "person-outline"},
|
|
{Title: "@iptv_aggregator", Url: "https://t.me/iptv_aggregator", Icon: "megaphone-outline"},
|
|
},
|
|
},
|
|
Check: CheckConfig{
|
|
StartOnServe: false,
|
|
Playlists: CheckPlaylistsConfig{
|
|
UserAgent: UserAgents{"Mozilla/5.0 WINK/1.31.1 (AndroidTV/9) HlsWinkPlayer"},
|
|
Timeout: defaultTimeoutSeconds * 1000,
|
|
AllCooldown: IntRange{Min: defaultAllCooldownSeconds * 1000, Max: defaultAllCooldownSeconds * 1000},
|
|
OneCooldown: IntRange{Min: defaultOneCooldownSeconds * 1000, Max: defaultOneCooldownSeconds * 1000},
|
|
MaxRoutines: 1,
|
|
PerRoutine: 1,
|
|
},
|
|
Channels: CheckChannelsConfig{
|
|
UserAgent: UserAgents{"Mozilla/5.0 WINK/1.31.1 (AndroidTV/9) HlsWinkPlayer"},
|
|
Timeout: defaultTimeoutSeconds * 1000,
|
|
ByteRange: 512,
|
|
Cooldown: IntRange{Min: defaultChannelsCooldownSeconds * 1000, Max: defaultChannelsCooldownSeconds * 1000},
|
|
MaxRoutines: 50,
|
|
PerRoutine: 10,
|
|
},
|
|
},
|
|
Cache: CacheConfig{
|
|
Enabled: false,
|
|
Host: "localhost",
|
|
Port: 6379,
|
|
Username: "",
|
|
Password: "",
|
|
Db: 0,
|
|
Ttl: defaultCacheTtlSeconds,
|
|
},
|
|
}
|
|
}
|
|
|
|
// Init инициализирует объект конфигурации.
|
|
// Порядок приоритета (от низшего к высшему):
|
|
// 1. Значения по умолчанию
|
|
// 2. config.yml (путь можно задать через configPath)
|
|
// 3. Переменные окружения (.env загружается автоматически)
|
|
func Init(configPath string) *Config {
|
|
_ = godotenv.Load(".env")
|
|
|
|
cfg := defaults()
|
|
|
|
// загружаем config.yml
|
|
if configPath == "" {
|
|
configPath = "config.yml"
|
|
}
|
|
if data, err := os.ReadFile(configPath); err == nil {
|
|
if err := yaml.Unmarshal(data, cfg); err != nil {
|
|
log.Printf("Warning: cannot parse %s: %s", configPath, err)
|
|
}
|
|
} else if !os.IsNotExist(err) {
|
|
log.Printf("Warning: cannot read %s: %s", configPath, err)
|
|
}
|
|
|
|
// переопределяем значениями из переменных окружения
|
|
applyEnv(cfg)
|
|
|
|
// валидируем и исправляем
|
|
validate(cfg)
|
|
|
|
return cfg
|
|
}
|
|
|
|
// applyEnv переопределяет поля конфигурации значениями из переменных окружения.
|
|
func applyEnv(cfg *Config) {
|
|
// app
|
|
if v, ok := os.LookupEnv("APP_DEBUG"); ok {
|
|
cfg.App.Debug = parseBool(v, cfg.App.Debug)
|
|
}
|
|
if v, ok := os.LookupEnv("APP_LOG_LEVEL"); ok && v != "" {
|
|
cfg.App.LogLevel = v
|
|
}
|
|
if v, ok := os.LookupEnv("APP_TIMEZONE"); ok && v != "" {
|
|
cfg.App.Timezone = v
|
|
}
|
|
if v, ok := os.LookupEnv("APP_PLAYLISTS"); ok && v != "" {
|
|
cfg.App.Playlists = v
|
|
}
|
|
if v, ok := os.LookupEnv("APP_TAGS"); ok && v != "" {
|
|
cfg.App.Tags = v
|
|
}
|
|
|
|
// server
|
|
if v, ok := os.LookupEnv("SERVER_PORT"); ok && v != "" {
|
|
cfg.Server.Port = parseUint(v, cfg.Server.Port)
|
|
}
|
|
if v, ok := os.LookupEnv("SERVER_HOST"); ok {
|
|
cfg.Server.Host = v
|
|
}
|
|
|
|
// site
|
|
if v, ok := os.LookupEnv("APP_URL"); ok && v != "" {
|
|
cfg.Site.BaseUrl = v
|
|
}
|
|
if v, ok := os.LookupEnv("PAGE_SIZE"); ok && v != "" {
|
|
cfg.Site.PageSize = parseUint(v, cfg.Site.PageSize)
|
|
}
|
|
if v, ok := os.LookupEnv("REPO_URL"); ok && v != "" {
|
|
cfg.Site.RepoUrl = v
|
|
}
|
|
if v, ok := os.LookupEnv("SITE_FAVICON"); ok {
|
|
cfg.Site.Favicon = v
|
|
}
|
|
if v, ok := os.LookupEnv("APP_TITLE"); ok && v != "" {
|
|
cfg.Site.Header.Title = v
|
|
}
|
|
|
|
// check
|
|
if v, ok := os.LookupEnv("CHECK_START_ON_SERVE"); ok {
|
|
cfg.Check.StartOnServe = parseBool(v, cfg.Check.StartOnServe)
|
|
}
|
|
|
|
// check.playlists
|
|
pp := &cfg.Check.Playlists
|
|
if v, ok := os.LookupEnv("CHECK_PLAYLISTS_TIMEOUT"); ok && v != "" {
|
|
pp.Timeout = DurationSeconds(parseSecondsToMillis(v, int(pp.Timeout)))
|
|
}
|
|
applyEnvIntRange(&pp.AllCooldown, "CHECK_PLAYLISTS_ALL_COOLDOWN")
|
|
applyEnvIntRange(&pp.OneCooldown, "CHECK_PLAYLISTS_ONE_COOLDOWN")
|
|
if v, ok := os.LookupEnv("CHECK_PLAYLISTS_MAX_ROUTINES"); ok && v != "" {
|
|
pp.MaxRoutines = parseInt(v, pp.MaxRoutines)
|
|
}
|
|
if v, ok := os.LookupEnv("CHECK_PLAYLISTS_PER_ROUTINE"); ok && v != "" {
|
|
pp.PerRoutine = parseInt(v, pp.PerRoutine)
|
|
}
|
|
if uas := applyEnvStringSlice("CHECK_PLAYLISTS_USER_AGENT"); uas != nil {
|
|
pp.UserAgent = UserAgents(uas)
|
|
}
|
|
|
|
// check.channels
|
|
cc := &cfg.Check.Channels
|
|
if v, ok := os.LookupEnv("CHECK_CHANNELS_TIMEOUT"); ok && v != "" {
|
|
cc.Timeout = DurationSeconds(parseSecondsToMillis(v, int(cc.Timeout)))
|
|
}
|
|
if v, ok := os.LookupEnv("CHECK_CHANNELS_BYTE_RANGE"); ok && v != "" {
|
|
cc.ByteRange = parseInt(v, cc.ByteRange)
|
|
}
|
|
applyEnvIntRange(&cc.Cooldown, "CHECK_CHANNELS_COOLDOWN")
|
|
if v, ok := os.LookupEnv("CHECK_CHANNELS_MAX_ROUTINES"); ok && v != "" {
|
|
cc.MaxRoutines = parseInt(v, cc.MaxRoutines)
|
|
}
|
|
if v, ok := os.LookupEnv("CHECK_CHANNELS_PER_ROUTINE"); ok && v != "" {
|
|
cc.PerRoutine = parseInt(v, cc.PerRoutine)
|
|
}
|
|
if uas := applyEnvStringSlice("CHECK_CHANNELS_USER_AGENT"); uas != nil {
|
|
cc.UserAgent = UserAgents(uas)
|
|
}
|
|
|
|
// cache
|
|
if v, ok := os.LookupEnv("CACHE_ENABLED"); ok {
|
|
cfg.Cache.Enabled = parseBool(v, cfg.Cache.Enabled)
|
|
}
|
|
if v, ok := os.LookupEnv("CACHE_HOST"); ok && v != "" {
|
|
cfg.Cache.Host = v
|
|
}
|
|
if v, ok := os.LookupEnv("CACHE_PORT"); ok && v != "" {
|
|
cfg.Cache.Port = parseUint(v, cfg.Cache.Port)
|
|
}
|
|
if v, ok := os.LookupEnv("CACHE_USERNAME"); ok {
|
|
cfg.Cache.Username = v
|
|
}
|
|
if v, ok := os.LookupEnv("CACHE_PASSWORD"); ok {
|
|
cfg.Cache.Password = v
|
|
}
|
|
if v, ok := os.LookupEnv("CACHE_DB"); ok && v != "" {
|
|
cfg.Cache.Db = parseUint(v, cfg.Cache.Db)
|
|
}
|
|
if v, ok := os.LookupEnv("CACHE_TTL"); ok && v != "" {
|
|
cfg.Cache.Ttl = parseUint(v, cfg.Cache.Ttl)
|
|
}
|
|
}
|
|
|
|
// parseBool разбирает строку в bool, при ошибке возвращает значение по умолчанию
|
|
func parseBool(s string, defaultValue bool) bool {
|
|
val, err := strconv.ParseBool(s)
|
|
if err == nil {
|
|
return val
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
// parseUint разбирает строку в uint, при ошибке возвращает значение по умолчанию
|
|
func parseUint(s string, defaultValue uint) uint {
|
|
val, err := strconv.Atoi(s)
|
|
if err == nil {
|
|
return uint(val)
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
// parseInt разбирает строку в int, при ошибке возвращает значение по умолчанию
|
|
func parseInt(s string, defaultValue int) int {
|
|
val, err := strconv.Atoi(s)
|
|
if err == nil {
|
|
return val
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
// parseSecondsToMillis разбирает строку с секундами и возвращает миллисекунды.
|
|
// При ошибке возвращает значение по умолчанию.
|
|
func parseSecondsToMillis(s string, defaultValue int) int {
|
|
val, err := strconv.ParseFloat(s, 64)
|
|
if err == nil && val >= 0 {
|
|
return int(val * 1000)
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
// applyEnvIntRange читает одно env-значение (в секундах) для IntRange,
|
|
// умножая результат на 1000, так как внутри значения хранятся в миллисекундах.
|
|
// Одно число задаёт фиксированное значение, два числа через запятую — диапазон.
|
|
func applyEnvIntRange(r *IntRange, name string) {
|
|
value, ok := os.LookupEnv(name)
|
|
if !ok || strings.TrimSpace(value) == "" {
|
|
return
|
|
}
|
|
|
|
parse := func(s string) (int, bool) {
|
|
s = strings.TrimSpace(s)
|
|
v, err := strconv.ParseFloat(s, 64)
|
|
if err != nil || v < 0 {
|
|
return 0, false
|
|
}
|
|
return int(v * 1000), true
|
|
}
|
|
|
|
parts := strings.Split(value, ",")
|
|
if len(parts) == 1 {
|
|
n, ok := parse(parts[0])
|
|
if !ok {
|
|
return
|
|
}
|
|
r.Min = n
|
|
r.Max = n
|
|
return
|
|
}
|
|
if len(parts) != 2 {
|
|
return
|
|
}
|
|
|
|
min, minOk := parse(parts[0])
|
|
max, maxOk := parse(parts[1])
|
|
if !minOk || !maxOk {
|
|
return
|
|
}
|
|
r.Min = min
|
|
r.Max = max
|
|
}
|
|
|
|
// applyEnvStringSlice читает индексированные env-переменные PREFIX_1, PREFIX_2, ...
|
|
// Возвращает nil, если ни одна не задана.
|
|
func applyEnvStringSlice(prefix string) []string {
|
|
var result []string
|
|
for i := 1; ; i++ {
|
|
key := fmt.Sprintf("%s_%d", prefix, i)
|
|
v, ok := os.LookupEnv(key)
|
|
if !ok {
|
|
break
|
|
}
|
|
if v != "" {
|
|
result = append(result, v)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// validate проверяет значения конфигурации и исправляет некорректные,
|
|
// логируя предупреждения для каждого исправления.
|
|
func validate(cfg *Config) {
|
|
// server
|
|
if cfg.Server.Port == 0 || cfg.Server.Port > 65535 {
|
|
log.Println("Config: server.port is not in the 0..65535 range, using default 8800")
|
|
cfg.Server.Port = 8800
|
|
}
|
|
if cfg.Site.BaseUrl == "" {
|
|
log.Println("Config: site.base-url is empty, using default")
|
|
cfg.Site.BaseUrl = fmt.Sprintf("http://localhost:%d", cfg.Server.Port)
|
|
}
|
|
|
|
// cache
|
|
if cfg.Cache.Enabled {
|
|
if cfg.Cache.Host == "" {
|
|
log.Println("Config: cache.host is empty, using default 'localhost'")
|
|
cfg.Cache.Host = "localhost"
|
|
}
|
|
if cfg.Cache.Port == 0 {
|
|
log.Println("Config: cache.port is 0, using default 6379")
|
|
cfg.Cache.Port = 6379
|
|
}
|
|
if cfg.Cache.Ttl == 0 {
|
|
log.Printf("Config: cache.ttl is 0, using default %d s", defaultCacheTtlSeconds)
|
|
cfg.Cache.Ttl = defaultCacheTtlSeconds
|
|
}
|
|
}
|
|
|
|
// check.playlists
|
|
pp := &cfg.Check.Playlists
|
|
if pp.Timeout <= 0 {
|
|
log.Printf("Config: check.playlists.timeout <= 0, using default %d s", defaultTimeoutSeconds)
|
|
pp.Timeout = defaultTimeoutSeconds * 1000
|
|
}
|
|
validateIntRange(&pp.AllCooldown, "check.playlists.all-cooldown", 0, 3600000)
|
|
validateIntRange(&pp.OneCooldown, "check.playlists.one-cooldown", 0, 3600000)
|
|
if pp.MaxRoutines < 1 {
|
|
log.Println("Config: check.playlists.max-routines < 1, using default 5")
|
|
pp.MaxRoutines = 5
|
|
}
|
|
if pp.PerRoutine < 1 {
|
|
log.Println("Config: check.playlists.per-routine < 1, using default 1")
|
|
pp.PerRoutine = 1
|
|
}
|
|
if len(pp.UserAgent) == 0 {
|
|
log.Println("Config: check.playlists.user-agent is empty, using default")
|
|
pp.UserAgent = UserAgents{"Mozilla/5.0 WINK/1.31.1 (AndroidTV/9) HlsWinkPlayer"}
|
|
}
|
|
|
|
// check.channels
|
|
cc := &cfg.Check.Channels
|
|
if cc.Timeout <= 0 {
|
|
log.Printf("Config: check.channels.timeout <= 0, using default %d s", defaultTimeoutSeconds)
|
|
cc.Timeout = defaultTimeoutSeconds * 1000
|
|
}
|
|
validateIntRange(&cc.Cooldown, "check.channels.cooldown", 0, 3600000)
|
|
if cc.MaxRoutines < 1 {
|
|
log.Println("Config: check.channels.max-routines < 1, using default 50")
|
|
cc.MaxRoutines = 50
|
|
}
|
|
if cc.PerRoutine < 1 {
|
|
log.Println("Config: check.channels.per-routine < 1, using default 10")
|
|
cc.PerRoutine = 10
|
|
}
|
|
if cc.ByteRange <= 0 {
|
|
log.Println("Config: check.channels.byte-range <= 0, using default 512")
|
|
cc.ByteRange = 512
|
|
}
|
|
if len(cc.UserAgent) == 0 {
|
|
log.Println("Config: check.channels.user-agent is empty, using default")
|
|
cc.UserAgent = UserAgents{"Mozilla/5.0 WINK/1.31.1 (AndroidTV/9) HlsWinkPlayer"}
|
|
}
|
|
}
|
|
|
|
// validateIntRange проверяет IntRange: Min <= Max, оба в пределах [minVal, maxVal].
|
|
// Некорректные значения исправляются с логированием предупреждения.
|
|
func validateIntRange(r *IntRange, name string, minVal, maxVal int) {
|
|
if r.Min > r.Max {
|
|
log.Printf("Config: %s min=%d > max=%d, swapping", name, r.Min, r.Max)
|
|
r.Min, r.Max = r.Max, r.Min
|
|
}
|
|
if r.Min < minVal {
|
|
log.Printf("Config: %s min=%d < %d, clamped", name, r.Min, minVal)
|
|
r.Min = minVal
|
|
}
|
|
if r.Max > maxVal {
|
|
log.Printf("Config: %s max=%d > %d, clamped", name, r.Max, maxVal)
|
|
r.Max = maxVal
|
|
}
|
|
if r.Min > r.Max {
|
|
r.Min = minVal
|
|
r.Max = minVal
|
|
}
|
|
}
|