A cloud-native .NET podcast feed server for Azure with Blob Storage integration. Host your audio content (like NotebookLM audio overviews) with iTunes-compatible RSS feeds.
- Multi-feed support - Host multiple podcast feeds from a single instance
- Role-based access control - Admin and FeedOwner roles with per-user API keys
- User management - Create users, manage permissions, and assign feed ownership via API and CLI
- Audio normalization - Loudness normalization (-16 LUFS) via FFmpeg, locally or server-side (async via Azure Functions)
- Azure Blob Storage - Scalable cloud storage for audio files
- RSS podcast feeds - iTunes spec compatible with per-feed configuration
- CLI tool - Command-line interface for episode, icon, feed, and user management
- REST API - REST API for management (consumed by CLI tool)
- Browser push page - Quick mobile uploads via
/{feedId}/push#API_KEYwith server-side normalization - Version tracking - Git SHA embedded in binaries and available via
/api/version - Hash-based episode IDs - Preserves play progress; re-uploading same file updates metadata
- Cross-feed operations - Move or copy episodes between feeds
- Managed Identity - Secure Azure authentication without secrets
- Automated PR testing - GitHub Actions deploys PRs to isolated test environment
- CI/CD pipeline - Test-before-merge workflow with automated deployments
- .NET 10 SDK
- Azure Storage Account (or Azurite for local development)
- Azure App Service (optional for deployment)
1. Install and start Azurite:
npm install -g azurite
azurite --silent --location $env:USERPROFILE\.azurite2. Run FeatherPod:
dotnet run --project FeatherPod.Server3. Access feeds:
http://localhost:8080/api/feeds # List all feeds
http://localhost:8080/{feedId}/feed.xml # RSS feed
The development configuration is already set up to use Azurite.
Deploy infrastructure with Bicep:
az login
az group create --name <your-resource-group> --location swedencentral
az deployment group create \
--resource-group <your-resource-group> \
--template-file infrastructure/main.bicep \
--parameters infrastructure/parameters.jsonNote: You'll need to choose unique names for your resources (resource group, storage account, app service) in
parameters.json. Azure resource names must be globally unique.
This creates: Storage Account, blob containers, App Service, Managed Identity, and RBAC.
Deploy application:
# Deploy to production
.\Deploy-FeatherPod.ps1 -Environment Prod
# Deploy to test environment
.\Deploy-FeatherPod.ps1 -Environment TestSubscribe in your podcast app:
https://<your-app-name>.azurewebsites.net/{feedId}/feed.xml
PRs auto-deploy to test environment via GitHub Actions. See .github/DEPLOYMENT.md for setup.
# Create a feed
curl -X POST https://<your-app>.azurewebsites.net/api/feeds \
-H "X-API-Key: <your-api-key>" \
-H "Content-Type: application/json" \
-d '{"id":"my-podcast","title":"My Podcast","author":"Your Name",...}'
# List all feeds
curl https://<your-app>.azurewebsites.net/api/feedscurl -X POST https://<your-app>.azurewebsites.net/api/feeds/{feedId}/episodes \
-H "X-API-Key: <your-api-key>" \
-F "file=@audio.mp3" \
-F "title=Episode Title" \
-F "description=Full episode description for RSS" \
-F "summary=Short summary for iTunes (optional)"Optional parameters:
description- Full description for RSS feedsummary- Short summary for iTunes (defaults to description if not provided)publishedDate- Set explicit date (ISO 8601 format)normalize=true(query param) - Async server-side audio normalization to -16 LUFS (returns 202 with job ID)
curl -X DELETE https://<your-app>.azurewebsites.net/api/feeds/{feedId}/episodes/{episode-id} \
-H "X-API-Key: <your-api-key>"curl https://<your-app>.azurewebsites.net/api/feeds/{feedId}/episodes \
-H "X-API-Key: <your-api-key>"# Create user
curl -X POST https://<your-app>.azurewebsites.net/api/users \
-H "X-API-Key: <admin-api-key>" \
-H "Content-Type: application/json" \
-d '{"id":"user123","name":"John Doe","email":"john@example.com","role":"FeedOwner","ownedFeeds":["my-podcast"]}'
# List users
curl https://<your-app>.azurewebsites.net/api/users \
-H "X-API-Key: <admin-api-key>"
# Grant feed ownership
curl -X POST https://<your-app>.azurewebsites.net/api/users/{userId}/feeds \
-H "X-API-Key: <admin-api-key>" \
-H "Content-Type: application/json" \
-d '{"feedId":"my-podcast"}'| Endpoint | Method | Auth | Description |
|---|---|---|---|
/api/version |
GET | Public | Version info (with git SHA) |
/api/feeds |
GET | Public | List all feeds |
/api/feeds/{feedId} |
GET | Public | Get feed configuration |
/api/feeds |
POST | Admin | Create new feed |
/api/feeds/{feedId} |
PUT | Admin/Owner | Update feed metadata |
/api/feeds/{feedId}/rename?newId=... |
POST | Admin | Rename feed ID |
/api/feeds/{feedId} |
DELETE | Admin | Delete feed and all episodes |
/api/feeds/check-integrity |
GET | Admin/Owner | Verify episode metadata and audio blobs exist |
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/api/feeds/{feedId}/episodes |
GET | Admin/Owner | List episodes for feed |
/api/feeds/{feedId}/episodes |
POST | Admin/Owner | Upload episode (201), or with ?normalize=true for async normalization (202) |
/api/feeds/{feedId}/episodes/recent-uploads |
GET | Admin/Owner | Recent uploads with optional ?source=Browser&limit=5 |
/api/feeds/{feedId}/episodes/{id} |
DELETE | Admin/Owner | Delete episode |
/api/feeds/{feedId}/episodes/{id}/move |
POST | Admin/Owner | Move episode between feeds |
/api/feeds/{feedId}/episodes/{id}/copy |
POST | Admin/Owner | Copy episode between feeds |
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/api/jobs/{jobId} |
GET | Any | Get normalization job status (Queued/Processing/Completed/Failed) |
/api/jobs/{jobId}/progress |
GET | Any | SSE stream for real-time progress updates |
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/api/feeds/{feedId}/icon |
POST | Admin/Owner | Upload/replace feed icon |
/api/feeds/{feedId}/icon |
DELETE | Admin/Owner | Remove feed icon |
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/api/users/me |
GET | Any | Get current authenticated user |
/api/users |
GET | Admin | List all users |
/api/users/{userId} |
GET | Admin | Get user by ID |
/api/users |
POST | Admin | Create user (returns API key once) |
/api/users/{userId} |
DELETE | Admin | Delete user |
/api/users/{userId}/key/regenerate |
POST | Admin/Self | Regenerate user API key |
/api/users/{userId}/feeds |
POST | Admin | Grant feed ownership |
/api/users/{userId}/feeds/{feedId} |
DELETE | Admin | Revoke feed ownership |
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/{feedId}/feed.xml |
GET | Public | RSS podcast feed |
/{feedId}/icon.png |
GET | Public | Feed icon |
/{feedId}/audio/{filename} |
GET | Public | Stream audio (RFC 7233 range requests) |
/{feedId}/push |
GET | Public | Browser upload page (API key via URL fragment) |
/health |
GET | Public | Health check (returns blob storage status) |
Authentication: X-API-Key header required for protected endpoints. Admin has full access; FeedOwner limited to owned feeds.
Minimal configuration (appsettings.json):
{
"Azure": {
"AccountName": "<your-storage-account>",
"ContainerName": "<your-container>"
},
"Podcast": {
"Title": "My Podcast",
"Author": "Your Name",
"Email": "your@email.com",
"BaseUrl": "https://<your-app>.azurewebsites.net",
"ImageUrl": "https://<your-app>.azurewebsites.net/icon.png"
}
}Podcast icon: Upload via API (POST /api/feeds/{feedId}/icon) or CLI (FeatherPod icon set icon.png)
Additional options: See configuration files for published date behavior, language, category, and more.
FeatherPod includes a command-line tool for managing feeds, episodes, icons, and users:
# Episode management
FeatherPod episode push *.mp3 -f my-podcast -x # -x extracts date from file before normalization
FeatherPod episode push *.mp3 -f my-podcast -n # -n uses server-side normalization (SSE progress)
FeatherPod episode list -f my-podcast # List episodes
FeatherPod episode delete -f my-podcast # Interactive delete (supports multi-select)
FeatherPod episode delete abc123 -f my-podcast --force # Delete by ID
FeatherPod push episode.mp3 --title "Episode Title" --description "Full description" # Alias
# Move/copy episodes between feeds
FeatherPod episode move --from feed1 --to feed2 --episode "Episode*" # Pattern matching
FeatherPod episode copy --from feed1 --to feed2 # Interactive multi-select
# Feed management
FeatherPod feed list
FeatherPod feed create --id my-podcast --title "My Podcast" --author "John Doe"
FeatherPod feed update my-podcast --title "New Title"
FeatherPod feed rename old-id new-id
FeatherPod feed delete my-podcast --force
FeatherPod feed set-icon icon.png my-podcast
FeatherPod feed unset-icon my-podcast
# User management (Admin only)
FeatherPod user create
FeatherPod user list
FeatherPod user delete
FeatherPod user rotate-key
FeatherPod user grant
FeatherPod user revoke
# Data integrity
FeatherPod feed check-integrity # Verify all accessible feeds have valid audio blobs
FeatherPod feed check-integrity -f my-podcast # Check a specific feed
# Preferences
FeatherPod preferences show # Show all preferences
FeatherPod preferences api-key show # Show current API key
FeatherPod preferences api-key set <key> # Set API key
FeatherPod preferences normalization enable # Enable audio normalization
FeatherPod preferences normalization disable # Disable audio normalization
FeatherPod preferences auto-connect enable # Enable auto-connect on startup
FeatherPod preferences auto-connect disable # Disable auto-connect on startup
FeatherPod prefs ... # Alias for preferences
# Configuration files
FeatherPod config generate # Generate appsettings files from defaults
FeatherPod config generate --select # Choose which files to generate
# Version info
FeatherPod version # Shows CLI and server versions
FeatherPod --version # Shows CLI version
# Environment selection (defaults to Prod)
FeatherPod -e Test feed list
# Interactive mode (default)
FeatherPodInteractive mode provides full feature parity with CLI commands - all operations (push, move, copy, delete, icon management, user management, etc.) are available through menus with arrow key navigation. When pushing episodes, choose between Local (client-side), Server (server-side), or no normalization.
User preferences are stored in %APPDATA%\FeatherPod\preferences.json:
- API keys (per environment)
- Audio normalization enabled/disabled (per environment, defaults to enabled)
- Auto-connect on startup enabled/disabled (per environment, defaults to enabled)
The CLI prompts for the API key on first use and saves it automatically. If auto-connect is disabled, interactive mode starts in disconnected mode and you can connect manually via Preferences.
Single-file distribution: The CLI embeds default configuration as resources. Run FeatherPod config generate to create local config files for customization.
dotnet build # Build solution
dotnet test # Run tests (starts integration tests if Azurite is running)- .NET 10 ASP.NET Core - Controllers-based REST API
- Azure Functions - Queue-triggered async audio normalization
- Multi-feed - Single instance, multiple isolated feeds
- Role-based access - Admin and FeedOwner roles with per-user API keys
- Azure Blob Storage - Managed Identity support, hash-based episode IDs
- Azure Queue/Table Storage - Job queuing and status tracking for async operations
- Range requests - Seeking/resuming in podcast apps
Supported formats: MP3, M4A, AAC, WAV, OGG, FLAC
MIT
Pull requests welcome! The automated test environment will deploy your changes for validation before merge.