Skip to content

Commit faef047

Browse files
committed
v1.3.0: bridge and relay servers, Docker support
## What's New **Bridge origin server** — `tltv bridge` takes external streaming sources (HLS URLs, M3U playlists, XMLTV guides, local directories) and publishes them as TLTV channels with Ed25519 identities, signed metadata, and the full protocol HTTP API. Supports private channels with token authentication and automatic re-polling. **Relay node** — `tltv relay` re-serves existing TLTV channels from upstream nodes with full signature verification. Documents served verbatim (raw bytes preserved). Refuses private, on-demand, and retired channels per spec. Follows migration chains. Participates in peer exchange with validated gossip. **Docker** — Multi-stage Dockerfile produces a ~10 MB image from scratch. All flags work as environment variables. CI pushes `:dev` on every push, releases push `:vX.Y.Z` and `:latest`. ```bash docker run -p 8000:8000 -v keys:/data tltv bridge \ --stream http://provider.example.com/channels.m3u docker run -p 8000:8000 tltv relay \ --node origin.example.com:443 ``` **84 new tests** — 58 bridge tests (source parsing, manifest rewriting, path traversal, upstream proxy, private channels, concurrency) and 26 relay tests (fetch+verify, access checks, migration, endpoints, access transitions). 159 total.
1 parent 53f49ca commit faef047

17 files changed

Lines changed: 5178 additions & 25 deletions

.dockerignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
dist/
2+
tltv
3+
*.key
4+
AGENTS.md
5+
.git

.github/workflows/ci.yml

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ on:
66
pull_request:
77
branches: [main, dev]
88

9+
permissions:
10+
contents: read
11+
packages: write
12+
913
jobs:
1014
test:
1115
runs-on: ubuntu-latest
@@ -44,3 +48,56 @@ jobs:
4448

4549
- name: Test (race detector)
4650
run: go test -race -count=1 ./...
51+
52+
push:
53+
needs: [test, race]
54+
if: github.event_name == 'push'
55+
runs-on: ubuntu-latest
56+
steps:
57+
- uses: actions/checkout@v4
58+
59+
- name: Setup Docker
60+
run: |
61+
if ! command -v docker >/dev/null 2>&1; then
62+
curl -fsSL https://download.docker.com/linux/static/stable/x86_64/docker-27.5.1.tgz \
63+
| tar xz --strip-components=1 -C /usr/local/bin docker/docker
64+
fi
65+
if ! docker info >/dev/null 2>&1; then
66+
export DOCKER_HOST=tcp://docker-proxy:2375
67+
echo "DOCKER_HOST=tcp://docker-proxy:2375" >> "$GITHUB_ENV"
68+
fi
69+
docker info >/dev/null
70+
71+
- name: Detect registry
72+
id: detect
73+
run: |
74+
SERVER="${{ github.server_url }}"
75+
REPO="${{ github.repository }}"
76+
BRANCH="${{ github.ref_name }}"
77+
if echo "$SERVER" | grep -q "github\.com"; then
78+
echo "registry=ghcr.io" >> "$GITHUB_OUTPUT"
79+
echo "image=ghcr.io/${REPO}" >> "$GITHUB_OUTPUT"
80+
else
81+
HOST="${FORGEJO_REGISTRY:-$(echo "$SERVER" | sed 's|https\?://||' | sed 's|:[0-9]*$||')}"
82+
echo "registry=${HOST}" >> "$GITHUB_OUTPUT"
83+
echo "image=${HOST}/${REPO}" >> "$GITHUB_OUTPUT"
84+
fi
85+
if [ "$BRANCH" = "dev" ]; then
86+
echo "tag=dev" >> "$GITHUB_OUTPUT"
87+
elif [ "$BRANCH" = "main" ]; then
88+
echo "tag=latest" >> "$GITHUB_OUTPUT"
89+
fi
90+
91+
- name: Login to registry
92+
run: |
93+
TOKEN="${{ secrets.PACKAGES_TOKEN || secrets.GITHUB_TOKEN }}"
94+
echo "${TOKEN}" | \
95+
docker login "${{ steps.detect.outputs.registry }}" \
96+
-u "${{ github.repository_owner }}" --password-stdin
97+
98+
- name: Build and push image
99+
run: |
100+
IMAGE="${{ steps.detect.outputs.image }}"
101+
TAG="${{ steps.detect.outputs.tag }}"
102+
docker build --build-arg VERSION=${TAG} -t "${IMAGE}:${TAG}" .
103+
docker push "${IMAGE}:${TAG}"

