Skip to content
kentcdoddsPublic

About

An RSS feed generator for media (audio/video).

Resources

Contributing

Stars

47 stars

Watchers

1 watching

Forks

Repository files navigation

MediaRSS

MediaRSS

An RSS feed generator for audio files (audiobooks, podcasts, audio series).

Note: While MediaRSS is primarily designed and tested for audio files (MP3, M4A, M4B, etc.), video files should also work. However, video support is not the primary focus and may have limitations (especially with regard to performance) with certain formats.

Development

Node.js 24 (engines.node is >=24.12.0), npm, Remix 3 (remix@3.0.0, remix/component, no React). Format with oxfmt, lint with oxlint. How to work in the repo, including npm run validate, the data layer, and npx remix db, is in docs/contributing.md. Installed Remix docs live at node_modules/remix/INDEX.md.

Screenshots

Dashboard

Homepage

Feed Details

Feed Details

Create Feed

Create New Curated Feed

Media Library

Media Library Selection

Media Library Modal

Media Details

Media Details

Media library

The admin media pages list files from every MEDIA_PATHS root, with search, sort, feed assignment, and per-file analytics. Uploading a file or writing tags (the media editor or MCP update_media_metadata) rewrites the file on disk and needs a read/write media mount. The Docker image includes ffmpeg for those writes. Mount media read-only (:ro) if you only serve existing files.

Analytics

The admin dashboard shows feed and media analytics:

  • Feed analytics (per feed + per token):
    • RSS fetches
    • media requests
    • download starts
    • bytes served
    • unique clients
    • top media items
    • top clients
    • daily activity trends
  • Media analytics (aggregated across all feed tokens):
    • requests/download starts
    • bytes served
    • unique clients
    • per-feed/per-token breakdowns
    • top clients
    • daily activity trends
  • Time windows: switch between 7d / 30d / 90d directly in the UI

MCP

MediaRSS exposes a Model Context Protocol server at /mcp (Streamable HTTP 2026-07-28). Clients authenticate with OAuth (PKCE). Discovery is public:

  • /.well-known/oauth-authorization-server
  • /.well-known/oauth-protected-resource/mcp

Consent is /admin/authorize, behind Cloudflare Access with the rest of admin. Scopes are mcp:read and mcp:write. After you approve a client, it can connect to /mcp with the issued token.

RSS subscribe URLs from MCP tools are /feed/{token}. The player widget is /mcp/widget/{token}/... (same feed token). Agent conventions for the server live in docs/mcp-server-best-practices.md.

Self-Hosting

Prerequisites

  • Docker
  • A server or machine to host the application
  • Basic understanding of Docker volumes for data persistence
  • Media files organized in one or more directories on your host machine
  • (Recommended) A domain name and Cloudflare account for secure remote access (see Securing the Admin Dashboard)

Synology NAS Setup

If you're running this on a Synology NAS, follow these specific instructions:

  1. Install Docker from the Synology Package Center if you haven't already.

  2. Create a shared folder for the database files:

    • Open Control Panel → Shared Folder
    • Create a new shared folder named docker (or your preferred name)
    • Inside this folder, create a directory called mediarss
  3. Note your media locations:

    • Synology typically stores media files in /volume1/[shared-folder-name]
    • You can mount any number of media directories from your Synology
  4. Open Docker in Synology DSM:

    • Go to "Registry" and search for kentcdodds/mediarss
    • Download the image
    • Go to "Container" and launch using the image
  5. When setting up the container in the Synology Docker UI:

    • In the "Advanced Settings" → "Volume" tab:

      • Add a volume mount for the database:
        • Mount path: /data
        • Local path: /volume1/docker/mediarss
      • Add a volume mount for artwork (to persist uploaded feed images):
        • Mount path: /app/data/artwork
        • Local path: /volume1/docker/mediarss/artwork
      • Add your media volume mounts (add as many as you need):
        • Mount path: /media/[your-name] (e.g., /media/shows, /media/personal, etc.)
        • Local path: /volume1/[your-folder]
        • Check "Read-only" if you only need to serve existing files
        • Uncheck "Read-only" to upload files or edit metadata (see Media library)
    • In the "Port Settings" tab:

      • Local Port: 22050 (or your preferred port)
      • Container Port: 22050 (or whatever you set PORT to in the env settings)
    • In the "Environment" tab:

      • Add the standard environment variables as needed
      • For multiple media paths, specify them in MEDIA_PATHS:
        • Variable: MEDIA_PATHS
        • Value: shows:/media/shows,personal:/media/personal
        • Format: name:path,name:path (comma-separated, each with a name and path)

The rest of the standard instructions apply for managing the container.

Quick Start Script

For the easiest setup, use the provided NAS setup script:

  1. Copy scripts/nas-setup.sh to your NAS
  2. Edit the configuration section at the top of the script
  3. Run it:
chmod +x nas-setup.sh
./nas-setup.sh

The script will pull the latest image, set up the container with your media directories, and configure automatic restarts.

Manual Setup (Non-Synology)

  1. Pull the Docker image:
