1/** 2 * NPE Stories â episode loader 3 * Pulls from Buzzsprout RSS. Episodes update automatically when new ones 4 * are published â no redeployment needed. 5 */ 6(function () { 7 var RSS_URL = 'https://feeds.buzzsprout.com/334406.rss'; 8 var PODCAST_ID = '334406'; 9 var CAROUSEL_N = 8; 10 var LIST_N = 10; 11 12 // ââ helpers ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 13 14 function epId(str) { 15 if (!str) return null; 16 // "Buzzsprout-18875007" 17 var m1 = str.match(/Buzzsprout-(\d+)/i); 18 if (m1) return m1[1]; 19 // ".../334406/episodes/18875007-slug.mp3" 20 var m2 = str.match(/\/episodes\/(\d+)/); 21 if (m2) return m2[1]; 22 // fallback: ".../334406/18875007" 23 var m3 = str.match(/buzzsprout\.com\/\d+\/(\d+)/); 24 return m3 ? m3[1] : null; 25 } 26 27 function fmtDate(s) { 28 if (!s) return ''; 29 return new Date(s).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }); 30 } 31 32 function el(tag, cls, text) { 33 var e = document.createElement(tag); 34 if (cls) e.className = cls; 35 if (text) e.textContent = text; 36 return e; 37 } 38 39 // Fetch RSS, fall back to CORS proxy if needed 40 async function fetchRSS() { 41 try { 42 var r = await fetch(RSS_URL); 43 if (!r.ok) throw new Error(); 44 return await r.text(); 45 } catch (_) { 46 var r2 = await fetch('https://api.allorigins.win/raw?url=' + encodeURIComponent(RSS_URL)); 47 return await r2.text(); 48 } 49 } 50 51 function parseEpisodes(xml) { 52 var doc = new DOMParser().parseFromString(xml, 'text/xml'); 53 var items = doc.querySelectorAll('item'); 54 var n = 0; 55 return Array.from(items).map(function (item) { 56 n++; 57 var guid = (item.querySelector('guid') || {}).textContent || ''; 58 var enc = item.querySelector('enclosure'); 59 var encUrl = enc ? (enc.getAttribute('url') || '') : ''; 60 var epNum = (item.querySelector('itunes\\:episode, episode') || {}).textContent; 61 return { 62 num: epNum ? parseInt(epNum, 10) : null, 63 idx: n, 64 title: (item.querySelector('title') || {}).textContent || 'Untitled', 65 date: (item.querySelector('pubDate') || {}).textContent || '', 66 id: epId(guid) || epId(encUrl), 67 }; 68 }); 69 } 70 71 // Lazy-load a Buzzsprout small player into div when it scrolls into view 72 function lazyPlayer(container, episodeId) { 73 container.id = 'buzzsprout-player-' + episodeId; 74 var loaded = false; 75 var obs = new IntersectionObserver(function (entries, o) { 76 if (entries[0].isIntersecting && !loaded) { 77 loaded = true; 78 var s = document.createElement('script'); 79 s.type = 'text/javascript'; 80 s.charset = 'utf-8'; 81 s.src = 'https://www.buzzsprout.com/' + PODCAST_ID + '/' + episodeId + 82 '.js?container_id=buzzsprout-player-' + episodeId + '&player=small'; 83 document.head.appendChild(s); 84 o.unobserve(container); 85 } 86 }, { threshold: 0.1 }); 87 obs.observe(container); 88 } 89 90 // ââ episode player slider (main page, 5 most recent) âââââââââââââââââââââ 91 92 function buildSlide(ep) { 93 var slide = el('div', 'ep-slide'); 94 95 // metadata row 96 var meta = el('div', 'ep-slide-meta'); 97 meta.appendChild(el('span', 'ep-badge', ep.num ? 'EP ' + ep.num : 'EP ' + ep.idx)); 98 meta.appendChild(el('span', 'ep-date', fmtDate(ep.date))); 99 meta.appendChild(el('span', 'ep-slide-title', ep.title)); 100 slide.appendChild(meta); 101 102 // player mount â individual episode embed with full controls 103 var mount = el('div', 'ep-slide-player'); 104 slide.appendChild(mount); 105 106 slide._epId = ep.id; 107 slide._mount = mount; 108 slide._loaded = false; 109 110 return slide; 111 } 112
113 function loadSlidePlayer(slide) { 114 if (slide._loaded || !slide._epId) return; 115 slide._loaded = true; 116 var mountId = 'buzzsprout-player-' + slide._epId; 117 slide._mount.id = mountId; 118 var s = document.createElement('script'); 119 s.type = 'text/javascript'; 120 s.charset = 'utf-8'; 121 s.src = 'https://www.buzzsprout.com/' + PODCAST_ID + '/' + slide._epId + 122 '.js?container_id=' + mountId + '&player=small'; 123 document.head.appendChild(s); 124 } 125 126 async function initSlider() { 127 var shell = document.getElementById('ep-slider-shell'); 128 var nav = document.getElementById('ep-slider-nav'); 129 if (!shell) return; 130 131 try { 132 var eps = parseEpisodes(await fetchRSS()).slice(0, 5); 133 var slides = eps.map(buildSlide); 134 var cur = 0; 135 136 shell.innerHTML = ''; 137 slides.forEach(function (s) { shell.appendChild(s); }); 138 139 var counter = document.getElementById('ep-counter'); 140 var prevBtn = document.getElementById('ep-prev'); 141 var nextBtn = document.getElementById('ep-next'); 142 143 function show(i) { 144 slides[cur].classList.remove('active'); 145 cur = i; 146 slides[cur].classList.add('active');
147 loadSlidePlayer(slides[cur]); 148 // preload adjacent 149 if (slides[cur + 1]) loadSlidePlayer(slides[cur + 1]); 150 if (counter) counter.textContent = (cur + 1) + ' of ' + slides.length; 151 if (prevBtn) prevBtn.disabled = cur === 0; 152 if (nextBtn) nextBtn.disabled = cur === slides.length - 1; 153 } 154 155 if (prevBtn) prevBtn.addEventListener('click', function () { if (cur > 0) show(cur - 1); }); 156 if (nextBtn) nextBtn.addEventListener('click', function () { if (cur < slides.length - 1) show(cur + 1); }); 157 158 show(0); 159 if (nav) nav.style.display = 'flex'; 160 161 } catch (err) { 162 shell.innerHTML = '<div class="ep-error">Could not load episodes. ' + 163 '<a href="https://www.buzzsprout.com/334406">Listen on Buzzsprout →</a></div>'; 164 } 165 } 166 167 // ââ episode list (episodes.html) ââââââââââââââââââââââââââââââââââââââââââ 168 169 function buildListRow(ep) { 170 var row = el('div', 'ep-list-row'); 171 172 var hdr = el('div', 'ep-list-header'); 173 hdr.appendChild(el('span', 'ep-badge', ep.num ? 'EP ' + ep.num : 'EP ' + ep.idx)); 174 hdr.appendChild(el('span', 'ep-date', fmtDate(ep.date))); 175 hdr.appendChild(el('p', 'ep-list-title', ep.title)); 176 row.appendChild(hdr); 177 178 if (ep.id) { 179 var wrap = el('div', 'ep-player'); 180 lazyPlayer(wrap, ep.id); 181 row.appendChild(wrap); 182 } 183 184 return row; 185 } 186 187 async function initList() { 188 var list = document.getElementById('episodes-list'); 189 if (!list) return; 190 191 list.innerHTML = '<div class="ep-loading">Loading episodes\u2026</div>'; 192 193 try { 194 var all = parseEpisodes(await fetchRSS()); 195 var loaded = 0; 196 var busy = false; 197 list.innerHTML = ''; 198 199 function loadMore() { 200 if (busy || loaded >= all.length) return; 201 busy = true; 202 all.slice(loaded, loaded + LIST_N).forEach(function (ep) { 203 list.appendChild(buildListRow(ep)); 204 }); 205 loaded += LIST_N; 206 busy = false; 207 if (loaded >= all.length) { 208 var s = document.getElementById('list-sentinel'); 209 if (s) s.remove(); 210 } 211 } 212 213 loadMore(); 214 215 var sentinel = document.getElementById('list-sentinel'); 216 if (sentinel) { 217 new IntersectionObserver(function (e) { 218 if (e[0].isIntersecting) loadMore(); 219 }, { rootMargin: '300px' }).observe(sentinel); 220 } 221 222 } catch (err) { 223 list.innerHTML = '<div class="ep-error">Could not load episodes. ' + 224 '<a href="https://www.buzzsprout.com/334406">Listen on Buzzsprout →</a></div>'; 225 } 226 } 227 228 // ââ topbar ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 229 230 function initTopbar() { 231 var hbtn = document.getElementById('hamburger-btn'); 232 var nav = document.getElementById('topbar-nav'); 233 if (hbtn && nav) { 234 hbtn.addEventListener('click', function () { nav.classList.toggle('open'); }); 235 nav.querySelectorAll('.topbar-link').forEach(function (l) { 236 l.addEventListener('click', function () { nav.classList.remove('open'); }); 237 }); 238 } 239 } 240 241 // ââ boot ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 242 243 function boot() { 244 initTopbar(); 245 initSlider(); 246 initList(); 247 } 248 249 document.readyState === 'loading' 250 ? document.addEventListener('DOMContentLoaded', boot) 251 : boot(); 252})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.