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

@@ -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 |

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()) };
}