docker pull kentcdodds/mediarss
  1. Create directories for persistent storage and ensure your media directories exist:
# Create directories for database and artwork storage
mkdir -p /path/to/your/data
mkdir -p /path/to/your/data/artwork

# Your media directories should already exist
  1. Run the container:
docker run -d \
  --name mediarss \
  -p 22050:22050 \
  -v /path/to/your/data:/data \
  -v /path/to/your/data/artwork:/app/data/artwork \
  -v /path/to/audiobooks:/media/audiobooks:ro \
  -v /path/to/audio-series:/media/audio-series:ro \
  -e MEDIA_PATHS=audiobooks:/media/audiobooks,audio-series:/media/audio-series \
  kentcdodds/mediarss

Media volume permissions (read-only vs upload and metadata edits) are under Volume Mounts.

Volume Mounts

The application requires these volume mounts:

  1. Database Volume (/data):

    • Purpose: Stores SQLite databases
    • Mount point: /data
    • Example: -v /path/to/your/data:/data
  2. Artwork Volume (/app/data/artwork):

    • Purpose: Stores uploaded feed artwork images
    • Mount point: /app/data/artwork
    • Example: -v /path/to/your/data/artwork:/app/data/artwork
    • Important: Without this mount, uploaded artwork will be lost on container restart
  3. Media Volumes (any number allowed):

    • Purpose: Access to your audio files
    • Mount point pattern: /media/[your-name]
    • Examples:
      • -v /path/to/audiobooks:/media/audiobooks:ro
      • -v /path/to/audio-series:/media/audio-series:ro
    • Mount read-only (:ro) if you only need to serve existing files
    • Mount read/write (no :ro) to upload files or edit metadata; see Media library
    • Name the mount points anything that makes sense for your use case

Media Directory Structure

You can organize your media directories however you prefer. Here's an example structure for audiobooks and audio series:

/media/audiobooks/
├── fiction/
│   ├── Harry Potter Series/
│   └── Lord of the Rings/
└── non-fiction/
    ├── Atomic Habits/
    └── Deep Work/

/media/audio-series/
├── podcasts/
│   ├── show1/
│   └── show2/
└── courses/
    ├── course1/
    └── course2/

Environment Variables

