feat: updated README

This commit is contained in:
2026-05-06 11:54:00 +02:00
parent 9eaabdae9a
commit eb43106315
+122 -6
View File
@@ -1,8 +1,124 @@
# PZ8 relay # pz8-relay
A simple utility for my iptv needs. A small Go service that sits between a frequently-rotating upstream IPTV
Upstream links change frequently and need to be updated on devices, which is annoying. provider and one or more client devices. It exposes stable URLs for the
This is a simple web service that will accept an HTTP request with basic authentication playlist and (optionally) a merged EPG, so client devices can be configured
and will act as a proxy against a configured HTTP link. 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 `<base>/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 `<PUBLIC_URL>/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 `<channel>` elements (first
source wins for duplicates), pass 2 emits `<programme>` 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.