5d0ff1a put ipx-play's /metrics on 9189, but hnopml, in the same
compose project, has published 192.168.1.130:9189 since 2026-11-12 and
Prometheus scrapes it there as job="hnopml". Deployed as written,
ipx-play's port would have clashed with it. 9187 to 9190 are ipx, the
directory, hnopml and ipx-web; 9191 is free.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
107 lines
4.7 KiB
Markdown
107 lines
4.7 KiB
Markdown
# ipx-play: shared episodes (#447)
|
|
|
|
When someone shares an episode in iPX, their server sends it to ipx-play, which keeps it and
|
|
answers with a short link, `https://play.ipxreader.app/e/<id>`. Anyone can open that link: it
|
|
shows the episode, plays it from the publisher's own file (from the time it was shared at, with
|
|
`?t=`), and offers to subscribe in the reader's own podcast app. Overcast's links are the model.
|
|
|
|
Only episodes of public feeds are sent, by the same rule that decides what the directory is sent:
|
|
nothing from a feed marked private, one with a login, one whose address carries a key, or one in
|
|
a private OPML. ipx-play refuses an address that looks like one too. Everything else is shared by
|
|
its own address, as before. ipx-play keeps what an episode is and where its file is; it serves no
|
|
file itself.
|
|
|
|
## Running it
|
|
|
|
A container of its own, `ipx-play`, in the Arcane project `ipx`, from its own image:
|
|
|
|
```sh
|
|
sh scripts/build-image.sh play
|
|
docker compose -f /mnt/fast/arcane/projects/ipx/compose.yaml pull ipx-play
|
|
docker compose -f /mnt/fast/arcane/projects/ipx/compose.yaml up -d ipx-play
|
|
```
|
|
|
|
The compose service, beside `ipx-directory`:
|
|
|
|
```yaml
|
|
ipx-play:
|
|
image: 192.168.1.130:5000/ipx-play:latest
|
|
container_name: ipx-play
|
|
restart: unless-stopped
|
|
env_file: play.env # PLAY_DATABASE_URL=postgres://...
|
|
environment:
|
|
PLAY_LOG_FORMAT: json
|
|
OTEL_EXPORTER_OTLP_ENDPOINT: http://192.168.1.130:4318 # Tempo is not on this network
|
|
# The dashboard's trace panels show only spans with this tag, as the others' are.
|
|
OTEL_RESOURCE_ATTRIBUTES: deployment.environment.name=production
|
|
volumes:
|
|
- /mnt/fast/appdata/ipx-play:/config
|
|
ports:
|
|
- "8102:8102"
|
|
- "9191:9191" # /metrics, for Prometheus on the LAN; not in the tunnel
|
|
```
|
|
|
|
Its config, `/mnt/fast/appdata/ipx-play/play.toml`:
|
|
|
|
```toml
|
|
bind = "0.0.0.0:8102"
|
|
public_url = "https://play.ipxreader.app"
|
|
# Prometheus metrics, on a port of their own and not the one the tunnel publishes. Empty, or
|
|
# left out, serves none.
|
|
metrics_bind = "0.0.0.0:9191"
|
|
|
|
# One for each ipx server allowed to make links; `ipx-play token <name>` prints the lines.
|
|
[[tokens]]
|
|
name = "tower"
|
|
token_sha256 = "..."
|
|
```
|
|
|
|
Tokens are read at start: after adding one, restart the container.
|
|
|
|
Its database is Postgres, `ipx_play` on the `databases` project's server, its URL in `play.env`
|
|
beside the compose file (mode 600), as the directory's is. It makes its one table, `shares`, on
|
|
start.
|
|
|
|
The tunnel publishes `play.ipxreader.app` to `http://192.168.1.130:8102` with no Access in front:
|
|
the pages are anyone's. cloudflared is not on the project's network, so not `ipx-play:8102`, as
|
|
the directory's route is the host's port too. `/v1` takes a token for everything.
|
|
|
|
## Watching it
|
|
|
|
Its traces go to Tempo as service `ipx-play`: a span a request, a child of ipx's when ipx shared
|
|
an episode, so one trace runs from the page's Share through ipx to here. Its log is a JSON line a
|
|
request (`target` `ipx_play::http`: `method`, `path`, `status`, `duration_ms`, `token`, the
|
|
server's name and never its token) and one a share (`an episode was shared`: `server`, `id`).
|
|
|
|
Its metrics (#458), on `metrics_bind`, scraped as `job="ipx-play"`, beside the directory's job in
|
|
Prometheus's config:
|
|
|
|
```yaml
|
|
- job_name: ipx-play
|
|
static_configs:
|
|
- targets: ["192.168.1.130:9191"]
|
|
```
|
|
|
|
`play_http_request_duration_seconds{method,route,status}` (a page opened is `route="/e/{id}"`, an
|
|
address no route has `route=""`), `play_shares_total`, `play_process_resident_bytes`,
|
|
`play_process_open_fds`, `play_uptime_seconds` and `play_build_info`. The iPX dashboard's
|
|
`ipx-play` row draws them, and the alert `ipx-play is down` fires when Prometheus cannot reach it.
|
|
|
|
## Setting up ipx
|
|
|
|
In Admin, Directory, Sharing episodes: the address ipx reaches it at, `http://ipx-play:8102`
|
|
inside Docker (the public address would go out through Cloudflare and back), and the token
|
|
`ipx-play token tower` printed. Without them Share hands over the episode's own address.
|
|
|
|
## The page
|
|
|
|
`/e/<id>` shows the show's art, its name, the episode's title, date and length, a player starting
|
|
at `?t=` (seconds, or `12:34`), the episode's own page if it has one, its notes (sanitized by
|
|
ammonia), and buttons that open Apple Podcasts, Overcast, Pocket Casts and Castro at the feed, with
|
|
the feed's address to paste into anything else. Open Graph tags give it a preview in Messages,
|
|
Slack and the like. It runs no script: the start time is the media fragment `#t=` on the file. Its
|
|
Content-Security-Policy lets it load only its stylesheet, the art and the file.
|
|
|
|
A show that inserts its ads per listener can move the time by an ad's length, as Overcast's links
|
|
can: the file is the publisher's, fetched fresh by each listener.
|