Add Episode Highlights player with Spotify embed auto-resolution and direct embed URL support
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user