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.
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 `<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.