// Pure helpers for detecting embeddable media URLs and building their embed // URLs. No React/DOM/CSS imports so this stays unit-testable in isolation. // // "kind" drives how the card lays the embed out: // landscape → 16:9 video portrait → 9:16 video // audio → short fixed-height player (Spotify/SoundCloud/Tidal/Apple Music) // rich → tall fixed-height post embed (Instagram / Reddit) export type EmbedKind = 'landscape' | 'portrait' | 'audio' | 'rich'; export type MediaEmbed = { provider: string; kind: EmbedKind; embedUrl: string; /** Fixed pixel height for audio/rich embeds (aspect-ratio is used for video). */ height?: number; }; // --- YouTube -------------------------------------------------------------- const YOUTUBE_HOSTS = ['www.youtube.com', 'youtube.com', 'm.youtube.com', 'music.youtube.com']; export function getYouTubeVideoId(url: string): string | null { try { const { hostname, pathname, searchParams } = new URL(url); if (hostname === 'youtu.be') return pathname.slice(1).split('/')[0] || null; if (YOUTUBE_HOSTS.includes(hostname)) { if (pathname === '/watch') return searchParams.get('v'); const m = pathname.match(/^\/embed\/([A-Za-z0-9_-]+)/) || pathname.match(/^\/live\/([A-Za-z0-9_-]+)/) || pathname.match(/^\/shorts\/([A-Za-z0-9_-]+)/); if (m) return m[1]; } } catch { /* ignore */ } return null; } export function isYouTubeShorts(url: string): boolean { try { const { hostname, pathname } = new URL(url); if (!YOUTUBE_HOSTS.includes(hostname)) return false; return /^\/shorts\/[A-Za-z0-9_-]+/.test(pathname); } catch { return false; } } export function getYoutubeShortsId(url: string): string | null { try { const { hostname, pathname } = new URL(url); if (!YOUTUBE_HOSTS.includes(hostname)) return null; const m = pathname.match(/^\/shorts\/([A-Za-z0-9_-]+)/); return m ? m[1] : null; } catch { return null; } } // --- Vimeo ---------------------------------------------------------------- /** Vimeo id + optional private/unlisted hash (vimeo.com/{id}/{hash}). */ export function getVimeoParts(url: string): { id: string; hash?: string } | null { try { const { hostname, pathname } = new URL(url); if (hostname !== 'vimeo.com' && hostname !== 'www.vimeo.com') return null; // Canonical /{id} or unlisted /{id}/{hash}. The hash is a lowercase-hex token // (constrain it so a normal video's trailing segment — /likes, /settings, a // review slug — isn't captured as a bogus `h=` param that Vimeo then rejects). let m = pathname.match(/^\/(\d+)(?:\/([0-9a-f]{6,}))?/); if (m) return { id: m[1], hash: m[2] }; // channels/groups/album share a trailing numeric video id m = pathname.match(/\/(?:channels\/[^/]+|groups\/[^/]+\/videos|album\/[^/]+\/video)\/(\d+)/); if (m) return { id: m[1] }; return null; } catch { return null; } } export function getVimeoVideoId(url: string): string | null { return getVimeoParts(url)?.id ?? null; } // --- TikTok --------------------------------------------------------------- /** Canonical /video/ links only; short links resolve via oEmbed (below). */ export function getTikTokVideoId(url: string): string | null { try { const { hostname, pathname } = new URL(url); const h = hostname.replace(/^www\./, ''); if (h !== 'tiktok.com') return null; const m = pathname.match(/\/video\/(\d+)/); return m ? m[1] : null; } catch { return null; } } /** Any embeddable TikTok video link — canonical, short (/t/, /v/), or vm/vt.tiktok.com. */ export function isTikTokLink(url: string): boolean { try { const { hostname, pathname } = new URL(url); const h = hostname.replace(/^www\./, ''); if (h === 'vm.tiktok.com' || h === 'vt.tiktok.com') return true; if (h === 'tiktok.com') return /^\/(t\/|v\/|embed\/|@[^/]+\/video\/)/.test(pathname); return false; } catch { return false; } } export function tiktokOembedUrl(url: string): string { return `https://www.tiktok.com/oembed?url=${encodeURIComponent(url)}`; } /** Extract the numeric video id from a TikTok oEmbed JSON response. */ export function tiktokIdFromOembed(data: { embed_product_id?: unknown; html?: unknown; }): string | null { const pid = String(data.embed_product_id ?? '').match(/\d+/)?.[0]; if (pid) return pid; if (typeof data.html === 'string') { return ( data.html.match(/data-video-id="(\d+)"/)?.[1] ?? data.html.match(/\/video\/(\d+)/)?.[1] ?? null ); } return null; } /** * Read a content height out of the postMessage shapes used by the resizable * embeds. Shapes verified against the providers' live 2026 embed scripts: * Instagram: { type: 'MEASURE', details: { height } } * Reddit: { type: 'resize.embed', data: } * Twitter/X: { 'twttr.embed': [{ method: 'twttr.private.resize', params: [{ height }] }] } */ export function extractEmbedHeight(data: unknown): number | undefined { if (!data || typeof data !== 'object') return undefined; const d = data as { type?: string; height?: number; data?: unknown; details?: { height?: number }; 'twttr.embed'?: unknown; }; if (d.type === 'MEASURE' && typeof d.details?.height === 'number') return d.details.height; if (d.type === 'resize.embed' && typeof d.data === 'number') return d.data; const tw = d['twttr.embed']; if (tw) { const calls = (Array.isArray(tw) ? tw : [tw]) as Array<{ method?: string; params?: Array<{ height?: number }>; }>; for (let i = 0; i < calls.length; i += 1) { const c = calls[i]; if (c?.method === 'twttr.private.resize' && typeof c.params?.[0]?.height === 'number') { return c.params[0].height; } } } if (typeof d.height === 'number') return d.height; return undefined; } export function tiktokPlayerEmbedUrl(id: string): string { // Pure 9:16 video player. music_info/description default OFF (they'd switch // TikTok to a wide "video + info panel" layout); controls/progress/play/volume/ // fullscreen buttons all default ON, so only autoplay + rel are load-bearing. return `https://www.tiktok.com/player/v1/${encodeURIComponent(id)}?autoplay=1&rel=0`; } // --- Dailymotion ---------------------------------------------------------- export function getDailymotionId(url: string): string | null { try { const { hostname, pathname } = new URL(url); const h = hostname.replace(/^www\./, ''); if (h === 'dai.ly') return pathname.slice(1).split('/')[0] || null; if (h === 'dailymotion.com') { const m = pathname.match(/^\/video\/([A-Za-z0-9]+)/); return m ? m[1] : null; } } catch { /* ignore */ } return null; } // --- Streamable ----------------------------------------------------------- // Streamable's own utility/first-path pages that are not video ids. const STREAMABLE_RESERVED = new Set([ 'e', 'login', 'signup', 'settings', 'account', 'dashboard', 'help', 'terms', 'privacy', 'about', ]); export function getStreamableId(url: string): string | null { try { const { hostname, pathname } = new URL(url); if (hostname.replace(/^www\./, '') !== 'streamable.com') return null; const m = pathname.match(/^\/([A-Za-z0-9]+)/); return m && !STREAMABLE_RESERVED.has(m[1].toLowerCase()) ? m[1] : null; } catch { return null; } } // --- Steam ---------------------------------------------------------------- export type SteamTarget = // a game/app store page → gets the official click-to-play store widget | { kind: 'app'; appId: string } // a news/announcement post → a rich card (no official widget for these) | { kind: 'news'; appId: string; gid: string } // bundle / sub / dlc pages → an OG store card (no per-app widget) | { kind: 'store'; label: string }; /** * Classify a store.steampowered.com content URL. Only content pages (app / news / * bundle / sub / dlc) match; the homepage, search, wishlist, cart etc. return * null and fall through to the generic preview card. */ export function getSteamTarget(url: string): SteamTarget | null { try { const { hostname, pathname } = new URL(url); if (hostname.replace(/^www\./, '') !== 'store.steampowered.com') return null; let m = pathname.match(/^\/news\/app\/(\d+)\/view\/(\d+)/); if (m) return { kind: 'news', appId: m[1], gid: m[2] }; m = pathname.match(/^\/app\/(\d+)/); if (m) return { kind: 'app', appId: m[1] }; m = pathname.match(/^\/(bundle|sub|dlc)\/\d+/); if (m) return { kind: 'store', label: m[1] }; return null; } catch { return null; } } /** Steam's official embeddable store widget (live price / discount / Buy). */ export function steamWidgetEmbedUrl(appId: string): string { return `https://store.steampowered.com/widget/${encodeURIComponent(appId)}/`; } // --- Twitch --------------------------------------------------------------- export type TwitchTarget = | { type: 'channel'; value: string } | { type: 'video'; value: string } | { type: 'clip'; value: string }; // Twitch's own reserved first-path segments — single-segment paths that are // utility pages, not channels, and must NOT be embedded as `channel=`. const TWITCH_RESERVED = new Set([ 'directory', 'videos', 'settings', 'subscriptions', 'following', 'followers', 'friends', 'inventory', 'wallet', 'drops', 'prime', 'turbo', 'downloads', 'jobs', 'store', 'search', 'dashboard', 'popout', 'p', 'u', 'team', ]); export function getTwitchTarget(url: string): TwitchTarget | null { try { const { hostname, pathname } = new URL(url); const h = hostname.replace(/^www\./, ''); const parts = pathname.replace(/^\/+|\/+$/g, '').split('/'); if (h === 'clips.twitch.tv' && parts[0]) return { type: 'clip', value: parts[0] }; if (h === 'twitch.tv' || h === 'm.twitch.tv') { if (parts[0] === 'videos' && parts[1]) return { type: 'video', value: parts[1] }; if (parts[1] === 'clip' && parts[2]) return { type: 'clip', value: parts[2] }; if (parts.length === 1 && parts[0] && !TWITCH_RESERVED.has(parts[0].toLowerCase())) { return { type: 'channel', value: parts[0] }; } } } catch { /* ignore */ } return null; } // --- Spotify -------------------------------------------------------------- const SPOTIFY_TYPES = ['track', 'album', 'playlist', 'episode', 'show', 'artist'] as const; export type SpotifyType = (typeof SPOTIFY_TYPES)[number]; export function getSpotifyEmbedTarget(url: string): { type: SpotifyType; id: string } | null { try { const { hostname, pathname } = new URL(url); if (hostname !== 'open.spotify.com') return null; // /intl-xx/track/ or /track/ const parts = pathname.replace(/^\/+/, '').split('/'); const idx = parts.findIndex((p) => (SPOTIFY_TYPES as readonly string[]).includes(p)); if (idx === -1 || !parts[idx + 1]) return null; return { type: parts[idx] as SpotifyType, id: parts[idx + 1].split('?')[0] }; } catch { return null; } } // --- SoundCloud ----------------------------------------------------------- // SoundCloud's own site sections (first segment) that are never ``. const SOUNDCLOUD_RESERVED = new Set([ 'discover', 'you', 'stream', 'search', 'upload', 'settings', 'notifications', 'messages', 'tags', 'charts', 'people', 'pages', 'terms', 'pro', ]); // Profile tabs — `//` is a listing, not a single track (a real set // is the deeper `//sets/`, which has length >= 3 and is allowed). const SOUNDCLOUD_PROFILE_TABS = new Set([ 'tracks', 'sets', 'albums', 'reposts', 'likes', 'following', 'followers', 'comments', 'popular-tracks', 'toptracks', ]); export function isSoundCloudTrack(url: string): boolean { try { const { hostname, pathname } = new URL(url); // NOTE: on.soundcloud.com short links are NOT handled here — the w.soundcloud // widget resolver doesn't follow the redirect; supporting them needs an oEmbed // round-trip (soundcloud.com/oembed is CORS-enabled) to get the canonical URL. if (hostname.replace(/^www\./, '') !== 'soundcloud.com') return false; // // — at least two segments, not a bare profile const parts = pathname .replace(/^\/+|\/+$/g, '') .split('/') .filter(Boolean); if (parts.length < 2) return false; if (SOUNDCLOUD_RESERVED.has(parts[0].toLowerCase())) return false; // `//` profile-tab listing (not a playable single track/set). if (parts.length === 2 && SOUNDCLOUD_PROFILE_TABS.has(parts[1].toLowerCase())) return false; return true; } catch { return false; } } // --- Apple Music ---------------------------------------------------------- /** music.apple.com//album|playlist|song//[?i=] → embed player. */ export function getAppleMusicEmbed( url: string, ): { embedUrl: string; height: number; video: boolean } | null { try { const u = new URL(url); if (u.hostname !== 'music.apple.com' && u.hostname !== 'embed.music.apple.com') return null; if (!/\/(album|playlist|song|music-video)\//.test(u.pathname)) return null; const embedUrl = `https://embed.music.apple.com${u.pathname}${u.search}`; const video = /\/music-video\//.test(u.pathname); // A single song (?i=… on an album, or a /song/ link) is compact; collections are tall. const isSong = u.searchParams.has('i') || /\/song\//.test(u.pathname); return { embedUrl, height: isSong ? 175 : 450, video }; } catch { return null; } } // --- X / Twitter ---------------------------------------------------------- export function getTweetId(url: string): string | null { try { const { hostname, pathname } = new URL(url); const h = hostname.replace(/^www\./, ''); if (h !== 'twitter.com' && h !== 'x.com' && h !== 'mobile.twitter.com') return null; const m = pathname.match(/\/status(?:es)?\/(\d+)/); return m ? m[1] : null; } catch { return null; } } // --- Tidal ---------------------------------------------------------------- export function getTidalEmbed( url: string, ): { kind: 'audio' | 'landscape'; embedUrl: string; height?: number } | null { try { const u = new URL(url); const h = u.hostname.replace(/^(www|listen|desktop)\./, ''); if (h !== 'tidal.com') return null; const p = u.pathname.replace(/^\/browse/, ''); let m = p.match(/^\/track\/(\d+)/); if (m) return { kind: 'audio', embedUrl: `https://embed.tidal.com/tracks/${m[1]}`, height: 120 }; m = p.match(/^\/album\/(\d+)/); if (m) // layout=gridify → full-width grid that fills the container (fixes the // narrow/centered default); ~275px is Tidal's own album embed height. return { kind: 'audio', embedUrl: `https://embed.tidal.com/albums/${m[1]}?layout=gridify`, height: 275, }; m = p.match(/^\/playlist\/([0-9a-fA-F-]+)/); if (m) return { kind: 'audio', embedUrl: `https://embed.tidal.com/playlists/${m[1]}?layout=gridify`, height: 275, }; m = p.match(/^\/video\/(\d+)/); if (m) return { kind: 'landscape', embedUrl: `https://embed.tidal.com/videos/${m[1]}` }; } catch { /* ignore */ } return null; } // --- Instagram ------------------------------------------------------------ export function getInstagramEmbed(url: string): string | null { try { const u = new URL(url); if (u.hostname !== 'instagram.com' && u.hostname !== 'www.instagram.com') return null; const m = u.pathname.match(/^\/(p|reel|reels|tv)\/([A-Za-z0-9_-]+)/); if (!m) return null; const type = m[1] === 'reels' ? 'reel' : m[1]; return `https://www.instagram.com/${type}/${m[2]}/embed/`; } catch { return null; } } // --- Reddit (post embed via embed.reddit.com — reddit's own oEmbed widget host; // www.redditmedia.com now 301s here. Bypasses the homeserver's blocked preview // fetch, which Reddit serves a bot-check "please wait" page to) --- const REDDIT_EMBED_QS = '?ref_source=embed&ref=share&embed=true&theme=dark'; export function getRedditPostEmbed(url: string): string | null { try { const u = new URL(url); const h = u.hostname.replace(/^(www|old|new|np|i)\./, ''); // embed.reddit.com ONLY renders the /r//comments/ path (verified against // reddit's own embed widgets.js). A bare /comments/ — all we could build from a // redd.it short link or an i./v.redd.it media host — serves a "not found" page, so // return null for those and let the caller's og:url fallback resolve the canonical // /r//comments/ URL (from the redirect) before re-parsing into a real embed. if (h !== 'reddit.com') return null; const m = u.pathname.match(/^\/r\/([A-Za-z0-9_]+)\/comments\/([A-Za-z0-9]+)/); if (!m) return null; return `https://embed.reddit.com/r/${m[1]}/comments/${m[2]}/${REDDIT_EMBED_QS}`; } catch { return null; } } // --- Loom ----------------------------------------------------------------- export function getLoomId(url: string): string | null { try { const { hostname, pathname } = new URL(url); if (hostname.replace(/^www\./, '') !== 'loom.com') return null; const m = pathname.match(/^\/(?:share|embed)\/([A-Za-z0-9]+)/); return m ? m[1] : null; } catch { return null; } } // --- Kick (live channels only; VODs/clips have no clean iframe) ------------ // Kick's own reserved first-path segments (nav pages, not channels). const KICK_RESERVED = new Set([ 'browse', 'following', 'category', 'categories', 'search', 'messages', 'subscriptions', 'settings', 'wallet', 'help', 'clips', 'dashboard', ]); export function getKickChannel(url: string): string | null { try { const { hostname, pathname } = new URL(url); if (hostname.replace(/^www\./, '') !== 'kick.com') return null; const parts = pathname .replace(/^\/+|\/+$/g, '') .split('/') .filter(Boolean); return parts.length === 1 && /^[A-Za-z0-9_]+$/.test(parts[0]) && !KICK_RESERVED.has(parts[0].toLowerCase()) ? parts[0] : null; } catch { return null; } } // --- Bluesky -------------------------------------------------------------- export function getBlueskyEmbed(url: string): string | null { try { const { hostname, pathname } = new URL(url); if (hostname !== 'bsky.app' && hostname !== 'www.bsky.app') return null; const m = pathname.match(/^\/profile\/([^/]+)\/post\/([A-Za-z0-9]+)/); // authority is a handle or did; embed.bsky.app resolves either. return m ? `https://embed.bsky.app/embed/${m[1]}/app.bsky.feed.post/${m[2]}` : null; } catch { return null; } } // --- Mixcloud ------------------------------------------------------------- // Mixcloud's own site sections (first segment) that are never a ``. const MIXCLOUD_RESERVED = new Set([ 'discover', 'categories', 'upload', 'live', 'settings', 'notifications', 'search', 'tag', 'select', 'browse', ]); // Profile tabs — `//` is a listing, not a single cloudcast. const MIXCLOUD_PROFILE_TABS = new Set([ 'uploads', 'favorites', 'listens', 'following', 'followers', 'playlists', 'stream', 'reposts', ]); /** * Canonical Mixcloud cloudcast feed URL (`///`) for the widget's * `feed=` param, or null. Bare profiles / profile-tab listings / site sections * are excluded (they aren't a single playable cloudcast). */ export function getMixcloudFeed(url: string): string | null { try { const u = new URL(url); if (u.hostname.replace(/^www\./, '') !== 'mixcloud.com') return null; const parts = u.pathname .replace(/^\/+|\/+$/g, '') .split('/') .filter(Boolean); if (parts.length < 2) return null; if (MIXCLOUD_RESERVED.has(parts[0].toLowerCase())) return null; if (MIXCLOUD_PROFILE_TABS.has(parts[1].toLowerCase())) return null; return `https://www.mixcloud.com/${parts[0]}/${parts[1]}/`; } catch { return null; } } // --- Deezer --------------------------------------------------------------- // NB: Deezer podcast pages are `/show/`, not `/podcast/` — the latter // 404s on their own oEmbed API, and `widget.deezer.com/widget/dark/show/` // is the matching widget path. const DEEZER_TYPES = ['track', 'album', 'playlist', 'artist', 'show', 'episode'] as const; export type DeezerType = (typeof DEEZER_TYPES)[number]; /** deezer.com[/]// → widget target, or null. */ export function getDeezerEmbed(url: string): { type: DeezerType; id: string } | null { try { const u = new URL(url); if (u.hostname.replace(/^www\./, '') !== 'deezer.com') return null; const parts = u.pathname.replace(/^\/+/, '').split('/').filter(Boolean); // optional locale prefix (/en/, /us/, /fr/…) then / const idx = parts.findIndex((p) => (DEEZER_TYPES as readonly string[]).includes(p)); if (idx === -1 || !parts[idx + 1]) return null; const id = parts[idx + 1].split('?')[0]; if (!/^\d+$/.test(id)) return null; return { type: parts[idx] as DeezerType, id }; } catch { return null; } } /** * Deezer single track/episode players are compact; collections show a scrollable * tracklist and need room. Mirrors the Spotify sizing (152 / 352) — the widget * requests `tracklist=true`, so 352 keeps the list from being clipped the way a * shorter box would. */ export function deezerEmbedHeight(type: DeezerType): number { return type === 'track' || type === 'episode' ? 152 : 352; } // --- Embed-URL builders --------------------------------------------------- const enc = encodeURIComponent; export function buildVideoEmbedUrl( provider: 'youtube' | 'vimeo', id: string, hash?: string, ): string { if (provider === 'vimeo') { // dnt=1 = Do Not Track (no non-essential cookies); h={hash} required for unlisted. return `https://player.vimeo.com/video/${enc(id)}?autoplay=1&dnt=1${ hash ? `&h=${enc(hash)}` : '' }`; } // playsinline=1 keeps iOS Safari from forcing the native fullscreen player. return `https://www.youtube-nocookie.com/embed/${enc(id)}?autoplay=1&rel=0&playsinline=1`; } /** Spotify compact players (track/episode) are short; collections are taller. */ export function spotifyEmbedHeight(type: SpotifyType): number { return type === 'track' || type === 'episode' ? 152 : 352; } /** * Resolve any supported media URL to an embed spec, or null if none applies. * `host` is the current page hostname — required by Twitch's `parent` param. */ export function parseMediaEmbed(url: string, host: string): MediaEmbed | null { const shortsId = getYoutubeShortsId(url); if (shortsId) return { provider: 'youtube', kind: 'portrait', embedUrl: buildVideoEmbedUrl('youtube', shortsId), }; const ytId = getYouTubeVideoId(url); if (ytId) return { provider: 'youtube', kind: 'landscape', embedUrl: buildVideoEmbedUrl('youtube', ytId), }; const vimeo = getVimeoParts(url); if (vimeo) return { provider: 'vimeo', kind: 'landscape', embedUrl: buildVideoEmbedUrl('vimeo', vimeo.id, vimeo.hash), }; // NOTE: TikTok is handled by its own card (TikTokEmbedCard) — short "copy-link" // URLs (vm.tiktok.com, tiktok.com/t/…) need a client-side oEmbed lookup to // resolve the video id, which a sync parser can't do. See isTikTokLink below. const dmId = getDailymotionId(url); if (dmId) return { provider: 'dailymotion', kind: 'landscape', // geo.dailymotion.com is the current player; the old /embed/video path was // deprecated in Sept 2024. embedUrl: `https://geo.dailymotion.com/player.html?video=${enc(dmId)}&autoplay=1`, }; const streamableId = getStreamableId(url); if (streamableId) return { provider: 'streamable', kind: 'landscape', embedUrl: `https://streamable.com/e/${enc(streamableId)}?autoplay=1`, }; const twitch = getTwitchTarget(url); if (twitch) { const parent = enc(host); const embedUrl = twitch.type === 'clip' ? `https://clips.twitch.tv/embed?clip=${enc(twitch.value)}&parent=${parent}&autoplay=true` : `https://player.twitch.tv/?${twitch.type}=${enc(twitch.value)}&parent=${parent}&autoplay=true`; return { provider: 'twitch', kind: 'landscape', embedUrl }; } const spotify = getSpotifyEmbedTarget(url); if (spotify) return { provider: 'spotify', kind: 'audio', embedUrl: `https://open.spotify.com/embed/${spotify.type}/${enc(spotify.id)}`, height: spotifyEmbedHeight(spotify.type), }; if (isSoundCloudTrack(url)) return { provider: 'soundcloud', kind: 'audio', embedUrl: `https://w.soundcloud.com/player/?url=${enc( url, )}&color=%23ff5500&auto_play=true&show_comments=false&hide_related=true`, height: 166, }; const apple = getAppleMusicEmbed(url); if (apple) return { provider: 'applemusic', kind: apple.video ? 'landscape' : 'audio', embedUrl: apple.embedUrl, height: apple.video ? undefined : apple.height, }; const tidal = getTidalEmbed(url); if (tidal) return { provider: 'tidal', kind: tidal.kind, embedUrl: tidal.embedUrl, height: tidal.height }; const mixFeed = getMixcloudFeed(url); if (mixFeed) return { provider: 'mixcloud', kind: 'audio', embedUrl: `https://www.mixcloud.com/widget/iframe/?feed=${enc(mixFeed)}&light=0`, height: 120, }; const deezer = getDeezerEmbed(url); if (deezer) return { provider: 'deezer', kind: 'audio', embedUrl: `https://widget.deezer.com/widget/dark/${deezer.type}/${enc( deezer.id, )}?app_id=457142&autoplay=false&radius=true&tracklist=true`, height: deezerEmbedHeight(deezer.type), }; const insta = getInstagramEmbed(url); if (insta) return { provider: 'instagram', kind: 'rich', embedUrl: insta, height: 720 }; const reddit = getRedditPostEmbed(url); if (reddit) return { provider: 'reddit', kind: 'rich', embedUrl: reddit, height: 480 }; const bsky = getBlueskyEmbed(url); if (bsky) return { provider: 'bluesky', kind: 'rich', embedUrl: bsky, height: 600 }; const loomId = getLoomId(url); if (loomId) return { provider: 'loom', kind: 'landscape', embedUrl: `https://www.loom.com/embed/${enc(loomId)}`, }; const kick = getKickChannel(url); if (kick) return { provider: 'kick', kind: 'landscape', embedUrl: `https://player.kick.com/${enc(kick)}?autoplay=true`, }; return null; }