Publish Container / build (push) Successful in 15m38s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
90 lines
3.7 KiB
JavaScript
90 lines
3.7 KiB
JavaScript
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
|
|
// - highlights and 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.length === 0 && 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 || link.lane === 'general') return [link]
|
|
const season = link.lane === 'second' ? seasonB : seasonA
|
|
const item = season && link.episodeNumber ? index.get(`${season}-${String(link.episodeNumber).trim()}`) : null
|
|
if (!item) return link.holdUntilReleased ? [] : [link]
|
|
// Highlights with no audio of their own pick it up from the matching feed episode
|
|
return [{
|
|
...link,
|
|
url: link.url || item.link || '',
|
|
embedUrl: link.embedUrl?.trim() ? link.embedUrl : (item.audioUrl || link.embedUrl),
|
|
}]
|
|
})
|
|
|
|
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 }
|
|
}
|