diff --git a/docs/src/content/docs/reference/configuration.md b/docs/src/content/docs/reference/configuration.md index 6eae82b..2af573c 100644 --- a/docs/src/content/docs/reference/configuration.md +++ b/docs/src/content/docs/reference/configuration.md @@ -64,6 +64,8 @@ If the environment variable is defined without **and** with the suffix at the sa ##### KOITO_CONFIG_DIR - Default: `/etc/koito` - Description: The location where import folders and image caches are stored. +##### KOITO_FORCE_TZ +- Description: A canonical IANA database time zone name (https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) that Koito will use to serve all clients. Overrides any timezones requested via a `tz` cookie or `tz` query parameter. Koito will fail to start if this value is invalid. ##### KOITO_DISABLE_DEEZER - Default: `false` - Description: Disables Deezer as a source for finding artist and album images. diff --git a/engine/engine.go b/engine/engine.go index 7de9254..979667e 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -96,6 +96,10 @@ func Run( defer store.Close(ctx) l.Info().Msg("Engine: Database connection established") + if cfg.ForceTZ() != nil { + l.Debug().Msgf("Engine: Forcing the use of timezone '%s'", cfg.ForceTZ().String()) + } + l.Debug().Msg("Engine: Initializing MusicBrainz client") var mbzC mbz.MusicBrainzCaller if !cfg.MusicBrainzDisabled() { diff --git a/engine/handlers/handlers.go b/engine/handlers/handlers.go index 06127aa..78bc228 100644 --- a/engine/handlers/handlers.go +++ b/engine/handlers/handlers.go @@ -6,7 +6,9 @@ import ( "strconv" "strings" "time" + _ "time/tzdata" + "github.com/gabehf/koito/internal/cfg" "github.com/gabehf/koito/internal/db" "github.com/gabehf/koito/internal/logger" ) @@ -107,14 +109,143 @@ func TimeframeFromRequest(r *http.Request) db.Timeframe { func parseTZ(r *http.Request) *time.Location { + // this map is obviously AI. + // i manually referenced as many links as I could and couldn't find any + // incorrect entries here so hopefully it is all correct. + overrides := map[string]string{ + // --- North America --- + "America/Indianapolis": "America/Indiana/Indianapolis", + "America/Knoxville": "America/Indiana/Knoxville", + "America/Louisville": "America/Kentucky/Louisville", + "America/Montreal": "America/Toronto", + "America/Shiprock": "America/Denver", + "America/Fort_Wayne": "America/Indiana/Indianapolis", + "America/Virgin": "America/Port_of_Spain", + "America/Santa_Isabel": "America/Tijuana", + "America/Ensenada": "America/Tijuana", + "America/Rosario": "America/Argentina/Cordoba", + "America/Jujuy": "America/Argentina/Jujuy", + "America/Mendoza": "America/Argentina/Mendoza", + "America/Catamarca": "America/Argentina/Catamarca", + "America/Cordoba": "America/Argentina/Cordoba", + "America/Buenos_Aires": "America/Argentina/Buenos_Aires", + "America/Coral_Harbour": "America/Atikokan", + "America/Atka": "America/Adak", + "US/Alaska": "America/Anchorage", + "US/Aleutian": "America/Adak", + "US/Arizona": "America/Phoenix", + "US/Central": "America/Chicago", + "US/Eastern": "America/New_York", + "US/East-Indiana": "America/Indiana/Indianapolis", + "US/Hawaii": "Pacific/Honolulu", + "US/Indiana-Starke": "America/Indiana/Knoxville", + "US/Michigan": "America/Detroit", + "US/Mountain": "America/Denver", + "US/Pacific": "America/Los_Angeles", + "US/Samoa": "Pacific/Pago_Pago", + "Canada/Atlantic": "America/Halifax", + "Canada/Central": "America/Winnipeg", + "Canada/Eastern": "America/Toronto", + "Canada/Mountain": "America/Edmonton", + "Canada/Newfoundland": "America/St_Johns", + "Canada/Pacific": "America/Vancouver", + + // --- Asia --- + "Asia/Calcutta": "Asia/Kolkata", + "Asia/Saigon": "Asia/Ho_Chi_Minh", + "Asia/Katmandu": "Asia/Kathmandu", + "Asia/Rangoon": "Asia/Yangon", + "Asia/Ulan_Bator": "Asia/Ulaanbaatar", + "Asia/Macao": "Asia/Macau", + "Asia/Tel_Aviv": "Asia/Jerusalem", + "Asia/Ashkhabad": "Asia/Ashgabat", + "Asia/Chungking": "Asia/Chongqing", + "Asia/Dacca": "Asia/Dhaka", + "Asia/Istanbul": "Europe/Istanbul", + "Asia/Kashgar": "Asia/Urumqi", + "Asia/Thimbu": "Asia/Thimphu", + "Asia/Ujung_Pandang": "Asia/Makassar", + "ROC": "Asia/Taipei", + "Iran": "Asia/Tehran", + "Israel": "Asia/Jerusalem", + "Japan": "Asia/Tokyo", + "Singapore": "Asia/Singapore", + "Hongkong": "Asia/Hong_Kong", + + // --- Europe --- + "Europe/Kiev": "Europe/Kyiv", + "Europe/Belfast": "Europe/London", + "Europe/Tiraspol": "Europe/Chisinau", + "Europe/Nicosia": "Asia/Nicosia", + "Europe/Moscow": "Europe/Moscow", + "W-SU": "Europe/Moscow", + "GB": "Europe/London", + "GB-Eire": "Europe/London", + "Eire": "Europe/Dublin", + "Poland": "Europe/Warsaw", + "Portugal": "Europe/Lisbon", + "Turkey": "Europe/Istanbul", + + // --- Australia / Pacific --- + "Australia/ACT": "Australia/Sydney", + "Australia/Canberra": "Australia/Sydney", + "Australia/LHI": "Australia/Lord_Howe", + "Australia/North": "Australia/Darwin", + "Australia/NSW": "Australia/Sydney", + "Australia/Queensland": "Australia/Brisbane", + "Australia/South": "Australia/Adelaide", + "Australia/Tasmania": "Australia/Hobart", + "Australia/Victoria": "Australia/Melbourne", + "Australia/West": "Australia/Perth", + "Australia/Yancowinna": "Australia/Broken_Hill", + "Pacific/Samoa": "Pacific/Pago_Pago", + "Pacific/Yap": "Pacific/Chuuk", + "Pacific/Truk": "Pacific/Chuuk", + "Pacific/Ponape": "Pacific/Pohnpei", + "NZ": "Pacific/Auckland", + "NZ-CHAT": "Pacific/Chatham", + + // --- Africa --- + "Africa/Asmera": "Africa/Asmara", + "Africa/Timbuktu": "Africa/Bamako", + "Egypt": "Africa/Cairo", + "Libya": "Africa/Tripoli", + + // --- Atlantic --- + "Atlantic/Faeroe": "Atlantic/Faroe", + "Atlantic/Jan_Mayen": "Europe/Oslo", + "Iceland": "Atlantic/Reykjavik", + + // --- Etc / Misc --- + "UTC": "UTC", + "Etc/UTC": "UTC", + "Etc/GMT": "UTC", + "GMT": "UTC", + "Zulu": "UTC", + "Universal": "UTC", + } + + if cfg.ForceTZ() != nil { + return cfg.ForceTZ() + } + if tz := r.URL.Query().Get("tz"); tz != "" { + if fixedTz, exists := overrides[tz]; exists { + tz = fixedTz + } if loc, err := time.LoadLocation(tz); err == nil { return loc } } if c, err := r.Cookie("tz"); err == nil { - if loc, err := time.LoadLocation(c.Value); err == nil { + var tz string + if fixedTz, exists := overrides[c.Value]; exists { + tz = fixedTz + } else { + tz = c.Value + } + if loc, err := time.LoadLocation(tz); err == nil { return loc } } diff --git a/internal/cfg/cfg.go b/internal/cfg/cfg.go index 36478b1..e74d6b9 100644 --- a/internal/cfg/cfg.go +++ b/internal/cfg/cfg.go @@ -49,6 +49,7 @@ const ( FETCH_IMAGES_DURING_IMPORT_ENV = "KOITO_FETCH_IMAGES_DURING_IMPORT" ARTIST_SEPARATORS_ENV = "KOITO_ARTIST_SEPARATORS_REGEX" LOGIN_GATE_ENV = "KOITO_LOGIN_GATE" + FORCE_TZ = "KOITO_FORCE_TZ" ) type config struct { @@ -87,6 +88,7 @@ type config struct { importAfter time.Time artistSeparators []*regexp.Regexp loginGate bool + forceTZ *time.Location } var ( @@ -213,6 +215,13 @@ func loadConfig(getenv func(string) string, version string) (*config, error) { cfg.loginGate = true } + if getenv(FORCE_TZ) != "" { + cfg.forceTZ, err = time.LoadLocation(getenv(FORCE_TZ)) + if err != nil { + return nil, fmt.Errorf("forced timezone '%s' is not a valid timezone", getenv(FORCE_TZ)) + } + } + switch strings.ToLower(getenv(LOG_LEVEL_ENV)) { case "debug": cfg.logLevel = 0 @@ -430,3 +439,9 @@ func LoginGate() bool { defer lock.RUnlock() return globalConfig.loginGate } + +func ForceTZ() *time.Location { + lock.RLock() + defer lock.RUnlock() + return globalConfig.forceTZ +}