PageSourceSearch

https://npestories.com/js/episodes.js

js npestories.com collected 2026-10-02 16:03:06 UTC 8,779 bytes, 252 lines download raw bytes

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 &rarr;</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 &rarr;</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.