Background refresh runs when iOS chooses, and for this app that has been rarely, so the icon's unread count lagged until the app was opened (#421). Now ipx sends the count: - ipx: a push_devices table and POST/DELETE /api/push/devices. With [push] set to an APNs key, a task compares each phone's All Subscriptions unread count with the last sent, once a minute, and sends a changed one to Apple as a badge-only push: no alert, no sound. A token Apple says is gone is forgotten. The signing token is ES256 over the team and time, made again every forty minutes, inside Apple's limits. - iOS: the aps-environment entitlement. The app registers for push once badge permission is given, each launch, and hands its token to the page. - Page: registers the token with ipx as the person signed in, since the player's address (ipxapi) lets through only the player's paths, and removes it on signing out. jsonwebtoken here is built without PEM support, so the .p8 key's base64 is decoded in push.rs rather than turning on a feature and changing Cargo.lock for one file. cargo test -p ipx on macOS: 182 pass; the 3 that fail read /proc and fail there without this change too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
237 lines
15 KiB
Markdown
237 lines
15 KiB
Markdown
# Configuration
|
|
|
|
Two places. **config.toml** holds what ipx needs before it reaches its database, and what decides
|
|
who gets in: where things are (`download_dir`, `socket`, `organize`), `[torrent]` and `[web]`.
|
|
**The database** holds the catalogue of feeds (`[feeds.<id>]` below) and the server settings the
|
|
admin page edits (`schedule`, `max_total_gb`, `max_age_days`, `max_new_per_check`,
|
|
`media_types`, `art_cache_mb`). Change those in the web UI, or with `ipx add`, `ipx rm` and `ipx import`; they
|
|
take effect without a restart.
|
|
|
|
The first time ipx meets a database that holds no catalogue, it takes the feeds and those
|
|
settings from config.toml, then rewrites config.toml without them, keeping the original beside it
|
|
as `config.toml.pre-database`. After that, feeds or those settings written into config.toml are
|
|
ignored, with a warning in the log saying so. The sections below describe them as they were
|
|
written in config.toml, which is still how a fresh install begins.
|
|
|
|
config.toml's default location is `$XDG_CONFIG_HOME/ipx/config.toml`
|
|
(`~/.config/ipx/config.toml`), overridden with `--config` or `$IPX_CONFIG`.
|
|
|
|
| What | Where | Override |
|
|
|---|---|---|
|
|
| Config | `~/.config/ipx/config.toml` | `--config`, `$IPX_CONFIG` |
|
|
| Database | `~/.local/share/ipx/state.db` | `$IPX_DATA_DIR` |
|
|
| Control socket | `$XDG_RUNTIME_DIR/ipx.sock` | `[general] socket` |
|
|
| Downloads | `[general] download_dir` | — |
|
|
|
|
`~` is expanded in paths. The database is SQLite in WAL mode unless `IPX_DATABASE_URL` names a
|
|
Postgres database instead. Back SQLite up by copying `state.db` while the daemon is stopped, or
|
|
with `sqlite3 state.db .backup`; back Postgres up with `pg_dump`.
|
|
|
|
ipx does not wait for the disk to confirm each write: SQLite syncs at checkpoints
|
|
(`synchronous=NORMAL`), and ipx's own Postgres sessions set `synchronous_commit=off`, which leaves
|
|
other databases on the server as they are. Waiting made a scan take most of a second a feed. A
|
|
crash of ipx loses nothing; a power cut, or a crash of the Postgres server, can lose the last moment
|
|
of changes, a read mark or a saved position, and never corrupts the database.
|
|
|
|
## `[general]`
|
|
|
|
```toml
|
|
[general]
|
|
download_dir = "~/Podcasts"
|
|
socket = "/run/user/1000/ipx.sock"
|
|
schedule = "every 1h" # "every 30m", "every 4h", "2d", "90" (minutes)
|
|
organize = "feed" # "feed" | "date"
|
|
max_total_gb = 50 # 0 = unlimited
|
|
max_age_days = 30 # 0 = keep forever
|
|
max_new_per_check = 3 # per feed, per scan. 0 = unlimited
|
|
media_types = ["audio", "video"]
|
|
art_cache_mb = 500 # artwork kept on disk. 0 = none
|
|
fetch_internal = [] # internal addresses fetches may reach, e.g. ["192.168.1.0/24"]
|
|
```
|
|
|
|
`schedule`, `max_total_gb`, `max_age_days`, `max_new_per_check`, `media_types` and `art_cache_mb` move into the
|
|
database as described above; `download_dir`, `socket`, `organize` and `fetch_internal` stay in config.toml.
|
|
|
|
* **`schedule`** — how often feeds are re-checked. A feed's own `<ttl>` still wins when it asks to
|
|
be polled *less* often, and a per-feed `schedule` overrides both. Without a schedule of its own,
|
|
a feed that sends the same body check after check (Patreon, Substack, which ignore "not
|
|
modified") is checked less often the longer it stays the same, up to every four hours, and is
|
|
back on schedule once it changes. A feed that keeps failing
|
|
waits as long as it has been failing before the next try, up to a day, and is back on schedule
|
|
after its first success. A moment's failure to look its host up does not count: the network, not
|
|
the feed, failed, and it is tried again in ten minutes. Admin-only from the UI.
|
|
* **`organize`** — `feed` files downloads under the feed's folder; `date` under `YYYY-MM-DD`.
|
|
* **`max_total_gb`** — the reaper deletes to get back under this, oldest first, keeping a 50 MB
|
|
pad. Kept items are never deleted, and a file only counts as read once every subscriber has
|
|
read it. `0` disables it entirely.
|
|
* **`max_age_days`** — items older than this with no file on disk are pruned from the database.
|
|
Kept ones stay. `0` disables it.
|
|
* **`art_cache_mb`** — how much show and episode artwork iPX keeps on disk, in `art/` beside the
|
|
database, so the page loads it from iPX rather than from every publisher's server. Over this,
|
|
what has gone longest unshown goes first. `0` keeps none: artwork still comes through iPX, fetched
|
|
each time. 500 by default.
|
|
* **`max_new_per_check`** — how many of a feed's newest episodes are downloaded; older ones stay
|
|
listed to download by hand. It stops a new subscription pulling a whole back catalogue.
|
|
`0` means every episode, for an archive; set it on the feeds you want archived, since on the
|
|
global default it applies to every feed: subscribing to an OPML of 80 feeds with no limit
|
|
fetched 216 files and 22 GB in one scan.
|
|
* **`media_types`** — top-level MIME types taken automatically. Anything else is still listed and
|
|
can be fetched by hand; blog feeds put each article's header image in an `<enclosure>`, and
|
|
without this the disk fills with artwork. Empty takes everything.
|
|
* **`fetch_internal`** — internal addresses and networks that feeds, episodes, artwork and
|
|
`.torrent` files may still be fetched from, as `["192.168.1.20", "10.0.0.0/8"]`. Anything off
|
|
the public internet (loopback, the LAN, Docker's networks, link-local) is refused otherwise,
|
|
redirects included, because a feed's publisher or anyone signed in chooses what is fetched. List
|
|
addresses, not names: a feed on `nas.lan` needs `nas.lan`'s address here. The directory is not
|
|
affected. Empty by default; read at start.
|
|
|
|
## `[torrent]`
|
|
|
|
```toml
|
|
[torrent]
|
|
enabled = true
|
|
seed_ratio = 1.0 # stop seeding at this ratio ...
|
|
seed_time_mins = 60 # ... or after this long, whichever comes first
|
|
port_range = "6881-6889"
|
|
stall_mins = 30 # give up on a torrent making no progress
|
|
```
|
|
|
|
A `.torrent` body is handed to the torrent path whatever MIME type it was advertised as. Torrents
|
|
run on their own tasks (two at a time) so a slow swarm never blocks a scan.
|
|
|
|
## `[web]`
|
|
|
|
```toml
|
|
[web]
|
|
enabled = true
|
|
bind = "0.0.0.0:8099" # 127.0.0.1:8080 by default
|
|
token = "" # generated and saved on first run
|
|
trusted_header = "" # e.g. "Cf-Access-Authenticated-User-Email"
|
|
trusted_proxies = ["127.0.0.1", "::1"]
|
|
access_team = "" # e.g. "<team>.cloudflareaccess.com"
|
|
access_aud = "" # the Access application's AUD tag
|
|
auto_create_users = true
|
|
sign_out_url = "" # e.g. "/cdn-cgi/access/logout"
|
|
session_days = 30
|
|
metrics_bind = "" # e.g. "0.0.0.0:9187"; empty serves no metrics
|
|
```
|
|
|
|
* **`token`** — the shared secret, which signs in as the **admin**. `?token=…` sets a cookie, so
|
|
you paste it once per browser. It is what the Docker healthcheck and any scripts use.
|
|
* **`trusted_header`** — a header naming the signed-in user, set by whatever fronts ipx. Empty
|
|
disables that path. See [sso.md](sso.md).
|
|
* **`trusted_proxies`** — addresses allowed to assert that header, and the entire security boundary
|
|
for it. Name the proxy, never a subnet.
|
|
* **`access_team`**, **`access_aud`** — with both set, a request through the proxy also has to
|
|
carry the `Cf-Access-Jwt-Assertion` Cloudflare Access signed for this application, and the name
|
|
comes from that token instead of the header. See [sso.md](sso.md#verifying-cloudflares-token).
|
|
* **`auto_create_users`** — create an account the first time the proxy vouches for a new name.
|
|
* **`sign_out_url`** — where Sign out sends someone the proxy signed in: the proxy's own sign-out,
|
|
`/cdn-cgi/access/logout` behind Cloudflare Access. Empty sends them to the sign-in page, where
|
|
the proxy signs them straight back in.
|
|
* **`session_days`** — sign a session out after this long without a request.
|
|
* **`metrics_bind`** — where to serve `/metrics` for Prometheus: database connections in use, the
|
|
worker's queue, the runtime's tasks, memory and open files. It has a thread and a port of its own
|
|
and no sign-in, so publish it to the LAN only. Empty, the default, serves none.
|
|
|
|
It is plain HTTP. On a LAN bind everything crosses the network in the clear — and a feed URL can
|
|
itself carry a credential. Put TLS in front of it if that matters.
|
|
|
|
## `[push]`
|
|
|
|
```toml
|
|
[push]
|
|
key_file = "/config/AuthKey_ABC123DEFG.p8" # the APNs key Apple gives once
|
|
key_id = "ABC123DEFG" # shown beside it in the developer account
|
|
team_id = "8743L8D729" # the team the iOS app is signed by
|
|
topic = "net.sdf1.ipx" # the app's bundle id, the default
|
|
```
|
|
|
|
The iOS app's icon shows All Subscriptions' unread count. With this set, ipx keeps it right while
|
|
the app is closed (#433): once a minute it compares each phone's count with the last one sent, and
|
|
sends a changed one through Apple's push service, a badge alone, with no alert or sound. Without
|
|
it, the count waits on the background refresh iOS runs when it chooses.
|
|
|
|
The key is made in the Apple developer account under Certificates, Identifiers & Profiles, Keys,
|
|
with Apple Push Notifications service enabled, and can be downloaded once. One key serves every
|
|
app of the team, the TestFlight and debug builds both. Keep it as private as the database: it lets
|
|
anyone send to the team's apps. A key, id or team that is wrong shows as a warning in the log,
|
|
`push: Apple refused the signing token`, and nothing is sent until it is right.
|
|
|
|
The app registers each phone through the page as the person signed in there
|
|
(`POST /api/push/devices`), and signing out removes it.
|
|
|
|
## `[feeds.<id>]`
|
|
|
|
Kept in the database once ipx has moved them in: a feed's settings are changed in the web UI, and
|
|
feeds come and go with `ipx add`, `ipx rm` and `ipx import`. The table key is the feed id: stable,
|
|
human-readable, and used in paths and the API. `ipx add` derives it from the feed title.
|
|
|
|
```toml
|
|
[feeds.atp]
|
|
url = "https://atp.fm/rss"
|
|
folder = "Accidental Tech Podcast" # default: the feed title
|
|
schedule = "every 6h" # overrides [general] for this feed
|
|
media_types = ["audio"] # overrides [general] for this feed
|
|
category = "Technology" # the Directory's, if the feed names none
|
|
listed = true # shared with the directory with no subscribers
|
|
username = "ray" # HTTP basic auth
|
|
password_env = "IPX_ATP_PASS" # preferred over a literal `password`
|
|
```
|
|
|
|
With more than one account, **`keywords`, `auto_download`, `allow_explicit` and
|
|
`max_new_per_check` live on each person's subscription in the database**, not here — the values in
|
|
config.toml are the fallback for a feed nobody has claimed. The keys above describe the feed itself
|
|
and are the same for everyone. See [users.md](users.md).
|
|
|
|
`username` and its password go to the feed's own host alone: not to episodes on another host, nor to
|
|
the feeds an OPML lists on other hosts, which would hand the password to whoever runs them.
|
|
|
|
**The Directory** is the directory service's ([directory.md](directory.md)), which an admin sets
|
|
up in Admin, Directory: its address, this server's token and whether to share. They are server
|
|
settings, kept in the database (`directory_url`, `directory_token`, `directory_share`), not here.
|
|
With sharing on, this server sends it every feed in the catalogue that is not private, with how
|
|
many people here subscribe, and the `category` above for a feed that names none. To offer one with
|
|
nobody subscribed, `ipx add --list --category News <url>`: it subscribes no one, and a listed feed
|
|
stays in the catalogue, and so in what is shared, when its last subscriber leaves, where another
|
|
is dropped. A feed nobody
|
|
subscribes to is checked once a day. One that is dead (failing for 30 days) or quiet (nothing
|
|
new in a year), with nobody subscribed and no file on disk, is removed from the catalogue.
|
|
|
|
Feeds derived from a subscribed OPML are **not** in the catalogue: the OPML is the source of truth
|
|
and they are re-derived on every scan. Editing one in the UI promotes it to a catalogue entry. So
|
|
does Keep, the plus on a show's page, for whoever turns it on (`kept` on their subscription, #280): when
|
|
the OPML stops listing the show, or nobody subscribes to the OPML any more, it becomes a catalogue
|
|
entry outside the OPML instead of going, and stays on the lists of those who kept it. Anyone who
|
|
had it only through the OPML loses it as before. Leaving the OPML yourself keeps what you kept.
|
|
|
|
## Environment
|
|
|
|
| Variable | Effect |
|
|
|---|---|
|
|
| `IPX_CONFIG` | Config file path |
|
|
| `IPX_ADMIN_PASSWORD` | The first admin's password, for a database with no accounts; random, in `admin-password` beside config.toml, without it |
|
|
| `IPX_DATA_DIR` | Directory holding `state.db` |
|
|
| `IPX_DATABASE_URL` | A `postgres://user:password@host:port/database` URL: use that database instead of `state.db` |
|
|
| `IPX_TEST_DATABASE_URL` | For `cargo test`: run the database tests on this Postgres database too, each in a schema of its own |
|
|
| `IPX_LOG` | What reaches stderr (`ipx=debug`, `ipx::scan=debug`, …) |
|
|
| `IPX_LOG_FORMAT` | `json` for one JSON object a line, with each request's and event's fields as its own (for Loki and the like); text otherwise |
|
|
| `IPX_UI_LOG` | What the in-process log buffer captures for the UI's Log view |
|
|
| `OTEL_EXPORTER_OTLP_ENDPOINT` | An OTLP/HTTP collector, such as Tempo at `http://host:4318`: the daemon sends it traces of scans, downloads and web requests. The other `OTEL_EXPORTER_OTLP_*` variables apply too |
|
|
| `http_proxy` / `https_proxy` | Honoured for feed and enclosure fetches |
|
|
|
|
## ipx-web
|
|
|
|
The page's container reads only its environment (`web/Caddyfile`):
|
|
|
|
| Variable | Effect |
|
|
|---|---|
|
|
| `IPX_UPSTREAM` | Where ipx is, `host:port`; `ipx:8099` by default |
|
|
| `WEB_BIND` | Where to listen; `:8080` by default |
|
|
| `WEB_ROOT` | The built page; `/srv` in the image |
|
|
| `TUNNEL_PEER` | The one address whose `Cf-Access-*` headers are passed on to ipx: where cloudflared's requests come from. Unset passes none. See [sso.md](sso.md) |
|
|
| `TRUSTED_HEADER` | ipx's `[web] trusted_header`, when it is not Cloudflare's: it is passed on from `TUNNEL_PEER` alone too, and dropped from anyone else. Set it to the same name, or anyone could sign in as anyone |
|
|
| `WEB_TRACING` | `on` sends a span for each request to the collector the `OTEL_*` variables name (`OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf`, `OTEL_SERVICE_NAME`), continuing the page's trace into ipx. Off by default |
|
|
|
|
ipx-directory's settings are in [directory.md](directory.md).
|