chore: rename project from podpuddle to podsteadr
Renames the repo directory and every podpuddle/PODPUDDLE reference across code, config, and docs to podsteadr/PODSTEADR (package names, Docker Compose project/service/volume names, env var names, UI/RSS strings). Existing Docker volume data (uploaded blobs, mediamtx recordings, the server's sqlite DB and its nostr identity key) was migrated to new podsteadr_-prefixed volumes with matching filenames so it isn't orphaned by the rename.
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
||||
# Hostname or IP your users will reach this box on (no scheme, no port)
|
||||
PUBLIC_HOST=localhost
|
||||
|
||||
# Where the podpuddle UI/API is reachable from browsers
|
||||
# Where the podsteadr UI/API is reachable from browsers
|
||||
PUBLIC_URL=http://${PUBLIC_HOST}:8095
|
||||
|
||||
# Public endpoints handed out for streaming / playback / uploads
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# podpuddle
|
||||
# podsteadr
|
||||
|
||||
Self-hosted, nostr-native podcast publishing and livestreaming.
|
||||
|
||||
@@ -26,7 +26,7 @@ Then open http://localhost:8095, log in with a NIP-07 extension, and follow the
|
||||
|
||||
| Service | Host port | Purpose |
|
||||
|---|---|---|
|
||||
| podpuddle | 8095 | Web UI + API + RSS feeds |
|
||||
| podsteadr | 8095 | Web UI + API + RSS feeds |
|
||||
| MediaMTX | 1935 | RTMP ingest (OBS) |
|
||||
| MediaMTX | 8889 (+8189/udp) | WebRTC/WHIP ingest (browser) |
|
||||
| MediaMTX | 8890 | HLS playback |
|
||||
@@ -54,7 +54,7 @@ Tests: `cd server && npm test`.
|
||||
|
||||
## Data
|
||||
|
||||
Everything lives in named docker volumes: `podpuddle-data` (SQLite, server nostr key,
|
||||
Everything lives in named docker volumes: `podsteadr-data` (SQLite, server nostr key,
|
||||
covers), `mediamtx-recordings` (stream recordings, 7-day retention), `blossom-data`
|
||||
(media blobs). `docker compose down` keeps them; `down -v` wipes them.
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# blossom-server (v4.x) configuration for podpuddle.
|
||||
# blossom-server (v4.x) configuration for podsteadr.
|
||||
# Uploads require a signed nostr auth event (BUD-02, kind 24242);
|
||||
# reads are public so podcast apps can fetch enclosures.
|
||||
|
||||
|
||||
+7
-7
@@ -1,9 +1,9 @@
|
||||
name: podpuddle
|
||||
name: podsteadr
|
||||
|
||||
services:
|
||||
podpuddle:
|
||||
podsteadr:
|
||||
build: .
|
||||
container_name: podpuddle
|
||||
container_name: podsteadr
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8095:8095"
|
||||
@@ -21,7 +21,7 @@ services:
|
||||
BLOSSOM_URL_INTERNAL: http://blossom:3000
|
||||
NOSTR_RELAYS: ${NOSTR_RELAYS:-wss://relay.damus.io,wss://nos.lol,wss://relay.nostr.band}
|
||||
volumes:
|
||||
- podpuddle-data:/data
|
||||
- podsteadr-data:/data
|
||||
- mediamtx-recordings:/recordings:ro
|
||||
depends_on:
|
||||
- mediamtx
|
||||
@@ -29,7 +29,7 @@ services:
|
||||
|
||||
mediamtx:
|
||||
image: bluenviron/mediamtx:1.19.2
|
||||
container_name: podpuddle-mediamtx
|
||||
container_name: podsteadr-mediamtx
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "1935:1935" # RTMP ingest
|
||||
@@ -45,7 +45,7 @@ services:
|
||||
|
||||
blossom:
|
||||
image: ghcr.io/hzrd149/blossom-server:4
|
||||
container_name: podpuddle-blossom
|
||||
container_name: podsteadr-blossom
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8098:3000"
|
||||
@@ -54,6 +54,6 @@ services:
|
||||
- blossom-data:/app/data
|
||||
|
||||
volumes:
|
||||
podpuddle-data:
|
||||
podsteadr-data:
|
||||
mediamtx-recordings:
|
||||
blossom-data:
|
||||
|
||||
+13
-13
@@ -1,4 +1,4 @@
|
||||
# podpuddle — project status & handoff
|
||||
# podsteadr — project status & handoff
|
||||
|
||||
_Last updated: 2026-07-10. State: **working, verified end-to-end, parked.**_
|
||||
|
||||
@@ -18,7 +18,7 @@ fully standalone compose stack today.
|
||||
## How to resume
|
||||
|
||||
```sh
|
||||
cd ~/podpuddle
|
||||
cd ~/podsteadr
|
||||
docker compose up -d # data volumes were kept; everything resumes as-is
|
||||
node scripts/e2e.mjs # 14 automated checks against the running stack
|
||||
```
|
||||
@@ -26,14 +26,14 @@ node scripts/e2e.mjs # 14 automated checks against the running stack
|
||||
Open http://localhost:8095 and log in with a NIP-07 extension (Alby, nos2x).
|
||||
For a non-localhost box, edit `.env` (`PUBLIC_HOST=…`) and `docker compose up -d --build`.
|
||||
|
||||
State that persists in the named volumes: `podpuddle-data` (SQLite DB, the
|
||||
State that persists in the named volumes: `podsteadr-data` (SQLite DB, the
|
||||
server's nostr key in `config/server-nostr-key`, admin = first pubkey that ever
|
||||
logged in), `blossom-data` (media blobs), `mediamtx-recordings` (stream
|
||||
recordings, 7-day retention). `docker compose down -v` wipes all of it.
|
||||
|
||||
Note: the automated e2e run left test data behind (an "E2E Test Show" podcast
|
||||
with 2 episodes, test streams, and the e2e identity as **admin**). The e2e
|
||||
identity's secret key is in `/tmp/podpuddle-e2e-key` (hex). For real use you
|
||||
identity's secret key is in `/tmp/podsteadr-e2e-key` (hex). For real use you
|
||||
probably want a clean slate: `docker compose down -v && docker compose up -d`,
|
||||
then log in with your own extension first so *your* pubkey becomes admin.
|
||||
|
||||
@@ -43,9 +43,9 @@ Three containers on one compose network:
|
||||
|
||||
| Container | Image | Host ports | Role |
|
||||
|---|---|---|---|
|
||||
| `podpuddle` | built from `Dockerfile` (node:22 + ffmpeg) | 8095 | Fastify API + built Vue UI + RSS feeds |
|
||||
| `podpuddle-mediamtx` | `bluenviron/mediamtx:1.19.2` | 1935 (RTMP), 8889 (WHIP), 8189/udp (ICE), 8890→8888 (HLS) | ingest + HLS output + recording |
|
||||
| `podpuddle-blossom` | `ghcr.io/hzrd149/blossom-server:4` (4.4.1) | 8098→3000 | sha256-addressed media blobs |
|
||||
| `podsteadr` | built from `Dockerfile` (node:22 + ffmpeg) | 8095 | Fastify API + built Vue UI + RSS feeds |
|
||||
| `podsteadr-mediamtx` | `bluenviron/mediamtx:1.19.2` | 1935 (RTMP), 8889 (WHIP), 8189/udp (ICE), 8890→8888 (HLS) | ingest + HLS output + recording |
|
||||
| `podsteadr-blossom` | `ghcr.io/hzrd149/blossom-server:4` (4.4.1) | 8098→3000 | sha256-addressed media blobs |
|
||||
|
||||
Key flows:
|
||||
|
||||
@@ -55,7 +55,7 @@ Key flows:
|
||||
directly on any API route (scripting). First pubkey to log in becomes admin.
|
||||
- **Episode upload** — browser hashes the file (`crypto.subtle`, ≤2 GB), signs a
|
||||
kind-24242 BUD-02 auth via NIP-07, PUTs **directly to blossom** (file never
|
||||
transits podpuddle), then registers `{sha256, size, …}` with
|
||||
transits podsteadr), then registers `{sha256, size, …}` with
|
||||
`POST /api/podcasts/:id/episodes`; server HEAD-verifies the blob before
|
||||
accepting. Enclosure URL = `<blossom>/<sha256>.mp4`.
|
||||
- **RSS** — `GET /feeds/:podcastId/feed.xml`, RSS 2.0 + itunes + podcast
|
||||
@@ -68,7 +68,7 @@ Key flows:
|
||||
travels as `?key=` (OBS stream key) or WHIP `Bearer`, is stored only as a
|
||||
sha256 hash, and never appears in viewer URLs. MediaMTX delegates auth to
|
||||
`POST /api/mediamtx/auth` (2xx = allow); reads are public.
|
||||
- **Status / NIP-53** — podpuddle polls the MediaMTX API (`:9997/v3/paths/list`,
|
||||
- **Status / NIP-53** — podsteadr polls the MediaMTX API (`:9997/v3/paths/list`,
|
||||
every 3 s) and flips `planned → live → ended` (the stock MediaMTX image has no
|
||||
shell, so `runOn*` curl hooks are impossible — polling is deliberate). On each
|
||||
transition it (re)publishes the kind-30311 live event **signed by the server's
|
||||
@@ -76,7 +76,7 @@ Key flows:
|
||||
with the user `p`-tagged as `host`, to the relay list in Settings. 30311 is
|
||||
replaceable, which is why the server key signs rather than the user's.
|
||||
- **Recordings** — MediaMTX records fmp4 segments to the shared volume;
|
||||
podpuddle mounts it read-only, lists per-stream files, and
|
||||
podsteadr mounts it read-only, lists per-stream files, and
|
||||
`POST /api/streams/:id/recordings/publish` remuxes (`ffmpeg -c copy
|
||||
+faststart`), uploads to blossom **with the server's key** (the only
|
||||
server-signed upload path), and creates the episode.
|
||||
@@ -112,7 +112,7 @@ frontend/ # Vue 3 + Vite + Tailwind + Pinia
|
||||
admin claim, episode blob verification, feed ETag/304, stream key
|
||||
create/verify/rotate, webhook allow/deny paths, settings admin gating).
|
||||
- `node scripts/e2e.mjs` against the compose stack — **14/14**.
|
||||
- Live leg (manual, via ffmpeg in the podpuddle container):
|
||||
- Live leg (manual, via ffmpeg in the podsteadr container):
|
||||
- RTMP publish with the right key → HLS `index.m3u8` 200 (after MediaMTX's
|
||||
`?cookieCheck=1` redirect — use `curl -L`).
|
||||
- Wrong key → connection reset by MediaMTX (webhook denial confirmed live).
|
||||
@@ -139,7 +139,7 @@ frontend/ # Vue 3 + Vite + Tailwind + Pinia
|
||||
streaming/seeking is degraded. Options later: upgrade to the Deno rewrite,
|
||||
or front blossom with nginx.
|
||||
3. **Split-horizon blossom URL**: browsers use `http://<host>:8098`, but inside
|
||||
the podpuddle container that address doesn't exist. `BLOSSOM_URL_INTERNAL`
|
||||
the podsteadr container that address doesn't exist. `BLOSSOM_URL_INTERNAL`
|
||||
(compose sets `http://blossom:3000`) is used server-side whenever the active
|
||||
blossom URL equals the bundled default; feeds always carry the public URL.
|
||||
4. **Stock MediaMTX image has no shell** — `runOnReady`-style hooks can't run
|
||||
@@ -154,7 +154,7 @@ frontend/ # Vue 3 + Vite + Tailwind + Pinia
|
||||
## Remaining work
|
||||
|
||||
- **Archipelago packaging** (the original deployment target, archy repo):
|
||||
`apps/podpuddle/manifest.yml` + `apps/podpuddle-mediamtx` + `apps/podpuddle-blossom`
|
||||
`apps/podsteadr/manifest.yml` + `apps/podsteadr-mediamtx` + `apps/podsteadr-blossom`
|
||||
following the `apps/btcpay-server` (dependencies) + `apps/monero-ui`
|
||||
(`container.build` on `/opt/archipelago/docker/...`) patterns; bind volumes
|
||||
under `/var/lib/archipelago/<app>`; add ports **8095, 1935, 8889, 8189/udp,
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>podpuddle</title>
|
||||
<title>podsteadr</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
Generated
+2
-2
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "podpuddle-frontend",
|
||||
"name": "podsteadr-frontend",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "podpuddle-frontend",
|
||||
"name": "podsteadr-frontend",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"hls.js": "^1.6.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "podpuddle-frontend",
|
||||
"name": "podsteadr-frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -16,7 +16,7 @@ async function logout() {
|
||||
<header class="border-b border-slate-200 bg-white">
|
||||
<div class="mx-auto flex max-w-4xl items-center justify-between px-4 py-3">
|
||||
<RouterLink to="/" class="flex items-center gap-2 text-lg font-bold text-puddle-700">
|
||||
<span aria-hidden="true">💧</span> podpuddle
|
||||
<span aria-hidden="true">💧</span> podsteadr
|
||||
</RouterLink>
|
||||
<nav v-if="auth.pubkey" class="flex items-center gap-4 text-sm">
|
||||
<RouterLink to="/" class="hover:text-puddle-600">Home</RouterLink>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Thin fetch wrapper for the podpuddle API (session cookie auth).
|
||||
// Thin fetch wrapper for the podsteadr API (session cookie auth).
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(public status: number, message: string) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Browser-direct Blossom (BUD-02) upload: hash locally, sign the kind-24242
|
||||
// authorization with the user's NIP-07 extension, PUT straight to the blossom
|
||||
// server. The file never transits podpuddle.
|
||||
// server. The file never transits podsteadr.
|
||||
|
||||
import { nip07 } from './nip07';
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ async function login() {
|
||||
<div class="mx-auto max-w-md">
|
||||
<div class="card text-center">
|
||||
<div class="mb-2 text-4xl" aria-hidden="true">💧</div>
|
||||
<h1 class="mb-1 text-2xl font-bold">podpuddle</h1>
|
||||
<h1 class="mb-1 text-2xl font-bold">podsteadr</h1>
|
||||
<p class="mb-6 text-slate-500">Publish podcasts and go live — on nostr, from your own server.</p>
|
||||
|
||||
<template v-if="hasNip07()">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# MediaMTX configuration for podpuddle.
|
||||
# MediaMTX configuration for podsteadr.
|
||||
# Ingest: RTMP (OBS) + WebRTC/WHIP (browser). Output: HLS. Publish auth is
|
||||
# delegated to podpuddle via HTTP; stream status is polled from the API.
|
||||
# delegated to podsteadr via HTTP; stream status is polled from the API.
|
||||
|
||||
logLevel: info
|
||||
|
||||
@@ -9,7 +9,7 @@ apiAddress: :9997
|
||||
|
||||
# ---- authentication ------------------------------------------------------
|
||||
authMethod: http
|
||||
authHTTPAddress: http://podpuddle:8095/api/mediamtx/auth
|
||||
authHTTPAddress: http://podsteadr:8095/api/mediamtx/auth
|
||||
authHTTPExclude:
|
||||
- action: api
|
||||
- action: metrics
|
||||
@@ -45,5 +45,5 @@ pathDefaults:
|
||||
|
||||
paths:
|
||||
# Streams live at live/<streamId>; publish requires the stream secret,
|
||||
# which podpuddle checks in the auth webhook.
|
||||
# which podsteadr checks in the auth webhook.
|
||||
"~^live/[A-Za-z0-9]+$": {}
|
||||
|
||||
+3
-3
@@ -10,12 +10,12 @@ import { createHash } from 'node:crypto';
|
||||
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { finalizeEvent, generateSecretKey } from '../server/node_modules/nostr-tools/lib/esm/pure.js';
|
||||
|
||||
const BASE = process.env.PODPUDDLE_URL ?? 'http://localhost:8095';
|
||||
const BASE = process.env.PODSTEADR_URL ?? 'http://localhost:8095';
|
||||
const BLOSSOM = process.env.BLOSSOM_URL ?? 'http://localhost:8098';
|
||||
|
||||
// Persist the test identity across runs — the first pubkey to log in becomes
|
||||
// admin, so reruns must reuse it for the settings test to pass.
|
||||
const KEY_FILE = '/tmp/podpuddle-e2e-key';
|
||||
const KEY_FILE = '/tmp/podsteadr-e2e-key';
|
||||
let sk;
|
||||
if (existsSync(KEY_FILE)) {
|
||||
sk = Uint8Array.from(Buffer.from(readFileSync(KEY_FILE, 'utf8').trim(), 'hex'));
|
||||
@@ -89,7 +89,7 @@ ok('podcast created', podcast.status === 201 && podcast.json.id);
|
||||
const podcastId = podcast.json.id;
|
||||
|
||||
// ---- blossom upload (BUD-02, signed like the browser would) ----
|
||||
const media = readFileSync(process.env.TEST_MP4 ?? '/tmp/podpuddle-test.mp4');
|
||||
const media = readFileSync(process.env.TEST_MP4 ?? '/tmp/podsteadr-test.mp4');
|
||||
const sha256 = createHash('sha256').update(media).digest('hex');
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const auth24242 = finalizeEvent(
|
||||
|
||||
Generated
+2
-2
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "podpuddle-server",
|
||||
"name": "podsteadr-server",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "podpuddle-server",
|
||||
"name": "podsteadr-server",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@fastify/cookie": "^11.0.2",
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "podpuddle-server",
|
||||
"name": "podsteadr-server",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -29,7 +29,7 @@ function nip98Header(url: string, method: string): string {
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
dataDir = mkdtempSync(join(tmpdir(), 'podpuddle-test-'));
|
||||
dataDir = mkdtempSync(join(tmpdir(), 'podsteadr-test-'));
|
||||
const config = loadConfig({
|
||||
DATA_DIR: dataDir,
|
||||
PUBLIC_URL: 'http://localhost:8095',
|
||||
@@ -59,7 +59,7 @@ describe('auth', () => {
|
||||
expect(res.json().pubkey).toBe(pk);
|
||||
expect(res.json().isAdmin).toBe(true); // first login claims admin
|
||||
const setCookie = res.headers['set-cookie'] as string;
|
||||
expect(setCookie).toContain('podpuddle_session=');
|
||||
expect(setCookie).toContain('podsteadr_session=');
|
||||
cookie = setCookie.split(';')[0];
|
||||
});
|
||||
|
||||
@@ -168,7 +168,7 @@ describe('streams + mediamtx auth webhook', () => {
|
||||
method: 'POST',
|
||||
url: '/api/streams',
|
||||
headers: { cookie },
|
||||
payload: { title: 'Live Test', summary: 'hi', hashtags: ['podpuddle'] },
|
||||
payload: { title: 'Live Test', summary: 'hi', hashtags: ['podsteadr'] },
|
||||
});
|
||||
expect(res.statusCode).toBe(201);
|
||||
const body = res.json();
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ export interface BuildAppOptions {
|
||||
|
||||
export async function buildApp(opts: BuildAppOptions): Promise<FastifyInstance> {
|
||||
const { config } = opts;
|
||||
const db = openDatabase(opts.dbPath ?? join(config.DATA_DIR, 'podpuddle.sqlite3'));
|
||||
const db = openDatabase(opts.dbPath ?? join(config.DATA_DIR, 'podsteadr.sqlite3'));
|
||||
const settings = new SettingsService(db, config);
|
||||
const serverKey = loadServerKey(config.DATA_DIR);
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { DB } from '../db/database.js';
|
||||
import type { Config } from '../config.js';
|
||||
import { Nip98Error, verifyNip98 } from '../services/nip98.js';
|
||||
|
||||
export const SESSION_COOKIE = 'podpuddle_session';
|
||||
export const SESSION_COOKIE = 'podsteadr_session';
|
||||
|
||||
declare module 'fastify' {
|
||||
interface FastifyInstance {
|
||||
|
||||
@@ -92,7 +92,7 @@ export function buildFeedXml(podcast: Podcast, episodes: Episode[], ctx: FeedCon
|
||||
` <link>${xmlEscape(link)}</link>`,
|
||||
` <description>${xmlEscape(podcast.description)}</description>`,
|
||||
` <language>${xmlEscape(podcast.language)}</language>`,
|
||||
` <generator>podpuddle</generator>`,
|
||||
` <generator>podsteadr</generator>`,
|
||||
` <lastBuildDate>${rfc2822(lastPub)}</lastBuildDate>`,
|
||||
` <atom:link href="${xmlEscape(feedUrl)}" rel="self" type="application/rss+xml"/>`,
|
||||
` <podcast:guid>${podcast.podcast_guid}</podcast:guid>`,
|
||||
|
||||
Reference in New Issue
Block a user