244 lines
No EOL
6.8 KiB
Go
244 lines
No EOL
6.8 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/joho/godotenv"
|
|
"log/slog"
|
|
)
|
|
|
|
const (
|
|
// Environment variable prefix for the application
|
|
envPrefix = "JUKEBOX_"
|
|
)
|
|
|
|
// Config holds all the configuration for the application
|
|
type Config struct {
|
|
// Discord configuration
|
|
DiscordToken string
|
|
DiscordGuildID string
|
|
DiscordChannelID string
|
|
|
|
// Subsonic configuration
|
|
SubsonicServer string
|
|
SubsonicUsername string
|
|
SubsonicPassword string
|
|
SubsonicVersion string
|
|
|
|
// Jukebox configuration
|
|
AudioVolume float64
|
|
TimeoutSec int
|
|
|
|
// Web server configuration
|
|
WebEnabled bool
|
|
WebAddr string
|
|
}
|
|
|
|
// Load loads the configuration from environment variables and .env file
|
|
func Load() (*Config, error) {
|
|
// Try to load .env file
|
|
if err := godotenv.Load(); err != nil {
|
|
slog.Info("No .env file found, using environment variables only")
|
|
} else {
|
|
slog.Info("Loaded environment variables from .env file")
|
|
}
|
|
|
|
// Load the raw values first
|
|
rawGuildID := getEnv(envPrefix+"DISCORD_GUILD_ID", "")
|
|
|
|
// Clean up Guild ID - ensure we have just the snowflake
|
|
cleanGuildID := cleanSnowflake(rawGuildID)
|
|
if cleanGuildID != rawGuildID && rawGuildID != "" {
|
|
slog.Warn("Cleaned Discord Guild ID", "original", rawGuildID, "cleaned", cleanGuildID)
|
|
}
|
|
|
|
config := &Config{
|
|
// Discord
|
|
DiscordToken: getEnv(envPrefix+"DISCORD_TOKEN", ""),
|
|
DiscordGuildID: cleanGuildID,
|
|
DiscordChannelID: getEnv(envPrefix+"DISCORD_CHANNEL_ID", ""),
|
|
|
|
// Subsonic
|
|
SubsonicServer: getEnv(envPrefix+"SUBSONIC_SERVER", ""),
|
|
SubsonicUsername: getEnv(envPrefix+"SUBSONIC_USERNAME", ""),
|
|
SubsonicPassword: getEnv(envPrefix+"SUBSONIC_PASSWORD", ""),
|
|
SubsonicVersion: getEnv(envPrefix+"SUBSONIC_VERSION", "1.16.1"),
|
|
|
|
// Jukebox
|
|
AudioVolume: getEnvFloat(envPrefix+"AUDIO_VOLUME", 0.5),
|
|
TimeoutSec: getEnvInt(envPrefix+"TIMEOUT_SEC", 30),
|
|
|
|
// Web server
|
|
WebEnabled: getEnvBool(envPrefix+"WEB_ENABLED", true),
|
|
WebAddr: getEnv(envPrefix+"WEB_ADDR", ":8080"),
|
|
}
|
|
|
|
if err := config.validate(); err != nil {
|
|
// Print helpful debug information
|
|
PrintDebugInfo()
|
|
return nil, err
|
|
}
|
|
|
|
return config, nil
|
|
}
|
|
|
|
// validate checks if the required configuration values are set
|
|
func (c *Config) validate() error {
|
|
if c.DiscordToken == "" {
|
|
return errorMissingConfig("discord token")
|
|
}
|
|
|
|
if c.SubsonicServer == "" {
|
|
return errorMissingConfig("subsonic server")
|
|
}
|
|
|
|
if c.SubsonicUsername == "" {
|
|
return errorMissingConfig("subsonic username")
|
|
}
|
|
|
|
if c.SubsonicPassword == "" {
|
|
return errorMissingConfig("subsonic password")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// errorMissingConfig returns a formatted error for missing configuration
|
|
func errorMissingConfig(name string) error {
|
|
return fmt.Errorf("%s is required", name)
|
|
}
|
|
|
|
// GetTimeout returns the timeout duration
|
|
func (c *Config) GetTimeout() time.Duration {
|
|
return time.Duration(c.TimeoutSec) * time.Second
|
|
}
|
|
|
|
// getEnv gets an environment variable or returns a default value
|
|
func getEnv(key, defaultValue string) string {
|
|
if value, exists := os.LookupEnv(key); exists {
|
|
return value
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
// getEnvInt gets an environment variable as an integer or returns a default value
|
|
func getEnvInt(key string, defaultValue int) int {
|
|
if value, exists := os.LookupEnv(key); exists {
|
|
if intValue, err := strconv.Atoi(value); err == nil {
|
|
return intValue
|
|
}
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
// getEnvFloat gets an environment variable as a float or returns a default value
|
|
func getEnvFloat(key string, defaultValue float64) float64 {
|
|
if value, exists := os.LookupEnv(key); exists {
|
|
if floatValue, err := strconv.ParseFloat(value, 64); err == nil {
|
|
return floatValue
|
|
}
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
// getEnvBool gets an environment variable as a boolean or returns a default value
|
|
func getEnvBool(key string, defaultValue bool) bool {
|
|
if value, exists := os.LookupEnv(key); exists {
|
|
if strings.ToLower(value) == "true" || value == "1" {
|
|
return true
|
|
} else if strings.ToLower(value) == "false" || value == "0" {
|
|
return false
|
|
}
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
// PrintDebugInfo prints helpful debugging information for environment variables
|
|
func PrintDebugInfo() {
|
|
slog.Info("=== Configuration Debug Information ===")
|
|
slog.Info("Required environment variables:")
|
|
checkEnvVar(envPrefix + "DISCORD_TOKEN")
|
|
checkEnvVar(envPrefix + "DISCORD_GUILD_ID")
|
|
checkEnvVar(envPrefix + "SUBSONIC_SERVER")
|
|
checkEnvVar(envPrefix + "SUBSONIC_USERNAME")
|
|
checkEnvVar(envPrefix + "SUBSONIC_PASSWORD")
|
|
|
|
slog.Info("Optional environment variables:")
|
|
checkEnvVar(envPrefix + "DISCORD_CHANNEL_ID")
|
|
checkEnvVar(envPrefix + "SUBSONIC_VERSION")
|
|
checkEnvVar(envPrefix + "AUDIO_VOLUME")
|
|
checkEnvVar(envPrefix + "TIMEOUT_SEC")
|
|
checkEnvVar(envPrefix + "WEB_ENABLED")
|
|
checkEnvVar(envPrefix + "WEB_ADDR")
|
|
|
|
slog.Info("Troubleshooting tips:")
|
|
slog.Info("1. Your .env file is in the correct directory")
|
|
slog.Info("2. All variable names have the JUKEBOX_ prefix")
|
|
slog.Info("3. There are no spaces around the = sign in your .env file")
|
|
slog.Info(" Example: JUKEBOX_DISCORD_TOKEN=your_token_here")
|
|
slog.Info("4. Your .env file has been loaded (check for errors above)")
|
|
slog.Info("===================================")
|
|
}
|
|
|
|
// checkEnvVar checks if an environment variable is set and prints its status
|
|
func checkEnvVar(name string) {
|
|
if value, exists := os.LookupEnv(name); exists {
|
|
// Mask sensitive values
|
|
displayValue := value
|
|
if contains(name, "TOKEN", "PASSWORD") {
|
|
if len(displayValue) > 8 {
|
|
displayValue = displayValue[:4] + "..." + displayValue[len(displayValue)-4:]
|
|
} else {
|
|
displayValue = "****"
|
|
}
|
|
}
|
|
|
|
// Special handling for Guild ID to detect common issues
|
|
if strings.Contains(name, "GUILD_ID") && strings.Contains(value, "=") {
|
|
slog.Warn("Environment variable contains equals sign",
|
|
"name", name,
|
|
"raw_value", displayValue,
|
|
"cleaned_value", cleanSnowflake(value))
|
|
} else {
|
|
slog.Info("Environment variable check", "name", name, "value", displayValue, "status", "set")
|
|
}
|
|
} else {
|
|
slog.Warn("Environment variable not set", "name", name)
|
|
}
|
|
}
|
|
|
|
// contains checks if the string contains any of the substrings
|
|
func contains(str string, substrings ...string) bool {
|
|
for _, substring := range substrings {
|
|
if strings.Contains(str, substring) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// cleanSnowflake cleans a Discord snowflake (ID) from potential contamination
|
|
// This handles cases where the ID might include the variable name or other text
|
|
func cleanSnowflake(input string) string {
|
|
// If the input contains an equals sign, it might be contaminated
|
|
if strings.Contains(input, "=") {
|
|
parts := strings.SplitN(input, "=", 2)
|
|
if len(parts) == 2 {
|
|
input = parts[1] // Take the part after the equals sign
|
|
}
|
|
}
|
|
|
|
// Extract only digits
|
|
var result strings.Builder
|
|
for _, char := range input {
|
|
if char >= '0' && char <= '9' {
|
|
result.WriteRune(char)
|
|
}
|
|
}
|
|
|
|
return result.String()
|
|
} |