.github/workflows/release.yml

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ on:
77

88
permissions:
99
contents: write
10+
packages: write
1011

1112
jobs:
1213
release:
@@ -66,9 +67,14 @@ jobs:
6667
if echo "$SERVER" | grep -q "github\.com"; then
6768
echo "api_base=https://api.github.com/repos/${REPO}" >> "$GITHUB_OUTPUT"
6869
echo "upload_mode=github" >> "$GITHUB_OUTPUT"
70+
echo "registry=ghcr.io" >> "$GITHUB_OUTPUT"
71+
echo "image=ghcr.io/${REPO}" >> "$GITHUB_OUTPUT"
6972
else
73+
HOST="${FORGEJO_REGISTRY:-$(echo "$SERVER" | sed 's|https\?://||' | sed 's|:[0-9]*$||')}"
7074
echo "api_base=${SERVER}/api/v1/repos/${REPO}" >> "$GITHUB_OUTPUT"
7175
echo "upload_mode=forgejo" >> "$GITHUB_OUTPUT"
76+
echo "registry=${HOST}" >> "$GITHUB_OUTPUT"
77+
echo "image=${HOST}/${REPO}" >> "$GITHUB_OUTPUT"
7278
fi
7379
7480
PRERELEASE=false
@@ -121,31 +127,15 @@ jobs:
121127
exit 1
122128
fi
123129
124-
BODY="## tltv-cli ${TAG}
130+
# Use the tagged commit message body as release notes.
131+
# First line is the title (ignored), rest is the body.
132+
BODY=$(git log -1 --format='%b' "${TAG}")
125133
126-
Single static binary. Zero dependencies.
134+
# Fallback if commit has no body
135+
if [ -z "$(echo "$BODY" | tr -d '[:space:]')" ]; then
136+
BODY="## What's New
127137
128-
\`\`\`bash
129-
curl -sSL https://raw.githubusercontent.com/tltv-org/cli/main/install.sh | sh
130-
\`\`\`
131-
132-
Pre-built for Linux, macOS, Windows (amd64 + arm64) and FreeBSD (amd64)."
133-
134-
# Auto-generate changelog on GitHub
135-
if [ "$UPLOAD_MODE" = "github" ]; then
136-
AUTO_NOTES=$(curl -sS -X POST \
137-
-H "Authorization: token ${TOKEN}" \
138-
-H "Content-Type: application/json" \
139-
-d "{\"tag_name\":\"${TAG}\"}" \
140-
"${API_BASE}/releases/generate-notes" 2>/dev/null \
141-
| python3 -c "import sys,json; print(json.load(sys.stdin).get('body',''))" 2>/dev/null || true)
142-
if [ -n "$AUTO_NOTES" ]; then
143-
BODY="${BODY}
144-
145-
---
146-
147-
${AUTO_NOTES}"
148-
fi
138+
See commit history for details."
149139
fi
150140
151141
BODY_JSON=$(echo "$BODY" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))")
@@ -207,3 +197,33 @@ jobs:
207197
done
208198
209199
echo "Uploaded $(ls dist/* | wc -l) assets"
200+
201+
- name: Setup Docker
202+
run: |
203+
if ! command -v docker >/dev/null 2>&1; then
204+
curl -fsSL https://download.docker.com/linux/static/stable/x86_64/docker-27.5.1.tgz \
205+
| tar xz --strip-components=1 -C /usr/local/bin docker/docker
206+
fi
207+
if ! docker info >/dev/null 2>&1; then
208+
export DOCKER_HOST=tcp://docker-proxy:2375
209+
echo "DOCKER_HOST=tcp://docker-proxy:2375" >> "$GITHUB_ENV"
210+
fi
211+
docker info >/dev/null
212+
213+
- name: Login to registry
214+
run: |
215+
TOKEN="${{ secrets.PACKAGES_TOKEN || secrets.GITHUB_TOKEN }}"
216+
echo "${TOKEN}" | \
217+
docker login "${{ steps.detect.outputs.registry }}" \
218+
-u "${{ github.repository_owner }}" --password-stdin
219+
220+
- name: Build and push Docker image
221+
env:
222+
TAG: ${{ github.ref_name }}
223+
IMAGE: ${{ steps.detect.outputs.image }}
224+
run: |
225+
docker build --build-arg VERSION=${TAG} \
226+
-t "${IMAGE}:${TAG}" \
227+
-t "${IMAGE}:latest" .
228+
docker push "${IMAGE}:${TAG}"
229+
docker push "${IMAGE}:latest"

Dockerfile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
FROM golang:1.22-alpine AS builder
2+
ARG VERSION=dev
3+
WORKDIR /src
4+
COPY go.mod ./
5+
COPY *.go ./
6+
RUN CGO_ENABLED=0 go build -ldflags "-s -w -X main.version=${VERSION}" -o /tltv .
7+
8+
FROM scratch
9+
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
10+
COPY --from=builder /tltv /tltv
11+
EXPOSE 8000
12+
WORKDIR /data
13+
ENTRYPOINT ["/tltv"]

README.md

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@ curl -sSL https://raw.githubusercontent.com/tltv-org/cli/main/install.sh | sh
1616

1717
Download the latest `.zip` from the [releases page](https://github.com/tltv-org/cli/releases/latest) and add `tltv.exe` to your PATH.
1818

19+
### Docker
20+
21+
```bash
22+
docker run --rm -v tltv-keys:/data -p 8000:8000 tltv bridge \
23+
--stream http://provider.example.com/channels.m3u
24+
25+
docker run --rm -v tltv-data:/data -p 8000:8000 tltv relay \
26+
--node origin.example.com:443
27+
```
28+
29+
Or build locally: `docker build -t tltv .`
30+
1931
### From source
2032

2133
Requires [Go](https://go.dev/dl/) 1.22+:
@@ -69,6 +81,12 @@ tltv stream "tltv://TVabc...@example.com:443"
6981
# Discover channels across the network
7082
tltv crawl example.com
7183

84+
# Bridge external streams as TLTV channels
85+
tltv bridge --stream http://provider.com/channels.m3u --guide http://provider.com/guide.xml
86+
87+
# Relay channels from another TLTV node
88+
tltv relay --node origin.example.com:443
89+
7290
# Install shell completions
7391
tltv completion --install zsh
7492
```
@@ -110,6 +128,13 @@ tltv completion --install zsh
110128
| `stream <uri\|id@host>` | Check HLS stream availability. Shows stream URL, segment count, target duration. Use `--url` to print only the stream URL for piping. |
111129
| `crawl <host>` | BFS-crawl the gossip network starting from a host. Discovers channels across nodes via peer exchange. Use `--depth` (`-d`) to set max depth. |
112130

