Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 23 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,57 +6,63 @@ Automate TV guides to XMLTV format. Easy to use, up-to-date. See below for getti

A version of the original Perl implementation is preserved in the [historical-perl branch](https://github.com/JCBird1012/zap2xml/tree/historical-perl) if you're interested in that.

## First Time [Installation in node.js](https://github.com/JCBird1012/zap2xml/wiki/Installation), [How to Run](https://github.com/JCBird1012/zap2xml/wiki/How-to-Run), [Scheduling](https://github.com/JCBird1012/zap2xml/wiki/Scheduling) or [using Docker](https://github.com/JCBird1012/zap2xml/wiki/Using-Docker) see [Wiki](https://github.com/JCBird1012/zap2xml/wiki) for instructions
## Getting started

### Need help? [Finding a lineup](https://github.com/JCBird1012/zap2xml/wiki/Finding-a-Lineup-ID) or for [Dish and DirecTV lineups](https://github.com/JCBird1012/zap2xml/wiki/US-Dish-Directv-Lineups). Other help? Drop a line in the [Discussions](https://github.com/JCBird1012/zap2xml/discussions)
See the [Wiki](https://github.com/JCBird1012/zap2xml/wiki) for guides on [Installation in node.js](https://github.com/JCBird1012/zap2xml/wiki/Installation), [How to Run](https://github.com/JCBird1012/zap2xml/wiki/How-to-Run), [Scheduling](https://github.com/JCBird1012/zap2xml/wiki/Scheduling), and [using Docker](https://github.com/JCBird1012/zap2xml/wiki/Using-Docker).

# Recent updates
Need help? See [Finding a lineup](https://github.com/JCBird1012/zap2xml/wiki/Finding-a-Lineup-ID) or [Dish and DirecTV lineups](https://github.com/JCBird1012/zap2xml/wiki/US-Dish-Directv-Lineups), or drop a line in the [Discussions](https://github.com/JCBird1012/zap2xml/discussions).

# (2025-08-20)
## Multi-listing YAML config

Use `--config=path/to/listings.yml` to fetch multiple TV lineups in a single run. See the [Multi-Listing Config](https://github.com/JCBird1012/zap2xml/wiki/Multi-Listing-Config) wiki page for the full YAML format, precedence rules, and Docker setup.

## Recent Updates

### (2025-08-20)

* Changed default Sort option to the Channel Number in Lineup if available
* Added '--stationid' to sort the previous way by StationID
* Added `--sortname` to sort by the Call Sign

# (2025-08-18)
### (2025-08-18)

* Changed URL pull to match output from page when stopped working
* Added Display Name with Channel Number and Call Sign to mirror previous Perl Script
* Added `--nextpvr` option to list Channel Number Call Sign first

# (2025-08-09)
### (2025-08-09)

* Restored `<episode-num system="dd_progid">` tag that Plex uses that was missing.
* Fixed Sorting so output is listed by Channel ID (common station/gracenote id) then by date/time.

# (2025-08-07)
### (2025-08-07)

* Reordered Program fields to match original Perl script output
* `--postalCode` not required as long as Country and lineup Id correct except Over the Air
* Moved `<date>` above `<category>` to match original Perl output. Corrected where Movie Release Year is properly displayed.
* Added `<length>` tag.
* Updated channel logo no longer has fixed width so can display in better quality

# (2025-08-06)
### (2025-08-06)

* Added Valid Country Codes that can be used
* Added `--mediaportal` option to use `<episode-num system="xmltv_ns">` before others so Media Portal will display Season/Episode properly

# (2025-08-05)

# Changes since previous release

These changes are currently on the [jesmannstl/zap2xml](https://github.com/jesmannstl/zap2xml) fork
### Changes from jef's [original](https://github.com/jef/zap2xml) (by [jesmannstl](https://github.com/jesmannstl/zap2xml))

* Added Category if available (Movie, Sports, News, Talk, Family etc)
* Added Category "Series" to all programs that did not return a category
* Added additional Season Episode formats for various players
* Added year as Season for programs that only list an episode number like daily cable news
* Added <date> tag to all programs without an aired date normalized to America/New York
* Added `<date>` tag to all programs without an aired date normalized to America/New York
* Added xmltv_ns with the date aired as Season YYYY Episode MMYY to Non Movie or Sports with no other Season/Episode like local news so would have the ability to record as Series is most players.
* Added URL to program details from old Perl function.
* Added --appendAsterisk to add * to title on programs that are New and/or Live
* Added <previously-shown /> tag to programs that are not <New> and/or <Live>
* Updated affiliateId after orbebb stopped working
* Added `<previously-shown />` tag to programs that are not `<New>` and/or `<Live>`
* Updated `affiliateId` after `orbebb` stopped working
* Updated Docker with these changes use APPEND_ASTERISK: TRUE for the --appendAsterisk option

### Changes in this fork from jessman's [fork](https://github.com/jesmannstl/zap2xml)

Updated Docker with these changes use APPEND_ASTERISK: TRUE for the --appendAsterisk option
* Docker image uses [Bun](https://bun.com) runtime instead of Node.js - smaller image, and probably some (likely unnoticeable with typical use) performance/memory usage improvements!
* Ability to fetch multiple lineups using a single `zap2xml` instance - no longer have to run multiple containers to fetch multiple lineups! (see [Multi-Listing Config](https://github.com/JCBird1012/zap2xml/wiki/Multi-Listing-Config))
75 changes: 37 additions & 38 deletions bun.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"vitest": "^3.2.4"
},
"dependencies": {
"commander": "^11.1.0"
"valibot": "^1.4.0",
"yaml": "^2.9.0"
}
}
193 changes: 190 additions & 3 deletions src/config.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,56 @@
import { describe, it, expect } from "vitest";
import { processLineupId, getHeadendId } from "./config.js";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { getConfig, getHeadendId, processLineupId } from "./config.js";

const originalArgv = [...process.argv];
const trackedEnvVars = [
"APPEND_ASTERISK",
"CONFIG_FILE",
"COUNTRY",
"LINEUP_ID",
"MEDIA_PORTAL",
"NEXTPVR",
"OUTPUT_FILE",
"POSTAL_CODE",
"PREF",
"SORTNAME",
"STATIONID",
"TIMESPAN",
"TZ",
"USER_AGENT",
];

const tempDirs: string[] = [];

function clearEnv(): void {
for (const key of trackedEnvVars) {
delete process.env[key];
}
}

function createConfigFile(contents: string): string {
const dir = mkdtempSync(join(tmpdir(), "zap2xml-config-"));
const filePath = join(dir, "listings.yml");
writeFileSync(filePath, contents, { encoding: "utf-8" });
tempDirs.push(dir);
return filePath;
}

beforeEach(() => {
process.argv = [...originalArgv];
clearEnv();
});

afterEach(() => {
process.argv = [...originalArgv];
clearEnv();

for (const dir of tempDirs.splice(0)) {
rmSync(dir, { recursive: true, force: true });
}
});

describe("processLineupId", () => {
it("returns env LINEUP_ID if set", () => {
Expand All @@ -11,7 +62,12 @@ describe("processLineupId", () => {
it("returns argv --lineupId if set", () => {
process.argv.push("--lineupId=USA-54321");
expect(processLineupId()).toBe("USA-54321");
process.argv = process.argv.filter((arg) => !arg.startsWith("--lineupId="));
});

it("prefers argv --lineupId over env LINEUP_ID", () => {
process.env.LINEUP_ID = "USA-11111";
process.argv.push("--lineupId=USA-54321");
expect(processLineupId()).toBe("USA-54321");
});

it("returns default if nothing set", () => {
Expand All @@ -38,3 +94,134 @@ describe("getHeadendId", () => {
expect(getHeadendId("")).toBe("lineup");
});
});

describe("getConfig", () => {
it("merges YAML defaults, listing overrides, env, and CLI overrides", () => {
const configFile = createConfigFile(`
defaults:
timespan: 72
country: USA
appendAsterisk: true
userAgent: Config User Agent

listings:
- name: atlanta
lineupId: USA-GA42500-X
postalCode: "30309"
outputFile: atlanta.xml

- name: ottawa
lineupId: CAN-0008861-X
country: CAN
postalCode: K1A0B1
timespan: 48
outputFile: ottawa.xml
`);

process.env.USER_AGENT = "Env User Agent";
process.argv.push(`--config=${configFile}`, "--timespan=24", "--sortname");

const config = getConfig();

expect(config.configFile).toBe(configFile);
expect(config.listings).toHaveLength(2);
expect(config.listings[0]).toMatchObject({
name: "atlanta",
country: "USA",
timespan: "24",
appendAsterisk: true,
sortname: true,
userAgent: "Env User Agent",
outputFile: "atlanta.xml",
});
expect(config.listings[1]).toMatchObject({
name: "ottawa",
country: "CAN",
timespan: "24",
appendAsterisk: true,
sortname: true,
userAgent: "Env User Agent",
outputFile: "ottawa.xml",
});
});

it("coerces YAML scalar values and applies listing overrides", () => {
const configFile = createConfigFile(`
defaults:
timespan: 72
postalCode: 30309
appendAsterisk: true
mediaportal: false

listings:
- lineupId: USA-GA42500-X
outputFile: atlanta.xml
timespan: 48
stationid: true
`);

process.argv.push(`--config=${configFile}`);

const config = getConfig();

expect(config.listings).toHaveLength(1);
expect(config.listings[0]).toMatchObject({
lineupId: "USA-GA42500-X",
country: "USA",
postalCode: "30309",
timespan: "48",
appendAsterisk: true,
mediaportal: false,
stationid: true,
outputFile: "atlanta.xml",
});
});

it("rejects listing-specific CLI overrides when using YAML", () => {
const configFile = createConfigFile(`
listings:
- lineupId: USA-GA42500-X
country: USA
postalCode: "30309"
outputFile: atlanta.xml
`);

process.argv.push(`--config=${configFile}`, "--lineupId=USA-OVERRIDE");

expect(() => getConfig()).toThrow("Cannot combine --config with listing-specific overrides");
});

it("rejects duplicate output files in multi-listing mode", () => {
const configFile = createConfigFile(`
defaults:
outputFile: xmltv.xml

listings:
- lineupId: USA-GA42500-X
country: USA
postalCode: "30309"
- lineupId: CAN-0008861-X
country: CAN
postalCode: K1A0B1
`);

process.argv.push(`--config=${configFile}`);

expect(() => getConfig()).toThrow("Multiple listings resolve to the same outputFile");
});

it("rejects invalid YAML field values", () => {
const configFile = createConfigFile(`
listings:
- lineupId: USA-GA42500-X
country: USA
postalCode: "30309"
outputFile: atlanta.xml
appendAsterisk: maybe
`);

process.argv.push(`--config=${configFile}`);

expect(() => getConfig()).toThrow(/Invalid config file .*Expected \(boolean \| "" \| string\)/);
});
});
Loading
Loading