Compare commits

...

10 Commits

Author SHA1 Message Date
Jef LeCompte
6e6fa26b75 fix: series and episode info
Some checks failed
Release / Build, tag, and release Docker image (push) Has been cancelled
Related to #58
2025-07-24 11:37:09 -07:00
github-actions[bot]
421d0ad616 chore(main): release 2.2.0 (#50) 2025-07-22 08:25:06 -07:00
Jef LeCompte
1696b15712 fix: headendId when OTA, add tests 2025-07-21 22:44:27 -07:00
Jef LeCompte
ad7aa8e581 fix: args work now (#54) 2025-07-21 22:30:56 -07:00
Jef LeCompte
e077f2721c feat: update rating, new, stereo, and cc
Based on https://github.com/jef/zap2xml/issues/47#issuecomment-3100061128 and https://github.com/jef/zap2xml/issues/47#issuecomment-3097884632
2025-07-21 19:28:30 -07:00
Jef LeCompte
3ab0370d72 fix: add thumbnails for programs
Part of #51 and #47
2025-07-21 09:06:17 -07:00
Jef LeCompte
60321a37e6 ci: clean up and conventions (#52) 2025-07-20 22:13:17 -07:00
Jef LeCompte
4ac37de08e docs: add FAQ 2025-07-20 14:08:27 -07:00
Jef LeCompte
b5cec7c951 docs: include so links to wiki, update SLEEP_TIME default
SLEEP_TIME is now 6 hours instead of 3 because of the default TIMESPAN of 6 hours.
2025-07-20 14:05:49 -07:00
github-actions[bot]
f52018fa62 chore(main): release 2.1.1 (#45)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-19 18:03:20 -07:00
24 changed files with 672 additions and 207 deletions

View File

@@ -1,27 +0,0 @@
---
name: 🐛 Bug report
about: Report a bug for this project
---
## Expected Behavior
<!-- Tell us what should happen -->
## Current Behavior
<!-- Tell us what happens instead of the expected behavior -->
## Steps to Reproduce
<!-- Provide a link to a live example, or an unambiguous set of steps to reproduce this bug. -->
<!-- Include code to reproduce, if relevant -->
## Environment
- OS:
</details>
## Logs
<!-- Provide a brief log -->

23
.github/ISSUE_TEMPLATE/bug.yaml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: 🐛 Bug Report
description: File a bug report.
title: "[Bug]: "
labels: ["bug"]
body:
- type: markdown
attributes:
value: Thanks for taking the time to fill out this bug report!
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Also tell us, what did you expect to happen?
placeholder: Tell us what you see!
value: "A bug happened!"
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: 💡 Have an idea for a new feature?
url: https://github.com/jef/zap2xml/discussions
about: Create a new idea discussion!
- name: 🙇 Need help?
url: https://github.com/jef/zap2xml/discussions
about: Create a new help discussion if it hasn't been asked before!

View File

@@ -1,12 +0,0 @@
---
name: 🚀 Feature request
about: Suggest a new idea
---
### Description
<!-- Describe the feature here. -->
### Possible solution
<!-- Describe the possible solution here. -->

View File

@@ -27,6 +27,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version-file: package.json
cache: npm
- name: Install dependencies
run: npm ci
@@ -37,5 +38,16 @@ jobs:
- name: Lint
run: npm run lint
- name: Test
- name: Unit tests
run: npm run test:run
- name: Integration tests
run: |
node dist/index.js --lineupId=USA-DITV751-X --timespan=3 --postalCode=80020 --outputFile=dtv.xml
node dist/index.js --lineupId=USA-OTA80020 --timespan=3 --postalCode=80020 --outputFile=ota.xml
# Error if they are the same
if cmp -s dtv.xml ota.xml; then
echo "DTV and OTA outputs are the same, which is unexpected."
exit 1
fi

19
.github/workflows/pr-lint.yaml vendored Normal file
View File

@@ -0,0 +1,19 @@
name: Pull Request Title Linter
on:
pull_request:
types:
- opened
- edited
- reopened
- synchronize
jobs:
pr_lint:
name: Lint pull request title
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Lint pull request title
uses: jef/conventional-commits-pr-action@v1

9
.gitignore vendored
View File

@@ -1,6 +1,9 @@
dist/
node_modules/
.idea
.vscode
node_modules
dist
compose.yaml
xmltv.xml

1
.prettierignore Normal file
View File

@@ -0,0 +1 @@
dist

1
.prettierrc Normal file
View File

@@ -0,0 +1 @@
{}

View File

@@ -1,5 +1,38 @@
# Changelog
## [2.2.0](https://github.com/jef/zap2xml/compare/v2.1.1...v2.2.0) (2025-07-22)
### Features
* update rating, new, stereo, and cc ([e077f27](https://github.com/jef/zap2xml/commit/e077f2721c78d278db14037776ebdeb4cdee660d))
### Bug Fixes
* add thumbnails for programs ([3ab0370](https://github.com/jef/zap2xml/commit/3ab0370d725c029d64441febb981eeec04f2e1ef))
* args work now ([#54](https://github.com/jef/zap2xml/issues/54)) ([ad7aa8e](https://github.com/jef/zap2xml/commit/ad7aa8e5815b80f6fcb5ae118f29281d481e03d1))
* headendId when OTA, add tests ([1696b15](https://github.com/jef/zap2xml/commit/1696b15712753039d896a6fcbe3145331f9b5b76))
### Continuous Integration
* clean up and conventions ([#52](https://github.com/jef/zap2xml/issues/52)) ([60321a3](https://github.com/jef/zap2xml/commit/60321a37e6410f120be4c8198d39896b8ebea017))
### Documentation
* add FAQ ([4ac37de](https://github.com/jef/zap2xml/commit/4ac37de08e6e4adaeb060465a246558bdc6c2bb7))
* include so links to wiki, update SLEEP_TIME default ([b5cec7c](https://github.com/jef/zap2xml/commit/b5cec7c951da794041820407860bcee8e0c5b24a))
## [2.1.1](https://github.com/jef/zap2xml/compare/v2.1.0...v2.1.1) (2025-07-19)
### Documentation
* fix defaults ([a655a3e](https://github.com/jef/zap2xml/commit/a655a3e84c2bc7191803d48d581c20b340f3c4e6))
* make ref to historical perl ([2756b07](https://github.com/jef/zap2xml/commit/2756b0766f9e62c85cd8b25178763819cfe8cc51))
## [2.1.0](https://github.com/jef/zap2xml/compare/v2.0.0...v2.1.0) (2025-07-19)

View File

@@ -8,6 +8,7 @@ COPY package-lock.json package-lock.json
RUN npm ci
COPY tsconfig.json tsconfig.json
COPY rollup.config.ts rollup.config.ts
COPY entrypoint.sh entrypoint.sh
COPY src/ src/

View File

@@ -6,10 +6,6 @@ I also _somewhat_ maintain a version of the original in the [historical-perl bra
## How to use
### Retrieving your Lineup ID
Visit the [Retrieving Lineup ID](https://github.com/jef/zap2xml/wiki/Retrieving-Lineup-ID) in the Wiki.
### Node.js
```bash
@@ -54,7 +50,7 @@ See [Environment variables](#environment-variables) for configuration options.
| `POSTAL_CODE` | Postal code of where shows are available. | 30309 |
| `USER_AGENT` | Custom user agent string for HTTP requests. | Uses random if not specified |
| `TZ` | Timezone | System default |
| `SLEEP_TIME` | Sleep time before next run in seconds (default: 10800, Only used with Docker.) | 10800 |
| `SLEEP_TIME` | Sleep time before next run in seconds (default: 21600, Only used with Docker.) | 21600 |
| `OUTPUT_FILE` | Output file name (default: xmltv.xml) | xmltv.xml |
### Command line arguments
@@ -69,3 +65,27 @@ See [Environment variables](#environment-variables) for configuration options.
| `--userAgent` | Custom user agent string for HTTP requests. | Uses random if not specified |
| `--timezone` | Timezone | System default |
| `--outputFile` | Output file name (default: xmltv.xml) | xmltv.xml |
## Setup and running in intervals
### Running natively
You can run zap2xml natively on your system. It is recommended to use a task scheduler to run it in intervals.
Here are some links to get you started on your machine:
- Linux and Raspberry Pi: https://github.com/jef/zap2xml/wiki/Running-on-Linux-and-Raspberry-Pi
- macOS: https://github.com/jef/zap2xml/wiki/Running-on-macOS
- Windows: https://github.com/jef/zap2xml/wiki/Running-on-Windows
If you want to run zap2xml in intervals, you can use a task scheduler like `cron` on Linux or the Task Scheduler on Windows. Each of the wiki pages above has a section on how to set up zap2xml to run in intervals.
### Running in Docker
You can run zap2xml in a Docker container. The `SLEEP_TIME` environment variable can be used to set the interval between runs. The default is 21600 seconds (6 hours).
## FAQ
### How do I get my Lineup ID?
Visit https://github.com/jef/zap2xml/wiki/Retrieving-Lineup-ID

View File

@@ -1,6 +1,6 @@
#!/bin/sh
SLEEP_TIME=${SLEEP_TIME:-10800}
SLEEP_TIME=${SLEEP_TIME:-21600}
while true; do
DATE=$(date)

View File

@@ -1,15 +1,22 @@
// @ts-check
import eslint from "@eslint/js";
import { defineConfig } from "eslint/config";
import js from "@eslint/js";
import globals from "globals";
import tseslint from "typescript-eslint";
import prettierConfig from "eslint-plugin-prettier/recommended";
import eslintConfigPrettier from "eslint-config-prettier/flat";
export default tseslint.config(
eslint.configs.recommended,
tseslint.configs.strict,
tseslint.configs.stylistic,
prettierConfig,
export default defineConfig([
{
ignores: ["dist/**", "node_modules/**"],
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"],
},
]);

370
package-lock.json generated
View File

@@ -1,18 +1,24 @@
{
"name": "@jef/zap2xml",
"version": "2.1.0",
"version": "2.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@jef/zap2xml",
"version": "2.1.0",
"version": "2.2.0",
"devDependencies": {
"@eslint/js": "^9.31.0",
"@types/node": "^24.0.14",
"@rollup/plugin-commonjs": "^28.0.6",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-typescript": "^12.1.4",
"@types/node": "^24.0.15",
"eslint": "^9.31.0",
"eslint-plugin-prettier": "^5.5.3",
"eslint-config-prettier": "^10.1.8",
"globals": "^16.3.0",
"prettier": "^3.6.2",
"rollup": "^4.45.1",
"tslib": "^2.8.1",
"tsx": "^4.20.3",
"typescript": "^5.8.3",
"typescript-eslint": "^8.37.0",
@@ -568,6 +574,19 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@eslint/eslintrc/node_modules/globals": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@eslint/js": {
"version": "9.31.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz",
@@ -716,17 +735,161 @@
"node": ">= 8"
}
},
"node_modules/@pkgr/core": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz",
"integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==",
"node_modules/@rollup/plugin-commonjs": {
"version": "28.0.6",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.6.tgz",
"integrity": "sha512-XSQB1K7FUU5QP+3lOQmVCE3I0FcbbNvmNT4VJSj93iUjayaARrTQeoRdiYQoftAJBLrR9t2agwAd3ekaTgHNlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
"commondir": "^1.0.1",
"estree-walker": "^2.0.2",
"fdir": "^6.2.0",
"is-reference": "1.2.1",
"magic-string": "^0.30.3",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=16.0.0 || 14 >= 14.17"
},
"peerDependencies": {
"rollup": "^2.68.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/plugin-commonjs/node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true,
"license": "MIT"
},
"node_modules/@rollup/plugin-commonjs/node_modules/fdir": {
"version": "6.4.6",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/@rollup/plugin-commonjs/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
"node": ">=12"
},
"funding": {
"url": "https://opencollective.com/pkgr"
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/@rollup/plugin-node-resolve": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.1.tgz",
"integrity": "sha512-tk5YCxJWIG81umIvNkSod2qK5KyQW19qcBF/B78n1bjtOON6gzKoVeSzAE8yHCZEDmqkHKkxplExA8KzdJLJpA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
"@types/resolve": "1.20.2",
"deepmerge": "^4.2.2",
"is-module": "^1.0.0",
"resolve": "^1.22.1"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^2.78.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/plugin-typescript": {
"version": "12.1.4",
"resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.1.4.tgz",
"integrity": "sha512-s5Hx+EtN60LMlDBvl5f04bEiFZmAepk27Q+mr85L/00zPDn1jtzlTV6FWn81MaIwqfWzKxmOJrBWHU6vtQyedQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.1.0",
"resolve": "^1.22.1"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^2.14.0||^3.0.0||^4.0.0",
"tslib": "*",
"typescript": ">=3.7.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
},
"tslib": {
"optional": true
}
}
},
"node_modules/@rollup/pluginutils": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.2.0.tgz",
"integrity": "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0",
"estree-walker": "^2.0.2",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/pluginutils/node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true,
"license": "MIT"
},
"node_modules/@rollup/pluginutils/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
@@ -1050,6 +1213,13 @@
"undici-types": "~7.8.0"
}
},
"node_modules/@types/resolve": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
"dev": true,
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.37.0.tgz",
@@ -1611,6 +1781,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/commondir": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
"integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
"dev": true,
"license": "MIT"
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -1668,6 +1845,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/es-module-lexer": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
@@ -1791,35 +1978,20 @@
}
}
},
"node_modules/eslint-plugin-prettier": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.3.tgz",
"integrity": "sha512-NAdMYww51ehKfDyDhv59/eIItUVzU0Io9H2E8nHNGKEeeqlnci+1gCvrHib6EmZdf6GxF+LCV5K7UC65Ezvw7w==",
"node_modules/eslint-config-prettier": {
"version": "10.1.8",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz",
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true,
"license": "MIT",
"dependencies": {
"prettier-linter-helpers": "^1.0.0",
"synckit": "^0.11.7"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
"funding": {
"url": "https://opencollective.com/eslint-plugin-prettier"
"url": "https://opencollective.com/eslint-config-prettier"
},
"peerDependencies": {
"@types/eslint": ">=8.0.0",
"eslint": ">=8.0.0",
"eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0",
"prettier": ">=3.0.0"
},
"peerDependenciesMeta": {
"@types/eslint": {
"optional": true
},
"eslint-config-prettier": {
"optional": true
}
"eslint": ">=7.0.0"
}
},
"node_modules/eslint-scope": {
@@ -1943,13 +2115,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/fast-diff": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
"integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/fast-glob": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
@@ -2083,6 +2248,16 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-tsconfig": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz",
@@ -2110,9 +2285,9 @@
}
},
"node_modules/globals": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz",
"integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2139,6 +2314,19 @@
"node": ">=8"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -2176,6 +2364,22 @@
"node": ">=0.8.19"
}
},
"node_modules/is-core-module": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"dev": true,
"license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -2199,6 +2403,13 @@
"node": ">=0.10.0"
}
},
"node_modules/is-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
"dev": true,
"license": "MIT"
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -2209,6 +2420,16 @@
"node": ">=0.12.0"
}
},
"node_modules/is-reference": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
"integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "*"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -2474,6 +2695,13 @@
"node": ">=8"
}
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true,
"license": "MIT"
},
"node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
@@ -2566,19 +2794,6 @@
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/prettier-linter-helpers": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
"integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-diff": "^1.1.2"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -2610,6 +2825,27 @@
],
"license": "MIT"
},
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-core-module": "^2.16.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -2811,20 +3047,17 @@
"node": ">=8"
}
},
"node_modules/synckit": {
"version": "0.11.11",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz",
"integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==",
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@pkgr/core": "^0.2.9"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
"node": ">= 0.4"
},
"funding": {
"url": "https://opencollective.com/synckit"
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/tinybench": {
@@ -2942,6 +3175,13 @@
"typescript": ">=4.8.4"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD"
},
"node_modules/tsx": {
"version": "4.20.3",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "@jef/zap2xml",
"version": "2.1.0",
"version": "2.2.0",
"description": "JavaScript implementation of zap2xml",
"type": "module",
"exports": {
@@ -18,17 +18,24 @@
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"build": "tsc",
"dev": "tsx src/index.ts",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
"lint:fix": "eslint . --fix",
"typecheck": "tsc --noEmit",
"build": "npm run typecheck && rollup --config rollup.config.ts --configPlugin @rollup/plugin-typescript"
},
"devDependencies": {
"@eslint/js": "^9.31.0",
"@types/node": "^24.0.14",
"@rollup/plugin-commonjs": "^28.0.6",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-typescript": "^12.1.4",
"@types/node": "^24.0.15",
"eslint": "^9.31.0",
"eslint-plugin-prettier": "^5.5.3",
"eslint-config-prettier": "^10.1.8",
"globals": "^16.3.0",
"prettier": "^3.6.2",
"rollup": "^4.45.1",
"tslib": "^2.8.1",
"tsx": "^4.20.3",
"typescript": "^5.8.3",
"typescript-eslint": "^8.37.0",

16
rollup.config.ts Normal file
View File

@@ -0,0 +1,16 @@
import commonjs from "@rollup/plugin-commonjs";
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()],
};
export default config;

40
src/config.test.ts Normal file
View File

@@ -0,0 +1,40 @@
import { describe, it, expect } from "vitest";
import { processLineupId, getHeadendId } from "./config.js";
describe("processLineupId", () => {
it("returns env LINEUP_ID if set", () => {
process.env.LINEUP_ID = "USA-12345";
expect(processLineupId()).toBe("USA-12345");
delete process.env.LINEUP_ID;
});
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("returns default if nothing set", () => {
expect(processLineupId()).toBe("USA-lineupId-DEFAULT");
});
it("returns default if lineupId contains OTA", () => {
process.env.LINEUP_ID = "USA-OTA12345";
expect(processLineupId()).toBe("USA-lineupId-DEFAULT");
delete process.env.LINEUP_ID;
});
});
describe("getHeadendId", () => {
it("extracts headend from valid lineupId", () => {
expect(getHeadendId("USA-OTA12345")).toBe("lineupId");
expect(getHeadendId("USA-NY31587-L")).toBe("NY31587");
expect(getHeadendId("CAN-OTAT1L0A1")).toBe("lineupId");
expect(getHeadendId("CAN-0008861-X")).toBe("0008861");
});
it("returns 'lineup' if no match", () => {
expect(getHeadendId("INVALID")).toBe("lineup");
expect(getHeadendId("")).toBe("lineup");
});
});

View File

@@ -1,38 +1,68 @@
import { UserAgent } from "./useragents.js";
export const config = {
baseUrl: "https://tvlistings.gracenote.com/api/grid",
lineupId:
export function processLineupId(): string {
const lineupId =
process.env["LINEUP_ID"] ||
process.argv.find((arg) => arg.startsWith("--lineupId="))?.split("=")[1] ||
"USA-lineupId-DEFAULT",
timespan:
process.env["TIMESPAN"] ||
process.argv.find((arg) => arg.startsWith("--timespan="))?.split("=")[1] ||
"6",
country:
process.env["COUNTRY"] ||
process.argv.find((arg) => arg.startsWith("--country="))?.split("=")[1] ||
"USA",
postalCode:
process.env["POSTAL_CODE"] ||
process.argv
.find((arg) => arg.startsWith("--postalCode="))
?.split("=")[1] ||
"30309",
pref:
process.env["PREF"] ||
process.argv.find((arg) => arg.startsWith("--pref="))?.split("=")[1] ||
"",
timezone: process.env.TZ || "America/New_York",
userAgent:
process.env["USER_AGENT"] ||
process.argv.find((arg) => arg.startsWith("--userAgent="))?.split("=")[1] ||
UserAgent,
outputFile:
process.env["OUTPUT_FILE"] ||
process.argv
.find((arg) => arg.startsWith("--outputFile="))
?.split("=")[1] ||
"xmltv.xml",
};
"USA-lineupId-DEFAULT";
if (lineupId.includes("OTA")) {
return "USA-lineupId-DEFAULT";
}
return lineupId;
}
export function getHeadendId(lineupId: string): string {
if (lineupId.includes("OTA")) {
return "lineupId";
}
const match = lineupId.match(/^(USA|CAN)-(.*?)(?:-[A-Z]+)?$/);
return match?.[2] || "lineup";
}
export function getConfig() {
const lineupId = processLineupId();
const headendId = getHeadendId(lineupId);
return {
baseUrl: "https://tvlistings.gracenote.com/api/grid",
lineupId,
headendId,
timespan:
process.env["TIMESPAN"] ||
process.argv
.find((arg) => arg.startsWith("--timespan="))
?.split("=")[1] ||
"6",
country:
process.env["COUNTRY"] ||
process.argv.find((arg) => arg.startsWith("--country="))?.split("=")[1] ||
"USA",
postalCode:
process.env["POSTAL_CODE"] ||
process.argv
.find((arg) => arg.startsWith("--postalCode="))
?.split("=")[1] ||
"30309",
pref:
process.env["PREF"] ||
process.argv.find((arg) => arg.startsWith("--pref="))?.split("=")[1] ||
"",
timezone: process.env.TZ || "America/New_York",
userAgent:
process.env["USER_AGENT"] ||
process.argv
.find((arg) => arg.startsWith("--userAgent="))
?.split("=")[1] ||
UserAgent,
outputFile:
process.env["OUTPUT_FILE"] ||
process.argv
.find((arg) => arg.startsWith("--outputFile="))
?.split("=")[1] ||
"xmltv.xml",
};
}

View File

@@ -1,7 +1,9 @@
import { writeFileSync } from "node:fs";
import { getTVListings } from "./tvlistings.js";
import { buildXmltv } from "./xmltv.js";
import { config } from "./config.js";
import { getConfig } from "./config.js";
const config = getConfig();
function isHelp() {
if (process.argv.includes("--help")) {

View File

@@ -1,4 +1,6 @@
import { config } from "./config.js";
import { getConfig } from "./config.js";
const config = getConfig();
export interface Program {
/** "title": "GMA3" */
@@ -82,7 +84,7 @@ function buildUrl(time: number, timespan: number): string {
const params = {
lineupId: config.lineupId,
timespan: timespan.toString(),
headendId: "lineupId",
headendId: config.headendId,
country: config.country,
timezone: config.timezone,
postalCode: config.postalCode,
@@ -91,6 +93,8 @@ function buildUrl(time: number, timespan: number): string {
aid: "orbebb",
languagecode: "en-us",
time: time.toString(),
device: "X",
userId: "-",
};
const urlParams = new URLSearchParams(params).toString();
@@ -119,7 +123,7 @@ export async function getTVListings(): Promise<GridApiResponse> {
}).then(async (response) => {
if (!response.ok) {
throw new Error(
`Failed to fetch: ${response.status} ${response.statusText}`
`Failed to fetch: ${response.status} ${response.statusText}`,
);
}
const chunkData = (await response.json()) as GridApiResponse;

View File

@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
import type { GridApiResponse } from "./tvlistings.js";
import {
buildChannelsXml,
buildProgrammesXml,
buildProgramsXml,
buildXmltv,
escapeXml,
formatDate,
@@ -57,7 +57,9 @@ describe("buildXmltv", () => {
it("should generate valid XML structure", () => {
const result = buildXmltv(mockData);
expect(result).toContain('<?xml version="1.0" encoding="UTF-8"?>');
expect(result).toContain('<tv generator-info-name="zap2it-grid">');
expect(result).toContain(
'<tv generator-info-name="jef/zap2xml" generator-info-url="https://github.com/jef/zap2xml">',
);
expect(result).toContain("</tv>");
});
@@ -85,14 +87,16 @@ describe("buildXmltv", () => {
it("should include rating information", () => {
const result = buildXmltv(mockData);
expect(result).toContain("<rating><value>TV-PG</value></rating>");
expect(result).toContain(
'<rating system="MPAA"><value>TV-PG</value></rating>',
);
});
it("should include categories from flags and tags", () => {
const result = buildXmltv(mockData);
expect(result).toContain("<category>New</category>");
expect(result).toContain("<category>Stereo</category>");
expect(result).toContain("<category>CC</category>");
expect(result).toContain("<new />");
expect(result).toContain('<audio type="stereo" />');
expect(result).toContain('<audio type="cc" />');
});
it("should include episode information", () => {
@@ -108,7 +112,9 @@ describe("buildXmltv", () => {
const emptyData: GridApiResponse = { channels: [] };
const result = buildXmltv(emptyData);
expect(result).toContain('<?xml version="1.0" encoding="UTF-8"?>');
expect(result).toContain('<tv generator-info-name="zap2it-grid">');
expect(result).toContain(
'<tv generator-info-name="jef/zap2xml" generator-info-url="https://github.com/jef/zap2xml">',
);
expect(result).toContain("</tv>");
expect(result).not.toContain("<channel");
expect(result).not.toContain("<programme");
@@ -252,9 +258,9 @@ describe("buildChannelsXml", () => {
});
});
describe("buildProgrammesXml", () => {
describe("buildProgramsXml", () => {
it("should build programme XML correctly", () => {
const result = buildProgrammesXml(mockData);
const result = buildProgramsXml(mockData);
expect(result).toContain(
'<programme start="20250718190000 +0000" stop="20250718200000 +0000" channel="19629">',
);
@@ -263,16 +269,20 @@ describe("buildProgrammesXml", () => {
expect(result).toContain(
"<desc>BIA performs; comic Zarna Garg; lifestyle contributor Lori Bergamotto; ABC News chief medical correspondent Dr. Tara Narula.</desc>",
);
expect(result).toContain("<rating><value>TV-PG</value></rating>");
expect(result).toContain("<category>New</category>");
expect(result).toContain("<category>Stereo</category>");
expect(result).toContain("<category>CC</category>");
expect(result).toContain(
'<rating system="MPAA"><value>TV-PG</value></rating>',
);
expect(result).toContain("<new />");
expect(result).toContain('<audio type="stereo" />');
expect(result).toContain('<audio type="cc" />');
expect(result).toContain('<episode-num system="season">5</episode-num>');
expect(result).toContain('<episode-num system="episode">217</episode-num>');
expect(result).toContain(
'<episode-num system="series">SH05918266</episode-num>',
);
expect(result).toContain('<icon src="p30687311_b_v13_aa" />');
expect(result).toContain(
'<icon src="https://zap2it.tmsimg.com/assets/p30687311_b_v13_aa.jpg" />',
);
});
it("should handle programmes without optional fields", () => {
@@ -318,7 +328,7 @@ describe("buildProgrammesXml", () => {
},
],
};
const result = buildProgrammesXml(minimalProgramme);
const result = buildProgramsXml(minimalProgramme);
expect(result).toContain(
'<programme start="20250718190000 +0000" stop="20250718193000 +0000" channel="123">',
);

View File

@@ -55,7 +55,7 @@ export function buildChannelsXml(data: GridApiResponse): string {
return xml;
}
export function buildProgrammesXml(data: GridApiResponse): string {
export function buildProgramsXml(data: GridApiResponse): string {
let xml = "";
for (const channel of data.channels) {
@@ -79,43 +79,71 @@ export function buildProgrammesXml(data: GridApiResponse): string {
}
if (event.rating) {
xml += ` <rating><value>${escapeXml(
xml += ` <rating system="MPAA"><value>${escapeXml(
event.rating,
)}</value></rating>\n`;
}
if (event.flag && event.flag.length > 0) {
for (const flag of event.flag) {
xml += ` <category>${escapeXml(flag)}</category>\n`;
if (event.flag.includes("New")) {
xml += ` <new />\n`;
}
if (event.flag.includes("Live")) {
xml += ` <live />\n`;
}
if (event.flag.includes("Premiere")) {
xml += ` <premiere />\n`;
}
if (event.flag.includes("Finale")) {
xml += ` <last-chance />\n`;
}
}
if (
!event.flag ||
(event.flag &&
event.flag.length > 0 &&
!event.flag.includes("New") &&
!event.flag.includes("Live"))
) {
xml += ` <previously-shown />\n`;
}
if (event.tags && event.tags.length > 0) {
for (const tag of event.tags) {
xml += ` <category>${escapeXml(tag)}</category>\n`;
if (event.tags.includes("Stereo")) {
xml += ` <audio type="stereo" />\n`;
}
if (event.tags.includes("CC")) {
xml += ` <subtitles type="teletext" />\n`;
}
}
if (event.program.season) {
xml += ` <episode-num system="season">${escapeXml(
event.program.season,
if (event.program.season && event.program.episode) {
xml += ` <episode-num system="onscreen">${escapeXml(
`S${event.program.season.padStart(2, "0")}E${event.program.episode.padStart(2, "0")}`,
)}</episode-num>\n`;
}
if (event.program.episode) {
xml += ` <episode-num system="episode">${escapeXml(
event.program.episode,
xml += ` <episode-num system="common">${escapeXml(
`S${event.program.season.padStart(2, "0")}E${event.program.episode.padStart(2, "0")}`,
)}</episode-num>\n`;
}
if (event.program.seriesId) {
xml += ` <episode-num system="series">${escapeXml(
event.program.seriesId,
)}</episode-num>\n`;
if (/..\d{8}\d{4}/.test(event.program.id)) {
xml += ` <episode-num system="dd_progid">${escapeXml(event.program.id)}</episode-num>\n`;
}
xml += ` <episode-num system="xmltv_ns">${escapeXml(
`${event.program.season} . ${event.program.episode}`,
)}.</episode-num>\n`;
}
if (event.thumbnail) {
xml += ` <icon src="${escapeXml(event.thumbnail)}" />\n`;
const src = event.thumbnail.startsWith("http")
? event.thumbnail
: "https://zap2it.tmsimg.com/assets/" + event.thumbnail + ".jpg";
xml += ` <icon src="${escapeXml(src)}" />\n`;
}
xml += " </programme>\n";
@@ -128,10 +156,11 @@ export function buildProgrammesXml(data: GridApiResponse): string {
export function buildXmltv(data: GridApiResponse): string {
console.log("Building XMLTV file");
let xml =
'<?xml version="1.0" encoding="UTF-8"?>\n<tv generator-info-name="zap2it-grid">\n';
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
xml +=
'<tv generator-info-name="jef/zap2xml" generator-info-url="https://github.com/jef/zap2xml">\n';
xml += buildChannelsXml(data);
xml += buildProgrammesXml(data);
xml += buildProgramsXml(data);
xml += "</tv>\n";
return xml;

View File

@@ -29,13 +29,11 @@
"useDefineForClassFields": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"sourceMap": true
},
"include": [
"src/**/*.ts"
],
"exclude": [
"src/**/*.test.ts",
"node_modules/**"
]
}