131+
### Server
132+
133+
| Command | Description |
134+
|---|---|
135+
| `bridge` | Start a bridge origin server. Takes external streaming sources (HLS URLs, M3U playlists, JSON channel lists, directories of .m3u8 files) and publishes them as TLTV channels with Ed25519 identities and signed metadata. Supports private channels with token authentication, XMLTV guide output, and automatic re-polling. All flags also work as environment variables for Docker. |
136+
| `relay` | Start a relay node. Re-serves existing TLTV channels from upstream nodes with full signature verification. Serves upstream-signed documents verbatim (preserves unknown fields). Refuses private, on-demand, and retired channels per spec. Participates in peer exchange with validated gossip. Supports `--channels` (specific URIs), `--node` (relay all from a node), and `--config` (JSON config file). |
137+
113138
### Operations
114139

115140
| Command | Description |
@@ -215,6 +240,94 @@ tltv --json peers example.com | jq '.peers[].id'
215240
tltv --json crawl example.com | jq '.channels | length'
216241
```
217242

243+
## Bridge
244+
245+
The bridge is a long-running origin server that takes external streaming sources and publishes them as first-class TLTV channels with Ed25519 identities, signed metadata, and the full protocol HTTP API.
246+
247+
```bash
248+
# Bridge a single HLS stream
249+
tltv bridge --stream http://example.com/live.m3u8 --name "My Channel"
250+
251+
# Bridge an M3U playlist with XMLTV guide
252+
tltv bridge --stream http://provider.com/channels.m3u --guide http://provider.com/guide.xml
253+
254+
# Bridge a directory of .m3u8 files (with optional sidecar .json metadata)
255+
tltv bridge --stream /media/hls
256+
257+
# With on-demand channels and custom settings
258+
tltv bridge --stream http://mediaserver:8000/api/channels.m3u \
259+
--guide http://mediaserver:8000/api/xmltv.xml --on-demand \
260+
--listen :8000 --hostname origin.example.com:443
261+
```
262+
263+
Source formats are auto-detected: M3U playlists (with tvg-id/tvg-name attributes), JSON channel arrays, local directories with sidecar `.json` files, or single HLS streams. Guide data can be XMLTV or JSON.
264+
265+
All flags also work as environment variables for Docker: `STREAM`, `GUIDE`, `NAME`, `ON_DEMAND=1`, `POLL`, `LISTEN`, `KEYS_DIR`, `HOSTNAME`, `PEERS`.
266+
267+
Docker Compose example:
268+
```yaml
269+
services:
270+
bridge:
271+
image: tltv
272+
command: bridge
273+
ports: ["8000:8000"]
274+
volumes: [bridge-keys:/data]
275+
environment:
276+
STREAM: http://mediaserver:8000/api/channels.m3u
277+
GUIDE: http://mediaserver:8000/api/xmltv.xml
278+
HOSTNAME: bridge.example.com:443
279+
volumes:
280+
bridge-keys:
281+
```
282+
283+
Mount `/data` to persist channel keys across restarts. Set `HOSTNAME` explicitly -- Docker's default `HOSTNAME` is the container ID.
284+
285+
Private channels (`access: "token"`) are supported: hidden from node info and peers, token required on all endpoints, token injected into every URI in the HLS playlist graph.
286+
287+
## Relay
288+
289+
The relay is a long-running node that re-serves existing TLTV channels from upstream nodes with full signature verification. It does not have channel private keys and cannot modify signed documents.
290+
291+
```bash
292+
# Relay specific channels
293+
tltv relay --channels "tltv://TVabc...@origin.example.com:443"
294+
295+
# Relay all public channels from a node
296+
tltv relay --node origin.example.com:443
297+
298+
# Relay with a config file
299+
tltv relay --config relay.json --hostname relay.example.com:443
300+
```
301+
302+
The relay verifies every metadata and guide document against the channel's Ed25519 public key before caching. Documents are served verbatim (raw bytes preserved, unknown fields intact). Private, on-demand, and retired channels are refused per spec. If a channel transitions to any of these states, the relay stops immediately.
303+
304+
Migration chains are followed automatically (up to 5 hops). Peer exchange participates in gossip with validated entries, 7-day staleness cutoff, and 100-entry limit.
305+
306+
Config file format:
307+
```json
308+
{
309+
"channels": ["tltv://TVabc...@origin.example.com:443"],
310+
"nodes": ["origin.example.com:443"]
311+
}
312+
```
313+
314+
Environment variables: `CHANNELS`, `NODE`, `CONFIG`, `LISTEN`, `HOSTNAME`, `PEERS`, `META_POLL`, `GUIDE_POLL`, `PEER_POLL`.
315+
316+
Docker Compose example:
317+
```yaml
318+
services:
319+
relay:
320+
image: tltv
321+
command: relay
322+
ports: ["8000:8000"]
323+
volumes: [relay-data:/data]
324+
environment:
325+
NODE: origin.example.com:443
326+
HOSTNAME: relay.example.com:443
327+
volumes:
328+
relay-data:
329+
```
330+
218331
## Project Structure
219332
220333
```
@@ -228,11 +341,16 @@ network.go Network commands (node, fetch, guide, peers, stream, crawl)
228341
vanity.go Multi-threaded vanity channel ID miner
229342
output.go Terminal output formatting and colors
230343
signal.go OS signal handling
344+
bridge*.go Bridge origin server (source parsing, identity, HLS rewriting, HTTP)
345+
relay*.go Relay node (upstream fetch+verify, caching, gossip, HTTP)
231346
main_test.go 75 tests against all protocol test vectors + edge cases
347+
bridge_test.go 58 bridge tests (source parsing, manifest rewriting, endpoints)
348+
relay_test.go 26 relay tests (fetch+verify, access checks, migration, endpoints)
232349
Makefile Build, test, install, cross-compile (CGO_ENABLED=0)
350+
Dockerfile Multi-stage: golang:1.22-alpine -> scratch (~10 MB)
233351
```
234352
235-
Zero external dependencies. Everything uses the Go standard library (`crypto/ed25519`, `encoding/json`, `net/http`, `math/big`).
353+
Zero external dependencies. Everything uses the Go standard library (`crypto/ed25519`, `encoding/json`, `net/http`, `math/big`). 159 tests.
236354

237355
## License
238356

0 commit comments

Comments
 (0)