Add Episode Highlights player with Spotify embed auto-resolution and direct embed URL support

This commit is contained in:
nmemmert
2026-05-07 11:36:07 -04:00
parent 93548b1ac8
commit 7568be368e
5 changed files with 527 additions and 143 deletions
+124
View File
@@ -2702,6 +2702,130 @@ async function fetchAllEpisodes() {
return episodes
}
function toSpotifyEpisodeEmbedUrl(urlValue) {
if (!urlValue) return ''
try {
const parsed = new URL(urlValue)
if (parsed.protocol !== 'https:') return ''
const host = parsed.hostname.toLowerCase()
const parts = parsed.pathname.split('/').filter(Boolean)
if (host === 'open.spotify.com') {
if (parts[0] === 'embed' && parts[1] === 'episode' && parts[2]) {
return `https://open.spotify.com/embed/episode/${parts[2]}?utm_source=generator`
}
if (parts[0] === 'episode' && parts[1]) {
return `https://open.spotify.com/embed/episode/${parts[1]}?utm_source=generator`
}
}
} catch {
return ''
}
return ''
}
function extractSpotifyEpisodeIdFromCreatorHtml(html, sourceUrl) {
const input = String(html || '')
if (!input) return ''
const sourceEpisodeSlug = /-([A-Za-z0-9]+)(?:\/|$)/.exec(sourceUrl)?.[1] ?? ''
const blockRegex = /"episodeId":"([^"]+)"[\s\S]*?"spotifyUrl":"([^"]+)"/g
let match
let firstEpisodeId = ''
while ((match = blockRegex.exec(input)) !== null) {
const episodeSlug = match[1]
const spotifyUrl = decodeEscapedJsonUrl(match[2])
const episodeId = /\/episode\/([A-Za-z0-9]+)/.exec(spotifyUrl)?.[1]
if (!firstEpisodeId && episodeId) firstEpisodeId = episodeId
if (sourceEpisodeSlug && episodeSlug === sourceEpisodeSlug && episodeId) {
return episodeId
}
}
if (firstEpisodeId) return firstEpisodeId
const urlMatch = /"spotifyUrl":"(https:\\u002F\\u002Fopen\.spotify\.com\\u002Fepisode\\u002F([A-Za-z0-9]+))/.exec(input)
return urlMatch ? (urlMatch[2] || '') : ''
}
function isAllowedSpotifyResolverHost(hostname) {
const host = String(hostname || '').toLowerCase()
return host === 'open.spotify.com'
|| host === 'creators.spotify.com'
|| host === 'anchor.fm'
|| host === 'podcasters.spotify.com'
}
function decodeEscapedJsonUrl(value) {
return String(value || '').replace(/\\u002F/g, '/').replace(/\\\//g, '/')
}
app.get('/api/spotify/embed-url', async (req, res) => {
const incoming = typeof req.query.url === 'string' ? req.query.url.trim() : ''
const safeInput = sanitizeUrl(incoming)
if (!safeInput || safeInput.startsWith('/')) {
res.status(400).json({ message: 'A valid episode URL is required.' })
return
}
let parsed
try {
parsed = new URL(safeInput)
} catch {
res.status(400).json({ message: 'Malformed URL.' })
return
}
if (parsed.protocol !== 'https:' || !isAllowedSpotifyResolverHost(parsed.hostname)) {
res.status(400).json({ message: 'Unsupported episode URL host.' })
return
}
const directEmbed = toSpotifyEpisodeEmbedUrl(safeInput)
if (directEmbed) {
res.json({ embedUrl: directEmbed, resolvedFrom: 'direct' })
return
}
try {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 8000)
const response = await fetch(safeInput, {
signal: controller.signal,
headers: {
'User-Agent': 'Siteforge/1.0 (+https://versebyversewithnate.us)',
Accept: 'text/html',
},
})
clearTimeout(timeout)
if (!response.ok) {
res.status(404).json({ message: 'Could not fetch episode page.' })
return
}
const html = await response.text()
const spotifyEpisodeId = extractSpotifyEpisodeIdFromCreatorHtml(html, safeInput)
if (!spotifyEpisodeId) {
res.status(404).json({ message: 'Could not resolve Spotify episode ID from page.' })
return
}
const embedUrl = `https://open.spotify.com/embed/episode/${spotifyEpisodeId}?utm_source=generator`
res.json({ embedUrl, resolvedFrom: 'page-fetch' })
} catch (err) {
console.error('[spotify/embed-url] resolve error:', err.message)
res.status(500).json({ message: 'Could not resolve Spotify embed URL right now.' })
}
})
app.get('/api/episodes', async (_req, res) => {
try {
const episodes = await fetchAllEpisodes()