Add proper support for music podcasts — shows like Upbeats from RSS Blue where DJs play other artists' songs, using Podcasting 2.0 tags (podcast:chapters, podcast:valueTimeSplit, podcast:remoteItem) to identify tracks and route payments to the correct artists in real time.
Test feed: https://feeds.rssblue.com/upbeats
Branch: claude/add-podcast-feed-sUR9X
- Feed import via
/admin— paste URL, feed + tracks created podcast:chaptersJSON fetched and parsedpodcast:valueTimeSplitwithpodcast:remoteItemextracted- Per-track V4V resolution via Podcast Index API
- BoostButton re-renders with new track's value data on track change (reactive
currentTrackIndex→ fresh props) - Keysend + LNURL payment paths with BoostBox integration
File: lib/music-track-parser/utils.ts:55-96
isMusicChapter() requires either a music keyword ("song", "guitar", "remix", etc.) or an "Artist - Title" format. A chapter titled simply "Sunshine" or "Blue Sky" gets rejected. For a music podcast where every chapter is a song, this filter drops most tracks.
Fix: When the feed has podcast:medium = "music" or "musicL", skip the music-keyword filter entirely — treat all chapters as music tracks (except those with toc: false).
Spec: Chapters with toc: false are "silent" markers — metadata only, not displayed to users.
File: lib/music-track-parser/index.ts:219-243
Currently no check for toc. Silent chapters become visible tracks.
Fix: Add toc to ChapterData type, skip chapters where toc === false.
Spec: The JSON chapters spec uses img for chapter art.
Code: ChapterData type uses image.
Real feeds from RSS Blue will use img. The chapter art won't load.
Fix: Map img → image during chapter JSON parsing, support both field names.
File: lib/music-track-parser/index.ts:232
When a chapter has no endTime, code defaults to startTime + 300 (5 min). The spec implies chaining — one chapter ends when the next begins.
Fix: When endTime is missing, use the next chapter's startTime. Only use a fallback for the last chapter.
Spec: If remotePercentage is not defined, it defaults to 100 — the remote artist gets everything (minus fees).
File: lib/music-track-parser/extractors.ts:83
Currently: parseFloat(... || '0') — remote artist gets 0%.
Fix: Change default to '100'.
Spec: Sets the correct timestamp in value metadata sent to the remote recipient. Important for the receiving artist's podcast app to show accurate play position.
File: lib/music-track-parser/extractors.ts and types.ts
Not extracted or passed to Helipad metadata.
Fix: Parse remoteStartTime from VTS, store in valueForValue, include in Helipad metadata ts field.
File: lib/music-track-parser/extractors.ts:58
if (startTime > 0 && ...) — a song at position 0:00 (first track in the episode) is silently dropped.
Fix: Change to startTime >= 0.
Spec: When resolving a remote value block, ignore any podcast:valueTimeSplit children inside it. Only root-level splits apply.
Fix: Add a guard in the V4V resolver to strip nested VTS when fetching remote value blocks.
Current filters: Albums (>= 6 tracks), EPs (2-5), Singles (1), Publishers, Playlists, Videos
Music podcasts imported as feeds have no dedicated filter tab. They'd show up mixed in with albums based on track count, which is misleading — a 50-episode podcast isn't an "album."
Fix: Add a 'podcasts' filter option that filters by feed type: 'podcast' or feeds with podcast:medium = "music" / "musicL". This separates music podcasts from albums in the UI while keeping them discoverable.
Files:
components/ControlsBar.tsx— add "Podcasts" button to filter rowapp/api/albums-fast/route.ts— add'podcasts'filter case (filter by type or medium)app/page.tsx— handle'podcasts'filter state
| # | File | Change |
|---|---|---|
| 1a | lib/music-track-parser/types.ts |
Add toc?: boolean and img?: string to ChapterData chapter type |
| 1b | lib/music-track-parser/index.ts |
Map img → image in chapter JSON parsing; skip toc: false chapters; chain endTime to next chapter's startTime |
| 1c | lib/music-track-parser/utils.ts |
Add isMusicMediumFeed() check; bypass isMusicChapter() keyword filter when feed medium is music or musicL |
| 1d | lib/music-track-parser/index.ts |
Pass feed medium context through EpisodeContext so chapter extraction knows the feed type |
| # | File | Change |
|---|---|---|
| 2a | lib/music-track-parser/extractors.ts:83 |
Default remotePercentage to 100 instead of 0 |
| 2b | lib/music-track-parser/extractors.ts:58 |
Change startTime > 0 to startTime >= 0 |
| 2c | lib/music-track-parser/extractors.ts + types.ts |
Parse and store remoteStartTime attribute |
| 2d | lib/music-track-parser/extractors.ts:390 |
Same startTime >= 0 fix in extractV4VTracksFromChapters |
| # | File | Change |
|---|---|---|
| 3a | components/Lightning/BoostButton.tsx |
Include remoteStartTime in Helipad metadata ts field when available |
| 3b | lib/v4v-resolver.ts |
Strip nested podcast:valueTimeSplit when resolving remote value blocks |
| # | File | Change |
|---|---|---|
| 4a | components/ControlsBar.tsx |
Add 'podcasts' to filter options array, render "Podcasts" button in both desktop and mobile layouts |
| 4b | app/api/albums-fast/route.ts |
Add case 'podcasts' to filter switch — match feeds where type = 'podcast' with podcast:medium of music or musicL, or feeds explicitly tagged as music podcasts during import |
| 4c | app/page.tsx |
Wire up 'podcasts' filter to handleFilterChange() and URL params |
| # | Step |
|---|---|
| 5a | Import https://feeds.rssblue.com/upbeats via admin panel |
| 5b | Verify chapters are extracted as tracks (no false filtering) |
| 5c | Verify toc: false chapters are hidden |
| 5d | Verify chapter art loads (img field) |
| 5e | Verify VTS at position 0:00 is included |
| 5f | Verify remotePercentage defaults to 100 when omitted |
| 5g | Play through tracks — confirm BoostButton target switches per-track |
| 5h | Send test boost — confirm payment routes to the track's artist, not the show host |
| 5i | Verify "Podcasts" filter tab appears on main page and shows only music podcasts |
lib/music-track-parser/types.ts—ChapterData,EpisodeContext,ValueTimeSplitlib/music-track-parser/index.ts— chapter parsing, endTime chaining, toc filter, medium contextlib/music-track-parser/utils.ts— medium-aware chapter filteringlib/music-track-parser/extractors.ts— remotePercentage default, startTime >= 0, remoteStartTimelib/v4v-resolver.ts— nested VTS guardcomponents/Lightning/BoostButton.tsx— remoteStartTime in Helipad metadatacomponents/ControlsBar.tsx— "Podcasts" filter buttonapp/api/albums-fast/route.ts— podcasts filter caseapp/page.tsx— podcasts filter state
- Low risk: All changes are additive or fix defaults. Existing album/playlist flows are unaffected because:
- Chapter filter bypass only activates for
medium: music/musicLfeeds remotePercentagedefault change only matters when the attribute is absent (existing feeds that set it explicitly are unchanged)startTime >= 0adds one previously-skipped track per episode at most
- Chapter filter bypass only activates for
- Build verification:
npm run buildbefore commit per CLAUDE.md rules
After merging claude/add-podcast-feed-sUR9X to main:
-
Run database migration — two new nullable columns on Track:
ALTER TABLE "Track" ADD COLUMN "chaptersUrl" TEXT; ALTER TABLE "Track" ADD COLUMN "chapters" JSONB;
Or via Prisma:
npx prisma migrate dev --name add_chapters_to_track -
Reparse the UpBeats feed to backfill chaptersUrl + chapters for existing episodes:
curl -X POST https://stablekraft.app/api/admin/feeds/3aebb7a8-5942-5ee7-a148-8bdc14f1f3d4/reparse -
Verify — play an UpBeats episode; skip forward should jump to the next chapter (not next episode). Chapter title appears below the artist in both the mini bar and fullscreen player.