Skip to content

Instantly share code, notes, and snippets.

@levino
Created February 5, 2026 08:09
Show Gist options
  • Select an option

  • Save levino/d73ab679d27cb6fcc23b2b02c8f3c790 to your computer and use it in GitHub Desktop.

Select an option

Save levino/d73ab679d27cb6fcc23b2b02c8f3c790 to your computer and use it in GitHub Desktop.
media

Git-Based Media Platform: Architecture Specification

Vision

A fully git-based, headless video and audio podcast platform. No web interfaces, no databases, no CMS. Content lives in a git repository, media assets in S3-compatible storage, and everything is controlled from the terminal and text editor. AI coding tools handle the heavy lifting of implementation.

Why Not PeerTube or Castopod?

PeerTube

  • Full server application with database, background workers, transcoding pipeline — heavy operational burden
  • Content is locked inside PeerTube's database; backups and migrations are complex
  • Customization means forking a large Node.js codebase
  • Overkill for a single-creator setup
  • Fediverse integration is valuable, but can be achieved independently with much less complexity

Castopod

  • PHP/Laravel app with MySQL/Postgres dependency — another server to maintain
  • Web interface is the primary workflow; API exists but is secondary
  • Good Podcast 2.0 and ActivityPub support, but these are just XML tags and JSON endpoints that can be generated statically
  • Ties you to their data model and update cycle

Core Argument

Both tools solve the "I need a server application" problem. But with modern static site generation, S3-compatible storage, and edge workers, there is no need for a server application. The content workflow should be: edit text files → run a script → publish. Everything else is unnecessary complexity for a single creator.


Architecture Overview

┌─────────────────────────────────────────────────────┐
│                   Git Repository                     │
│              (Source of Truth for everything)         │
│                                                      │
│  content/episodes/    Markdown + frontmatter          │
│  subtitles/           Whisper-generated .vtt files    │
│  chapters/            JSON chapter files              │
│  scripts/             Build & publish tooling         │
│  feeds/               Generated RSS/XML (build output)│
│  site/                Optional minimal Astro site     │
└──────────────┬──────────────────────────────────────┘
               │ git lfs → originals
               ▼
┌──────────────────────┐     ┌──────────────────────────┐
│  Bucket 1: Originals │     │  Bucket 2: Delivery      │
│  (Backblaze B2)      │     │  (Backblaze B2 + CF)     │
│                      │     │                          │
│  - Raw video files   │     │  - HLS segments          │
│  - Git LFS backend   │     │  - MP3/Opus audio        │
│  - Private           │     │  - Thumbnails            │
│                      │     │  - Subtitles             │
│                      │     │  - Soundbite clips       │
│                      │     │  - Public via Cloudflare │
└──────────────────────┘     └──────────────────────────┘
                                        │
                              ┌─────────┴─────────┐
                              ▼                   ▼
                    ┌──────────────┐    ┌──────────────────┐
                    │  RSS Feeds   │    │  Optional:       │
                    │              │    │  Minimal Website  │
                    │  - Audio     │    │  (Astro/CF Pages) │
                    │  - Video     │    │                  │
                    │              │    │  ActivityPub     │
                    │  Podcast 2.0 │    │  endpoints       │
                    │  compatible  │    │  (CF Worker)     │
                    └──────────────┘    └──────────────────┘

Storage Strategy

Backblaze B2 + Cloudflare (Bandwidth Alliance)

Backblaze B2 is significantly cheaper than S3 or R2 for storage (~$6/TB/month). Through the Cloudflare Bandwidth Alliance, egress from B2 through Cloudflare is free. This makes it the most cost-effective option for video hosting.

Two-Bucket Separation

Bucket 1 — Originals: Also serves as Git LFS backend. Contains raw recordings. Private, not publicly accessible. This is the archive and backup.

Bucket 2 — Delivery: Contains transcoded, optimized assets. Public, served through Cloudflare CDN. All URLs in RSS feeds and on the website point here.

Git LFS on Backblaze B2

Git LFS is configured with a custom transfer agent pointing to B2 instead of GitHub's LFS (which is expensive). This means git clone pulls the full repo including original media files. The same B2 bucket serves double duty as LFS storage and raw archive.


Content Model

Each episode is a directory in the git repo:

content/episodes/2025-02-05-interview-topic/
├── index.md            # Metadata, description, shownotes
├── subtitles.de.vtt    # German subtitles (Whisper-generated)
├── subtitles.en.vtt    # English subtitles (optional)
├── chapters.json       # Podcast 2.0 chapter format
└── thumbnail.jpg       # Episode artwork

Frontmatter Schema

---
title: "Episode Title"
date: 2025-02-05
type: conversation | screencast | tutorial | talk
slug: interview-topic

# Feed routing
feeds:
  audio: true       # Include in audio podcast feed
  video: true       # Include in video podcast feed

# Media references (URLs in delivery bucket)
audio:
  mp3:
    url: https://cdn.example.com/episodes/interview-topic/audio.mp3
    size: 48234567
  opus:
    url: https://cdn.example.com/episodes/interview-topic/audio.opus
    size: 24117283
video:
  hls: https://cdn.example.com/episodes/interview-topic/master.m3u8
  mp4:
    1080p:
      url: https://cdn.example.com/episodes/interview-topic/1080p.mp4
      size: 2048234567
    720p:
      url: https://cdn.example.com/episodes/interview-topic/720p.mp4
      size: 1024117283

duration: "01:23:45"

# Podcast 2.0 metadata
season: 1
episode: 5
persons:
  - name: "Host Name"
    role: host
  - name: "Guest Name"
    role: guest
location:
  name: "Remote"
soundbites:
  - start: "00:15:30"
    end: "00:16:15"
    title: "Key insight about open source"

tags: [open-source, interview, ai]
---

Episode description and shownotes in Markdown.
Supports full Markdown including links, code blocks, etc.

Episode Types and Feed Routing

Type Audio Feed Video Feed Notes
conversation ✅ (audio extract) ✅ (video) Talking heads — audio is the primary format, video is optional extra
screencast Visuals are essential, audio-only makes no sense
tutorial Same as screencast
talk ✅ (audio extract) ✅ (video) Conference talks — usually work as audio too

This is controlled by the feeds.audio and feeds.video flags in frontmatter.


RSS Feed Generation

Two separate RSS/XML feeds are generated at build time. Both fully implement the Podcast 2.0 namespace (xmlns:podcast="https://podcastindex.org/namespace/1.0").

Audio Feed (/feeds/audio.xml)

Standard podcast feed. Enclosure points to MP3. Additional podcast:alternateEnclosure for Opus. Only includes episodes where feeds.audio: true.

Video Feed (/feeds/video.xml)

Enclosure points to MP4 (most compatible). Additional podcast:alternateEnclosure for HLS streaming. Only includes episodes where feeds.video: true.

Podcast 2.0 Tags to Implement

Channel-level:

  • podcast:locked — Prevent unauthorized claiming (important without a big host)
  • podcast:guid — Persistent global identifier, survives domain/URL changes
  • podcast:funding — Link to support/donation page
  • podcast:person — Show hosts
  • podcast:mediumpodcast for audio feed, video for video feed

Item-level:

  • podcast:chapters — URL to JSON chapters file
  • podcast:transcript — URL to VTT subtitle file
  • podcast:person — Per-episode guests
  • podcast:soundbite — Start time + duration for audio clips
  • podcast:alternateEnclosure — Multiple formats (MP3/Opus, MP4/HLS)
  • podcast:season + podcast:episode — Numbering
  • podcast:location — Optional geo-tagging

Feed Distribution

After generation, submit feeds to:

  1. podcastindex.org — Open index, propagates to many apps
  2. Apple Podcasts — Via Podcasts Connect
  3. Spotify — Via Spotify for Podcasters (separate submission)

Local Processing Pipeline

All media processing runs locally on a Mac Studio (Apple Silicon). No cloud transcoding needed for a low-volume single-creator setup (a few videos per month).

Tools Required

  • FFmpeg with VideoToolbox hardware acceleration
  • Whisper (OpenAI, runs locally) for subtitle generation
  • b2 CLI or rclone for B2 uploads
  • Node.js or Python for feed generation script
  • ImageMagick/FFmpeg for thumbnail and soundbite audiogram generation

Processing Steps (single make publish or shell script)

  1. Transcode video → HLS segments (multiple resolutions) + fallback MP4
  2. Extract audio → MP3 (192kbps) + Opus (96kbps) for conversation/talk episodes
  3. Generate subtitles → Whisper outputs VTT, commit to git
  4. Generate soundbite clips → FFmpeg cuts audio segment, optionally generates audiogram video (waveform + title overlay) for social sharing
  5. Generate thumbnails → Extract frame or use provided image, resize for feed requirements
  6. Upload to delivery bucket → Sync transcoded assets to B2 Bucket 2 via Cloudflare
  7. Update frontmatter → Fill in URLs and file sizes (or script does this automatically)
  8. Generate feeds → Build audio.xml and video.xml from content directory
  9. Deploy → Push feeds (and optional site) to Cloudflare Pages or B2

FFmpeg Notes

  • Use h264_videotoolbox / hevc_videotoolbox for hardware-accelerated encoding on Apple Silicon
  • HLS output: 10-second segments, generate master playlist for adaptive bitrate
  • For MVP: single resolution MP4 is fine, HLS with multiple resolutions is a later optimization

Optional: Minimal Website (Astro)

A very lightweight Astro site on Cloudflare Pages. Not strictly necessary (feeds are the primary distribution), but useful for:

  • Shareable URLs for individual episodes (link someone to a specific video)
  • SEO (static HTML ranks well, use schema.org VideoObject and PodcastEpisode)
  • Embedded video player (hls.js or Video.js)
  • Show notes rendered from Markdown
  • Episode archive/listing

This is intentionally minimal — a single template for episode pages, a listing page, and an index. No design overhead. Content comes from the same git repo.


Optional: Fediverse / ActivityPub Integration

Phase 1: Passive (Statically Generated)

These endpoints can be static JSON files generated at build time:

  • /.well-known/webfinger — Account discovery
  • /activitypub/actor — Actor profile (type: Service or Person)
  • /activitypub/outbox — Ordered collection of Create activities for each episode

This allows Fediverse users to find the account and see published content. No interaction possible yet.

Phase 2: Active (Cloudflare Worker)

A small Cloudflare Worker (~200-300 lines) handles:

  • Inbox POST endpoint — Receives Follow requests, Likes, Announces (boosts)
  • HTTP Signature verification — Required by ActivityPub spec
  • Follower storage — Cloudflare KV or D1 for the follower list
  • Push notifications — On new episode publish, send Create activities to all followers

Phase 3: PeerTube Compatibility

PeerTube uses ActivityPub objects of type Video with specific extensions. Generating PeerTube-compatible JSON-LD would allow PeerTube instances to natively federate and display the content. This is an advanced feature and not required for launch.


Implementation Phases

Phase 1: MVP (Weekend Project)

  • Git repo structure with content model
  • Shell script: transcode video (single resolution MP4, no HLS yet)
  • Shell script: extract audio (MP3)
  • Shell script: generate subtitles with Whisper
  • Shell script: upload assets to B2
  • Feed generator: audio RSS feed with Podcast 2.0 tags
  • Feed generator: video RSS feed with Podcast 2.0 tags
  • Submit feeds to podcastindex.org
  • Makefile or master script tying it all together

Phase 2: Polish

  • HLS transcoding with multiple resolutions (adaptive bitrate)
  • Opus audio as alternate enclosure
  • Chapter marks support (JSON chapters file → podcast:chapters tag)
  • Soundbite clip generation + audiogram for social sharing
  • Minimal Astro website with hls.js player
  • Deploy site to Cloudflare Pages
  • schema.org structured data for SEO

Phase 3: Fediverse

  • Static ActivityPub endpoints (webfinger, actor, outbox)
  • Cloudflare Worker for inbox/follow handling
  • Push new episodes to Fediverse followers
  • PeerTube-compatible Video objects

Phase 4: Nice to Have

  • Automated chapter generation (AI-based, from transcript)
  • Multi-language subtitle generation
  • Clip/highlight extraction (AI-suggested soundbites)
  • Analytics (privacy-respecting, maybe Plausible or simple CF Workers analytics)
  • Comments via ActivityPub replies

Key Design Principles

  1. Git is the source of truth. Everything except binary media lives in the repo. git clone = full backup minus raw video files (those are in B2/LFS).
  2. No databases. Feeds and site are statically generated from Markdown frontmatter.
  3. No web interfaces. All workflows happen in terminal and text editor. AI tools can assist with any step.
  4. Portable and exit-friendly. Standard formats everywhere (Markdown, RSS, VTT, JSON, MP4). Zero lock-in.
  5. Local-first processing. Transcoding, subtitle generation, feed building all run locally. No cloud dependencies for the build pipeline.
  6. Progressive enhancement. Start with feeds only, add website later, add Fediverse later. Each phase is independently useful.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment