From e53adcbeaf0dfdcc6cdb1ac84bd8871fa3207730 Mon Sep 17 00:00:00 2001 From: Salem <40477317+SalemOurabi@users.noreply.github.com> Date: Tue, 21 Jul 2026 09:08:37 +0200 Subject: [PATCH] fix(catchup): parse negative sub-hour XMLTV offsets correctly (#1216) * fix(catchup): parse negative sub-hour XMLTV offsets correctly XMLTV timestamps with offsets like "-0030" were treated as UTC because the hour part "-00" numerically evaluates to -0 and Math.sign(-0) dropped the minutes' sign. Derive the sign from the offset string instead, so sub-hour negative offsets shift the catch-up start time correctly. * test(catchup): cover positive sub-hour XMLTV offsets --------- Co-authored-by: 4gray --- .../m3u-utils/src/lib/catchup.utils.spec.ts | 68 +++++++++++++++++++ .../shared/m3u-utils/src/lib/catchup.utils.ts | 8 ++- 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/libs/shared/m3u-utils/src/lib/catchup.utils.spec.ts b/libs/shared/m3u-utils/src/lib/catchup.utils.spec.ts index 676c415d0..cd9fc21c8 100644 --- a/libs/shared/m3u-utils/src/lib/catchup.utils.spec.ts +++ b/libs/shared/m3u-utils/src/lib/catchup.utils.spec.ts @@ -217,6 +217,74 @@ describe('catchup.utils', () => { ); }); + it('applies negative sub-hour XMLTV offsets such as -0030', () => { + // Regression: Number('-00') === -0, so a Math.sign()-based conversion + // dropped the minutes' sign and treated -0030 as UTC. + expect( + resolveM3uCatchupUrl( + baseChannel, + { + ...archivedProgram, + start: '202604100800 -0030', + startTimestamp: null, + }, + 1_775_820_000 + ) + ).toBe( + 'https://streams.example.com/live/channel-1.m3u8?utc=1775809800&lutc=1775820000' + ); + }); + + it('applies positive offsets with minutes such as +0530', () => { + expect( + resolveM3uCatchupUrl( + baseChannel, + { + ...archivedProgram, + start: '202604100800 +0530', + startTimestamp: null, + }, + 1_775_820_000 + ) + ).toBe( + 'https://streams.example.com/live/channel-1.m3u8?utc=1775788200&lutc=1775820000' + ); + }); + + it('applies positive sub-hour XMLTV offsets such as +0030', () => { + // Regression: Number('+00') === 0, so a Math.sign()-based conversion + // dropped the minutes and treated +0030 as UTC. + expect( + resolveM3uCatchupUrl( + baseChannel, + { + ...archivedProgram, + start: '202604100800 +0030', + startTimestamp: null, + }, + 1_775_820_000 + ) + ).toBe( + 'https://streams.example.com/live/channel-1.m3u8?utc=1775806200&lutc=1775820000' + ); + }); + + it('treats a -0000 offset as UTC', () => { + expect( + resolveM3uCatchupUrl( + baseChannel, + { + ...archivedProgram, + start: '202604100800 -0000', + startTimestamp: null, + }, + 1_775_820_000 + ) + ).toBe( + 'https://streams.example.com/live/channel-1.m3u8?utc=1775808000&lutc=1775820000' + ); + }); + it('preserves catchup metadata when storing parsed playlist items', () => { const playlist = createPlaylistObject('Catchup playlist', { header: { diff --git a/libs/shared/m3u-utils/src/lib/catchup.utils.ts b/libs/shared/m3u-utils/src/lib/catchup.utils.ts index e65782a7e..612d14113 100644 --- a/libs/shared/m3u-utils/src/lib/catchup.utils.ts +++ b/libs/shared/m3u-utils/src/lib/catchup.utils.ts @@ -112,9 +112,13 @@ function getEpgProgramTimestampSeconds( Number(hour), Number(minute) ); + // Derive the sign from the string: for offsets like "-0030" the hour part + // is "-00", and Number("-00") === -0, so Math.sign() would drop the + // minutes' sign and yield a zero offset instead of -30 minutes. + const offsetSign = offsetHours.startsWith('-') ? -1 : 1; const offsetTotalMinutes = - Number(offsetHours) * 60 + - Math.sign(Number(offsetHours)) * Number(offsetMinutes); + offsetSign * + (Math.abs(Number(offsetHours)) * 60 + Number(offsetMinutes)); return Math.floor((utcMillis - offsetTotalMinutes * 60_000) / 1000); }