fix: include time zone name overrides and add KOITO_FORCE_TZ cfg option (#176)

* timezone overrides and force_tz option

* docs for force_tz

* add link to time zone names in docs
This commit is contained in:
Gabe Farrell 2026-01-24 13:19:04 -05:00 committed by GitHub
parent 1ed055d098
commit 937f9062b5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 153 additions and 1 deletions

View file

@ -64,6 +64,8 @@ If the environment variable is defined without **and** with the suffix at the sa
##### KOITO_CONFIG_DIR ##### KOITO_CONFIG_DIR
- Default: `/etc/koito` - Default: `/etc/koito`
- Description: The location where import folders and image caches are stored. - 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 ##### KOITO_DISABLE_DEEZER
- Default: `false` - Default: `false`
- Description: Disables Deezer as a source for finding artist and album images. - Description: Disables Deezer as a source for finding artist and album images.

View file

@ -96,6 +96,10 @@ func Run(
defer store.Close(ctx) defer store.Close(ctx)
l.Info().Msg("Engine: Database connection established") 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") l.Debug().Msg("Engine: Initializing MusicBrainz client")
var mbzC mbz.MusicBrainzCaller var mbzC mbz.MusicBrainzCaller
if !cfg.MusicBrainzDisabled() { if !cfg.MusicBrainzDisabled() {

View file

@ -6,7 +6,9 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
_ "time/tzdata"
"github.com/gabehf/koito/internal/cfg"
"github.com/gabehf/koito/internal/db" "github.com/gabehf/koito/internal/db"
"github.com/gabehf/koito/internal/logger" "github.com/gabehf/koito/internal/logger"
) )
@ -107,14 +109,143 @@ func TimeframeFromRequest(r *http.Request) db.Timeframe {
func parseTZ(r *http.Request) *time.Location { 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 tz := r.URL.Query().Get("tz"); tz != "" {
if fixedTz, exists := overrides[tz]; exists {
tz = fixedTz
}
if loc, err := time.LoadLocation(tz); err == nil { if loc, err := time.LoadLocation(tz); err == nil {
return loc return loc
} }
} }
if c, err := r.Cookie("tz"); err == nil { 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 return loc
} }
} }

View file

@ -49,6 +49,7 @@ const (
FETCH_IMAGES_DURING_IMPORT_ENV = "KOITO_FETCH_IMAGES_DURING_IMPORT" FETCH_IMAGES_DURING_IMPORT_ENV = "KOITO_FETCH_IMAGES_DURING_IMPORT"
ARTIST_SEPARATORS_ENV = "KOITO_ARTIST_SEPARATORS_REGEX" ARTIST_SEPARATORS_ENV = "KOITO_ARTIST_SEPARATORS_REGEX"
LOGIN_GATE_ENV = "KOITO_LOGIN_GATE" LOGIN_GATE_ENV = "KOITO_LOGIN_GATE"
FORCE_TZ = "KOITO_FORCE_TZ"
) )
type config struct { type config struct {
@ -87,6 +88,7 @@ type config struct {
importAfter time.Time importAfter time.Time
artistSeparators []*regexp.Regexp artistSeparators []*regexp.Regexp
loginGate bool loginGate bool
forceTZ *time.Location
} }
var ( var (
@ -213,6 +215,13 @@ func loadConfig(getenv func(string) string, version string) (*config, error) {
cfg.loginGate = true 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)) { switch strings.ToLower(getenv(LOG_LEVEL_ENV)) {
case "debug": case "debug":
cfg.logLevel = 0 cfg.logLevel = 0
@ -430,3 +439,9 @@ func LoginGate() bool {
defer lock.RUnlock() defer lock.RUnlock()
return globalConfig.loginGate return globalConfig.loginGate
} }
func ForceTZ() *time.Location {
lock.RLock()
defer lock.RUnlock()
return globalConfig.forceTZ
}