The following environment variables can be configured:

  • PORT: Server port (default: 22050)
  • NODE_ENV: Environment mode (production, development, or test)
  • DATABASE_PATH: Path to the main SQLite database (default: ./data/sqlite.db)
  • CACHE_DATABASE_PATH: Path to the cache SQLite database (default: ./data/cache.db)
  • MEDIA_PATHS: Named media roots in format name:path,name:path
    • Example: shows:/media/shows,personal:/media/personal,other:/media/other
    • Each media root has a name (used in URLs) and a path (filesystem location)
    • Names must be unique - duplicate names are not allowed
    • Names should be URL-safe (alphanumeric, hyphens, underscores)
  • ANALYTICS_RETENTION_DAYS: Number of days to retain feed analytics events (default: 180)
    • Older analytics events are pruned automatically on startup
  • RATE_LIMIT_ADMIN_READ: Admin GET/HEAD/OPTIONS requests per minute per IP (default: 5000)
  • RATE_LIMIT_ADMIN_WRITE: Admin write requests per minute per IP (default: 30)
  • RATE_LIMIT_MEDIA: /media/* enclosure requests and admin artwork / stream reads (/admin/api/artwork, /admin/api/media-stream, feed artwork GET) per minute per IP (default: 300). Public /art/* uses RATE_LIMIT_DEFAULT.
  • RATE_LIMIT_DEFAULT: Other requests (feeds, /art/*, MCP, OAuth, …) per minute per IP (default: 1000)
  • GITHUB_REPO: Repository URL used when linking to commits (default: https://github.com/kentcdodds/mediarss)
  • COMMIT_SHA: Git SHA reported by /health and /admin/version. The published image sets this at build time (SOURCE_COMMIT is an accepted alias).
  • ALLOWED_HOSTS: Optional comma-separated hosts allowed on /oauth/token. Unset means any Host header is accepted.
  • MEDIA_ANALYTICS_MAX_SQLITE_VARIABLE_NUMBER: Optional cap on SQLite IN-clause bind variables when aggregating per-file analytics. Unset uses the compiled SQLite MAX_VARIABLE_NUMBER (or 999).

The Docker image sets DATABASE_PATH=/data/sqlite.db and CACHE_DATABASE_PATH=/data/cache.db. The ./data/… defaults above apply to local development.

Securing the Admin Dashboard

MediaRSS routes fall into five groups:

  • Public token routes (/feed/*, /media/*, /art/*, /mcp/widget/*) — podcast apps and the MCP player widget. Access is the secret token in the URL.
  • Admin routes (/admin, /admin/api/*) — the dashboard. / redirects here. /admin/health is the same snapshot as /health. Protect these at the edge.
  • MCP (/mcp) — OAuth bearer token for the Streamable HTTP server. Leave /mcp off the Access policy so clients can connect after you approve them.
  • OAuth discovery and tokens (/.well-known/*, /oauth/*) — public so clients can register and redeem codes. Consent itself is /admin/authorize.
  • Health (/health) — public JSON snapshot of process, database, media roots, MCP, and OAuth. Not rate-limited unless ?probe= is present (?probe=cimd live-fetches Kody's client metadata).

Podcast apps need unauthenticated token URLs, so use Cloudflare Tunnel and an Access policy on /admin rather than app-level login.

Why Cloudflare Tunnel?

  • Free HTTPS - Automatic SSL certificates, no configuration needed
  • No port forwarding - Outbound-only connection, your router stays locked down
  • Hidden IP - Your home IP address is never exposed
  • Access policies - Add authentication at the edge (email OTP, SSO, etc.)
  • Works with any DNS - You don't need to move your domain to Cloudflare

Setting Up Cloudflare Tunnel

  1. Create a free Cloudflare account and add your domain (you can use the free plan).

  2. Create a tunnel in the Zero Trust dashboard:

    • Go to Networks → Tunnels → Create a tunnel
    • Name it (e.g., "mediarss")
    • Choose Docker as the connector
    • Copy the tunnel token
  3. Run cloudflared alongside MediaRSS on your NAS/server:

    docker run -d \
      --name cloudflared \
      --restart unless-stopped \
      --network host \
      cloudflare/cloudflared:latest \
      tunnel run --token YOUR_TUNNEL_TOKEN

    Note: The --network host flag is required so that cloudflared can reach MediaRSS via localhost. Without it, localhost would refer to the cloudflared container itself, not your NAS.

  4. Configure the public hostname in the tunnel settings:

    • Hostname: media.yourdomain.com (a domain name you control)
    • Service: http://localhost:22050 (or your MediaRSS port)
  5. Add an Access policy to protect the admin dashboard:

    • Go to Access → Applications → Add an application
    • Choose "Self-hosted"
    • Set the domain to media.yourdomain.com
    • Set the path to /admin (just the admin pages)
    • Add an authentication method (email OTP is easiest)
    • Important: Don't protect /feed/*, /media/*, /art/*, /mcp, /.well-known/*, /oauth/*, or /health

Using Cloudflare Tunnel Without Moving DNS

If your domain's DNS is managed elsewhere (not Cloudflare), you can still use Cloudflare Tunnel with a CNAME record:

  1. Complete steps 1-4 above
  2. Find your tunnel's hostname in the Cloudflare dashboard - it looks like: a1b2c3d4-e5f6-7890-abcd-ef1234567890.cfargotunnel.com
  3. At your DNS provider, create a CNAME record:
    media.yourdomain.com  CNAME  <tunnel-uuid>.cfargotunnel.com
    

Note: This only works for subdomains (e.g., media.yourdomain.com), not the root domain.

Alternative: Local Network Only

If you only access MediaRSS from your local network and don't need remote access, you can skip Cloudflare Tunnel entirely. The token-based URLs for feeds provide sufficient security for trusted networks.

Data Persistence

Persistent files live on the /data and /app/data/artwork mounts (see Volume Mounts):

  • /data/sqlite.db — main database
  • /data/cache.db — media metadata cache (safe to delete; it rebuilds)
  • /app/data/artwork — uploaded feed cover images

Schema migrations run at container start, before the app serves requests. Take a backup before upgrading. See the rollback runbook when a migration has to be reversed.

Backup and Restore

To backup your data, simply copy the files from your mounted data directory. For example:

# Stop the container before backup
docker stop mediarss

# Backup the databases and artwork
cp /path/to/your/data/sqlite.db /path/to/backup/sqlite.db
cp /path/to/your/data/cache.db /path/to/backup/cache.db
cp -r /path/to/your/data/artwork /path/to/backup/artwork

# Restart the container
docker start mediarss

To restore from backup:

# Stop the container
docker stop mediarss

# Restore the databases and artwork
cp /path/to/backup/sqlite.db /path/to/your/data/sqlite.db
cp /path/to/backup/cache.db /path/to/your/data/cache.db
cp -r /path/to/backup/artwork /path/to/your/data/artwork

# Restart the container
docker start mediarss

Upgrading

To upgrade to a new version:

# Pull the new image
docker pull kentcdodds/mediarss

# Stop the current container
docker stop mediarss

# Remove the old container
docker rm mediarss

# Run the new container (using the same data directory and media mount)
# Note: Remove :ro from media volumes to upload files or edit metadata
docker run -d \
  --name mediarss \
  -p 22050:22050 \
  -v /path/to/your/data:/data \
  -v /path/to/your/data/artwork:/app/data/artwork \
  -v /path/to/audiobooks:/media/audiobooks:ro \
  -v /path/to/audio-series:/media/audio-series:ro \
  -e MEDIA_PATHS=audiobooks:/media/audiobooks,audio-series:/media/audio-series \
  kentcdodds/mediarss

Your data will be preserved as long as you use the same volume mount points.

Alternatives

Audiobookshelf is a fully featured audio media server. It doesn't support private RSS feeds and it's a little too much for what I want, so I built my own thing. But you might like it.

About

An RSS feed generator for media (audio/video).

Resources

Contributing

Stars

47 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages