Skip to content

Latest commit

 

History

8 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Podcast episode system — working demo

A split-screen demo of an episode system built on an RSS.com feed, with no RSS.com embed anywhere in it.

Left: the editor screen — every field is always editable. Fetching an episode URL is an optional shortcut that fills them in; you can also type an episode from scratch and never touch RSS.com. Right: what visitors see, rendered from those stored records.

Live: https://oddbird.github.io/rss-episode-player-demo/

Run it locally

python3 -m http.server 8899

Then open http://127.0.0.1:8899/. It has to be served over HTTP rather than opened as a file:// URL.

There is no build step and no server code. Published episodes are kept in localStorage, so the demo survives a reload; Reset to seed clears it.

The data model

A record is flat. Once published, an episode is just a row we own:

{
  "id": "rec-1756150000000",
  "episode_id": "1048583",
  "show_slug": "podcasting101",
  "permalink": "https://rss.com/podcasts/podcasting101/1048583",
  "audio_url": "https://content.rss.com/episodes/…mp3",
  "artwork": "https://media.rss.com/…jpg",
  "title": "Growing a Show on Discord",
  "summary": "",
  "season": 2, "number": 4,
  "duration_seconds": 2104,
  "published": "2023-08-02T12:00:00.000Z"
}

Every one of those fields is editable, including the audio and image URLs — so an episode can be entered entirely by hand, pointing at media hosted anywhere. Leave the audio URL empty and the episode still publishes, just with an inactive play button; leave the image URL empty and it gets a placeholder cover.

There is no memory of which fields came from RSS.com, because after publishing it does not matter. Fetch is only a prefill helper: it fills the form with the episode's current details so nobody types them by hand. Press it again on a different URL and the form simply refills. Nothing keeps syncing afterwards — to reset a field to what RSS.com says, re-fetch the episode.

The URL carries both the show and the episode, so there is no separate show setting. Paste a URL from any show and it works; the demo will happily mix episodes from different podcasts.

Open Stored record under any episode to see exactly what is kept.

Three things this establishes

1. The audio URLs do not expire, and nothing needs regenerating

There are two different URLs, and it matters which one we keep:

URL Signed? Expires?
What the feed gives us content.rss.com/episodes/{show}/{ep}/{slug}/{file}.mp3 no never
What that redirects to rsscom.pdn.tritondigital.com/...?Expires=…&Signature=… yes 10 hours

The feed URL has no query string at all. Every request to it mints a fresh signed URL at RSS.com's edge — requested twice two seconds apart, it returns two different signatures, each valid for exactly 10 hours.

So: store the feed URL, hand it to <audio src>, let the browser follow the redirect. No signing on our side, no regeneration per page render, no cron job to refresh links. The 10-hour window belongs to a URL the browser resolves and discards; it never reaches our database.

Because playback still travels through the tracking redirect, plays continue to count in RSS.com's analytics. Building our own player does not route around their stats.

2. The episode ID is enough to paste, but it needs a feed lookup

The ID cannot be turned into an audio URL by string-building. The filename carries an upload timestamp and a random UUID:

content.rss.com/episodes/88524/1253951/podcasting101/2023_12_06_20_29_49_d52d5176-….mp3
                         ^show ^episode ^slug        ^upload time    ^random

Only the middle segments are predictable. The filename has to be read from the feed — which is what pressing Fetch does. One request gets every episode, so one request covers whatever you paste. Nothing is cached — each fetch pulls the whole feed and keeps only the episode asked for.

The feed is served no-cache, so in production this should happen on save, not on render. Resolve once, store the fields, serve pages from our own database — exactly what the demo does.

3. It can all be client-side

media.rss.com serves the feed with access-control-allow-origin: *, on both the actual request and the preflight. So the browser can read and parse it directly, which is why this demo needs no proxy and no PHP — and why it could ship later as a WordPress admin script rather than server code.

That said, fetching server-side in WordPress is still the better default: wp_remote_get has no CORS constraint at all, it can be scheduled, and it keeps working if RSS.com ever tightens those headers. The client-side path is proven to work; it is not automatically the right choice.

What this does that the RSS.com embed cannot

  • One shared audio element. Press play on a second episode and the first stops. Four embed iframes each own their own audio and expose no inbound message channel, so they play over each other.
  • Titles wrap. The embed marquee-scrolls long titles sideways at card width.
  • Durations before playback. Read from the record, so preload="none" holds and no audio is fetched until someone presses play. The grid itself renders with no network calls at all — audio is the only thing ever fetched, and only on a click. While it loads, the play button spins and an indeterminate bar sweeps the scrub track; the same state shows if playback stalls to rebuffer.
  • Any design at all — the embed renders one fixed layout in one of three themes, and it is cross-origin, so CSS cannot reach in.
  • Editing. Titles, descriptions, artwork, season and episode numbers are ours to change. The embed shows whatever RSS.com holds, and nothing else.

Files

index.html the split screen
admin.js feed fetching, the record store, the editor
admin.css editor chrome
player.js the front end: cards and shared playback
player.css the site's own styles
resolve_feed.py the same lookup done server-side, if we go that route
episodes.json sample output of the above

resolve_feed.py is not used by the page. It stands in for a WordPress save hook and shows the shape of what would land in post meta:

python3 resolve_feed.py podcasting101 1253951 1207269

Caveats

  • Verified in Chromium via a scripted browser. Worth a manual pass on Safari and a real phone before committing to the approach.
  • Artwork is a URL, not an upload. Real WordPress would point this at the media library instead, which is the one field that would not stay a plain text input.
  • Autoplay policies mean the first play must come from a user gesture. Fine here, since playback only ever starts from a click.
  • If an episode is deleted at RSS.com, the stored URL 404s. Whatever refreshes the feed should flag episodes that have vanished.
  • The main test show does not use season numbers, so S2 E4 renders as E32 unless an editor types the season in. (The Spanish-language sample show does set seasons, which is why fetching from it fills that field.)

About

Demo: a custom podcast episode player rendered from an RSS.com feed, replacing the RSS.com embed

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages