Skip to content

astenlund/FeatherPod

Repository files navigation

FeatherPod

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.

Features

  • 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_KEY with 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

Prerequisites

  • .NET 10 SDK
  • Azure Storage Account (or Azurite for local development)
  • Azure App Service (optional for deployment)

Quick Start

Local Development

1. Install and start Azurite:

npm install -g azurite
azurite --silent --location $env:USERPROFILE\.azurite

2. Run FeatherPod:

dotnet run --project FeatherPod.Server

3. 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.

Azure Deployment

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.json

Note: 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 Test

Subscribe in your podcast app:

https://<your-app-name>.azurewebsites.net/{feedId}/feed.xml

Development Workflow

PRs auto-deploy to test environment via GitHub Actions. See .github/DEPLOYMENT.md for setup.

Usage

Managing Feeds

# 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/feeds

Adding Episodes

curl -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 feed
  • summary - 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)

Removing Episodes

curl -X DELETE https://<your-app>.azurewebsites.net/api/feeds/{feedId}/episodes/{episode-id} \
  -H "X-API-Key: <your-api-key>"

Listing Episodes

curl https://<your-app>.azurewebsites.net/api/feeds/{feedId}/episodes \
  -H "X-API-Key: <your-api-key>"

Managing Users (Admin only)

# 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"}'

API Reference

Feed Management

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

Episode Management

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

Job Status (Async Normalization)

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

Icon Management

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

User Management

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

Public Endpoints

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.

Configuration

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.

CLI Tool

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)
FeatherPod

Interactive 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.

Development

dotnet build          # Build solution
dotnet test           # Run tests (starts integration tests if Azurite is running)

Architecture

  • .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

License

MIT

Contributing

Pull requests welcome! The automated test environment will deploy your changes for validation before merge.

About

A podcast server for the cloud

Resources

Stars

Watchers

Forks

Contributors 2

  •  
  •