// Keyless YouTube metadata: id parsing plus title lookup via the public oEmbed // endpoint. Durations are not available here — they come from the probe player // in the player frontend and arrive over the socket later. const ID_PATTERN = /^[A-Za-z0-9_-]{11}$/; /** * Pull the 11-character video id out of anything a user might paste. * Returns null when the input is not a recognisable YouTube video. */ export function parseVideoId(input) { const raw = String(input || '').trim(); if (!raw) return null; // A bare id pasted on its own. if (ID_PATTERN.test(raw)) return raw; let url; try { url = new URL(raw.includes('://') ? raw : `https://${raw}`); } catch { return null; } const host = url.hostname.replace(/^www\./, ''); const segments = url.pathname.split('/').filter(Boolean); if (host === 'youtu.be') { return ID_PATTERN.test(segments[0]) ? segments[0] : null; } if (host === 'youtube.com' || host === 'm.youtube.com' || host === 'music.youtube.com' || host === 'youtube-nocookie.com') { const v = url.searchParams.get('v'); if (v && ID_PATTERN.test(v)) return v; // /shorts/ID, /embed/ID, /live/ID, /v/ID if (['shorts', 'embed', 'live', 'v'].includes(segments[0])) { return ID_PATTERN.test(segments[1]) ? segments[1] : null; } } return null; } /** * Fetch the video title. Falls back to the id so a failed lookup still leaves * a usable queue entry rather than blocking the add. */ export async function fetchTitle(videoId) { const endpoint = `https://www.youtube.com/oembed?url=${encodeURIComponent( `https://www.youtube.com/watch?v=${videoId}`, )}&format=json`; try { const res = await fetch(endpoint, { signal: AbortSignal.timeout(8000) }); if (!res.ok) throw new Error(`oembed ${res.status}`); const data = await res.json(); return data.title || videoId; } catch (err) { console.warn(`[youtube] title lookup failed for ${videoId}: ${err.message}`); return videoId; } }