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