mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 18:36:15 -08:00
156 lines
4.0 KiB
TypeScript
156 lines
4.0 KiB
TypeScript
import { Channel, EpgProgram } from 'shared-interfaces';
|
|
|
|
const XMLTV_TIMESTAMP_PATTERN =
|
|
/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})\s*([+-]\d{2})(\d{2})$/;
|
|
|
|
type CatchupSupportMode = 'none' | 'source' | 'shift';
|
|
|
|
export function getM3uArchiveDays(
|
|
channel: Pick<Channel, 'tvg' | 'timeshift' | 'catchup'> | null | undefined
|
|
): number {
|
|
const value = getFirstNonBlankValue(
|
|
channel?.catchup?.days,
|
|
channel?.timeshift,
|
|
channel?.tvg?.rec
|
|
);
|
|
return Math.max(0, Number(value ?? 0) || 0);
|
|
}
|
|
|
|
export function isM3uCatchupPlaybackSupported(
|
|
channel: Channel | null | undefined
|
|
): boolean {
|
|
return getM3uCatchupSupportMode(channel) !== 'none';
|
|
}
|
|
|
|
export function resolveM3uCatchupUrl(
|
|
channel: Channel | null | undefined,
|
|
program: Pick<EpgProgram, 'start' | 'startTimestamp'>,
|
|
nowTimestampSeconds = Math.floor(Date.now() / 1000)
|
|
): string | null {
|
|
const supportMode = getM3uCatchupSupportMode(channel);
|
|
if (supportMode === 'none') {
|
|
return null;
|
|
}
|
|
|
|
const startTimestamp = getEpgProgramTimestampSeconds(
|
|
program.start,
|
|
program.startTimestamp
|
|
);
|
|
if (startTimestamp === null) {
|
|
return null;
|
|
}
|
|
|
|
const playbackBaseUrl =
|
|
supportMode === 'source'
|
|
? channel?.catchup?.source
|
|
: channel?.url;
|
|
if (!playbackBaseUrl?.trim()) {
|
|
return null;
|
|
}
|
|
|
|
const normalizedNow =
|
|
Number.isFinite(nowTimestampSeconds) && nowTimestampSeconds > 0
|
|
? Math.floor(nowTimestampSeconds)
|
|
: Math.floor(Date.now() / 1000);
|
|
|
|
return setCatchupQueryParams(playbackBaseUrl, {
|
|
utc: startTimestamp,
|
|
lutc: normalizedNow,
|
|
});
|
|
}
|
|
|
|
function getM3uCatchupSupportMode(
|
|
channel: Channel | null | undefined
|
|
): CatchupSupportMode {
|
|
if (getM3uArchiveDays(channel) <= 0) {
|
|
return 'none';
|
|
}
|
|
|
|
const catchupSource = channel?.catchup?.source?.trim() ?? '';
|
|
if (isHttpUrl(catchupSource)) {
|
|
return 'source';
|
|
}
|
|
|
|
const streamUrl = channel?.url?.trim() ?? '';
|
|
const catchupType = channel?.catchup?.type?.trim().toLowerCase() ?? '';
|
|
if (catchupType === 'shift' && isHttpUrl(streamUrl)) {
|
|
return 'shift';
|
|
}
|
|
|
|
if (!catchupType && isHttpUrl(streamUrl)) {
|
|
return 'shift';
|
|
}
|
|
|
|
return 'none';
|
|
}
|
|
|
|
function getEpgProgramTimestampSeconds(
|
|
dateValue: string,
|
|
unixTimestampValue?: number | string | null
|
|
): number | null {
|
|
const unixTimestamp = Number.parseInt(String(unixTimestampValue ?? ''), 10);
|
|
if (Number.isFinite(unixTimestamp) && unixTimestamp > 0) {
|
|
return unixTimestamp;
|
|
}
|
|
|
|
const parsed = Date.parse(dateValue);
|
|
if (Number.isFinite(parsed)) {
|
|
return Math.floor(parsed / 1000);
|
|
}
|
|
|
|
const match = dateValue.match(XMLTV_TIMESTAMP_PATTERN);
|
|
if (!match) {
|
|
return null;
|
|
}
|
|
|
|
const [, year, month, day, hour, minute, offsetHours, offsetMinutes] =
|
|
match;
|
|
const utcMillis = Date.UTC(
|
|
Number(year),
|
|
Number(month) - 1,
|
|
Number(day),
|
|
Number(hour),
|
|
Number(minute)
|
|
);
|
|
const offsetTotalMinutes =
|
|
Number(offsetHours) * 60 +
|
|
Math.sign(Number(offsetHours)) * Number(offsetMinutes);
|
|
|
|
return Math.floor((utcMillis - offsetTotalMinutes * 60_000) / 1000);
|
|
}
|
|
|
|
function setCatchupQueryParams(
|
|
rawUrl: string,
|
|
params: Record<'utc' | 'lutc', number>
|
|
): string | null {
|
|
try {
|
|
const url = new URL(rawUrl.trim());
|
|
url.searchParams.set('utc', String(params.utc));
|
|
url.searchParams.set('lutc', String(params.lutc));
|
|
return url.toString();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function isHttpUrl(value: string): boolean {
|
|
if (!value) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
const url = new URL(value);
|
|
return url.protocol === 'http:' || url.protocol === 'https:';
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function getFirstNonBlankValue(
|
|
...values: Array<string | null | undefined>
|
|
): string | undefined {
|
|
return values.find(
|
|
(value): value is string => value != null && value.trim() !== ''
|
|
);
|
|
}
|