Files
ipx/docs/play.md
Ray Slakinski 5d0ff1ac57 ipx-play: metrics for Prometheus, a dashboard row and a down alert (#458)
ipx-play sent traces and logged JSON but served no /metrics, so nothing showed whether
shared links were opening or failing. It now serves them on metrics_bind (9189 in
production, off the tunnel's port), with the directory's histogram and /proc reading:
requests by method, route and status (by route, so each shared episode is not a row of its
own, and an unknown address is route=""), episodes shared, memory, open files, uptime and
version.

The iPX dashboard gets an ipx-play row, its warnings join the other containers' log panel,
and alerts.py fires when Prometheus cannot reach it. The prod-check skill and docs/play.md
name it, with the scrape job and the OTEL_RESOURCE_ATTRIBUTES tag the trace panels need.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 22:08:31 -05:00

4.7 KiB

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

  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"
      - "9189:9189"   # /metrics, for Prometheus on the LAN; not in the tunnel

Its config, /mnt/fast/appdata/ipx-play/play.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:9189"

# 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:

  - job_name: ipx-play
    static_configs:
      - targets: ["192.168.1.130:9189"]

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.