mirror of
https://github.com/gabehf/Koito.git
synced 2026-03-14 18:05:55 -07:00
* add dev branch container to workflow * correctly set the default range of ActivityGrid * fix: set name/short_name to koito (#61) * fix dev container push workflow * fix: race condition with using getComputedStyle primary color for dynamic activity grid darkening (#76) * Fix race condition with using getComputedStyle primary color for dynamic activity grid darkening Instead just use the color from the current theme directly. Tested works on initial load and theme changes. Fixes https://github.com/gabehf/Koito/issues/75 * Rework theme provider to provide the actual Theme object throughtout the app, in addition to the name Split name out of the Theme struct to simplify custom theme saving/reading * fix: set first artist listed as primary by default (#81) * feat: add server-side configuration with default theme (#90) * docs: add example for usage of the main listenbrainz instance (#71) * docs: add example for usage of the main listenbrainz instance * Update scrobbler.md --------- Co-authored-by: Gabe Farrell <90876006+gabehf@users.noreply.github.com> * feat: add server-side cfg and default theme * fix: repair custom theme --------- Co-authored-by: m0d3rnX <jesper@posteo.de> * docs: add default theme cfg option to docs * feat: add ability to manually scrobble track (#91) * feat: add button to manually scrobble from ui * fix: ensure timestamp is in the past, log fix * test: add integration test * feat: add first listened to dates for media items (#92) * fix: ensure error checks for ErrNoRows * feat: add now playing endpoint and ui (#93) * wip * feat: add now playing * fix: set default theme when config is not set * feat: fetch images from subsonic server (#94) * fix: useQuery instead of useEffect for now playing * feat: custom artist separator regex (#95) * Fix race condition with using getComputedStyle primary color for dynamic activity grid darkening Instead just use the color from the current theme directly. Tested works on initial load and theme changes. Fixes https://github.com/gabehf/Koito/issues/75 * Rework theme provider to provide the actual Theme object throughtout the app, in addition to the name Split name out of the Theme struct to simplify custom theme saving/reading * feat: add server-side configuration with default theme (#90) * docs: add example for usage of the main listenbrainz instance (#71) * docs: add example for usage of the main listenbrainz instance * Update scrobbler.md --------- Co-authored-by: Gabe Farrell <90876006+gabehf@users.noreply.github.com> * feat: add server-side cfg and default theme * fix: repair custom theme --------- Co-authored-by: m0d3rnX <jesper@posteo.de> * fix: rebase errors --------- Co-authored-by: pet <128837728+againstpetra@users.noreply.github.com> Co-authored-by: mlandry <mike.landry@gmail.com> Co-authored-by: m0d3rnX <jesper@posteo.de>
411 lines
10 KiB
Go
411 lines
10 KiB
Go
package cfg
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
// defaultBaseUrl = "http://127.0.0.1"
|
|
defaultListenPort = 4110
|
|
defaultMusicBrainzUrl = "https://musicbrainz.org"
|
|
)
|
|
|
|
const (
|
|
// BASE_URL_ENV = "KOITO_BASE_URL"
|
|
DATABASE_URL_ENV = "KOITO_DATABASE_URL"
|
|
BIND_ADDR_ENV = "KOITO_BIND_ADDR"
|
|
LISTEN_PORT_ENV = "KOITO_LISTEN_PORT"
|
|
ENABLE_STRUCTURED_LOGGING_ENV = "KOITO_ENABLE_STRUCTURED_LOGGING"
|
|
ENABLE_FULL_IMAGE_CACHE_ENV = "KOITO_ENABLE_FULL_IMAGE_CACHE"
|
|
LOG_LEVEL_ENV = "KOITO_LOG_LEVEL"
|
|
MUSICBRAINZ_URL_ENV = "KOITO_MUSICBRAINZ_URL"
|
|
MUSICBRAINZ_RATE_LIMIT_ENV = "KOITO_MUSICBRAINZ_RATE_LIMIT"
|
|
ENABLE_LBZ_RELAY_ENV = "KOITO_ENABLE_LBZ_RELAY"
|
|
LBZ_RELAY_URL_ENV = "KOITO_LBZ_RELAY_URL"
|
|
LBZ_RELAY_TOKEN_ENV = "KOITO_LBZ_RELAY_TOKEN"
|
|
CONFIG_DIR_ENV = "KOITO_CONFIG_DIR"
|
|
DEFAULT_USERNAME_ENV = "KOITO_DEFAULT_USERNAME"
|
|
DEFAULT_PASSWORD_ENV = "KOITO_DEFAULT_PASSWORD"
|
|
DEFAULT_THEME_ENV = "KOITO_DEFAULT_THEME"
|
|
DISABLE_DEEZER_ENV = "KOITO_DISABLE_DEEZER"
|
|
DISABLE_COVER_ART_ARCHIVE_ENV = "KOITO_DISABLE_COVER_ART_ARCHIVE"
|
|
DISABLE_MUSICBRAINZ_ENV = "KOITO_DISABLE_MUSICBRAINZ"
|
|
SUBSONIC_URL_ENV = "KOITO_SUBSONIC_URL"
|
|
SUBSONIC_PARAMS_ENV = "KOITO_SUBSONIC_PARAMS"
|
|
SKIP_IMPORT_ENV = "KOITO_SKIP_IMPORT"
|
|
ALLOWED_HOSTS_ENV = "KOITO_ALLOWED_HOSTS"
|
|
CORS_ORIGINS_ENV = "KOITO_CORS_ALLOWED_ORIGINS"
|
|
DISABLE_RATE_LIMIT_ENV = "KOITO_DISABLE_RATE_LIMIT"
|
|
THROTTLE_IMPORTS_MS = "KOITO_THROTTLE_IMPORTS_MS"
|
|
IMPORT_BEFORE_UNIX_ENV = "KOITO_IMPORT_BEFORE_UNIX"
|
|
IMPORT_AFTER_UNIX_ENV = "KOITO_IMPORT_AFTER_UNIX"
|
|
FETCH_IMAGES_DURING_IMPORT_ENV = "KOITO_FETCH_IMAGES_DURING_IMPORT"
|
|
ARTIST_SEPARATORS_ENV = "KOITO_ARTIST_SEPARATORS_REGEX"
|
|
)
|
|
|
|
type config struct {
|
|
bindAddr string
|
|
listenPort int
|
|
configDir string
|
|
// baseUrl string
|
|
databaseUrl string
|
|
musicBrainzUrl string
|
|
musicBrainzRateLimit int
|
|
logLevel int
|
|
structuredLogging bool
|
|
enableFullImageCache bool
|
|
lbzRelayEnabled bool
|
|
lbzRelayUrl string
|
|
lbzRelayToken string
|
|
defaultPw string
|
|
defaultUsername string
|
|
defaultTheme string
|
|
disableDeezer bool
|
|
disableCAA bool
|
|
disableMusicBrainz bool
|
|
subsonicUrl string
|
|
subsonicParams string
|
|
subsonicEnabled bool
|
|
skipImport bool
|
|
fetchImageDuringImport bool
|
|
allowedHosts []string
|
|
allowAllHosts bool
|
|
allowedOrigins []string
|
|
disableRateLimit bool
|
|
importThrottleMs int
|
|
userAgent string
|
|
importBefore time.Time
|
|
importAfter time.Time
|
|
artistSeparators []*regexp.Regexp
|
|
}
|
|
|
|
var (
|
|
globalConfig *config
|
|
once sync.Once
|
|
lock sync.RWMutex
|
|
)
|
|
|
|
// Initialize initializes the global configuration using the provided getenv function.
|
|
func Load(getenv func(string) string, version string) error {
|
|
var err error
|
|
once.Do(func() {
|
|
globalConfig, err = loadConfig(getenv, version)
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("cfg.Load: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// loadConfig loads the configuration from environment variables.
|
|
func loadConfig(getenv func(string) string, version string) (*config, error) {
|
|
cfg := new(config)
|
|
|
|
cfg.databaseUrl = getenv(DATABASE_URL_ENV)
|
|
if cfg.databaseUrl == "" {
|
|
return nil, errors.New("loadConfig: required parameter " + DATABASE_URL_ENV + " not provided")
|
|
}
|
|
cfg.bindAddr = getenv(BIND_ADDR_ENV)
|
|
var err error
|
|
cfg.listenPort, err = strconv.Atoi(getenv(LISTEN_PORT_ENV))
|
|
if err != nil {
|
|
cfg.listenPort = defaultListenPort
|
|
}
|
|
cfg.musicBrainzRateLimit, err = strconv.Atoi(getenv(MUSICBRAINZ_RATE_LIMIT_ENV))
|
|
if err != nil {
|
|
cfg.musicBrainzRateLimit = 1
|
|
}
|
|
cfg.musicBrainzUrl = getenv(MUSICBRAINZ_URL_ENV)
|
|
if cfg.musicBrainzUrl == "" {
|
|
cfg.musicBrainzUrl = defaultMusicBrainzUrl
|
|
}
|
|
|
|
if cfg.musicBrainzUrl == defaultMusicBrainzUrl && cfg.musicBrainzRateLimit != 1 {
|
|
return nil, fmt.Errorf("loadConfig: invalid configuration: %s cannot be altered when %s is default", MUSICBRAINZ_RATE_LIMIT_ENV, MUSICBRAINZ_URL_ENV)
|
|
}
|
|
|
|
if parseBool(getenv(ENABLE_LBZ_RELAY_ENV)) {
|
|
cfg.lbzRelayEnabled = true
|
|
cfg.lbzRelayToken = getenv(LBZ_RELAY_TOKEN_ENV)
|
|
cfg.lbzRelayUrl = getenv(LBZ_RELAY_URL_ENV)
|
|
}
|
|
|
|
beforeutx, _ := strconv.ParseInt(getenv(IMPORT_BEFORE_UNIX_ENV), 10, 64)
|
|
afterutx, _ := strconv.ParseInt(getenv(IMPORT_AFTER_UNIX_ENV), 10, 64)
|
|
|
|
if beforeutx > 0 {
|
|
cfg.importBefore = time.Unix(beforeutx, 0)
|
|
}
|
|
if afterutx > 0 {
|
|
cfg.importAfter = time.Unix(afterutx, 0)
|
|
}
|
|
|
|
cfg.importThrottleMs, _ = strconv.Atoi(getenv(THROTTLE_IMPORTS_MS))
|
|
|
|
cfg.disableRateLimit = parseBool(getenv(DISABLE_RATE_LIMIT_ENV))
|
|
|
|
cfg.structuredLogging = parseBool(getenv(ENABLE_STRUCTURED_LOGGING_ENV))
|
|
cfg.fetchImageDuringImport = parseBool(getenv(FETCH_IMAGES_DURING_IMPORT_ENV))
|
|
|
|
cfg.enableFullImageCache = parseBool(getenv(ENABLE_FULL_IMAGE_CACHE_ENV))
|
|
cfg.disableDeezer = parseBool(getenv(DISABLE_DEEZER_ENV))
|
|
cfg.disableCAA = parseBool(getenv(DISABLE_COVER_ART_ARCHIVE_ENV))
|
|
cfg.disableMusicBrainz = parseBool(getenv(DISABLE_MUSICBRAINZ_ENV))
|
|
cfg.subsonicUrl = getenv(SUBSONIC_URL_ENV)
|
|
cfg.subsonicParams = getenv(SUBSONIC_PARAMS_ENV)
|
|
cfg.subsonicEnabled = cfg.subsonicUrl != "" && cfg.subsonicParams != ""
|
|
if cfg.subsonicEnabled && (cfg.subsonicUrl == "" || cfg.subsonicParams == "") {
|
|
return nil, fmt.Errorf("loadConfig: invalid configuration: both %s and %s must be set in order to use subsonic image fetching", SUBSONIC_URL_ENV, SUBSONIC_PARAMS_ENV)
|
|
}
|
|
cfg.skipImport = parseBool(getenv(SKIP_IMPORT_ENV))
|
|
|
|
cfg.userAgent = fmt.Sprintf("Koito %s (contact@koito.io)", version)
|
|
|
|
if getenv(DEFAULT_USERNAME_ENV) == "" {
|
|
cfg.defaultUsername = "admin"
|
|
} else {
|
|
cfg.defaultUsername = getenv(DEFAULT_USERNAME_ENV)
|
|
}
|
|
if getenv(DEFAULT_PASSWORD_ENV) == "" {
|
|
cfg.defaultPw = "changeme"
|
|
} else {
|
|
cfg.defaultPw = getenv(DEFAULT_PASSWORD_ENV)
|
|
}
|
|
|
|
cfg.defaultTheme = getenv(DEFAULT_THEME_ENV)
|
|
|
|
cfg.configDir = getenv(CONFIG_DIR_ENV)
|
|
if cfg.configDir == "" {
|
|
cfg.configDir = "/etc/koito"
|
|
}
|
|
|
|
rawHosts := getenv(ALLOWED_HOSTS_ENV)
|
|
cfg.allowedHosts = strings.Split(rawHosts, ",")
|
|
cfg.allowAllHosts = cfg.allowedHosts[0] == "*"
|
|
|
|
rawCors := getenv(CORS_ORIGINS_ENV)
|
|
cfg.allowedOrigins = strings.Split(rawCors, ",")
|
|
|
|
if getenv(ARTIST_SEPARATORS_ENV) != "" {
|
|
for pattern := range strings.SplitSeq(getenv(ARTIST_SEPARATORS_ENV), ";;") {
|
|
regex, err := regexp.Compile(pattern)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to compile regex pattern %s", pattern)
|
|
}
|
|
cfg.artistSeparators = append(cfg.artistSeparators, regex)
|
|
}
|
|
} else {
|
|
cfg.artistSeparators = []*regexp.Regexp{regexp.MustCompile(`\s+·\s+`)}
|
|
}
|
|
|
|
switch strings.ToLower(getenv(LOG_LEVEL_ENV)) {
|
|
case "debug":
|
|
cfg.logLevel = 0
|
|
case "warn":
|
|
cfg.logLevel = 2
|
|
case "error":
|
|
cfg.logLevel = 3
|
|
case "fatal":
|
|
cfg.logLevel = 4
|
|
default:
|
|
cfg.logLevel = 1
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func parseBool(s string) bool {
|
|
if strings.ToLower(s) == "true" {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Global accessors for configuration values
|
|
|
|
func UserAgent() string {
|
|
lock.RLock()
|
|
defer lock.RUnlock()
|
|
return globalConfig.userAgent
|
|
}
|
|
|
|
func ListenAddr() string {
|
|
lock.RLock()
|
|
defer lock.RUnlock()
|
|
return fmt.Sprintf("%s:%d", globalConfig.bindAddr, globalConfig.listenPort)
|
|
}
|
|
|
|
func ConfigDir() string {
|
|
lock.RLock()
|
|
defer lock.RUnlock()
|
|
return globalConfig.configDir
|
|
}
|
|
|
|
func DatabaseUrl() string {
|
|
lock.RLock()
|
|
defer lock.RUnlock()
|
|
return globalConfig.databaseUrl
|
|
}
|
|
|
|
func MusicBrainzUrl() string {
|
|
lock.RLock()
|
|
defer lock.RUnlock()
|
|
return globalConfig.musicBrainzUrl
|
|
}
|
|
|
|
func MusicBrainzRateLimit() int {
|
|
lock.RLock()
|
|
defer lock.RUnlock()
|
|
return globalConfig.musicBrainzRateLimit
|
|
}
|
|
|
|
func LogLevel() int {
|
|
lock.RLock()
|
|
defer lock.RUnlock()
|
|
return globalConfig.logLevel
|
|
}
|
|
|
|
func StructuredLogging() bool {
|
|
lock.RLock()
|
|
defer lock.RUnlock()
|
|
return globalConfig.structuredLogging
|
|
}
|
|
|
|
func LbzRelayEnabled() bool {
|
|
lock.RLock()
|
|
defer lock.RUnlock()
|
|
return globalConfig.lbzRelayEnabled
|
|
}
|
|
|
|
func LbzRelayUrl() string {
|
|
lock.RLock()
|
|
defer lock.RUnlock()
|
|
return globalConfig.lbzRelayUrl
|
|
}
|
|
|
|
func LbzRelayToken() string {
|
|
lock.RLock()
|
|
defer lock.RUnlock()
|
|
return globalConfig.lbzRelayToken
|
|
}
|
|
|
|
func DefaultPassword() string {
|
|
lock.RLock()
|
|
defer lock.RUnlock()
|
|
return globalConfig.defaultPw
|
|
}
|
|
|
|
func DefaultUsername() string {
|
|
lock.RLock()
|
|
defer lock.RUnlock()
|
|
return globalConfig.defaultUsername
|
|
}
|
|
|
|
func DefaultTheme() string {
|
|
lock.RLock()
|
|
defer lock.RUnlock()
|
|
return globalConfig.defaultTheme
|
|
}
|
|
|
|
func FullImageCacheEnabled() bool {
|
|
lock.RLock()
|
|
defer lock.RUnlock()
|
|
return globalConfig.enableFullImageCache
|
|
}
|
|
|
|
func DeezerDisabled() bool {
|
|
lock.RLock()
|
|
defer lock.RUnlock()
|
|
return globalConfig.disableDeezer
|
|
}
|
|
|
|
func CoverArtArchiveDisabled() bool {
|
|
lock.RLock()
|
|
defer lock.RUnlock()
|
|
return globalConfig.disableCAA
|
|
}
|
|
|
|
func MusicBrainzDisabled() bool {
|
|
lock.RLock()
|
|
defer lock.RUnlock()
|
|
return globalConfig.disableMusicBrainz
|
|
}
|
|
|
|
func SubsonicEnabled() bool {
|
|
lock.RLock()
|
|
defer lock.RUnlock()
|
|
return globalConfig.subsonicEnabled
|
|
}
|
|
|
|
func SubsonicUrl() string {
|
|
lock.RLock()
|
|
defer lock.RUnlock()
|
|
return globalConfig.subsonicUrl
|
|
}
|
|
|
|
func SubsonicParams() string {
|
|
lock.RLock()
|
|
defer lock.RUnlock()
|
|
return globalConfig.subsonicParams
|
|
}
|
|
|
|
func SkipImport() bool {
|
|
lock.RLock()
|
|
defer lock.RUnlock()
|
|
return globalConfig.skipImport
|
|
}
|
|
|
|
func AllowedHosts() []string {
|
|
lock.RLock()
|
|
defer lock.RUnlock()
|
|
return globalConfig.allowedHosts
|
|
}
|
|
|
|
func AllowAllHosts() bool {
|
|
lock.RLock()
|
|
defer lock.RUnlock()
|
|
return globalConfig.allowAllHosts
|
|
}
|
|
|
|
func AllowedOrigins() []string {
|
|
lock.RLock()
|
|
defer lock.RUnlock()
|
|
return globalConfig.allowedOrigins
|
|
}
|
|
|
|
func RateLimitDisabled() bool {
|
|
lock.RLock()
|
|
defer lock.RUnlock()
|
|
return globalConfig.disableRateLimit
|
|
}
|
|
|
|
func ThrottleImportMs() int {
|
|
lock.RLock()
|
|
defer lock.RUnlock()
|
|
return globalConfig.importThrottleMs
|
|
}
|
|
|
|
// returns the before, after times, in that order
|
|
func ImportWindow() (time.Time, time.Time) {
|
|
lock.RLock()
|
|
defer lock.RUnlock()
|
|
return globalConfig.importBefore, globalConfig.importAfter
|
|
}
|
|
|
|
func FetchImagesDuringImport() bool {
|
|
lock.RLock()
|
|
defer lock.RUnlock()
|
|
return globalConfig.fetchImageDuringImport
|
|
}
|
|
|
|
func ArtistSeparators() []*regexp.Regexp {
|
|
lock.RLock()
|
|
defer lock.RUnlock()
|
|
return globalConfig.artistSeparators
|
|
}
|