import type { RichContent } from "../../model/interfaces.ts"; import type { RichContentProvider } from "../rich-content-service.ts"; import { extractOgTag, fetchWithTimeout } from "../rich-content-service.ts"; function extractVideoId(url: string): string | null { try { const u = new URL(url); if (u.hostname === "youtu.be") { return u.pathname.slice(1).split("/")[0] || null; } if (u.hostname === "youtube.com" || u.hostname === "www.youtube.com") { if (u.pathname === "/watch" || u.pathname.startsWith("/watch?")) { return u.searchParams.get("v"); } if ( u.pathname.startsWith("/embed/") || u.pathname.startsWith("/shorts/") ) { return u.pathname.split("/")[2] || null; } } } catch { // invalid URL } return null; } function extractPlaylistId(url: string): string | null { try { return new URL(url).searchParams.get("list"); } catch { return null; } } /** Matches /channel/UC…, /@handle, /c/name, /user/name */ function extractChannelPath(url: string): string | null { try { const u = new URL(url); if (u.hostname === "youtube.com" || u.hostname === "www.youtube.com") { if ( u.pathname.startsWith("/channel/") || u.pathname.startsWith("/@") || u.pathname.startsWith("/c/") || u.pathname.startsWith("/user/") ) { return u.pathname; } } } catch { // invalid URL } return null; } async function fetchOEmbed( url: string, ): Promise<{ title?: string; thumbnailUrl?: string }> { try { const oembedUrl = `https://www.youtube.com/oembed?url=${encodeURIComponent(url)}&format=json`; const res = await fetchWithTimeout(oembedUrl); if (res.ok) { const data = await res.json() as { title?: string; thumbnail_url?: string; }; return { title: data.title, thumbnailUrl: data.thumbnail_url }; } } catch { // oembed failed — carry on } return {}; } export const youtubeProvider: RichContentProvider = { name: "youtube", matches(url: string): boolean { return ( extractVideoId(url) !== null || extractPlaylistId(url) !== null || extractChannelPath(url) !== null ); }, async fetch(url: string): Promise { const videoId = extractVideoId(url); const listId = extractPlaylistId(url); const channelPath = extractChannelPath(url); // ── Channel ─────────────────────────────────────────────────────────────── if (channelPath && !videoId) { // oEmbed doesn't support channel URLs — scrape og:image from the page instead let title: string | undefined; let thumbnailUrl: string | undefined; try { const res = await fetchWithTimeout(url); if (res.ok) { const html = await res.text(); title = extractOgTag(html, "title"); thumbnailUrl = extractOgTag(html, "image"); } } catch { // scrape failed — carry on with undefined values } return { type: "youtube", siteName: "YouTube", url, title, thumbnailUrl, // channels are not embeddable as a player }; } // ── Playlist (no specific video) ────────────────────────────────────────── if (listId && !videoId) { const { title, thumbnailUrl } = await fetchOEmbed(url); return { type: "youtube", siteName: "YouTube", url, title, thumbnailUrl, embedUrl: `https://www.youtube.com/embed/videoseries?list=${listId}&rel=0`, }; } // ── Video (with optional playlist context) ──────────────────────────────── const thumbnailUrl = `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`; const { title } = await fetchOEmbed(url); const embedParams = new URLSearchParams({ rel: "0" }); if (listId) embedParams.set("list", listId); return { type: "youtube", siteName: "YouTube", url, videoId: videoId!, title, thumbnailUrl, embedUrl: `https://www.youtube.com/embed/${videoId}?${embedParams}`, }; }, };