Briefcast is a self-hosted podcast downloader and library manager with a Go API, scheduled workers, and a Vue web UI served from /app.
- Backend: Go, Gin, Gorm, SQLite by default, optional Postgres
- Frontend: Vue 3, Vite, TypeScript, Tailwind
- Automation: feed refresh, downloads, artwork sync, file checks, backups, retention cleanup, transcription
- Media layout: one mounted
/assetstree for audio, images, transcripts, and summaries - Transcription: WhisperX support in Docker builds, runtime enabled by configuration
- Summaries: OpenAI-compatible LLM summarization from episode transcripts
- Features
- Repository Layout
- Task Runner
- Quick Start
- Docker
- Storage Layout
- Configuration
- Back Catalog Downloads
- Transcripts And Summaries
- Scheduled Jobs
- Frontend Development
- Testing
- Release Basics
- Reset Runtime Data
- Migration Notes
- Subscribe to podcast RSS feeds and keep episodes current.
- Import and export OPML.
- Search external providers and local library records.
- Download, pause, resume, and cancel episode media through the queue.
- Store audio under
/assets/audio/<podcast>/. - Store artwork under
/assets/images/<podcast>/. - Generate and export transcripts under
/assets/transcripts/<podcast>/. - Generate and export summaries under
/assets/summaries/<podcast>/. - Backfill legacy transcript and summary exports through the
export-allAPI. - Configure initial back-catalog downloads by count, age in months, or all feed entries.
- Read embedded ID3 chapters and feed chapters.
- Tag podcasts and expose RSS feeds for all podcasts, individual podcasts, and tags.
- Mark episodes played/unplayed, bookmark episodes, and favorite summaries.
- Apply retention policies per podcast.
- Use WhisperX for local transcription with progress checkpoints and retry behavior.
- Use an OpenAI-compatible API for LLM summaries.
- Run scheduled backups and maintenance jobs.
main.go: application entrypoint, route registration, and scheduler wiringcontrollers/: HTTP handlers and API response shapingservice/: podcast, download, export, transcription, summary, file, and retention logicdb/: database models, migrations, connection setup, and helpersinternal/: shared internals such as logging and version helpersfrontend/: Vue application; production build is served at/appscripts/: Python helper scripts for feed parsing, ID3 extraction, and WhisperX.github/: CI and release workflowsdocker-compose.yml: runtime stack for Docker Composejustfile: common local, Docker, test, and release commands
Use just as the primary command surface.
Install just:
# Windows
winget install Casey.Just
# macOS
brew install just
# Any platform with Rust
cargo install justCommon commands:
just --list
just bootstrap
just up
just ps
just logs
just down
just test-full
just release-helpYou can override the compose and environment files per command:
ENV_FILE=.env COMPOSE_FILE=docker-compose.yml just up- Go 1.26+
- Node 24+
- Python 3.14+ for repository tooling
uvfor Python tooling: https://docs.astral.sh/uv/- Docker, if running the containerized app
The Docker image is the simplest runtime path because it includes the app server and Python helper environment. WhisperX remains opt-in at runtime.
just bootstrapFor local source runs outside Docker, install the helper packages into the Python interpreter used by FEEDPARSER_PYTHON and MUTAGEN_PYTHON:
python -m pip install feedparser mutagenFor go run, create a .env file or export the same values in your shell:
CONFIG=./config
DATA=./assets
DATABASE_URL=sqlite:///config/briefcast.db
CHECK_FREQUENCY=15
LOG_OUTPUT=stdout,file:./logs/briefcast-{startup_ts}.log
PASSWORD=Create the local folders if needed:
mkdir -p config assets logsThe backend serves frontend/dist at /app.
npm --prefix frontend run buildgo run ./main.goOpen:
- App UI:
http://localhost:8080/app - Root path
/redirects to/app
If PASSWORD is set, basic auth is enabled with username briefcast.
Start from .env.example:
cp .env.example .envEdit .env, especially these values:
BRIEFCAST_IMAGE=ghcr.io/ctaylor1/briefcast:latest
HOST_CONFIG_DIR=./config
HOST_ASSETS_DIR=./assets
HOST_LOGS_DIR=./logs
HOST_PORT=8080
PASSWORD=
DATABASE_URL=sqlite:///config/briefcast.dbStart the app:
just upUseful lifecycle commands:
just logs
just ps
just restart
just downdocker-compose.yml uses an image: reference. It does not contain a compose build: block, so just rebuild alone will not rebuild the image from your local source.
After code or .env changes, use:
just docker-build image=briefcast:latest
just down
just upIf you set a different BRIEFCAST_IMAGE in .env, build that same image tag:
just docker-build image=briefcast:localThen set:
BRIEFCAST_IMAGE=briefcast:localand restart with:
just down
just updocker pull ghcr.io/ctaylor1/briefcast:1.6.1
docker pull ghcr.io/ctaylor1/briefcast:latestExample docker run with SQLite:
docker run -d \
--name briefcast \
--restart unless-stopped \
-p 8080:8080 \
-v briefcast_config:/config \
-v briefcast_assets:/assets \
-v briefcast_logs:/logs \
-e CONFIG=/config \
-e DATA=/assets \
-e DATABASE_URL=sqlite:///config/briefcast.db \
ghcr.io/ctaylor1/briefcast:1.6.1docker run -d \
--name briefcast \
--restart unless-stopped \
-p 8080:8080 \
-v /srv/briefcast/config:/config \
-v /srv/briefcast/assets:/assets \
-v /srv/briefcast/logs:/logs \
-e CONFIG=/config \
-e DATA=/assets \
-e DB_DRIVER=postgres \
-e DATABASE_URL=postgres://operator:${BRIEFCAST_DB_PASSWORD}@192.168.1.2:5432/briefcast?sslmode=disable \
ghcr.io/ctaylor1/briefcast:1.6.1This is useful for NAS deployments that load images from a tar instead of pulling from GHCR.
docker buildx build --platform linux/amd64 --build-arg INSTALL_WHISPERX=true -t briefcast:with-whisperx --load .
docker image save -o builds/briefcast_with_whisperx.tar briefcast:with-whisperxCopy the tar to the NAS, load it, and set:
BRIEFCAST_IMAGE=briefcast:with-whisperx
WHISPERX_ENABLED=true
WHISPERX_CHECK_FREQUENCY=30
WHISPERX_MAX_CONCURRENCY=1Restart:
docker compose --env-file .env up -d --force-recreateBriefcast now uses /assets for all generated library output. There is no separate /exports mount or export directory.
| Container Path | Required | Purpose |
|---|---|---|
/config |
yes | SQLite database, app config, backups, caches |
/assets |
yes | audio, images, transcript exports, summary exports |
/logs |
yes | application logs |
Asset subfolders:
/assets/
audio/<podcast>/
images/<podcast>/
transcripts/<podcast>/
summaries/<podcast>/
markdown/transcripts/<podcast>/
markdown/summaries/<podcast>/
Audio files should be under audio/<podcast>/. Podcast and episode artwork should be under images/<podcast>/. Transcript and summary text exports are written to their respective folders, with markdown copies under markdown/.
To move assets to another drive or host path:
- Stop the container.
- Move the existing asset folder contents to the new host path, preserving the subfolder layout.
- Update
HOST_ASSETS_DIRin.env. - Start the container again.
No database update is normally required when only the host mount path changes and the files keep the same relative layout inside /assets.
For Docker Compose end users, most deployments only need .env values for:
BRIEFCAST_IMAGEHOST_CONFIG_DIRHOST_ASSETS_DIRHOST_LOGS_DIRHOST_PORTPASSWORDCHECK_FREQUENCYDATABASE_URLLOG_OUTPUT- WhisperX and LLM settings, if those features are enabled
CONFIG: config directory inside the container or local runtimeDATA: assets directory inside the container or local runtimeCHECK_FREQUENCY: base scheduler interval in minutes; invalid values fall back to30PASSWORD: optional basic auth password; username is alwaysbriefcastGIN_MODE: set toreleasefor productionPUID,PGID: optional ownership mapping for created files on NAS/Linux hosts
DATABASE_URL: recommended database connection stringDB_DRIVER: optionalsqliteorpostgresDATABASE_DRIVER: alias forDB_DRIVERDB_MAX_IDLE_CONNS: default10DB_MAX_OPEN_CONNS: default25DB_CONN_MAX_LIFETIME_MINUTES: default0
SQLite:
DATABASE_URL=sqlite:///config/briefcast.dbPostgres:
DATABASE_URL=postgres://briefcast:briefcast@postgres:5432/briefcast?sslmode=disable
DB_DRIVER=postgresPER_HOST_MAX_CONCURRENCY: per-host outbound request cap; default2PER_HOST_RATE_LIMIT_RPS: per-host rate limit; default2.0;0disables pacingHTTP_TIMEOUT_SECONDS: outbound HTTP timeout; default900;0disables
LOG_LEVEL:debug,info,warn, orerror; defaultinfoLOG_FORMAT:jsonortext; defaultjsonfor Go servicesLOG_OUTPUT: comma-separated outputs such asstdout,file:/logs/briefcast-{startup_ts}.logLOG_FILE_MAX_SIZE_MB: default50LOG_FILE_MAX_BACKUPS: default7LOG_FILE_MAX_AGE_DAYS: default14LOG_FILE_COMPRESS: defaulttrueLOG_RUN_TIMESTAMP: optional shared timestamp token
Log path tokens include {startup_ts}, {timestamp}, and {run_ts}.
PODCASTINDEX_KEY: optional PodcastIndex API keyPODCASTINDEX_SECRET: optional PodcastIndex API secret
Feed parsing:
FEEDPARSER_PYTHON: interpreter pathFEEDPARSER_SCRIPT: defaultscripts/feedparser_parse.pyFEEDPARSER_TIMEOUT_SECONDS: default30;0disables
ID3 extraction:
MUTAGEN_PYTHON: interpreter path; falls back toFEEDPARSER_PYTHONMUTAGEN_SCRIPT: defaultscripts/mutagen_id3_extract.pyMUTAGEN_TIMEOUT_SECONDS: default20;0disables
WhisperX dependencies are included in Docker builds by default for linux/amd64. Transcription is still controlled by runtime configuration.
WHISPERX_ENABLED: defaultfalseWHISPERX_PYTHON: interpreter pathWHISPERX_SCRIPT: defaultscripts/whisperx_transcribe.pyWHISPERX_TIMEOUT_SECONDS: default21600WHISPERX_MODEL: defaultmedium.enWHISPERX_LANGUAGE: defaultenWHISPERX_DEVICE:auto,cuda, orcpu; defaultautoWHISPERX_COMPUTE_TYPE:auto,float16,int8, orfloat32; defaultautoWHISPERX_BATCH_SIZE: auto-sized when unsetWHISPERX_BEAM_SIZE: default5WHISPERX_PATIENCE: default1WHISPERX_CONDITION_ON_PREVIOUS_TEXT: defaulttrueWHISPERX_INITIAL_PROMPT: optional promptWHISPERX_VAD_METHOD: defaultpyannote; falls back tosileroif pyannote VAD failsWHISPERX_ALIGN: defaulttrueWHISPERX_DIARIZATION: defaulttrueWHISPERX_DIARIZATION_MODEL: defaultpyannote/speaker-diarization-3.1WHISPERX_MIN_SPEAKERS: default2WHISPERX_MAX_SPEAKERS: default2WHISPERX_HF_TOKEN: Hugging Face token for pyannote diarizationWHISPERX_MAX_CONCURRENCY: default1WHISPERX_MAX_ITEMS: default0, no limitWHISPERX_RETRY_FAILED: defaulttrueWHISPERX_RETRY_DELAY_SECONDS: default300WHISPERX_RETRY_MAX_DELAY_SECONDS: default21600WHISPERX_CHUNK_SECONDS: default120WHISPERX_PROGRESS_POLL_MILLIS: default2000WHISPERX_CHECK_FREQUENCY: defaults toCHECK_FREQUENCYWHISPERX_HF_HOME: default/config/.cache/huggingfacewhen/configexistsWHISPERX_DISABLE_TELEMETRY: defaulttrueWHISPERX_THIRD_PARTY_LOG_LEVEL: defaultwarning
When diarization is enabled, Briefcast forces WhisperX worker count to 1 for host stability. Progress checkpoints are persisted so interrupted transcriptions can resume from the last completed chunk.
Advanced WhisperX overrides can live in .env.whisperx; docker-compose.yml loads it through WHISPERX_ENV_FILE.
Summarization uses an OpenAI-compatible chat completion API and requires transcripts.
LLM_ENABLED: defaultfalseLLM_PROVIDER: defaultopenaiLLM_API_KEY: API key; required when summaries are enabledLLM_BASE_URL: defaulthttps://api.openai.com/v1LLM_MODEL: defaultgpt-4o-miniLLM_MAX_TOKENS: default1024LLM_TEMPERATURE: default0.3LLM_TIMEOUT_SECONDS: default120LLM_SUMMARIZATION_PROMPT: optional system prompt overrideLLM_SUMMARIZATION_USER_PROMPT: optional user prompt template override
The app does not intentionally truncate RSS feed entries during parsing. It stores the entries returned by the podcast RSS feed.
The setting that controls what gets queued for download when a podcast is first added is InitialDownloadMode:
count: queue the firstInitialDownloadCountfeed entries.months: queue entries with a publish date in the lastInitialDownloadMonthsmonths.all: queue every entry exposed by the feed.
Values are managed in the Settings UI and through the settings API. A count or month value of 0 means no app-side limit for that mode.
If a provider's RSS feed only exposes the newest 10 to 20 episodes, Briefcast can only ingest those entries from that feed. To recover older episodes, use a full-history/archive feed, a provider-specific API, another catalog source such as PodcastIndex, or a custom import/backfill that has access to older episode metadata and media URLs.
The Episodes page defaults to paginated views, so check pagination and row count before assuming episodes are missing.
Newly generated transcripts and summaries are exported automatically:
- transcripts:
/assets/transcripts/<podcast>/<episode>.txt - summaries:
/assets/summaries/<podcast>/<episode>.txt - transcript markdown:
/assets/markdown/transcripts/<podcast>/<episode>.md - summary markdown:
/assets/markdown/summaries/<podcast>/<episode>.md
Feed-provided transcripts, WhisperX transcripts, and LLM summaries all write text and markdown files into /assets when they are saved.
The export-all API writes existing database transcripts and summaries back into the assets tree:
curl -X POST http://localhost:8080/settings/export-all
curl http://localhost:8080/settings/export-allexport-all also handles legacy transcript rows that only have raw transcript_json: it builds the canonical transcript text, persists the canonical fields, and writes the transcript file.
If basic auth is enabled:
curl -u briefcast:<password> -X POST http://localhost:8080/settings/export-allBased on CHECK_FREQUENCY=N minutes:
RefreshEpisodes: everyNCheckMissingFiles: everyNDownloadMissingImages: everyNUnlockMissedJobs: every2NUpdateAllFileSizes: every3NTranscribePendingEpisodes: everyWHISPERX_CHECK_FREQUENCY, or everyNwhen unsetRetentionCleanup: every24hCreateBackup: every48h
Run the Vite dev server:
npm --prefix frontend run devBuild for backend serving:
npm --prefix frontend run buildFull suite:
just test-fullTargeted suites:
just test-go
just test-python
just test-frontend
just test-integration
just test-whisperx-realFormatting and linting:
just lint
just format
just typecheckPython tooling:
uv sync --locked --group dev
uv run ruff check .
uv run ruff format --check .
uv run mypy src
uv run pytest
uv run pip-auditThe package version is defined in pyproject.toml. Current version: 1.6.1.
Release helpers:
just release-help
just release-test
just release-build
just release-publish -Bump patch
just release-deploy
just release-verify
just release-rollback
just ship -Bump patchOne-command GitHub Actions release:
gh workflow run release.yml --ref <default-branch> -f bump=patch
gh workflow run release.yml --ref <default-branch> -f bump=minor
gh workflow run release.yml --ref <default-branch> -f bump=major
gh workflow run release.yml --ref <default-branch> -f version=1.6.1Dry run:
gh workflow run release.yml --ref <default-branch> -f bump=patch -f dry_run=trueManual image publish:
docker buildx build --platform linux/amd64 --build-arg INSTALL_WHISPERX=true \
-t ghcr.io/ctaylor1/briefcast:1.6.1 \
-t ghcr.io/ctaylor1/briefcast:latest \
--push .Use scripts/reset_app_data.sh to wipe transactional runtime data while preserving repository files and local configuration.
./scripts/reset_app_data.sh --env-file /volume1/docker/podcasts-briefcast/.env --yesIt clears:
- Database records or the SQLite database file
- Downloaded assets
- Logs
- Generated backups
It keeps:
.env- compose files
- repository code
- other configuration files
Options:
--no-start: do not restart services after reset--yes: skip confirmation prompt--env-file <path>: select a runtime env file
Default SQLite filename is briefcast.db. If an older deployment used another SQLite filename, keep using the existing database by setting DATABASE_URL explicitly.
Older deployments may have audio, artwork, transcripts, or summaries directly under /assets/<podcast> or under a separate export directory. The current layout is:
/assets/audio/<podcast>/
/assets/images/<podcast>/
/assets/transcripts/<podcast>/
/assets/summaries/<podcast>/
/assets/markdown/transcripts/<podcast>/
/assets/markdown/summaries/<podcast>/
After moving files into the current layout, run export-all once to regenerate transcript and summary text and markdown files from the database.
For a clean publish state:
git status --short
git clean -fdX
uv sync --locked --group dev
just test-full
git add .
git commit -m "release: ship-ready"
git tag -a v1.6.1 -m "Briefcast v1.6.1"
git push origin HEAD
git push origin v1.6.1