Files
kube-zap2xml/src/tvlistings.ts
Jef LeCompte ee8c32dfbb feat: support upto 15 days of listings
- change default timespan to 6
- parallize tv listing chunks

special thanks to @majortom9
https://github.com/jef/zap2xml/issues/41#issuecomment-3092418305
2025-07-19 13:33:07 -07:00

148 lines
4.1 KiB
TypeScript

import { config } from "./config.js";
export interface Program {
/** "title": "GMA3" */
title: string;
/** "id": "EP059182660025" */
id: string;
/** "tmsId": "EP059182660025" */
tmsId: string;
/** "shortDesc": "BIA performs; comic Zarna Garg; lifestyle contributor Lori Bergamotto; ABC News chief medical correspondent Dr. Tara Narula." */
shortDesc: string;
/** "season": "5" */
season: string;
/** "releaseYear": null */
releaseYear: string | null;
/** "episode": "217" */
episode: string;
/** "episodeTitle": null */
episodeTitle: string | null;
/** "seriesId": "SH05918266" */
seriesId: string;
/** "isGeneric": "0" */
isGeneric: string;
}
export interface Event {
/** "callSign": "KOMODT" */
callSign: string;
/** "duration": "60" */
duration: string;
/** "startTime": "2025-07-18T19:00:00Z" */
startTime: string;
/** "endTime": "2025-07-18T20:00:00Z" */
endTime: string;
/** "thumbnail": "p30687311_b_v13_aa" */
thumbnail: string;
/** "channelNo": "4.1" */
channelNo: string;
/** "filter": ["filter-news"] */
filter: string[];
/** "seriesId": "SH05918266" */
seriesId: string;
/** "rating": "TV-PG" */
rating: string;
/** "flag": ["New"] */
flag: string[];
/** "tags": ["Stereo", "CC"] */
tags: string[];
/** "program": {...} */
program: Program;
}
export interface Channel {
/** "callSign": "KOMODT" */
callSign: string;
/** "affiliateName": "AMERICAN BROADCASTING COMPANY" */
affiliateName: string;
/** "affiliateCallSign": "null" */
affiliateCallSign: string | null;
/** "channelId": "19629" */
channelId: string;
/** "channelNo": "4.1" */
channelNo: string;
/** "events": [...] */
events: Event[];
/** "id": "196290" */
id: string;
/** "stationGenres": [false] */
stationGenres: boolean[];
/** "stationFilters": ["filter-news", "filter-talk"] */
stationFilters: string[];
/** "thumbnail": "//zap2it.tmsimg.com/h3/NowShowing/19629/s28708_ll_h15_ac.png?w=55" */
thumbnail: string;
}
export interface GridApiResponse {
/** "channels": [...] */
channels: Channel[];
}
function buildUrl(time: number, timespan: number): string {
const params = {
lineupId: config.lineupId,
timespan: timespan.toString(),
headendId: "lineupId",
country: config.country,
timezone: config.timezone,
postalCode: config.postalCode,
isOverride: "true",
pref: config.pref + "16,128" || "16,128",
aid: "orbebb",
languagecode: "en-us",
time: time.toString(),
};
const urlParams = new URLSearchParams(params).toString();
return `${config.baseUrl}?${urlParams}`;
}
export async function getTVListings(): Promise<GridApiResponse> {
console.log("Fetching TV listings");
const totalHours = parseInt(config.timespan, 10);
const chunkHours = 6; // Gracenote allows up to 6 hours per request
const now = Math.floor(Date.now() / 1000); // Current time in UNIX timestamp
const channelsMap: Map<string, Channel> = new Map();
const fetchPromises: Promise<void>[] = [];
for (let offset = 0; offset < totalHours; offset += chunkHours) {
const time = now + offset * 3600;
const url = buildUrl(time, chunkHours);
const fetchPromise = fetch(url, {
headers: {
"User-Agent": config.userAgent || "",
},
}).then(async (response) => {
if (!response.ok) {
throw new Error(
`Failed to fetch: ${response.status} ${response.statusText}`
);
}
const chunkData = (await response.json()) as GridApiResponse;
for (const newChannel of chunkData.channels) {
if (!channelsMap.has(newChannel.channelId)) {
// Clone channel with its events
channelsMap.set(newChannel.channelId, {
...newChannel,
events: [...newChannel.events],
});
} else {
const existingChannel = channelsMap.get(newChannel.channelId)!;
existingChannel.events.push(...newChannel.events);
}
}
});
fetchPromises.push(fetchPromise);
}
await Promise.all(fetchPromises);
return { channels: Array.from(channelsMap.values()) };
}