/** * Extract a YouTube `videoId` from a URL. * * Supports: * - youtube.com/watch?v=ID * - youtu.be/ID * - youtube.com/shorts/ID * - youtube.com/embed/ID * - youtube.com/v/ID * - m.youtube.com/* mirrors * * Returns `null` if the URL does not match. */ export function extractYouTubeId(input: string): string | null { let url: URL; try { url = new URL(input); } catch { return null; } const host = url.hostname.replace(/^www\.|^m\./, ''); if (host === 'youtu.be') { const id = url.pathname.replace(/^\//, '').split('/')[0]; return isValidId(id) ? id : null; } if (host === 'youtube.com' || host === 'youtube-nocookie.com') { if (url.pathname === '/watch') { const v = url.searchParams.get('v'); return v && isValidId(v) ? v : null; } const m = url.pathname.match(/^\/(?:shorts|embed|v|live)\/([^/?#]+)/); if (m) { return isValidId(m[1]) ? m[1] : null; } } return null; } /** Parse the YouTube `?t=` / `&t=` start-time param (`90`, `90s`, `1m30s`). */ export function parseYouTubeStartTime(input: string | null): number | undefined { if (!input) return undefined; // Pure seconds. if (/^\d+s?$/.test(input)) { const n = parseInt(input, 10); return Number.isFinite(n) && n > 0 ? n : undefined; } // `1h2m3s` / `2m30s` / `45s`. const match = input.match(/^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s?)?$/); if (!match) return undefined; const [, h, m, s] = match; const total = (h ? parseInt(h, 10) * 3600 : 0) + (m ? parseInt(m, 10) * 60 : 0) + (s ? parseInt(s, 10) : 0); return total > 0 ? total : undefined; } function isValidId(id: string | undefined): id is string { return !!id && /^[\w-]{6,}$/.test(id); }