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 <serega05@gmail.com>
This commit is contained in:
Salemand4gray authored and GitHub committed 2026-07-21 09:08:37 +02:00
1 parent d308749e2c
commit e53adcbeaf
2 files changed
+74 -2

No files matched your search

@@ -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: {
@@ -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);
}