feat: merge all listings into a single XMLTV file - #3
Conversation
Add --merge to combine every configured listing into one XMLTV document. Channels are deduplicated by channel ID and programmes by channel and start time, so stations carried by more than one lineup appear once. Merging runs on the already-fetched grid data rather than re-parsing the written XML, so it needs no new runtime dependencies, no subprocess, and no Dockerfile change. Dedupe semantics match jesmannstl/tvmerge (--dedupe first|last) with two refinements: start times are normalized to epoch milliseconds so equivalent timestamps in different formats collide, and channel metadata is union-merged so a lineup missing channelNo inherits it from one that has it. * feat: src/merge.ts with mergeListings() and first/last dedupe * feat: merge YAML block plus --merge, --mergeOutputFile, --mergeDedupe, --noKeepIndividual and matching MERGE* env vars * feat: keepIndividual controls whether per-listing files are written * fix: reject a merge outputFile that collides with a listing output * fix: use ebyl.tmsimg.com for channel icons * test: 24 new tests for merge logic and merge config parsing (47 -> 71) * ci: run unit tests and prettier check, add merge integration test * style: standardize on 4-space indentation via prettier
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The channel-icon host was changed to the likely-typo ebyl.tmsimg.com, which diverges from the known-working emby.tmsimg.com still used for programme thumbnails and will probably break channel logos.
Get a fresh assessment by requesting another Copilot review.
Review effort: Balanced
Findings: 2
Open (2)
What changed in this PR
This PR adds an opt-in --merge capability to the zap2xml tool so that multiple configured TV listings can be combined into a single XMLTV document. Merging operates on already-fetched grid data (no re-parsing of written XML), deduplicating channels by channel ID and programmes by channel + normalized start time, with configurable first/last dedupe semantics. It also standardizes the codebase on 4-space Prettier formatting and wires unit tests + a merge integration test into CI.
Changes:
- New
src/merge.ts(mergeListings,programmeKey, channel-metadata union merge) plus 24 new merge/config tests. - New merge configuration surface: YAML
mergeblock,--merge/--mergeOutputFile/--mergeDedupe/--noKeepIndividualflags, matchingMERGE*env vars, and a collision check between merge output and per-listing outputs. - Repo-wide reformatting to 4-space indentation (
.prettierrc,format/format:checkscripts, CI format + unit-test steps), plus a channel-icon host change insrc/xmltv.ts.
| File | Description |
|---|---|
| src/merge.ts | New merge/dedupe engine; channel metadata union uses ?? which misses empty-string values. |
| src/merge.test.ts | New unit tests covering dedupe modes, pooling, metadata fill, and immutability. |
| src/config.ts | Adds MergeConfig, schemas, env/CLI overrides, and merge-output collision assertion. |
| src/config.test.ts | New tests for merge config parsing and precedence. |
| src/index.ts | Orchestrates merge flow, keepIndividual behavior, and merged-file writing. |
| src/xmltv.ts | Reformatting plus channel-icon host changed to ebyl.tmsimg.com (likely a typo of emby). |
| src/xmltv.test.ts | Reformatting; icon expectation updated to ebyl.tmsimg.com. |
| src/tvlistings.ts / .test.ts | Formatting-only changes. |
| src/useragents.ts, rollup.config.ts, eslint.config.mjs | Formatting-only changes. |
| .prettierrc, package.json | Prettier 4-space/printWidth config and format scripts. |
| .github/workflows/ci.yml | Adds format check, unit tests, and a merge integration test. |
| README.md | Documents the new merge feature and options. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| // New line to replace the hostname | ||
| src = src.replace("zap2it.tmsimg.com", "emby.tmsimg.com"); | ||
| // New line to replace the hostname | ||
| src = src.replace("zap2it.tmsimg.com", "ebyl.tmsimg.com"); |
mergeChannelMetadata used ?? for affiliateName, affiliateCallSign, channelNo, and thumbnail, so it only fell back to another listing's value when the existing one was null or undefined. The rest of the codebase treats an empty string as absent for these fields: buildChannelsXml guards each with a truthiness check and channelComparator normalizes channelNo via (a.channelNo || "").trim(). A feed reporting "" instead of null therefore blocked inheritance, and the merged channel silently lost its number, affiliate name, and icon even though another listing supplied them. Switch to || to match callSign and the surrounding conventions. * test: cover empty-string metadata inheritance

Add --merge to combine every configured listing into one XMLTV document. Channels are deduplicated by channel ID and programmes by channel and start time, so stations carried by more than one lineup appear once.
Merging runs on the already-fetched grid data rather than re-parsing the written XML, so it needs no new runtime dependencies, no subprocess, and no Dockerfile change. Dedupe semantics match jesmannstl/tvmerge (--dedupe first|last) with two refinements: start times are normalized to epoch milliseconds so equivalent timestamps in different formats collide, and channel metadata is union-merged so a lineup missing channelNo inherits it from one that has it.