// Device hints only fill gaps; extractor metadata stays authoritative. export function mergeMissing(existing = {}, incoming = {}) { const out = { ...existing }; for (const [key, value] of Object.entries(incoming)) { const old = out[key]; if (old == null || old === '' || old === 0 || (key === 'title' && (old === '(untitled)' || old === out.id))) out[key] = value; } if (!existing.channel && (existing.uploader || existing.artist)) out.channel = existing.uploader || existing.artist; return out; } export function validateMeta(id, body) { if (!/^[A-Za-z0-9_-]{11}$/.test(id) || !body || typeof body !== 'object' || Array.isArray(body) || (body.id != null && body.id !== id) || body.custom || body.upload) throw new Error('Invalid YouTube video'); const clean = (value, required = false) => { if (value == null && !required) return ''; if (typeof value !== 'string' || value.length > 300 || /[\x00-\x08\x0b\x0c\x0e-\x1f]/.test(value)) throw new Error('Invalid title or artist'); const text = value.trim(); if (required && (!text || text === '(untitled)' || text === id)) throw new Error('A video title is required'); return text; }; const title = clean(body.title, true), channel = clean(body.channel || body.artist); const duration = body.duration ?? 0; if (typeof duration !== 'number' || !Number.isFinite(duration) || duration < 0 || duration > 86400) throw new Error('Invalid duration'); let thumbnail = `https://i.ytimg.com/vi/${id}/hqdefault.jpg`; if (body.thumbnail) { if (typeof body.thumbnail !== 'string' || body.thumbnail.length > 2048) throw new Error('Invalid thumbnail'); if (body.thumbnail !== `/api/catalog/${id}/thumbnail`) { let url; try { url = new URL(body.thumbnail); } catch { throw new Error('Invalid thumbnail'); } if (url.protocol !== 'https:' || !/^(?:i|i\d)\.ytimg\.com$/.test(url.hostname) || url.port || url.username || url.password) throw new Error('Invalid thumbnail'); thumbnail = url.href; } } return { id, title, channel, duration, thumbnail }; }