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>
95 lines
3.5 KiB
JavaScript
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 };
|