From c8a588062f31b00b8c302a3e9f5fff6b43110dc2 Mon Sep 17 00:00:00 2001 From: Tyler Hoang Date: Tue, 26 May 2026 18:23:46 -0700 Subject: podcast: switch to server-side php proxy for rss feed itunes api unreachable from browser on vps. podcast.php fetches anchor.fm rss server-to-server, parses xml, returns json. 1h cache header to avoid hammering the feed. Co-Authored-By: Claude Sonnet 4.6 --- aero.js | 23 +++------------------ podcast.php | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 20 deletions(-) create mode 100644 podcast.php diff --git a/aero.js b/aero.js index 01a9b83..98cb8ec 100644 --- a/aero.js +++ b/aero.js @@ -177,26 +177,9 @@ async function fetchVisitorCount() { } async function fetchReelMouthFeed(limit = 6) { - const url = `https://itunes.apple.com/lookup?id=1709836497&entity=podcastEpisode&limit=${limit + 1}`; - const r = await fetch(url); - if (!r.ok) throw new Error('itunes ' + r.status); - const j = await r.json(); - const podcast = j.results.find(x => x.kind === 'podcast'); - const episodes = j.results.filter(x => x.kind === 'podcast-episode').slice(0, limit); - return { - art: podcast ? podcast.artworkUrl600 : null, - episodes: episodes.map(e => { - const ms = e.trackTimeMillis || 0; - const mins = Math.floor(ms / 60000); - const h = Math.floor(mins / 60); - const m = mins % 60; - return { - title: e.trackName, - url: e.trackViewUrl, - duration: h ? `${h}:${m.toString().padStart(2, '0')}` : `${m}m`, - }; - }), - }; + const r = await fetch(`/podcast.php?limit=${limit}`); + if (!r.ok) throw new Error('podcast.php ' + r.status); + return await r.json(); } window.Aero = { spawnBubbles, makeClouds, sparkleCursor, makeDraggable, counterHTML, nowPlayingHTML, animateEq, musicToggleHTML, bindMusicToggle, fetchLastFm, fetchFilms, fetchVisitorCount, fetchReelMouthFeed }; diff --git a/podcast.php b/podcast.php new file mode 100644 index 0000000..2447fbf --- /dev/null +++ b/podcast.php @@ -0,0 +1,69 @@ + [ + 'timeout' => 8, + 'user_agent' => 'Mozilla/5.0 (compatible; podcast-proxy/1.0)', +]]); + +$xml = @file_get_contents($rss_url, false, $ctx); +if ($xml === false) { + http_response_code(502); + echo json_encode(['error' => 'could not fetch feed']); + exit; +} + +$feed = @simplexml_load_string($xml); +if (!$feed) { + http_response_code(502); + echo json_encode(['error' => 'could not parse feed']); + exit; +} + +$ns_itunes = 'http://www.itunes.com/dtds/podcast-1.0.dtd'; + +// artwork from channel-level itunes:image +$channel = $feed->channel; +$itunes_ch = $channel->children($ns_itunes); +$art = (string)($itunes_ch->image->attributes()['href'] ?? ''); + +$episodes = []; +foreach ($channel->item as $item) { + if (count($episodes) >= $limit) break; + $itunes = $item->children($ns_itunes); + $duration_raw = (string)($itunes->duration ?? ''); + + // normalize duration to h:mm or Xm + if (strpos($duration_raw, ':') !== false) { + $parts = array_map('intval', explode(':', $duration_raw)); + if (count($parts) === 3) { + [$h, $m, $s] = $parts; + $duration = $h > 0 ? "{$h}:" . str_pad($m, 2, '0', STR_PAD_LEFT) : "{$m}m"; + } else { + [$m, $s] = $parts; + $duration = "{$m}m"; + } + } elseif (is_numeric($duration_raw)) { + $secs = intval($duration_raw); + $h = intdiv($secs, 3600); + $m = intdiv($secs % 3600, 60); + $duration = $h > 0 ? "{$h}:" . str_pad($m, 2, '0', STR_PAD_LEFT) : "{$m}m"; + } else { + $duration = ''; + } + + $link = (string)($item->link ?? $item->enclosure->attributes()['url'] ?? '#'); + + $episodes[] = [ + 'title' => (string)$item->title, + 'url' => $link, + 'duration' => $duration, + ]; +} + +echo json_encode(['art' => $art, 'episodes' => $episodes]); -- cgit v1.3-2-g0d8e