import { parseRssItems } from '../rss-helpers.js' import { state } from '../state.js' import { requireAdminAuth } from '../auth.js' import { queueNuggetsTagsWrite } from '../data.js' let cache = null // { url, episodes } let cacheAt = 0 const CACHE_TTL = 30 * 60 * 1000 function feedUrl() { return state.cachedSiteContent?.nuggets?.rssFeedUrl?.trim() ?? '' } export async function fetchNuggetsEpisodes() { const url = feedUrl() if (!url) { cache = null; return [] } const now = Date.now() if (cache && cache.url === url && (now - cacheAt) < CACHE_TTL) { return cache.episodes } const controller = new AbortController() const timeout = setTimeout(() => controller.abort(), 8000) try { const response = await fetch(url, { signal: controller.signal }) clearTimeout(timeout) if (!response.ok) throw new Error(`RSS fetch failed: ${response.status}`) const xml = await response.text() const episodes = parseRssItems(xml) cache = { url, episodes } cacheAt = now return episodes } catch (err) { clearTimeout(timeout) // Serve stale cache for this same URL rather than a hard failure, if we have one. if (cache && cache.url === url) return cache.episodes throw err } } function withTags(episodes) { return episodes.map(ep => ({ ...ep, tags: state.nuggetsEpisodeTags[ep.audioUrl] ?? [] })) } function cleanTags(input) { const list = Array.isArray(input) ? input : [] const seen = new Set() const out = [] for (const t of list) { const tag = String(t ?? '').trim().replace(/\s+/g, ' ').slice(0, 40) const key = tag.toLowerCase() if (tag && !seen.has(key)) { seen.add(key); out.push(tag) } } return out.slice(0, 12) } export function register(app) { app.get('/api/admin/nuggets/episode-tags', requireAdminAuth, async (_req, res) => { try { const episodes = await fetchNuggetsEpisodes() res.json({ episodes: withTags(episodes).map(({ title, pubDate, audioUrl, episode, tags }) => ({ title, pubDate, audioUrl, episode, tags })) }) } catch (err) { res.status(502).json({ message: 'Could not load episodes from the feed.' }) } }) app.put('/api/admin/nuggets/episode-tags', requireAdminAuth, (req, res) => { const audioUrl = String(req.body?.audioUrl ?? '') if (!audioUrl) { res.status(400).json({ message: 'audioUrl is required.' }); return } const tags = cleanTags(req.body?.tags) if (tags.length) state.nuggetsEpisodeTags[audioUrl] = tags else delete state.nuggetsEpisodeTags[audioUrl] queueNuggetsTagsWrite() res.json({ ok: true, tags }) }) app.get('/api/nuggets/episodes', async (_req, res) => { const configured = Boolean(feedUrl()) try { const episodes = await fetchNuggetsEpisodes() res.json({ episodes: withTags(episodes.slice(0, 6)), configured }) } catch (err) { console.error('[nuggets/episodes] RSS fetch error:', err.message) res.json({ episodes: [], configured }) } }) app.get('/api/nuggets/episodes/all', async (_req, res) => { const configured = Boolean(feedUrl()) try { const episodes = await fetchNuggetsEpisodes() res.json({ episodes: withTags(episodes), configured }) } catch (err) { console.error('[nuggets/episodes/all] RSS fetch error:', err.message) res.json({ episodes: [], configured }) } }) }