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
67 changes: 67 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ jobs:
- name: Lint
run: bun run lint

- name: Check formatting
run: bun run format:check

- name: Unit tests
run: bun run test:run

- name: Integration tests
run: |
bun dist/index.js --lineupId=USA-DITV751-X --timespan=3 --postalCode=80020 --outputFile=dtv.xml
Expand All @@ -45,3 +51,64 @@ jobs:
echo "DTV and OTA outputs are the same, which is unexpected."
exit 1
fi

- name: Merge integration test
run: |
cat > ci-listings.yml <<'YAML'
defaults:
timespan: 3
country: USA
postalCode: "80020"
listings:
- name: dtv
lineupId: USA-DITV751-X
outputFile: merge-dtv.xml
- name: ota
lineupId: USA-OTA80020
outputFile: merge-ota.xml
merge:
enabled: true
outputFile: merged.xml
YAML

bun dist/index.js --config=ci-listings.yml

for f in merge-dtv.xml merge-ota.xml merged.xml; do
if [ ! -s "$f" ]; then
echo "Expected $f to be written and non-empty."
exit 1
fi
done

# The merged file must hold at least as many channels as either input.
dtv=$(grep -c '<channel id=' merge-dtv.xml)
ota=$(grep -c '<channel id=' merge-ota.xml)
merged=$(grep -c '<channel id=' merged.xml)
echo "channels: dtv=$dtv ota=$ota merged=$merged"

if [ "$merged" -lt "$dtv" ] || [ "$merged" -lt "$ota" ]; then
echo "Merged output has fewer channels than an input file."
exit 1
fi

# Channel IDs must be unique after merging.
dupes=$(grep -o '<channel id="[^"]*"' merged.xml | sort | uniq -d | wc -l)
if [ "$dupes" -ne 0 ]; then
echo "Merged output contains $dupes duplicate channel id(s)."
exit 1
fi

# Programmes must be unique per channel/start pair.
progdupes=$(grep -o '<programme start="[^"]*" stop="[^"]*" channel="[^"]*"' merged.xml \
| sed -E 's/.*start="([^"]*)".*channel="([^"]*)"/\2|\1/' | sort | uniq -d | wc -l)
if [ "$progdupes" -ne 0 ]; then
echo "Merged output contains $progdupes duplicate channel/start programme(s)."
exit 1
fi

# Output must be well-formed XML.
if command -v xmllint >/dev/null 2>&1; then
xmllint --noout merged.xml
else
echo "xmllint unavailable; skipping well-formedness check."
fi
5 changes: 4 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
{}
{
"tabWidth": 4,
"printWidth": 120
}
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,37 @@ Need help? See [Finding a lineup](https://github.com/JCBird1012/zap2xml/wiki/Fin

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.

## Merging listings into one file

Add `--merge` to also write every listing combined into a single XMLTV file. Channels are deduplicated by channel ID, and programmes by channel and start time, so stations carried by more than one lineup appear once.

```yaml
listings:
- name: dtv
lineupId: USA-DITV751-X
postalCode: "80020"
outputFile: dtv.xml
- name: ota
lineupId: USA-OTA80020
postalCode: "80020"
outputFile: ota.xml

merge:
enabled: true
outputFile: merged.xml
keepIndividual: true
dedupe: first
```

| Option | Env | Description |
| --- | --- | --- |
| `--merge` | `MERGE` | Write all listings combined into one file |
| `--mergeOutputFile=FILE` | `MERGE_OUTPUT_FILE` | Merged file name (implies `--merge`, default `merged.xml`) |
| `--mergeDedupe=MODE` | `MERGE_DEDUPE` | Which duplicate to keep: `first` (default) or `last` |
| `--noKeepIndividual` | `MERGE_KEEP_INDIVIDUAL=false` | Only write the merged file, skipping per-listing files |

By default the per-listing files are still written alongside the merged one. This works in single-listing mode too, though with one listing the merged file is just a copy.

## Recent Updates

### (2025-08-20)
Expand Down Expand Up @@ -66,3 +97,4 @@ Use `--config=path/to/listings.yml` to fetch multiple TV lineups in a single run

* 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))
* `--merge` combines all listings into one deduplicated XMLTV file, so you don't need a separate merge step for multi-lineup setups
28 changes: 14 additions & 14 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@ import tseslint from "typescript-eslint";
import eslintConfigPrettier from "eslint-config-prettier/flat";

export default defineConfig([
{
files: ["**/*.{js,mjs,cjs,ts,mts,cts}"],
plugins: { js },
extends: ["js/recommended"],
},
{
files: ["**/*.{js,mjs,cjs,ts,mts,cts}"],
languageOptions: { globals: globals.browser },
},
tseslint.configs.recommended,
eslintConfigPrettier,
{
ignores: ["dist"],
},
{
files: ["**/*.{js,mjs,cjs,ts,mts,cts}"],
plugins: { js },
extends: ["js/recommended"],
},
{
files: ["**/*.{js,mjs,cjs,ts,mts,cts}"],
languageOptions: { globals: globals.browser },
},
tseslint.configs.recommended,
eslintConfigPrettier,
{
ignores: ["dist"],
},
]);
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
"dev": "bun src/index.ts",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write \"**/*.{ts,mts,cts,js,mjs,cjs}\"",
"format:check": "prettier --check \"**/*.{ts,mts,cts,js,mjs,cjs}\"",
"typecheck": "tsc --noEmit",
"build": "bun run typecheck && rollup --config rollup.config.ts --configPlugin @rollup/plugin-typescript"
},
Expand Down
16 changes: 8 additions & 8 deletions rollup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import nodeResolve from "@rollup/plugin-node-resolve";
import typescript from "@rollup/plugin-typescript";

const config = {
input: "src/index.ts",
output: {
esModule: true,
file: "dist/index.js",
format: "es",
sourcemap: true,
},
plugins: [typescript(), nodeResolve({ preferBuiltins: true }), commonjs()],
input: "src/index.ts",
output: {
esModule: true,
file: "dist/index.js",
format: "es",
sourcemap: true,
},
plugins: [typescript(), nodeResolve({ preferBuiltins: true }), commonjs()],
};

export default config;
Loading
Loading