const YOUTUBE_VIDEO_ID = /^[A-Za-z0-9_-]{11}$/; const YOUTUBE_HOSTS = new Set([ "youtube.com", "www.youtube.com", "m.youtube.com", "music.youtube.com", ]); export interface YouTubeVideo { videoId: string; watchUrl: string; embedUrl: string; } export function parseYouTubeUrl(value: string): YouTubeVideo | null { let url: URL; try { url = new URL(value); } catch { return null; } if (url.protocol !== "https:" && url.protocol !== "http:") return null; const hostname = url.hostname.toLowerCase(); let videoId: string | null = null; if (hostname === "youtu.be") { videoId = url.pathname.split("/").filter(Boolean)[0] ?? null; } else if (YOUTUBE_HOSTS.has(hostname)) { if (url.pathname === "/watch") { videoId = url.searchParams.get("v"); } else { const [kind, id] = url.pathname.split("/").filter(Boolean); if (["embed", "live", "shorts"].includes(kind)) videoId = id ?? null; } } if (!videoId || !YOUTUBE_VIDEO_ID.test(videoId)) return null; return { videoId, watchUrl: `https://www.youtube.com/watch?v=${videoId}`, embedUrl: `https://www.youtube-nocookie.com/embed/${videoId}`, }; }