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
This commit is contained in:
Jef LeCompte
2025-07-19 13:32:56 -07:00
parent 51f66a1b26
commit ee8c32dfbb
4 changed files with 65 additions and 37 deletions

View File

@@ -9,7 +9,7 @@ export const config = {
timespan:
process.env["TIMESPAN"] ||
process.argv.find((arg) => arg.startsWith("--timespan="))?.split("=")[1] ||
"3",
"6",
country:
process.env["COUNTRY"] ||
process.argv.find((arg) => arg.startsWith("--country="))?.split("=")[1] ||

View File

@@ -11,7 +11,7 @@ Usage: node dist/index.js [options]
Options:
--help Show this help message
--lineupId=ID Lineup ID (default: USA-lineupId-DEFAULT)
--timespan=NUM Timespan in hours (default: 3)
--timespan=NUM Timespan in hours (up to 360 = 15 days, default: 6)
--pref=LIST User preferences, comma separated. Can be m, p, and h (default: empty)'
--country=CON Country code (default: USA)
--postalCode=ZIP Postal code (default: 30309)

View File

@@ -78,10 +78,10 @@ export interface GridApiResponse {
channels: Channel[];
}
function buildUrl() {
function buildUrl(time: number, timespan: number): string {
const params = {
lineupId: config.lineupId,
timespan: config.timespan,
timespan: timespan.toString(),
headendId: "lineupId",
country: config.country,
timezone: config.timezone,
@@ -90,7 +90,7 @@ function buildUrl() {
pref: config.pref + "16,128" || "16,128",
aid: "orbebb",
languagecode: "en-us",
time: Math.floor(Date.now() / 1000).toString(),
time: time.toString(),
};
const urlParams = new URLSearchParams(params).toString();
@@ -101,19 +101,47 @@ function buildUrl() {
export async function getTVListings(): Promise<GridApiResponse> {
console.log("Fetching TV listings");
const url = buildUrl();
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 response = await fetch(url, {
headers: {
"User-Agent": config.userAgent || "",
},
});
const fetchPromises: Promise<void>[] = [];
if (!response.ok) {
throw new Error(
`Failed to fetch: ${response.status} ${response.statusText}`,
);
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);
}
return (await response.json()) as GridApiResponse;
await Promise.all(fetchPromises);
return { channels: Array.from(channelsMap.values()) };
}