Files
NodecastV2/server/services/xmltvDate.js
Technomancer702 4ed22af9e5 Preserve the guide until a complete EPG refresh can activate
Adapted from MikaelKW/nodecast-tv-plus commits c267d98d734753a09b08d267c8ec2c3fa11c33f5, b761690bac7626a3cc5ddf754d96b14a02f506fe, and 5524fdaffed5c6a44ae69474e7eaa2f18d428c75. Preserve every parser batch, decode gzip once, and retain live channel metadata.

Co-authored-by: Mikael <mikaelkwessel@gmail.com>
2026-10-12 16:16:48 -07:00

95 lines
3.5 KiB
JavaScript

function createValidatedCalendarDate(year, month, day, hour, minute, second) {
if (
month < 1 || month > 12 ||
day < 1 || day > 31 ||
hour < 0 || hour > 23 ||
minute < 0 || minute > 59 ||
second < 0 || second > 59
) {
return null;
}
// setUTCFullYear avoids Date.UTC's special handling of years 0-99.
const calendarDate = new Date(0);
calendarDate.setUTCFullYear(year, month - 1, day);
calendarDate.setUTCHours(hour, minute, second, 0);
// Date normalizes impossible calendar values (for example February 30),
// so compare every component before accepting it.
if (
calendarDate.getUTCFullYear() !== year ||
calendarDate.getUTCMonth() !== month - 1 ||
calendarDate.getUTCDate() !== day ||
calendarDate.getUTCHours() !== hour ||
calendarDate.getUTCMinutes() !== minute ||
calendarDate.getUTCSeconds() !== second
) {
return null;
}
return calendarDate;
}
function parseTimezoneOffset(timezone) {
if (!timezone || timezone.toUpperCase() === 'Z') return 0;
const normalized = timezone.replace(':', '');
const offsetHours = Number(normalized.slice(1, 3));
const offsetRemainder = Number(normalized.slice(3, 5));
if (offsetHours > 23 || offsetRemainder > 59) return null;
const total = (offsetHours * 60) + offsetRemainder;
return normalized[0] === '-' ? -total : total;
}
/**
* Parse an XMLTV date/time. XMLTV permits YYYYMMDDHHmmss or a shorter
* initial precision (for example YYYYMMDDHHmm, YYYYMMDDHH, or YYYYMMDD).
* Missing components use the earliest value in that precision and UTC is
* assumed when no numeric offset is provided.
* @param {string} dateStr - XMLTV format date string
* @returns {Date|null}
*/
function parseXmltvDate(dateStr) {
if (!dateStr) return null;
const value = String(dateStr).trim();
const match = value.match(/^(\d{4}(?:\d{2}){0,5})\s*(Z|[+-]\d{4})?$/i);
if (!match) {
// Preserve the existing compatibility fallback for explicit ISO 8601
// timestamps without accepting arbitrary host-dependent date strings.
const isoMatch = value.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d{1,3})?)?(Z|[+-]\d{2}:?\d{2})$/i);
if (!isoMatch) return null;
const [, year, month, day, hour, minute, second = '0', timezone] = isoMatch;
if (!createValidatedCalendarDate(
Number(year),
Number(month),
Number(day),
Number(hour),
Number(minute),
Number(second)
) || parseTimezoneOffset(timezone) === null) return null;
const isoDate = new Date(value);
return Number.isNaN(isoDate.getTime()) ? null : isoDate;
}
const digits = match[1];
const timezone = match[2]?.toUpperCase();
const year = Number(digits.slice(0, 4));
const month = digits.length >= 6 ? Number(digits.slice(4, 6)) : 1;
const day = digits.length >= 8 ? Number(digits.slice(6, 8)) : 1;
const hour = digits.length >= 10 ? Number(digits.slice(8, 10)) : 0;
const minute = digits.length >= 12 ? Number(digits.slice(10, 12)) : 0;
const second = digits.length >= 14 ? Number(digits.slice(12, 14)) : 0;
const calendarDate = createValidatedCalendarDate(year, month, day, hour, minute, second);
const offsetMinutes = parseTimezoneOffset(timezone);
if (!calendarDate || offsetMinutes === null) return null;
return new Date(calendarDate.getTime() - (offsetMinutes * 60 * 1000));
}
module.exports = { parseXmltvDate };