Home page: a Docs page, for installing ipx and ipx-web and using their APIs and the directory's (#222)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
218
site/docs.html
Normal file
218
site/docs.html
Normal file
@@ -0,0 +1,218 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>iPX docs</title>
|
||||
<meta name="description" content="How to run iPX and its page on your own server, and how to use iPX's API and the directory's.">
|
||||
<meta name="color-scheme" content="light">
|
||||
<link rel="icon" href="/favicon.png">
|
||||
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="stylesheet" href="/site.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="bar">
|
||||
<a class="home" href="/"><img src="/logo.svg" alt="" width="36" height="36">iPX</a>
|
||||
<nav aria-label="Elsewhere">
|
||||
<a href="/docs" aria-current="page">Docs</a>
|
||||
<a href="https://directory.ipxreader.app">Directory</a>
|
||||
<a class="src" href="https://git.sdf1.net/rays/ipx" title="Source code on git.sdf1.net"><img src="/gitea.svg" alt="Source code" width="28" height="28"></a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="doc">
|
||||
<h1 class="title">Docs</h1>
|
||||
<p class="lede">iPX is two containers: <strong>ipx</strong>, which checks your podcasts, downloads them and answers the API, and <strong>ipx-web</strong>, which serves the page and passes the API on to ipx. The directory is a third, which you only need to run if you want a directory of your own.</p>
|
||||
|
||||
<nav class="toc" aria-label="On this page">
|
||||
<ol>
|
||||
<li><a href="#install">Install ipx and ipx-web</a></li>
|
||||
<li><a href="#api">Use ipx from a script</a></li>
|
||||
<li><a href="#directory">Use the directory</a></li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<section aria-labelledby="install">
|
||||
<h2 id="install">Install ipx and ipx-web</h2>
|
||||
<p>You need Docker with Compose. There are no published images yet, so you build both from the source:</p>
|
||||
<pre><code>git clone https://git.sdf1.net/rays/ipx.git
|
||||
cd ipx
|
||||
docker build -t ipx .
|
||||
docker build -f Dockerfile.web -t ipx-web .</code></pre>
|
||||
<p>The repository has a sample <code>docker-compose.yml</code> that runs both, with the database in SQLite and everything ipx keeps in three folders beside the file:</p>
|
||||
<pre><code>services:
|
||||
ipx:
|
||||
image: ipx
|
||||
container_name: ipx
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
PUID: "99" # who owns the files it writes
|
||||
PGID: "100"
|
||||
TZ: "UTC"
|
||||
ports:
|
||||
- "6881:6881/tcp" # BitTorrent
|
||||
- "6881:6881/udp"
|
||||
volumes:
|
||||
- ./config:/config # config.toml, written on first start
|
||||
- ./data:/data # the database
|
||||
- ./downloads:/downloads
|
||||
|
||||
ipx-web:
|
||||
image: ipx-web
|
||||
container_name: ipx-web
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
IPX_UPSTREAM: "ipx:8099"
|
||||
ports:
|
||||
- "8099:8080" # the page
|
||||
depends_on:
|
||||
- ipx</code></pre>
|
||||
<pre><code>docker compose up -d</code></pre>
|
||||
<p>ipx has no port of its own: everything reaches it through ipx-web, on port 8099. Open <code>http://<your server>:8099</code> and sign in as <strong>admin</strong> with the password <strong>ipodderx</strong>, then change it:</p>
|
||||
<pre><code>echo -n 'a good password' | docker exec -i ipx ipx user passwd admin</code></pre>
|
||||
<p>The password is read from what you pipe in, so it stays out of your shell's history.</p>
|
||||
|
||||
<h3>Adding people</h3>
|
||||
<p>An admin adds people in Admin, Accounts, or from the command line:</p>
|
||||
<pre><code>echo -n 'their password' | docker exec -i ipx ipx user add sam</code></pre>
|
||||
<p>Each person starts with no podcasts and subscribes to their own. Two people subscribing to the same show cost nothing extra: it is fetched once and each file is kept once.</p>
|
||||
|
||||
<h3>Settings</h3>
|
||||
<p>How often podcasts are checked, how much disk to use, how many new episodes to download and the rest are in Admin on the page, and take effect straight away. <code>/config/config.toml</code> holds only what ipx needs before it can read its database: where downloads go, BitTorrent, and who gets in.</p>
|
||||
<div class="scroll"><table>
|
||||
<thead><tr><th>Environment variable</th><th>What it does</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>IPX_DATABASE_URL</code></td><td>A <code>postgres://user:password@host:5432/database</code> address, to keep everything in Postgres instead of SQLite</td></tr>
|
||||
<tr><td><code>PUID</code>, <code>PGID</code></td><td>The user and group that own the files ipx writes</td></tr>
|
||||
<tr><td><code>IPX_LOG_FORMAT</code></td><td><code>json</code> logs one JSON object a line, for Loki and the like</td></tr>
|
||||
<tr><td><code>OTEL_EXPORTER_OTLP_ENDPOINT</code></td><td>Sends traces of requests, checks and downloads to an OpenTelemetry collector; set <code>WEB_TRACING=on</code> on ipx-web too</td></tr>
|
||||
<tr><td><code>IPX_UPSTREAM</code></td><td>On ipx-web: where ipx is, <code>ipx:8099</code> by default</td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
|
||||
<h3>Reaching it from outside</h3>
|
||||
<p>The page is plain HTTP. If anyone will reach it from outside your network, put HTTPS in front of it: a reverse proxy you already run, or a Cloudflare tunnel. People can then sign in through Cloudflare Access or Authentik instead of a password; <a href="https://git.sdf1.net/rays/ipx/src/branch/main/docs/sso.md">single sign-on</a> says how.</p>
|
||||
|
||||
<h3>Updating</h3>
|
||||
<pre><code>git pull
|
||||
docker build -t ipx .
|
||||
docker build -f Dockerfile.web -t ipx-web .
|
||||
docker compose up -d</code></pre>
|
||||
<p>Your podcasts, accounts and downloads are in the three folders and are kept.</p>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="api">
|
||||
<h2 id="api">Use ipx from a script</h2>
|
||||
<p>Anything the page does, a script can do through the same API.</p>
|
||||
|
||||
<h3>A token</h3>
|
||||
<p>In Settings, under API tokens, give a token a name and press Make. It is shown once, so copy it then. It acts as you: it sees your podcasts and changes them, and it is an admin only if you are. Revoke it in the same place.</p>
|
||||
<pre><code>curl -H "Authorization: Bearer ipx_…" http://<your server>:8099/api/feeds</code></pre>
|
||||
<p>Behind Cloudflare Access, a token alone does not get past Access. Give the script an Access service token as well, and send its <code>CF-Access-Client-Id</code> and <code>CF-Access-Client-Secret</code> beside the token.</p>
|
||||
|
||||
<h3>The API</h3>
|
||||
<p>Requests and answers are JSON. A feed's id is the one in its list's address, such as <code>99-invisible</code>; an episode is named by its feed and its guid, the guid URL-encoded.</p>
|
||||
<div class="scroll"><table>
|
||||
<thead><tr><th>Request</th><th>What it does</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>GET /api/me</code></td><td>Who you are, and the version</td></tr>
|
||||
<tr><td><code>GET /api/feeds</code></td><td>Your podcasts, each with its unread and downloaded counts</td></tr>
|
||||
<tr><td><code>POST /api/feeds</code></td><td>Subscribe: <code>{"url": "…"}</code>, a feed's address or a site that links one; optional <code>folder</code>, <code>keywords</code></td></tr>
|
||||
<tr><td><code>DELETE /api/feeds/{id}</code></td><td>Unsubscribe. The files stay for anyone else who subscribes</td></tr>
|
||||
<tr><td><code>PATCH /api/feeds/{id}</code></td><td>Your settings for a podcast</td></tr>
|
||||
<tr><td><code>GET /api/feeds/{id}/entries</code></td><td>A podcast's episodes, 50 at a time: <code>offset</code>, <code>limit</code>, <code>filter</code> (<code>unread</code>, <code>downloaded</code> or <code>flagged</code>), <code>q</code> to search, <code>sort</code> and <code>dir</code></td></tr>
|
||||
<tr><td><code>GET /api/entries</code></td><td>The same, across all your podcasts</td></tr>
|
||||
<tr><td><code>POST /api/entries/{feed}/{guid}/flags</code></td><td>Mark an episode read or kept: <code>{"read": true}</code>, <code>{"flagged": true}</code></td></tr>
|
||||
<tr><td><code>POST /api/entries/{feed}/{guid}/position</code></td><td>Where you are in it: <code>{"secs": 1234}</code></td></tr>
|
||||
<tr><td><code>POST /api/feeds/{id}/read-all</code></td><td>Mark a podcast's episodes read; <code>POST /api/read-all</code> marks them all</td></tr>
|
||||
<tr><td><code>POST /api/enclosures/{id}/download</code></td><td>Download an episode's file now</td></tr>
|
||||
<tr><td><code>GET /media/{id}</code></td><td>The file itself, with seeking</td></tr>
|
||||
<tr><td><code>POST /api/fetch</code></td><td>Check for new episodes now</td></tr>
|
||||
<tr><td><code>GET /api/opml</code>, <code>POST /api/opml</code></td><td>Your subscriptions as OPML, out or in</td></tr>
|
||||
<tr><td><code>GET /api/events</code></td><td>A live stream (server-sent events) of checks and downloads as they happen</td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
<p>For example, the newest unread episodes across everything you follow:</p>
|
||||
<pre><code>curl -H "Authorization: Bearer ipx_…" \
|
||||
"http://<your server>:8099/api/entries?filter=unread&limit=10"</code></pre>
|
||||
|
||||
<h3>The command line</h3>
|
||||
<p>The same <code>ipx</code> program that runs the server is its command line. Run it in the container with <code>docker exec ipx ipx …</code>; when the server is running, it does the work and the command shows what happened.</p>
|
||||
<div class="scroll"><table>
|
||||
<thead><tr><th>Command</th><th>What it does</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>ipx list</code></td><td>Every podcast and its state</td></tr>
|
||||
<tr><td><code>ipx status</code></td><td>How many podcasts, how many files waiting and downloaded</td></tr>
|
||||
<tr><td><code>ipx fetch [id] [--force]</code></td><td>Check everything that is due, or one podcast; <code>--force</code> checks it even if it is not due</td></tr>
|
||||
<tr><td><code>ipx add <url></code>, <code>ipx rm <id></code></td><td>Add a podcast, or take it out</td></tr>
|
||||
<tr><td><code>ipx import <file></code>, <code>ipx export <file></code></td><td>OPML in or out</td></tr>
|
||||
<tr><td><code>ipx reap [--dry-run]</code></td><td>Clear old files now, to stay within the space you set</td></tr>
|
||||
<tr><td><code>ipx user add|list|passwd|rm</code></td><td>Accounts</td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
<p>A program on the same machine can also talk to the running server over its control socket, one JSON command a line, and read back one event a line:</p>
|
||||
<pre><code>printf '{"cmd":"fetch","force":true}\n' | socat - UNIX-CONNECT:$XDG_RUNTIME_DIR/ipx.sock
|
||||
{"ev":"feed_start","feed":"atp"}
|
||||
{"ev":"download_done","feed":"atp","enclosure":42,"path":"…","bytes":3000000}
|
||||
{"ev":"feed_done","feed":"atp","new":1,"downloaded":1,"failed":0,"torrents":0}
|
||||
{"ev":"scan_done","feeds":1}</code></pre>
|
||||
<p>The commands are <code>fetch</code> (optional <code>feed</code>, <code>force</code>), <code>download</code> (<code>enclosure</code>), <code>reap</code> (optional <code>dry_run</code>) and <code>status</code>.</p>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="directory">
|
||||
<h2 id="directory">Use the directory</h2>
|
||||
<p>The directory gathers the public podcasts that iPX servers' listeners follow, and how many follow each. It never holds a file and never learns who listens.</p>
|
||||
|
||||
<h3>Browsing it</h3>
|
||||
<p>Anyone can browse <a href="https://directory.ipxreader.app">directory.ipxreader.app</a> without signing in: the most followed, the categories, everything A to Z, and a page for each podcast with its latest episodes and its address, to subscribe to in any podcast app.</p>
|
||||
|
||||
<h3>Connecting your iPX</h3>
|
||||
<p>In iPX, an admin opens Admin, Directory and enters the directory's address, <code>https://directory.ipxreader.app</code>, and the token its operator gave your server. Everyone on your server then has the Directory at the top of their list, and subscribes from it in one click.</p>
|
||||
<p>Turn on sharing to add your server's podcasts to it. iPX sends only the podcasts' addresses and how many people on your server follow each, never who. It leaves out anything that looks private: a podcast someone marked Private, one with a login or a key in its address, and paid feeds such as Patreon's.</p>
|
||||
|
||||
<h3>Its API</h3>
|
||||
<p>Everything under <code>/v1</code> takes a token, sent as <code>Authorization: Bearer <token></code>. A token reads (<code>ro</code>), reads and shares (<code>rw</code>), or also removes and blocks podcasts (<code>rwa</code>).</p>
|
||||
<div class="scroll"><table>
|
||||
<thead><tr><th>Request</th><th>Token</th><th>What it does</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>GET /v1/me</code></td><td>ro</td><td>The token's name and access</td></tr>
|
||||
<tr><td><code>GET /v1/feeds</code></td><td>ro</td><td>Every podcast, A to Z: <code>id</code>, <code>url</code>, <code>title</code>, <code>image</code>, <code>category</code>, <code>subscribers</code></td></tr>
|
||||
<tr><td><code>GET /v1/popular?limit=10</code></td><td>ro</td><td>The most followed, up to 100</td></tr>
|
||||
<tr><td><code>GET /v1/feeds/{id}</code></td><td>ro</td><td>One podcast, with its description and latest twenty episodes</td></tr>
|
||||
<tr><td><code>POST /v1/snapshot</code></td><td>rw</td><td>Share: <code>[{"url": "…", "subscribers": 3}]</code>, replacing what this token shared before</td></tr>
|
||||
<tr><td><code>DELETE /v1/feeds/{id}</code></td><td>rwa</td><td>Remove a podcast and keep it out</td></tr>
|
||||
<tr><td><code>POST /v1/feeds/{id}/refresh</code></td><td>rwa</td><td>Check a podcast now</td></tr>
|
||||
<tr><td><code>GET /v1/blocked</code>, <code>DELETE /v1/blocked/{id}</code></td><td>rwa</td><td>What is kept out; let one back in</td></tr>
|
||||
<tr><td><code>GET /v1/instances</code>, <code>DELETE /v1/instances/{name}</code></td><td>rwa</td><td>The servers sharing; drop one's share</td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
<p>A podcast's <code>id</code> is the first 16 hex digits of the SHA-256 of its address, with the scheme and host in lower case and no default port or fragment, so every server arrives at the same one. Descriptions are the publisher's own HTML: sanitize them before you show them.</p>
|
||||
<pre><code>curl -H "Authorization: Bearer <token>" "https://directory.ipxreader.app/v1/popular?limit=5"</code></pre>
|
||||
|
||||
<h3>Running your own</h3>
|
||||
<p>The directory is in the same repository. Build it, give it a database (Postgres or SQLite) and a config file, and make a token for each server that will use it:</p>
|
||||
<pre><code>docker build -f Dockerfile.directory -t ipx-directory .</code></pre>
|
||||
<pre><code>services:
|
||||
ipx-directory:
|
||||
image: ipx-directory
|
||||
container_name: ipx-directory
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
DIRECTORY_DATABASE_URL: "postgres://ipx_directory:password@db-host:5432/ipx_directory"
|
||||
ports:
|
||||
- "8100:8100"
|
||||
volumes:
|
||||
- ./directory:/config:ro # directory.toml</code></pre>
|
||||
<pre><code># directory/directory.toml
|
||||
bind = "0.0.0.0:8100"
|
||||
database_url_env = "DIRECTORY_DATABASE_URL"</code></pre>
|
||||
<pre><code>docker exec ipx-directory ipx-directory token new my-server --access rw</code></pre>
|
||||
<p>That prints the token, to give to the server, and the lines to add to <code>directory.toml</code>, which keeps only its SHA-256. The directory reads the file again within a minute, so adding or revoking a token needs no restart. It checks each podcast every six hours.</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>iPX is free and open source under the MIT licence. The full documentation is in the <a href="https://git.sdf1.net/rays/ipx/src/branch/main/docs">repository</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user