From eb431063154034aaec351e37d3c5f7c5055fe482 Mon Sep 17 00:00:00 2001 From: Domenico Testa Date: Wed, 6 May 2026 11:54:00 +0200 Subject: [PATCH] feat: updated README --- README.md | 128 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 122 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d3decd4..d3468ef 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,124 @@ -# PZ8 relay +# pz8-relay -A simple utility for my iptv needs. -Upstream links change frequently and need to be updated on devices, which is annoying. -This is a simple web service that will accept an HTTP request with basic authentication -and will act as a proxy against a configured HTTP link. +A small Go service that sits between a frequently-rotating upstream IPTV +provider and one or more client devices. It exposes stable URLs for the +playlist and (optionally) a merged EPG, so client devices can be configured +once and forgotten. -The proxied URL is configured via an environment variable. +## What it does + +- **Caches the upstream m3u_plus playlist** on a configurable interval and + serves it from a stable `/playlist` URL behind basic auth. The streams + inside the playlist are still hit directly upstream by clients — only the + playlist file is cached. +- **Optionally fetches one or more XMLTV EPG sources**, merges them, filters + channels down to those declared in the playlist, and rewrites their ids to + match the playlist's `tvg-id` values. The merged EPG is served at `/epg` + and the playlist's `#EXTM3U` header is rewritten so clients pick it up + automatically. + +Channel id matching is heuristic — the normalizer collapses cosmetic +divergences (`"Italia 1"` vs `"Italia 1.it"` vs `"Italia 1 HD.it"` all map +to the same key), which is enough for most Italian providers + open-epg +sources. + +For design background, see [adrs/01-epg-management.md](adrs/01-epg-management.md). + +## Endpoints + +| Path | Auth | Description | +| --- | --- | --- | +| `/playlist` | basic | Cached playlist; `Content-Type: application/vnd.apple.mpegurl` | +| `/` | basic | Alias for `/playlist` (back-compat) | +| `/epg` | none | Merged EPG; honours `Accept-Encoding: gzip` (~6× smaller) | +| `/healthz` | none | 200 once first refresh succeeds, else 503 | + +`/epg` is intentionally unauthenticated: XMLTV client apps (TiviMate, IPTV +Smarters, etc.) auto-discover the `url-tvg` URL from the playlist header +and fetch it without reusing the playlist's credentials. The threat model +is low — an anonymous fetcher gets a few hundred KB of TV listings. + +## Configuration + +All configuration is via environment variables. + +| Var | Purpose | Default | +| --- | --- | --- | +| `PZ8_RELAY_TARGET_URL` | Upstream m3u_plus playlist URL | **required** | +| `PZ8_RELAY_USERNAME` | Basic auth username for `/playlist` | **required** | +| `PZ8_RELAY_PASSWORD` | Basic auth password for `/playlist` | **required** | +| `PZ8_RELAY_LISTEN_ADDR` | TCP address to listen on | `:8080` | +| `PZ8_RELAY_PUBLIC_URL` | Public base URL of this relay; used to rewrite `url-tvg` to `/epg` | required when EPG is enabled | +| `PZ8_RELAY_EPG_URLS` | Comma-separated XMLTV URLs (gzipped or plain) | empty (EPG disabled) | +| `PZ8_RELAY_EPG_REFRESH` | Refresh interval (Go duration) | `12h` | +| `PZ8_RELAY_PREFER_PLAYLIST_EPG` | If `true` and the upstream playlist declares `url-tvg`, use that as the *only* EPG source for the cycle; otherwise fall back to `EPG_URLS` | `false` | +| `PZ8_RELAY_CACHE_DIR` | Where the cached playlist + EPG live | `/var/cache/pz8-relay` | +| `PZ8_RELAY_EPG_CONTENT_TYPE` | `Content-Type` returned by `/epg` | `application/xml` | + +EPG processing is enabled when `PZ8_RELAY_EPG_URLS` is non-empty **or** +`PZ8_RELAY_PREFER_PLAYLIST_EPG=true`. With EPG disabled, the relay simply +caches and serves the upstream playlist unchanged. + +## How a refresh works + +A single background worker drives the refresh on the configured interval +(starting immediately on boot): + +1. Download the upstream playlist, stream-rewrite the `#EXTM3U` header so + `url-tvg` points at `/epg`, extract every `tvg-id`, and + atomically swap the cached `playlist.m3u` into place. +2. If EPG is enabled, fetch each configured EPG URL (transparently + gunzipping `.gz` URLs) into a per-source temp file. +3. Walk the sources twice: pass 1 emits unique `` elements (first + source wins for duplicates), pass 2 emits `` entries scoped + to the source that "owns" each channel. Channels not in the playlist's + `tvg-id` set are dropped. Channel ids in the output are rewritten to the + exact playlist form. +4. The result is gzipped on disk as `epg.xml.gz` (XMLTV compresses ~10×) + and atomically swapped into place. + +If a refresh fails partway through, the previous good cache is preserved +and the next tick retries. `/playlist` and `/epg` only return 503 before +the very first successful refresh. + +## Running + +Local build: + +```sh +go build ./... +./pz8-relay +``` + +Docker: + +```sh +docker build -t pz8-relay . +docker run --rm -p 8080:8080 --env-file .env pz8-relay +``` + +The container runs as the distroless `nonroot` user (uid 65532). The +default cache directory is created in the image with the right ownership; +mount a volume on `/var/cache/pz8-relay` if you want the cache to survive +restarts. + +Minimum `.env` for EPG-enabled operation: + +``` +PZ8_RELAY_TARGET_URL=https://upstream.example/playlist.m3u +PZ8_RELAY_USERNAME=alice +PZ8_RELAY_PASSWORD=hunter2 +PZ8_RELAY_PUBLIC_URL=https://relay.example.com +PZ8_RELAY_EPG_URLS=https://www.open-epg.com/files/italy1.xml.gz +``` + +## Tests + +```sh +go test ./... +``` + +Covers id normalization, playlist header rewriting + tvg-id extraction, +and the EPG two-pass merge (filtering, id rewriting, first-source-wins +dedup). HTTP wiring and the refresh ticker are not unit-tested; smoke-test +manually with the endpoints above.