Talking Kotlin Website Update
This repo contains the website for the Talking Kotlin Podcast. Since we’ve last
updated the site, we’ve likely released new episodes that aren’t on the
website yet. Add all episodes released since the last one currently in the
repo (check _posts/ for the most recent date — ls _posts | tail -5).
Sources of truth (query these directly — do not use derivative sources)
- YouTube playlist: https://www.youtube.com/playlist?list=PLlFc5cFwUnmz1TwkP9SKCHU978dqLTANB
- Spotify show: https://open.spotify.com/show/5ZyWVfBi0WiPlmomrDQ6PM
- Apple Podcasts: https://podcasts.apple.com/us/podcast/talking-kotlin/id1194631266
-
Podcast RSS feed (canonical
publisheddates, descriptions, episode art). As of this writing it ishttps://anchor.fm/s/fdcf2fa8/podcast/rss, but don’t hardcode that — re-derive it each time in case the podcast host changes, via:curl -s "https://itunes.apple.com/lookup?id=1194631266&entity=podcast" | python3 -m json.toolLook at the
feedUrlfield in the result.
Always access these platforms directly — via yt-dlp or the agent-browser
CLI skill — instead of Google search results, the live talkingkotlin.com site,
or other secondary aggregators. If YouTube’s consent redirect gets in the way
of yt-dlp/WebFetch, fall back to agent-browser (accept the consent
screen, then use DOM snapshots) rather than working around it with indirect
sources. Load the agent-browser skill’s core workflow before using it:
agent-browser skills get core.
Convenience toolkit (copy-paste ready, tested against the live feed)
These are drop-in commands for the steps below — reach for these first instead of reinventing them.
Date formatting note: the shell environment used here doesn’t allow tools
to call date-like “now” functions from within scripts reliably across
runs — but pubDate/upload_date strings from feeds/yt-dlp are static text, so
just parse them directly with the string/regex logic below rather than
routing them through timezone-aware date libraries; no timezone conversion
is needed since all existing post dates are the feed’s UTC calendar date.
1. Find the RSS feed URL:
curl -s "https://itunes.apple.com/lookup?id=1194631266&entity=podcast" | jq -r '.results[0].feedUrl'
2. Download the RSS feed once, reuse it for every episode:
curl -s "$(curl -s "https://itunes.apple.com/lookup?id=1194631266&entity=podcast" | jq -r '.results[0].feedUrl')" -o /tmp/tk_feed.xml
3. List playlist video IDs (rough pass, to find candidates):
yt-dlp --flat-playlist --print "%(id)s" \
"https://www.youtube.com/playlist?list=PLlFc5cFwUnmz1TwkP9SKCHU978dqLTANB"
4. Get reliable per-video metadata (title/date/duration/description) with jq:
yt-dlp -j "https://www.youtube.com/watch?v=<video_id>" | \
jq -r '{title, upload_date, duration, description}'
5. One-shot Python script to pull everything needed from a single RSS <item>
(episode title, published date, rounded length, plain-text description, and
show-notes links) — run this once you’ve matched the episode by title to
find its index in the feed (index 0 is always the most recent episode):
python3 - "$INDEX" <<'PYEOF'
import xml.etree.ElementTree as ET
import re, sys
ns = {'itunes': 'http://www.itunes.com/dtds/podcast-1.0.dtd'}
tree = ET.parse('/tmp/tk_feed.xml')
items = tree.getroot().find('channel').findall('item')
it = items[int(sys.argv[1])]
def strip_html(html):
return re.sub(r'\s+', ' ', re.sub(r'<[^>]+>', '', html)).strip()
def extract_links(html):
return re.findall(r'href="([^"]+)"', html)
def hms_to_rounded(hms):
h, m, s = (int(p) for p in hms.split(':'))
return f"{round((h*3600 + m*60 + s) / 60)} mins"
desc_html = it.findtext('description')
duration = it.findtext('itunes:duration', namespaces=ns)
img = it.find('itunes:image', ns)
print("title: ", it.findtext('title'))
print("pubDate: ", it.findtext('pubDate')) # reformat to YYYY-MM-DD for `published:`
print("length: ", duration) # use directly for `length:`
print("len_rounded:", hms_to_rounded(duration)) # use directly for `length_rounded:`
print("image href: ", img.get('href') if img is not None else None)
print("description:", strip_html(desc_html))
print("links: ", extract_links(desc_html))
PYEOF
Search for an episode’s index by title first, e.g.:
grep -n "<title>" /tmp/tk_feed.xml | grep -i "exposed"
(items are in reverse-chronological order, same order as <title> matches
top-to-bottom in the raw XML — index 0 is the newest).
6. Download episode art (only if the itunes:image href path contains
podcast_uploaded_episode, not podcast_uploaded_nologo):
curl -s -o images/episodes/<slug>.jpg "<itunes:image href>"
7. YouTube thumbnail fallback, when there’s no usable feed art:
yt-dlp --skip-download --write-thumbnail --convert-thumbnails jpg \
-o "images/episodes/<slug>.%(ext)s" \
"https://www.youtube.com/watch?v=<video_id>"
8. Extract the episode number from a real (non-flat-playlist) YouTube title:
python3 -c "import re,sys; m=re.search(r'Talking Kotlin\s*#?(\d+)', sys.argv[1]); print(m.group(1) if m else 'UNNUMBERED')" "$TITLE"
9. Check what’s new/changed before wrapping up:
git status --short
git diff --stat
Step-by-step process
1. Find the starting point
Read the newest 1-2 post(s) in _posts/ to see the last episode already on
the site, and to confirm the frontmatter schema hasn’t changed (compare
against the exact schema given in step 6 below).
2. List candidate new episodes from YouTube
yt-dlp --flat-playlist --print "%(id)s | %(title)s | %(upload_date)s" \
"https://www.youtube.com/playlist?list=PLlFc5cFwUnmz1TwkP9SKCHU978dqLTANB"
This gives you video IDs quickly, but in --flat-playlist mode yt-dlp
sometimes mangles the title field (it can get truncated/split oddly around
a | character, and upload_date often comes back as NA). Treat this
listing only as a way to find video IDs and the rough order of new episodes.
For each candidate new video, get full, reliable metadata with:
yt-dlp -j "https://www.youtube.com/watch?v=<video_id>"
and read .title, .upload_date (format YYYYMMDD), .duration (seconds),
and .description out of the returned JSON.
Episode numbering: the real video title (from -j, not flat-playlist)
usually contains “Talking Kotlin” plus a number, but the exact placement and
formatting vary — all of these have been observed:
"How Major Metros Run on Kotlin Multiplatform | Talking Kotlin #145""Talking Kotlin #144 | Kotlin 2.3 Release Special (Audio Only)""Building AI Agents With Koog | Talking Kotlin 142"(no#)
So match loosely, e.g. a regex like Talking Kotlin\s*#?(\d+) against the
full title, checking both prefix and suffix. Some episodes have no
number at all (e.g. "Exposed 1.0 and Beyond | Talking Kotlin") — that’s a
real, intentional gap in numbering for special/standalone episodes, not a
sign you’re missing data. Leave such episodes unnumbered; don’t invent a
number for them.
Do not trust <itunes:episode> from the RSS feed as the episode number
— it has been observed to be duplicated across unrelated episodes (e.g. both
“Exposed 1.0 and Beyond” and “How Major Metros Run on Kotlin Multiplatform”
were tagged 145 in the feed at the same time). The YouTube title number is
the reliable one.
3. If the numbering has a gap, verify it independently — don’t assume it’s a fetch problem
If you find the episode numbers jump (e.g. straight from #142 to #144, no
#143 anywhere), don’t keep searching or fill it in — but also don’t assume
you just failed to find it. Verify directly on all three platforms with
agent-browser:
agent-browser open "https://www.youtube.com/playlist?list=PLlFc5cFwUnmz1TwkP9SKCHU978dqLTANB"
agent-browser snapshot -i -c
then the same open + snapshot -i -c pattern for the Spotify show URL and
the Apple Podcasts URL. Use agent-browser click @eN on element refs from
the snapshot to navigate (e.g. clicking “See All” on Apple Podcasts to get
the full episode list instead of just the most recent few). Note: Spotify’s
episode list uses non-standard virtualized scrolling that does not respond
to normal programmatic scrollTop manipulation — if you need Spotify’s full
episode history and infinite scroll isn’t loading more items, prefer Apple
Podcasts’ “See All” page instead, which reliably shows the complete
year-grouped episode list.
If all three platforms independently agree the number is skipped, it’s a genuine gap in the source’s own numbering — move on, don’t fabricate an episode for it.
4. Pull the RSS feed and cross-reference each episode
Download the feed once (toolkit command 2), then find each episode’s index
in it by title (toolkit command 5’s grep), then run the toolkit’s one-shot
Python script (toolkit command 5) per episode to get its title, pubDate,
length, length_rounded, image href, plain-text description, and show-notes
links all at once.
Match feed items to YouTube videos by title (titles match closely, sometimes not word-for-word — e.g. YouTube’s “How Major Metros Run on Kotlin Multiplatform” vs. Apple’s “How Major Metros Run on KMP”; match on guest name/topic if the title doesn’t match exactly, and pick the fuller/more descriptive title as the post title per the rule below).
For each episode, from this feed item you get:
published:— usepubDatefrom this feed, reformatted toYYYY-MM-DD. This is the canonical date and matches what’s already in existing_posts/*.mdfiles exactly. YouTube’supload_dateand what Spotify/Apple display can diverge from this by days or weeks (observed example: YouTubeupload_date20260223for an episode whose RSSpubDate— and the date actually used on the site — was2026-03-05). Always prefer the RSSpubDate.itunes:duration→ convert to bothlength: "H:MM:SS"and a roundedlength_rounded: "NN mins".description→ strip HTML tags for the plain-text version. Compare it against the YouTube description and Apple/Spotify (visible viaagent-browserif needed) and use whichever is longest/most detailed for the post’sdescription:field.- Links inside the HTML
description(i.e.<a href="...">...</a>) are a good source for a fewnotes:entries (guest’s project, personal site, socials) — see step 6 for how many to keep.
5. Get episode artwork
Priority order:
- The feed’s per-episode
itunes:image href. Check whether it’s real, episode-specific art or the show’s generic fallback image by looking at the URL path: art hosted under a path containing/podcast_uploaded_episode/is real per-episode art (safe to use); art hosted under a path containing/podcast_uploaded_nologo/is the generic placeholder (the exact same URL will repeat identically across multiple unrelated episodes) and should not be used. Example of each, observed in this feed:- Real:
https://d3t3ozftmdmh3i.cloudfront.net/staging/podcast_uploaded_episode/42482138/42482138-<id>.jpg - Generic fallback (skip this):
https://d3t3ozftmdmh3i.cloudfront.net/staging/podcast_uploaded_nologo/42482138/7eecf16af75ee8e7.jpg
Download real art with:
curl -s -o images/episodes/<slug>.jpg "<itunes:image href>" - Real:
- YouTube thumbnail fallback, whenever step 1 doesn’t yield real art:
yt-dlp --skip-download --write-thumbnail --convert-thumbnails jpg \ -o "images/episodes/<slug>.%(ext)s" \ "https://www.youtube.com/watch?v=<video_id>"This produces a 1280×720 JPEG (vs. the feed’s 3000×3000 JPEGs when available) — fine as a fallback, just visibly lower-res if compared side by side.
Pick <slug> to be short and filename-safe. Check images/episodes/ for
existing filename collisions first (e.g. a guest with the same name as a
prior guest needs a more specific slug, like appending the episode topic).
6. Fill in any missing details
- If a guest’s name is incomplete (e.g. only a first name given anywhere),
cross-check social links found in the RSS
descriptionHTML (e.g. a Twitter/X profile link likex.com/<handle>) or do a targeted web search to confirm the full name — don’t guess or leave it partial. - Write a short 1-2 sentence
bio:synthesized from context (RSS description + any bio blurb on the platforms) — there is no dedicated “guest bio” field in the feed to copy from directly.
7. Create one _posts/YYYY-MM-DD-slug.md file per episode
Match this frontmatter schema exactly (also check any existing post in
_posts/ for a live example, in case the schema has evolved since this was
written):
---
layout: post
title: "<episode title>"
guest: <Guest Name> # or "Sebastian Aigner and Márton Braun" for host-only/audio-exclusive episodes
recorded: YYYY-MM-DD # see note below
published: YYYY-MM-DD # from the RSS feed's pubDate, NOT YouTube's upload_date
trackid: # ALWAYS leave blank — SoundCloud is no longer used
youtubeid: <YouTube video id>
length: "H:MM:SS"
length_rounded: "NN mins"
tags: [Tag1, Tag2, Tag3]
image: <filename>.jpg
description: "<longest/best description across platforms, 2-4 sentences>"
bio: "<short guest bio, 1-2 sentences>"
notes:
- text: <label>
link: <url>
- text: <label>
link: <url>
---
trackid:is left blank on every new post (no SoundCloud publishing anymore). The post layout (_layouts/post.html) still renders a SoundCloud iframe unconditionally even with a blanktrackid— that’s a pre-existing quirk of the template, not something to fix as part of this task.recorded:— there is no reliable source for the actual recording date anywhere (YouTube/Spotify/Apple/RSS all only carry the publish date). Set it equal topublished:as a placeholder, and explicitly flag this in your final summary so the host can correct it if they remember the actual recording date.notes:should be a short curated list of 3-6 entries (the main project/topic link, guest’s personal site, guest’s social profiles) — not a dump of every link found in the RSS description, which tends to include a much longer, more exhaustive set of links than this site’s established convention.- Title: if YouTube/Spotify/Apple show meaningfully different titles for the same episode, use the fuller/more descriptive one (e.g. prefer “Creator of Spring: No Desire to Write Java at All” over YouTube’s truncated “No desire to write Java”).
- No manual index/listing files need updating —
index.htmlandtags/index.htmlauto-discover posts and tags via Jekyll’spaginator.posts/site.tags, so a new post automatically appears everywhere it needs to.
8. Verify before finishing
bundle install # only if gems aren't already installed
bundle exec jekyll serve --port 4000
Then either check the rendered HTML directly, or use agent-browser open
http://localhost:4000 + agent-browser snapshot -i -c to confirm:
- The new posts appear on the home page listing, most recent first.
- Each new post’s YouTube embed, description, bio, and show notes render.
- Any new tags show up correctly (tag pages are auto-generated).
Finally, confirm the diff is minimal and expected:
git status --short
This should show exactly one new .md file per episode plus one new image
per episode, and nothing else touched.
9. Report back — do not commit or push
Do not commit or push unless explicitly asked to. Just leave the new files staged/unstaged in the working tree and report:
- The list of new episodes added, with dates and episode numbers.
- Any gap(s) found in episode numbering and how you verified them (which platforms you checked directly).
- A reminder that
recorded:dates are best-guess placeholders (set equal topublished:) and may need manual correction.