import { fetchAllEpisodes } from './routes/episodes.js' import { loadPodcastWorkflow, laneSeasonOf } from './routes/podcast-workflow.js' // The Spotify (Anchor) RSS feed only lists episodes that are live, so a season + episode number // showing up there means the episode has been published. That drives two things on the public site: // - highlights marked "hold until released" stay hidden until their episode is in the feed // - study lessons with no audio of their own pick it up from the matching feed episode const SNAPSHOT_TTL_MS = 5 * 60 * 1000 let snapshot = { items: [], at: 0 } let inflight = null function refreshSnapshot() { if (!inflight) { inflight = fetchAllEpisodes() .then(items => { snapshot = { items, at: Date.now() } }) // Keep serving the last good copy; retry on the next TTL window .catch(() => { snapshot = { items: snapshot.items, at: Date.now() } }) .finally(() => { inflight = null }) } return inflight } export async function getFeedIndex() { if (!snapshot.at) await refreshSnapshot() else if (Date.now() - snapshot.at > SNAPSHOT_TTL_MS) refreshSnapshot() const index = new Map() for (const item of snapshot.items) { if (item.season && /^\d+$/.test(item.episode)) index.set(`${item.season}-${item.episode}`, item) } return index } export function warmFeedSnapshot() { return refreshSnapshot() } // Returns a copy of the published site content with the feed applied. Run it before // filterSiteContentByReleaseDate so audio is never added to a lesson that is still locked. export async function applyFeedRelease(content) { if (!content || typeof content !== 'object' || Array.isArray(content)) return content const links = Array.isArray(content.podcastFeaturedLinks) ? content.podcastFeaturedLinks : [] const studies = Array.isArray(content.studies) ? content.studies : [] if (!links.some(l => l?.holdUntilReleased) && studies.length === 0) return content const [index, wf] = await Promise.all([getFeedIndex(), loadPodcastWorkflow()]) const seasonA = laneSeasonOf('A', wf, content) const seasonB = laneSeasonOf('B', wf, content) const nextLinks = links.flatMap(link => { // General highlights aren't tied to a series, so there's no season to match against if (!link?.holdUntilReleased || link.lane === 'general') return [link] const season = link.lane === 'second' ? seasonB : seasonA const item = season && link.episodeNumber ? index.get(`${season}-${link.episodeNumber}`) : null if (!item) return [] return [{ ...link, url: link.url || item.link || '' }] }) const studyLane = study => { if (study?.lane === 'A' || study?.lane === 'B') return study.lane if (study?.slug && study.slug === content.seriesStudySlug) return 'A' if (study?.slug && study.slug === content.secondSeries?.studySlug) return 'B' return '' } const nextStudies = studies.map(study => { const lane = studyLane(study) const season = lane === 'A' ? seasonA : lane === 'B' ? seasonB : '' if (!season || !Array.isArray(study.sections)) return study return { ...study, sections: study.sections.map(section => { if (!section || section.audioEmbedUrl?.trim() || !section.episodeNumber) return section const item = index.get(`${season}-${String(section.episodeNumber).trim()}`) return item?.audioUrl ? { ...section, audioEmbedUrl: item.audioUrl } : section }), } }) return { ...content, podcastFeaturedLinks: nextLinks, studies: nextStudies } }