Add Episode Highlights player with Spotify embed auto-resolution and direct embed URL support
This commit is contained in:
+117
-3
@@ -13,6 +13,48 @@ const SPOTIFY_EMBED_URL =
|
||||
const CONSENT_KEY = 'vbn_analytics_consent_choice'
|
||||
const HEADLINER_WIDGET_ID = 'WI_cmou3b4q7000701p0o9qmmcfj'
|
||||
|
||||
function toSpotifyEpisodeEmbedUrl(url: string | undefined): string {
|
||||
if (!url) return ''
|
||||
|
||||
try {
|
||||
const parsed = new URL(url)
|
||||
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 isLikelySpotifyEpisodeUrl(url: string | undefined): boolean {
|
||||
if (!url) return false
|
||||
if (toSpotifyEpisodeEmbedUrl(url)) return true
|
||||
|
||||
try {
|
||||
const parsed = new URL(url)
|
||||
const host = parsed.hostname.toLowerCase()
|
||||
const path = parsed.pathname.toLowerCase()
|
||||
|
||||
if (host === 'creators.spotify.com' && path.includes('/episodes/')) return true
|
||||
if (host === 'anchor.fm' && path.includes('/episodes/')) return true
|
||||
if (host === 'podcasters.spotify.com' && path.includes('/episodes/')) return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function HeadlinerWidget() {
|
||||
const [iframeSrc, setIframeSrc] = useState('')
|
||||
const [status, setStatus] = useState<'loading' | 'ready' | 'empty'>('loading')
|
||||
@@ -683,7 +725,12 @@ function PodcastHighlightsSection({ content }: { content: SiteContent }) {
|
||||
</h2>
|
||||
<div className="resources-list">
|
||||
{links.map(link => {
|
||||
const hasRichContent = !!(link.embedUrl || (link.discussionQuestions ?? []).length > 0 || link.showNotes)
|
||||
const hasRichContent = !!(
|
||||
link.embedUrl
|
||||
|| isLikelySpotifyEpisodeUrl(link.url)
|
||||
|| (link.discussionQuestions ?? []).length > 0
|
||||
|| link.showNotes
|
||||
)
|
||||
const label = <>{link.episodeNumber ? `Ep. ${link.episodeNumber}: ` : ''}{link.title || link.url}</>
|
||||
// Use external link only when there's a url AND no rich detail-page content
|
||||
if (!hasRichContent && link.url) {
|
||||
@@ -1004,6 +1051,73 @@ function LandingPage({ content }: { content: SiteContent }) {
|
||||
function EpisodeDetailPage({ content }: { content: SiteContent }) {
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const episode = (content.podcastFeaturedLinks ?? []).find(e => e.id === id)
|
||||
const [resolvedEmbedUrl, setResolvedEmbedUrl] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
if (!episode) {
|
||||
setResolvedEmbedUrl('')
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}
|
||||
|
||||
const manualEmbed = episode.embedUrl?.trim() ?? ''
|
||||
if (manualEmbed) {
|
||||
setResolvedEmbedUrl(manualEmbed)
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!episode.url) {
|
||||
setResolvedEmbedUrl('')
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const urlObj = new URL(episode.url)
|
||||
const host = urlObj.hostname.toLowerCase()
|
||||
|
||||
if (host === 'open.spotify.com') {
|
||||
const fromUrl = toSpotifyEpisodeEmbedUrl(episode.url)
|
||||
if (fromUrl) {
|
||||
setResolvedEmbedUrl(fromUrl)
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
if (!isLikelySpotifyEpisodeUrl(episode.url)) {
|
||||
setResolvedEmbedUrl('')
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}
|
||||
|
||||
setResolvedEmbedUrl('')
|
||||
fetch(`/api/spotify/embed-url?url=${encodeURIComponent(episode.url)}`)
|
||||
.then(r => (r.ok ? r.json() : Promise.reject(new Error('embed resolve failed'))))
|
||||
.then((data: { embedUrl?: string }) => {
|
||||
if (cancelled) return
|
||||
setResolvedEmbedUrl(data.embedUrl?.trim() ?? '')
|
||||
})
|
||||
.catch(() => {
|
||||
if (cancelled) return
|
||||
setResolvedEmbedUrl('')
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [episode])
|
||||
|
||||
if (!episode) {
|
||||
return (
|
||||
@@ -1037,10 +1151,10 @@ function EpisodeDetailPage({ content }: { content: SiteContent }) {
|
||||
<h1 className="episode-detail-title">{episode.title}</h1>
|
||||
{episode.summary && <p className="episode-detail-summary">{episode.summary}</p>}
|
||||
|
||||
{episode.embedUrl && (
|
||||
{resolvedEmbedUrl && (
|
||||
<div className="episode-detail-embed">
|
||||
<iframe
|
||||
src={episode.embedUrl}
|
||||
src={resolvedEmbedUrl}
|
||||
title={episode.title}
|
||||
width="100%"
|
||||
height="152"
|
||||
|
||||
Reference in New Issue
Block a user