From ee8c32dfbb319225b181e8c0d956a56e8473d8cd Mon Sep 17 00:00:00 2001 From: Jef LeCompte Date: Sat, 19 Jul 2025 13:32:56 -0700 Subject: [PATCH] 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 --- README.md | 42 +++++++++++++++++------------------ src/config.ts | 2 +- src/index.ts | 2 +- src/tvlistings.ts | 56 +++++++++++++++++++++++++++++++++++------------ 4 files changed, 65 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 5efcdf3..f487b6e 100644 --- a/README.md +++ b/README.md @@ -44,27 +44,27 @@ See [Environment variables](#environment-variables) for configuration options. ### Environment variables -| Variable | Description | Type | Default | -| ------------- | --------------------------------------------------------------------------------------------------------------- | ------- | -------------------------------- | -| `LINEUP_ID` | Lineup ID; Read more in the [Wiki](https://github.com/jef/zap2xml/wiki/Retrieving-Lineup-ID) | String | `USA-lineupId-DEFAULT` (Attenna) | -| `TIMESPAN` | Either 3 or 6 hours of shows | Integer | 3 | -| `PREF` | User Preferences, comma separated list. `m` for showing music, `p` for showing pay-per-view, `h` for showing HD | String | (empty) | -| `COUNTRY` | Country code (default: `US`) | String | US | -| `POSTAL_CODE` | Postal code of where shows are available. | Integer | 30309 | -| `USER_AGENT` | Custom user agent string for HTTP requests. | String | Uses random if not specified | -| `TZ` | Timezone | String | System default | -| `SLEEP_TIME` | Sleep time before next run in seconds (default: 10800, Only used with Docker.) | Integer | 10800 | -| `OUTPUT_FILE` | Output file name (default: xmltv.xml) | String | xmltv.xml | +| Variable | Description | Type | Default | +| ------------- | --------------------------------------------------------------------------------------------------------------- | ------ | -------------------------------- | +| `LINEUP_ID` | Lineup ID; Read more in the [Wiki](https://github.com/jef/zap2xml/wiki/Retrieving-Lineup-ID) | String | `USA-lineupId-DEFAULT` (Attenna) | +| `TIMESPAN` | Either 3 or 6 hours of shows | String | 3 | +| `PREF` | User Preferences, comma separated list. `m` for showing music, `p` for showing pay-per-view, `h` for showing HD | String | (empty) | +| `COUNTRY` | Country code (default: `US`) | String | US | +| `POSTAL_CODE` | Postal code of where shows are available. | String | 30309 | +| `USER_AGENT` | Custom user agent string for HTTP requests. | String | Uses random if not specified | +| `TZ` | Timezone | String | System default | +| `SLEEP_TIME` | Sleep time before next run in seconds (default: 10800, Only used with Docker.) | String | 10800 | +| `OUTPUT_FILE` | Output file name (default: xmltv.xml) | String | xmltv.xml | ### Command line arguments -| Argument | Description | Type | Default | -| -------------- | --------------------------------------------------------------------------------------------------------------- | ------- | -------------------------------- | -| `--lineupId` | Lineup ID; Read more in the [Wiki](https://github.com/jef/zap2xml/wiki/Retrieving-Lineup-ID) | String | `USA-lineupId-DEFAULT` (Attenna) | -| `--timespan` | Either 3 or 6 hours of shows | Integer | 3 | -| `--pref` | User Preferences, comma separated list. `m` for showing music, `p` for showing pay-per-view, `h` for showing HD | String | (empty) | -| `--country` | Country code (default: `US`) | String | US | -| `--postalCode` | Postal code of where shows are available. | Integer | 30309 | -| `--userAgent` | Custom user agent string for HTTP requests. | String | Uses random if not specified | -| `--timezone` | Timezone | String | System default | -| `--outputFile` | Output file name (default: xmltv.xml) | String | xmltv.xml | +| Argument | Description | Type | Default | +| -------------- | --------------------------------------------------------------------------------------------------------------- | ------ | -------------------------------- | +| `--lineupId` | Lineup ID; Read more in the [Wiki](https://github.com/jef/zap2xml/wiki/Retrieving-Lineup-ID) | String | `USA-lineupId-DEFAULT` (Attenna) | +| `--timespan` | Either 3 or 6 hours of shows | String | 3 | +| `--pref` | User Preferences, comma separated list. `m` for showing music, `p` for showing pay-per-view, `h` for showing HD | String | (empty) | +| `--country` | Country code (default: `US`) | String | US | +| `--postalCode` | Postal code of where shows are available. | String | 30309 | +| `--userAgent` | Custom user agent string for HTTP requests. | String | Uses random if not specified | +| `--timezone` | Timezone | String | System default | +| `--outputFile` | Output file name (default: xmltv.xml) | String | xmltv.xml | diff --git a/src/config.ts b/src/config.ts index ed6a37f..af855b9 100644 --- a/src/config.ts +++ b/src/config.ts @@ -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] || diff --git a/src/index.ts b/src/index.ts index 1d8e46d..7ec504a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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) diff --git a/src/tvlistings.ts b/src/tvlistings.ts index 701b9ca..2841313 100644 --- a/src/tvlistings.ts +++ b/src/tvlistings.ts @@ -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 { 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 = new Map(); - const response = await fetch(url, { - headers: { - "User-Agent": config.userAgent || "", - }, - }); + const fetchPromises: Promise[] = []; - 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()) }; }