Built for the Duck Radio archive, Duck Feed combines streaming, ingest, metadata, and administration in one small self-hosted system.
Demo player: https://duckfeed.cmr.my
- 24/7 stream delivery: Liquidsoap + Icecast serve archive audio continuously, with support for scheduled external live-source windows.
- Public player: a React SPA provides playback controls, artwork, stream status, archive browsing, runtime branding, and Mixcloud links.
- Admin UI: authenticated pages cover appearance, episodes, ingest, stream controls, rotation queue management, live scheduling, and integration API keys.
- Local ingest pipeline: uploaded or dropped audio is copied, normalized, validated, fingerprinted, and published into the stream library without mutating the original source.
- Metadata repair: a dedicated worker uses Mixcloud's JSON API to repair or backfill titles, presenters, dates, artwork, descriptions, and Mixcloud URLs.
- Optional upstream archive: Duckhaus can supply catalog metadata and prepared audio, which Duck Feed caches locally within a configured size budget.
- Read-only integration API: external sites and apps can fetch stream metadata through revocable API keys.
- Backend: Node.js, TypeScript, Fastify, Drizzle ORM
- Frontend: React, TypeScript, Vite, Tailwind CSS
- Database: PostgreSQL 16
- Streaming: Liquidsoap + Icecast2
- Audio tooling: ffmpeg, fpcalc, AcoustID, MusicBrainz
- Infra: Docker Compose, Nginx
The main Compose stack contains:
postgresserveringest-workermetadata-workerclientliquidsoapicecastnginx
Duckhaus is an optional upstream service and lives outside this repository.
-
Copy the environment template:
cp .env.example .env
-
Set a real
SESSION_SECRET:openssl rand -hex 32
-
Fill in the required database and Icecast credentials in
.env. -
Start the stack:
make dev
-
In another terminal, run migrations and create an admin user:
make db-migrate make seed-admin
-
Open:
- public player:
http://localhost - admin UI:
http://localhost/admin - direct stream:
http://localhost/stream
- Upload audio in
/admin/ingest, or drop files into the watched dropzone. - The ingest worker copies the source, then normalizes, validates, fingerprints, and publishes stream-ready audio to the local library.
- Successful ingests become available for queueing and rotation.
- Duck Feed can sync episode metadata from Duckhaus and download prepared audio on demand.
- Cached audio is kept within
ROTATION_CACHE_MAX_BYTES. - Local ingest remains fully supported even when Duckhaus is disabled.
archive: local queue plus library playbacklive: scheduled external stream relayoffline: Liquidsoap unreachable
The public player uses the unified stream snapshot endpoint and SSE updates to reflect mode and playback changes.
GET /api/stream: unified stream snapshot used by the SPAGET /api/stream/statusGET /api/stream/now-playingGET /api/stream/eventsGET /api/episodesGET /api/episodes/:slugGET /api/site-settingsGET /api/site-assets/:filename
/admin: dashboard with stream and ingest overview/admin/appearance: colors, logo, favicon/admin/episodes: episode editing and track review/admin/ingest: uploads and ingest job history/admin/stream: request queue, rotation queue, restart/skip controls, integration keys/admin/schedule: live-source URL and weekly schedule management
Read-only integration keys are managed in the admin UI or through the CLI:
make stream-api-key-list
make stream-api-key-create LABEL="Partner app"
make stream-api-key-revoke ID=<uuid>Endpoints:
GET /api/stream/integration/metadataGET /api/stream/integration/now-playingGET /api/stream/integration/queue
Example:
curl \
-H "Authorization: Bearer dfs_your_key_here" \
http://localhost/api/stream/integration/metadataRequired:
POSTGRES_USERPOSTGRES_PASSWORDPOSTGRES_DBDATABASE_URLSESSION_SECRETICECAST_SOURCE_PASSWORDICECAST_ADMIN_PASSWORD
Optional but important:
ACOUSTID_API_KEY: enables fingerprint-based track suggestionsDUCKHAUS_BASE_URL: enables upstream catalog sync and on-demand prepared-audio downloadsDUCKHAUS_API_TOKEN: bearer token for DuckhausMIXCLOUD_USER_URL: source archive used by metadata recoveryMUSICBRAINZ_CONTACT_URL: public contact URL sent in MusicBrainz requestsBRANDING_DIR: overrides the branding asset directory inside the server containerROTATION_CACHE_MAX_BYTES: caps the local cache budget for Duckhaus-backed audio
docker-compose.prod.ymlkeeps PostgreSQL, the API, and Icecast off public host ports while leaving Nginx as the edge container.- The repository Nginx config is intentionally generic. Keep host-specific TLS and split-origin routing in local-only overrides.
make buildandmake deployautomatically includedocker-compose.prod.local.ymlwhen it exists.- For split-origin deployments, copy
docker-compose.prod.local.example.ymltodocker-compose.prod.local.ymland place host TLS server blocks undernginx/conf.d-extra/. - If app, API, and stream use different public origins, set
ICECAST_HOSTNAMEappropriately and build the client withclient/.env.production.
make dev # start the full stack in development mode
make stop # stop development services
make test # run server and client tests
make lint # run lint checks
make typecheck # run TypeScript checks
make db-migrate # run pending DB migrations
make seed-admin # create/reset the admin user
make stream-check-quick # fast public stream smoke test
make stream-check # deeper stream validation
make link-check # validate tracked Markdown links
make backup # create a PostgreSQL backup
make build # build production images
make deploy # run the deploy scriptserver/: Fastify API, workers, database schema, migrations, services, and testsclient/: React SPA for the public player and admin UIliquidsoap/: stream and live-source switching configurationicecast/: Icecast configurationnginx/: reverse proxy configurationscripts/: deploy, backup, restore, and verification helpersdocs/features/: focused notes on stream, library, ingest, admin, and public behaviour
MIT
