Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions api_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@
get_current_timezone_info,
get_timezone_from_env,
normalize_timezone_input,
list_supported_abbreviations
list_supported_abbreviations,
get_all_timezones,
)

# Global variable to track server start time
Expand Down Expand Up @@ -5419,14 +5420,23 @@ async def sync_collection(franchise_name: str):

@app.get("/api/timezone/supported")
async def get_supported_timezones():
"""Get list of all supported timezone abbreviations organized by region"""
"""Get all IANA timezones plus supported abbreviations organized by region"""
try:
timezones = get_all_timezones()
abbreviations = list_supported_abbreviations()
total_abbreviations = sum(
len(abbrevs) for abbrevs in abbreviations.values()
)
return {
"success": True,
"timezones": timezones,
"total_timezones": len(timezones),
"regions": abbreviations,
"total_abbreviations": sum(len(abbrevs) for abbrevs in abbreviations.values()),
"note": "Use these abbreviations in the TZ environment variable"
"total_abbreviations": total_abbreviations,
"note": (
"Use IANA names (preferred) or these abbreviations "
"in the TZ environment variable"
),
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
Expand Down
21 changes: 20 additions & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,26 @@ Returns Overseerr configuration details.
GET /api/timezone/supported
```

Returns list of supported timezones.
Returns all IANA timezones available on the server (sorted by UTC offset), plus supported abbreviations organized by region.

**Response**:
```json
{
"success": true,
"timezones": [
{
"value": "America/Sao_Paulo",
"label": "America/Sao_Paulo (-03)",
"offset": "UTC-03:00"
}
],
"total_timezones": 598,
"regions": {
"North America": ["EST", "PST"]
},
"total_abbreviations": 100
}
```

### Get Current Timezone
```http
Expand Down
6 changes: 4 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,9 +290,11 @@ TZ=GMT
3. Copy the webhook URL to `DISCORD_WEBHOOK_URL`

**Timezone Configuration:**
- Use standard timezone identifiers (e.g., `GMT+1`, `UTC-5`)
- Use IANA timezone names (e.g., `America/Sao_Paulo`, `Europe/London`), common abbreviations (e.g., `BRT`, `EST`, `CET`) or UTC/GMT offsets (e.g., `GMT+1`, `UTC-5`)
- All IANA timezones are supported and can also be selected in the dashboard (Settings → Sync Settings) and in the setup wizard
- Set `TIMEZONE_REGION` (e.g., `US`, `EU`, `BR`, `ASIA`, `AU`, `NZ`) to resolve ambiguous abbreviations like `AMT` or `IST`
- Affects scheduling and log timestamps
- Default: `GMT`
- Default: `UTC`

### Performance Tuning

Expand Down
103 changes: 90 additions & 13 deletions list_sync/utils/timezone_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

import os
import logging
from typing import Optional, Dict
from datetime import datetime, timezone
from typing import List, Optional, Dict
from datetime import datetime, timedelta, timezone
import zoneinfo

# Comprehensive mapping of common timezone abbreviations to their full timezone names
Expand Down Expand Up @@ -183,6 +183,9 @@
"BRT": "America/Sao_Paulo", # Brasília Time
"BRST": "America/Sao_Paulo", # Brasília Summer Time
"BST": "America/Sao_Paulo", # Brazil Summer Time (conflicts with British Summer Time)
"AMT": "America/Manaus", # Amazon Time (conflicts with Armenia Time)
"ACT": "America/Rio_Branco", # Acre Time (conflicts with ASEAN Common Time)
"FNT": "America/Noronha", # Fernando de Noronha Time

# South America - Argentina
"ART": "America/Argentina/Buenos_Aires",
Expand Down Expand Up @@ -285,6 +288,27 @@
"Z": "UTC", # Zulu Time Zone (UTC+0)
}

# South American zones (used to categorize abbreviations by region,
# since they share the "America/" prefix with North American zones)
SOUTH_AMERICAN_TIMEZONES = {
"America/Argentina/Buenos_Aires",
"America/Asuncion",
"America/Bogota",
"America/Caracas",
"America/Cayenne",
"America/Guayaquil",
"America/Guyana",
"America/La_Paz",
"America/Lima",
"America/Manaus",
"America/Montevideo",
"America/Noronha",
"America/Paramaribo",
"America/Rio_Branco",
"America/Santiago",
"America/Sao_Paulo",
}

# Regional preference mapping for conflicting abbreviations
REGIONAL_PREFERENCES: Dict[str, Dict[str, str]] = {
"US": {
Expand Down Expand Up @@ -323,6 +347,13 @@
"NZ": {
"NZST": "Pacific/Auckland",
"NZDT": "Pacific/Auckland",
},
"BR": {
"BRT": "America/Sao_Paulo",
"AMT": "America/Manaus",
"ACT": "America/Rio_Branco",
"FNT": "America/Noronha",
"BST": "America/Sao_Paulo",
}
}

Expand Down Expand Up @@ -556,6 +587,54 @@ def get_current_timezone_info() -> Dict[str, str]:
}


def get_all_timezones() -> List[Dict[str, str]]:
"""
Get all IANA timezones available on the system with display metadata.

Each entry contains the IANA name (``value``), a human-readable label
with the current abbreviation (``label``), and the current UTC offset
(``offset``). Zones are sorted by UTC offset, then alphabetically.

Returns:
List of dicts with ``value``, ``label`` and ``offset`` keys, e.g.
``{"value": "America/Sao_Paulo", "label": "America/Sao_Paulo (-03)",
"offset": "UTC-03:00"}``
"""
now = datetime.now(timezone.utc)
entries = []

for name in zoneinfo.available_timezones():
try:
local_now = now.astimezone(zoneinfo.ZoneInfo(name))
except Exception as e:
logging.debug(f"Skipping unloadable timezone '{name}': {e}")
continue

utc_offset = local_now.utcoffset() or timedelta(0)
total_minutes = int(utc_offset.total_seconds() // 60)
sign = "+" if total_minutes >= 0 else "-"
hours, minutes = divmod(abs(total_minutes), 60)
offset_str = f"UTC{sign}{hours:02d}:{minutes:02d}"

abbreviation = local_now.strftime("%Z")
label = f"{name} ({abbreviation})" if abbreviation else name

entries.append(
(
total_minutes,
name,
{
"value": name,
"label": label,
"offset": offset_str,
},
)
)

entries.sort(key=lambda entry: (entry[0], entry[1]))
return [entry[2] for entry in entries]


def list_supported_abbreviations() -> Dict[str, list]:
"""
Get a list of all supported timezone abbreviations organized by region.
Expand All @@ -577,28 +656,26 @@ def list_supported_abbreviations() -> Dict[str, list]:

# Categorize abbreviations
for abbrev, tz_name in TIMEZONE_ABBREVIATIONS.items():
if tz_name.startswith("America/"):
if len(abbrev) == 1: # Military single-letter codes
regions["Military"].append(abbrev)
elif abbrev in ["UTC", "GMT"]:
regions["Universal"].append(abbrev)
elif tz_name in SOUTH_AMERICAN_TIMEZONES:
regions["South America"].append(abbrev)
elif tz_name.startswith("America/"):
regions["North America"].append(abbrev)
elif tz_name.startswith("Europe/"):
regions["Europe"].append(abbrev)
elif tz_name.startswith("Asia/"):
regions["Asia"].append(abbrev)
elif tz_name.startswith("Australia/") or tz_name.startswith("Pacific/Auckland"):
elif tz_name.startswith("Australia/") or tz_name == "Pacific/Auckland":
regions["Australia/New Zealand"].append(abbrev)
elif tz_name.startswith("Africa/"):
regions["Africa"].append(abbrev)
elif tz_name.startswith("Pacific/"):
regions["Pacific"].append(abbrev)
elif len(abbrev) == 1: # Military single-letter codes
regions["Military"].append(abbrev)
elif abbrev in ["UTC", "GMT", "Z"]:
regions["Universal"].append(abbrev)
else:
# Determine by timezone name patterns
if any(continent in tz_name for continent in ["America/Argentina", "America/Sao_Paulo", "America/Santiago"]):
regions["South America"].append(abbrev)
else:
regions["Universal"].append(abbrev)
regions["Universal"].append(abbrev)

# Sort each region's abbreviations
for region in regions:
Expand Down
45 changes: 17 additions & 28 deletions listsync-nuxt/components/settings/SyncSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -177,27 +177,29 @@ const showTimezoneDropdown = ref(false)
const supportedTimezones = ref<any[]>([])
const currentTimezoneInfo = ref<any>(null)

// Fallback timezones if API fails
const fallbackTimezones = [
{ label: 'UTC', value: 'UTC', offset: '+00:00' },
{ label: 'America/New_York (EST/EDT)', value: 'America/New_York', offset: 'UTC-05:00' },
{ label: 'America/Chicago (CST/CDT)', value: 'America/Chicago', offset: 'UTC-06:00' },
{ label: 'America/Denver (MST/MDT)', value: 'America/Denver', offset: 'UTC-07:00' },
{ label: 'America/Los_Angeles (PST/PDT)', value: 'America/Los_Angeles', offset: 'UTC-08:00' },
{ label: 'Europe/London (GMT/BST)', value: 'Europe/London', offset: 'UTC+00:00' },
{ label: 'Europe/Paris (CET/CEST)', value: 'Europe/Paris', offset: 'UTC+01:00' },
{ label: 'Asia/Tokyo (JST)', value: 'Asia/Tokyo', offset: 'UTC+09:00' },
{ label: 'Australia/Sydney (AEDT)', value: 'Australia/Sydney', offset: 'UTC+11:00' },
]
// Fallback: IANA timezones provided by the browser if the API fails
const getBrowserTimezones = () => {
try {
return Intl.supportedValuesOf('timeZone').map((tz) => ({
label: tz,
value: tz,
offset: '',
}))
} catch {
return [{ label: 'UTC', value: 'UTC', offset: 'UTC+00:00' }]
}
}

// Load supported timezones
const loadTimezones = async () => {
try {
const response: any = await api.getSupportedTimezones()
supportedTimezones.value = response.timezones || fallbackTimezones
supportedTimezones.value = response.timezones?.length
? response.timezones
: getBrowserTimezones()
} catch (error) {
console.error('Error loading timezones, using fallback:', error)
supportedTimezones.value = fallbackTimezones
console.error('Error loading timezones, using browser fallback:', error)
supportedTimezones.value = getBrowserTimezones()
}
}

Expand Down Expand Up @@ -247,19 +249,6 @@ onMounted(() => {
timezoneSearch.value = localValue.value.timezone || ''
})

// Timezone options (common timezones)
const timezoneOptions = [
{ label: 'UTC', value: 'UTC' },
{ label: 'America/New_York (EST/EDT)', value: 'America/New_York' },
{ label: 'America/Chicago (CST/CDT)', value: 'America/Chicago' },
{ label: 'America/Denver (MST/MDT)', value: 'America/Denver' },
{ label: 'America/Los_Angeles (PST/PDT)', value: 'America/Los_Angeles' },
{ label: 'Europe/London (GMT/BST)', value: 'Europe/London' },
{ label: 'Europe/Paris (CET/CEST)', value: 'Europe/Paris' },
{ label: 'Asia/Tokyo (JST)', value: 'Asia/Tokyo' },
{ label: 'Australia/Sydney (AEDT/AEST)', value: 'Australia/Sydney' },
]

// Watch for external changes
watch(
() => props.modelValue,
Expand Down
53 changes: 30 additions & 23 deletions listsync-nuxt/components/setup/Step2Configuration.vue
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@
class="w-full px-3 py-2.5 sm:py-2 h-11 sm:h-10 bg-black/30 border border-purple-500/25 rounded-lg text-base sm:text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-purple-500/50 transition-all touch-manipulation"
:disabled="isValidating"
>
<option v-for="tz in commonTimezones" :key="tz" :value="tz">
{{ tz }}
<option v-for="tz in timezoneOptions" :key="tz.value" :value="tz.value">
{{ tz.label }}
</option>
</select>
</div>
Expand Down Expand Up @@ -274,27 +274,34 @@ watch(() => localValue.value.trakt_client_id, () => {
traktValidated.value = false
})

// Common timezones for quick selection
const commonTimezones = [
'UTC',
'America/New_York',
'America/Chicago',
'America/Denver',
'America/Los_Angeles',
'America/Toronto',
'America/Vancouver',
'Europe/London',
'Europe/Paris',
'Europe/Berlin',
'Europe/Amsterdam',
'Asia/Tokyo',
'Asia/Shanghai',
'Asia/Hong_Kong',
'Asia/Singapore',
'Australia/Sydney',
'Australia/Melbourne',
'Pacific/Auckland',
]
// Fallback: IANA timezones provided by the browser if the API fails
const getBrowserTimezones = () => {
try {
return Intl.supportedValuesOf('timeZone').map((tz) => ({
value: tz,
label: tz,
}))
} catch {
return [{ value: 'UTC', label: 'UTC' }]
}
}

// All IANA timezones, loaded from the API
const timezoneOptions = ref(getBrowserTimezones())

onMounted(async () => {
try {
const response: any = await api.getSupportedTimezones()
if (response?.timezones?.length) {
timezoneOptions.value = response.timezones.map((tz: any) => ({
value: tz.value,
label: `${tz.value} (${tz.offset})`,
}))
}
} catch (error) {
console.error('Error loading timezones, using browser fallback:', error)
}
})

// Check if we can proceed (basic validation)
const canProceed = computed(() => {
Expand Down
Loading