package config import ( "fmt" "log/slog" "os" "path/filepath" "strconv" "strings" ) type Config struct { Port int DataDir string DBPath string LibraryDir string TempDir string BinDir string YTDLPPath string YTDLPManaged bool DenoPath string DenoManaged bool // UpdateExternalTools lets VidArchive run the updater of a tool it did not // install. It is the escape hatch for a platform with no mapped release // build: install the binary once by hand, then manage it from here. UpdateExternalTools bool FFmpegPath string FFprobePath string BaseURL string Workers int SchedulerInterval int LogLevel string LogFormat string Username string PasswordHash string } func New() *Config { dataDir := getEnv("VIDARCHIVE_DATA_DIR", "./data") binDir := getEnv("VIDARCHIVE_BIN_DIR", filepath.Join(dataDir, "bin")) ytdlpPath, ytdlpManaged := managedPath("VIDARCHIVE_YTDLP_PATH", binDir, "yt-dlp") denoPath, denoManaged := managedPath("VIDARCHIVE_DENO_PATH", binDir, "deno") return &Config{ Port: getEnvInt("VIDARCHIVE_PORT", 8080, 1), DataDir: dataDir, DBPath: getEnv("VIDARCHIVE_DB_PATH", filepath.Join(dataDir, "vidarchive.db")), LibraryDir: getEnv("VIDARCHIVE_LIBRARY_DIR", filepath.Join(dataDir, "library")), TempDir: getEnv("VIDARCHIVE_TEMP_DIR", filepath.Join(dataDir, "temp")), BinDir: binDir, YTDLPPath: ytdlpPath, YTDLPManaged: ytdlpManaged, DenoPath: denoPath, DenoManaged: denoManaged, UpdateExternalTools: getEnvBool("VIDARCHIVE_UPDATE_EXTERNAL_TOOLS"), FFmpegPath: getEnv("VIDARCHIVE_FFMPEG_PATH", "ffmpeg"), FFprobePath: getEnv("VIDARCHIVE_FFPROBE_PATH", "ffprobe"), BaseURL: getEnv("VIDARCHIVE_BASE_URL", ""), Workers: getEnvInt("VIDARCHIVE_WORKERS", 2, 1), SchedulerInterval: getEnvInt("VIDARCHIVE_SCHEDULER_INTERVAL", 60, 1), LogLevel: getEnv("VIDARCHIVE_LOG_LEVEL", "info"), LogFormat: getEnv("VIDARCHIVE_LOG_FORMAT", "text"), Username: os.Getenv("VIDARCHIVE_USERNAME"), PasswordHash: os.Getenv("VIDARCHIVE_PASSWORD_HASH"), } } // SetupLogging also redirects the stdlib log package, so output from // dependencies is formatted and levelled the same way. func (c *Config) SetupLogging() { var level slog.Level if err := level.UnmarshalText([]byte(c.LogLevel)); err != nil { fmt.Fprintf(os.Stderr, "invalid VIDARCHIVE_LOG_LEVEL %q: %v, using info\n", c.LogLevel, err) level = slog.LevelInfo } opts := &slog.HandlerOptions{Level: level} var h slog.Handler = slog.NewTextHandler(os.Stderr, opts) if strings.EqualFold(c.LogFormat, "json") { h = slog.NewJSONHandler(os.Stderr, opts) } slog.SetDefault(slog.New(h)) } func (c *Config) IsHTTPS() bool { return strings.HasPrefix(c.BaseURL, "https://") } // VidArchive installs only what it owns, but it updates anything the operator // lets it update. func (c *Config) CanUpdateYTDLP() bool { return c.YTDLPManaged || c.UpdateExternalTools } func (c *Config) CanUpdateDeno() bool { return c.DenoManaged || c.UpdateExternalTools } // managedPath decides who owns a tool's binary. An explicit VIDARCHIVE_*_PATH // means the operator does, so VidArchive never installs over it. func managedPath(key, binDir, name string) (string, bool) { if v := os.Getenv(key); v != "" { return v, false } return filepath.Join(binDir, name), true } func getEnv(key, defaultVal string) string { if v := os.Getenv(key); v != "" { return v } return defaultVal } func getEnvBool(key string) bool { v, _ := strconv.ParseBool(os.Getenv(key)) return v } // getEnvInt falls back to the default below min: a zero or negative worker // count, tick interval or port stalls downloads, spins the scheduler or fails to // bind. func getEnvInt(key string, defaultVal, min int) int { v := os.Getenv(key) if v == "" { return defaultVal } i, err := strconv.Atoi(v) if err != nil { fmt.Fprintf(os.Stderr, "invalid %s: %v, using default %d\n", key, err, defaultVal) return defaultVal } if i < min { fmt.Fprintf(os.Stderr, "invalid %s: %d is below the minimum %d, using default %d\n", key, i, min, defaultVal) return defaultVal } return i }