timezone issues
Build and Push Docker Images / build-and-push (push) Successful in 28s

This commit is contained in:
Bram committed 2026-11-07 16:30:47 +01:00
1 parent 74df7780fd
commit d266a8258d
9 files changed
+32 -15

No files matched your search

@@ -55,7 +55,8 @@ View BE| DAZN 03 [Standard de Liège - SK Beveren | Jupiler Pro League (nl) (202
| `EVENT_DURATION_MIN` | `150` | nee | Duur waarna een event het slot verlaat | | `EVENT_DURATION_MIN` | `150` | nee | Duur waarna een event het slot verlaat |
| `START_GRACE_MIN` | `10` | nee | Hoeveel minuten voor aanvang een event eligibel wordt | | `START_GRACE_MIN` | `10` | nee | Hoeveel minuten voor aanvang een event eligibel wordt |
| `REFRESH_INTERVAL_SEC` | `60` | nee | Interval voor upstream-refresh | | `REFRESH_INTERVAL_SEC` | `60` | nee | Interval voor upstream-refresh |
| `TIMEZONE` | `Europe/Brussels` | nee | Tijdzone van de tijdstempels in kanaalnamen | | `SOURCE_TIMEZONE` | `UTC` | nee | Tijdzone van de tijdstempels in kanaalnamen |
| `TIMEZONE` | `Europe/Brussels` | nee | Tijdzone voor weergave in playlist en EPG |
| `LISTEN_PORT` | `8080` | nee | HTTP-poort | | `LISTEN_PORT` | `8080` | nee | HTTP-poort |
| `UPSTREAM_TIMEOUT_MS` | `15000` | nee | Timeout voor upstream-verzoeken | | `UPSTREAM_TIMEOUT_MS` | `15000` | nee | Timeout voor upstream-verzoeken |
| `PROXY_INSECURE_TLS` | `0` | nee | `1` = self-signed upstream-certificaten accepteren | | `PROXY_INSECURE_TLS` | `0` | nee | `1` = self-signed upstream-certificaten accepteren |
@@ -95,8 +96,9 @@ Ondersteun minimaal het opgegeven DAZN-formaat:
- Neem de laatste `[...]` als inhoud; splits op de eerste `|` in `title` en `rest`. - Neem de laatste `[...]` als inhoud; splits op de eerste `|` in `title` en `rest`.
- Parse uit `rest`: league, optionele taalcode en verplichte starttijd. - Parse uit `rest`: league, optionele taalcode en verplichte starttijd.
- `title`, `league`, `language`, `startAt` (UTC) en `endAt`. - `title`, `league`, `language`, `startAt` (UTC) en `endAt`.
- Tijdzone via `TIMEZONE`, correct omgaan met zomertijd (Intl-offsetberekening, geen - Tijdstempels worden standaard als UTC gelezen (`SOURCE_TIMEZONE`), correct omgaan met
afhankelijkheid van de proces-TZ). zomertijd (Intl-offsetberekening, geen afhankelijkheid van de proces-TZ). Weergave in
playlist/EPG gebeurt in `TIMEZONE`.
- Als een matchende naam niet parsebaar is: stream overslaan, één keer loggen en in - Als een matchende naam niet parsebaar is: stream overslaan, één keer loggen en in
`/status` als waarschuwing tonen. Geen gokken met "nu actief", om toekomstige streams `/status` als waarschuwing tonen. Geen gokken met "nu actief", om toekomstige streams
niet te vroeg te tonen. niet te vroeg te tonen.
+1
View File
@@ -20,6 +20,7 @@ ENV LISTEN_PORT=8080 \
EVENT_DURATION_MIN=150 \ EVENT_DURATION_MIN=150 \
START_GRACE_MIN=10 \ START_GRACE_MIN=10 \
REFRESH_INTERVAL_SEC=60 \ REFRESH_INTERVAL_SEC=60 \
SOURCE_TIMEZONE=UTC \
TIMEZONE=Europe/Brussels \ TIMEZONE=Europe/Brussels \
UPSTREAM_TIMEOUT_MS=15000 \ UPSTREAM_TIMEOUT_MS=15000 \
PROXY_INSECURE_TLS=0 \ PROXY_INSECURE_TLS=0 \
+6 -1
View File
@@ -12,6 +12,10 @@ competitie, taal en starttijd, bijvoorbeeld:
View BE| DAZN 03 [Standard de Liège - SK Beveren | Jupiler Pro League (nl) (2026-11-06 19:25:00)] View BE| DAZN 03 [Standard de Liège - SK Beveren | Jupiler Pro League (nl) (2026-11-06 19:25:00)]
``` ```
De tijdstempels in de kanaalnamen worden standaard als **UTC** gelezen. De app toont ze
vervolgens in `TIMEZONE` (standaard `Europe/Brussels`), dus `19:25:00` UTC wordt in de
playlist en EPG als `20:25` getoond.
Alleen streams met taalcode `(nl)` of zonder taalcode worden gebruikt. Een expliciete Alleen streams met taalcode `(nl)` of zonder taalcode worden gebruikt. Een expliciete
`(fr)`- of andere taalcode wordt overgeslagen. `(fr)`- of andere taalcode wordt overgeslagen.
@@ -68,7 +72,8 @@ staat.
| `EVENT_DURATION_MIN` | `150` | nee | Duur waarna een event het slot verlaat | | `EVENT_DURATION_MIN` | `150` | nee | Duur waarna een event het slot verlaat |
| `START_GRACE_MIN` | `10` | nee | Minuten voor aanvang dat een event eligibel wordt | | `START_GRACE_MIN` | `10` | nee | Minuten voor aanvang dat een event eligibel wordt |
| `REFRESH_INTERVAL_SEC` | `60` | nee | Hoe vaak kanaalnamen opnieuw worden gelezen | | `REFRESH_INTERVAL_SEC` | `60` | nee | Hoe vaak kanaalnamen opnieuw worden gelezen |
| `TIMEZONE` | `Europe/Brussels` | nee | Tijdzone van de tijdstempels in kanaalnamen | | `SOURCE_TIMEZONE` | `UTC` | nee | Tijdzone van de tijdstempels in de kanaalnamen |
| `TIMEZONE` | `Europe/Brussels` | nee | Tijdzone voor weergave in playlist en EPG |
| `LISTEN_PORT` | `8080` | nee | HTTP-poort | | `LISTEN_PORT` | `8080` | nee | HTTP-poort |
| `UPSTREAM_TIMEOUT_MS` | `15000` | nee | Timeout voor upstream-verzoeken | | `UPSTREAM_TIMEOUT_MS` | `15000` | nee | Timeout voor upstream-verzoeken |
| `PROXY_INSECURE_TLS` | `0` | nee | `1` = self-signed upstream-certificaten accepteren | | `PROXY_INSECURE_TLS` | `0` | nee | `1` = self-signed upstream-certificaten accepteren |
@@ -14,6 +14,7 @@ services:
EVENT_DURATION_MIN: "150" EVENT_DURATION_MIN: "150"
START_GRACE_MIN: "10" START_GRACE_MIN: "10"
REFRESH_INTERVAL_SEC: "600" REFRESH_INTERVAL_SEC: "600"
SOURCE_TIMEZONE: "UTC"
TIMEZONE: "Europe/Brussels" TIMEZONE: "Europe/Brussels"
# PROXY_INSECURE_TLS: "1" # alleen nodig bij self-signed upstream-certificaten # PROXY_INSECURE_TLS: "1" # alleen nodig bij self-signed upstream-certificaten
# LOG_LEVEL: "debug" # LOG_LEVEL: "debug"
+1
View File
@@ -40,6 +40,7 @@ function statusPayload() {
eventDurationMin: settings.eventDurationMin, eventDurationMin: settings.eventDurationMin,
startGraceMin: settings.startGraceMin, startGraceMin: settings.startGraceMin,
refreshIntervalSec: settings.refreshIntervalSec, refreshIntervalSec: settings.refreshIntervalSec,
sourceTimezone: settings.sourceTimezone,
timezone: settings.timezone, timezone: settings.timezone,
}, },
}; };
+1 -1
View File
@@ -160,7 +160,7 @@ function createScheduler() {
const { events, warnings } = buildEvents(streams, { const { events, warnings } = buildEvents(streams, {
searchTerm: settings.searchTerm, searchTerm: settings.searchTerm,
language: settings.language, language: settings.language,
timezone: settings.timezone, timezone: settings.sourceTimezone,
eventDurationMs: settings.eventDurationMs, eventDurationMs: settings.eventDurationMs,
}); });
const now = Date.now(); const now = Date.now();
+5 -3
View File
@@ -18,6 +18,7 @@ const settings = {
eventDurationMin: readNumber(process.env.EVENT_DURATION_MIN, 150, 1, 1440), eventDurationMin: readNumber(process.env.EVENT_DURATION_MIN, 150, 1, 1440),
startGraceMin: readNumber(process.env.START_GRACE_MIN, 10, 0, 1440), startGraceMin: readNumber(process.env.START_GRACE_MIN, 10, 0, 1440),
refreshIntervalSec: readNumber(process.env.REFRESH_INTERVAL_SEC, 60, 10, 3600), refreshIntervalSec: readNumber(process.env.REFRESH_INTERVAL_SEC, 60, 10, 3600),
sourceTimezone: (process.env.SOURCE_TIMEZONE || 'UTC').trim() || 'UTC',
timezone: (process.env.TIMEZONE || 'Europe/Brussels').trim() || 'Europe/Brussels', timezone: (process.env.TIMEZONE || 'Europe/Brussels').trim() || 'Europe/Brussels',
listenPort: Math.round(readNumber(process.env.LISTEN_PORT, 8080, 1, 65535)), listenPort: Math.round(readNumber(process.env.LISTEN_PORT, 8080, 1, 65535)),
upstreamTimeoutMs: readNumber(process.env.UPSTREAM_TIMEOUT_MS, 15000, 1000, 120000), upstreamTimeoutMs: readNumber(process.env.UPSTREAM_TIMEOUT_MS, 15000, 1000, 120000),
@@ -32,11 +33,11 @@ const settings = {
settings.eventDurationMs = settings.eventDurationMin * 60 * 1000; settings.eventDurationMs = settings.eventDurationMin * 60 * 1000;
settings.startGraceMs = settings.startGraceMin * 60 * 1000; settings.startGraceMs = settings.startGraceMin * 60 * 1000;
function validateTimezone(timezone) { function validateTimezone(timezone, envName) {
try { try {
new Intl.DateTimeFormat('en-US', { timeZone: timezone }); new Intl.DateTimeFormat('en-US', { timeZone: timezone });
} catch { } catch {
throw new Error(`Invalid TIMEZONE: ${timezone}`); throw new Error(`Invalid ${envName}: ${timezone}`);
} }
} }
@@ -60,7 +61,8 @@ function validate() {
throw new Error('XTREAM_URL must use http or https'); throw new Error('XTREAM_URL must use http or https');
} }
validateTimezone(settings.timezone); validateTimezone(settings.sourceTimezone, 'SOURCE_TIMEZONE');
validateTimezone(settings.timezone, 'TIMEZONE');
if (!settings.language) { if (!settings.language) {
throw new Error('LANGUAGE must not be empty'); throw new Error('LANGUAGE must not be empty');
@@ -7,18 +7,23 @@ const { parseChannelName, normalizeText } = require('../src/channel-name');
const sample = const sample =
'View BE| DAZN 03 [Standard de Liège - SK Beveren | Jupiler Pro League (nl) (2026-11-06 19:25:00)]'; 'View BE| DAZN 03 [Standard de Liège - SK Beveren | Jupiler Pro League (nl) (2026-11-06 19:25:00)]';
test('parses the DAZN sample', () => { test('parses the DAZN sample with UTC source timestamps', () => {
const parsed = parseChannelName(sample, { timezone: 'Europe/Brussels' }); const parsed = parseChannelName(sample, { timezone: 'UTC' });
assert.equal(parsed.title, 'Standard de Liège - SK Beveren'); assert.equal(parsed.title, 'Standard de Liège - SK Beveren');
assert.equal(parsed.league, 'Jupiler Pro League'); assert.equal(parsed.league, 'Jupiler Pro League');
assert.equal(parsed.language, 'nl'); assert.equal(parsed.language, 'nl');
assert.equal(parsed.startAt.toISOString(), '2026-11-06T19:25:00.000Z');
});
test('can parse the same timestamp in another source timezone', () => {
const parsed = parseChannelName(sample, { timezone: 'Europe/Brussels' });
assert.equal(parsed.startAt.toISOString(), '2026-11-06T18:25:00.000Z'); assert.equal(parsed.startAt.toISOString(), '2026-11-06T18:25:00.000Z');
}); });
test('parses a French stream', () => { test('parses a French stream', () => {
const parsed = parseChannelName( const parsed = parseChannelName(
'View BE| DAZN 03 [Standard de Liège - SK Beveren | Jupiler Pro League (fr) (2026-11-06 19:25:00)]', 'View BE| DAZN 03 [Standard de Liège - SK Beveren | Jupiler Pro League (fr) (2026-11-06 19:25:00)]',
{ timezone: 'Europe/Brussels' } { timezone: 'UTC' }
); );
assert.equal(parsed.language, 'fr'); assert.equal(parsed.language, 'fr');
}); });
@@ -26,14 +31,14 @@ test('parses a French stream', () => {
test('allows a missing language code', () => { test('allows a missing language code', () => {
const parsed = parseChannelName( const parsed = parseChannelName(
'DAZN 03 [Standard de Liège - SK Beveren | Jupiler Pro League (2026-11-06 19:25:00)]', 'DAZN 03 [Standard de Liège - SK Beveren | Jupiler Pro League (2026-11-06 19:25:00)]',
{ timezone: 'Europe/Brussels' } { timezone: 'UTC' }
); );
assert.equal(parsed.language, null); assert.equal(parsed.language, null);
}); });
test('returns null for unparseable names', () => { test('returns null for unparseable names', () => {
assert.equal(parseChannelName('DAZN 03', { timezone: 'Europe/Brussels' }), null); assert.equal(parseChannelName('DAZN 03', { timezone: 'UTC' }), null);
assert.equal(parseChannelName('', { timezone: 'Europe/Brussels' }), null); assert.equal(parseChannelName('', { timezone: 'UTC' }), null);
}); });
test('normalizes diacritics and whitespace', () => { test('normalizes diacritics and whitespace', () => {
@@ -4,7 +4,7 @@ const test = require('node:test');
const assert = require('node:assert/strict'); const assert = require('node:assert/strict');
const { buildEvents, compareEvents, isEligible } = require('../src/events'); const { buildEvents, compareEvents, isEligible } = require('../src/events');
const timezone = 'Europe/Brussels'; const timezone = 'UTC';
function stream(id, name) { function stream(id, name) {
return { stream_id: id, name }; return { stream_id: id, name };