Add audio player with timestamp seeking
Sticky audio element at the bottom of episode pages. Clicking a
timestamp seeks the player to that point and starts playback.
Assisted-by: Claude Opus 4.6 via pi
diff --git a/Rakefile b/Rakefile
index 0b54994..d47f72e 100644
--- a/Rakefile
+++ b/Rakefile
@@ -139,6 +139,7 @@ task :pages do
number: et.number,
title: et.episode.title.sub(/^Episode #{et.number}:\s+/, ""),
slug: et.slug,
+ audio_url: et.episode.audio_url,
text: File.read(et.text_path)
}
end
diff --git a/lib/pages/episode.html.erb b/lib/pages/episode.html.erb
index 12bde64..5f39cdb 100644
--- a/lib/pages/episode.html.erb
+++ b/lib/pages/episode.html.erb
@@ -6,15 +6,19 @@
<title><%= CGI.escapeHTML("#{ep[:number]}. #{ep[:title]}") %> — Cooking Issues</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
- body { font-family: system-ui, sans-serif; line-height: 1.6; color: #222; max-width: 50rem; margin: 0 auto; padding: 1rem; }
+ body { font-family: system-ui, sans-serif; line-height: 1.6; color: #222; max-width: 50rem; margin: 0 auto; padding: 1rem; padding-bottom: 5rem; }
h1 { font-size: 1.3rem; margin-bottom: 0.5rem; }
.back { display: inline-block; margin-bottom: 1rem; color: #0066cc; text-decoration: none; }
.back:hover { text-decoration: underline; }
p { margin-bottom: 1em; }
h6.timestamp { color: #888; font-size: 0.85rem; font-family: monospace; font-weight: normal; margin-bottom: 0.25em; }
+ h6.timestamp a { color: inherit; text-decoration: none; }
+ h6.timestamp a:hover { text-decoration: underline; color: #0066cc; }
h6.timestamp + p { margin-top: 0; }
:target { background: #fff3a8; }
[data-pagefind-highlight] { background: #fff3a8; padding: 0 2px; border-radius: 2px; }
+ .player { position: sticky; bottom: 0; background: #f8f8f8; border-top: 1px solid #ddd; padding: 0.5rem 0; margin: 0 -1rem; padding-left: 1rem; padding-right: 1rem; }
+ .player audio { width: 100%; }
</style>
</head>
<body>
@@ -25,14 +29,38 @@
<% ep[:text].split("\n\n").each_with_index do |para, i| %>
<% if (m = para.match(/\A\[([^\]]+)\]\s*(.*)/)) %>
<% anchor = m[1].gsub(/[^0-9:]/, "").gsub(":", "-") %>
- <h6 id="t-<%= anchor %>" class="timestamp">[<%= CGI.escapeHTML(m[1]) %>]</h6>
+ <% time = m[1].gsub(/[^0-9:]/, "") %>
+ <h6 id="t-<%= anchor %>" class="timestamp"><a href="#t-<%= anchor %>" data-seek="<%= time %>">[<%= CGI.escapeHTML(m[1]) %>]</a></h6>
<p><%= CGI.escapeHTML(m[2]) %></p>
<% else %>
<p><%= CGI.escapeHTML(para) %></p>
<% end %>
<% end %>
+<div class="player">
+ <audio id="audio" controls preload="none" src="<%= CGI.escapeHTML(ep[:audio_url]) %>"></audio>
+</div>
+
<script type="module">
+ const audio = document.getElementById("audio");
+
+ function parseTimestamp(ts) {
+ const parts = ts.split(":").map(Number);
+ if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2];
+ if (parts.length === 2) return parts[0] * 60 + parts[1];
+ return parts[0];
+ }
+
+ document.addEventListener("click", (e) => {
+ const link = e.target.closest("[data-seek]");
+ if (!link) return;
+ e.preventDefault();
+ const seconds = parseTimestamp(link.dataset.seek);
+ audio.currentTime = seconds;
+ audio.play();
+ history.replaceState(null, "", link.getAttribute("href"));
+ });
+
await import("./pagefind/pagefind-highlight.js");
new PagefindHighlight({ highlightParam: "highlight" });
</script>