Générateur de code structuré YouTube (local)

⚠️ Ne publie pas ce fichier en ligne : la clé API serait visible. Utilisation locale uniquement.
`.trim(); } function openModal(){ backdrop.style.display = 'flex'; } function closeModal(){ backdrop.style.display = 'none'; } closeBtn.addEventListener('click', closeModal); backdrop.addEventListener('click', (e)=>{ if(e.target === backdrop) closeModal(); }); openBtn.addEventListener('click', openModal); function renderGrid(items){ countPill.textContent = `${items.length} résultat(s)`; grid.innerHTML = items.map(v => `
${escAttr(v.title)}
${escHtml(v.title)}
Chaîne : ${escHtml(v.channel || '')}
Date : ${escHtml(formatFr(v.publishedAt) || String(v.publishedAt||'').slice(0,10))}
${v.views ? escHtml(formatViews(v.views) + ' vues') : ''}
`).join(''); grid.querySelectorAll('button[data-id]').forEach(btn=>{ btn.addEventListener('click', ()=>{ const id = btn.getAttribute('data-id'); const v = items.find(x=>x.id===id) || lastResults.find(x=>x.id===id); if(!v) return; out.value = buildCode(v); copyBtn.disabled = false; status.textContent = "Code généré ✅"; closeModal(); }); }); } async function ytSearch(){ status.textContent = ''; grid.innerHTML = ''; lastResults = []; lastResultsByRel = []; openBtn.disabled = true; copyBtn.disabled = true; out.value = ''; const apiKey = elKey.value.trim(); const q = elQ.value.trim(); if(!apiKey){ status.textContent = "Ajoute ta clé API."; return; } if(!q){ status.textContent = "Tape une requête."; return; } searchBtn.disabled = true; searchBtn.textContent = "Recherche…"; try{ const sUrl = new URL('https://www.googleapis.com/youtube/v3/search'); sUrl.searchParams.set('part','snippet'); sUrl.searchParams.set('type','video'); sUrl.searchParams.set('maxResults','20'); sUrl.searchParams.set('q', q); sUrl.searchParams.set('safeSearch','strict'); sUrl.searchParams.set('relevanceLanguage','fr'); sUrl.searchParams.set('key', apiKey); const sResp = await fetch(sUrl.toString()); const sData = await sResp.json(); if(!sResp.ok) throw new Error(sData?.error?.message || 'Erreur search.list'); const ids = (sData.items || []).map(it => it?.id?.videoId).filter(Boolean); if(!ids.length){ status.textContent = "Aucun résultat."; return; } const vUrl = new URL('https://www.googleapis.com/youtube/v3/videos'); vUrl.searchParams.set('part','snippet,statistics'); vUrl.searchParams.set('id', ids.join(',')); vUrl.searchParams.set('key', apiKey); const vResp = await fetch(vUrl.toString()); const vData = await vResp.json(); if(!vResp.ok) throw new Error(vData?.error?.message || 'Erreur videos.list'); const map = new Map(); (vData.items || []).forEach(v => map.set(v.id, v)); const results = ids.map(id => { const v = map.get(id); const sn = v?.snippet || {}; const thumbs = sn.thumbnails || {}; const thumb = thumbs.high?.url || thumbs.medium?.url || thumbs.default?.url || `https://i.ytimg.com/vi/${id}/hqdefault.jpg`; return { id, title: sn.title || '', channel: sn.channelTitle || '', publishedAt: sn.publishedAt || '', thumb, views: Number(v?.statistics?.viewCount || 0) }; }); lastResultsByRel = results.slice(); lastResults = results.slice(); openBtn.disabled = false; renderGrid(lastResults); openModal(); status.textContent = "Résultats chargés ✅"; }catch(err){ status.textContent = "Erreur : " + (err?.message || err); }finally{ searchBtn.disabled = false; searchBtn.textContent = "Rechercher"; } } searchBtn.addEventListener('click', ytSearch); sortRel.addEventListener('click', ()=>{ if(!lastResultsByRel.length) return; lastResults = lastResultsByRel.slice(); renderGrid(lastResults); }); sortDate.addEventListener('click', ()=>{ if(!lastResults.length) return; const sorted = lastResults.slice().sort((a,b)=> new Date(b.publishedAt||0) - new Date(a.publishedAt||0)); lastResults = sorted; renderGrid(lastResults); }); copyBtn.addEventListener('click', async ()=>{ try{ await navigator.clipboard.writeText(out.value || ''); status.textContent = "Copié ✅"; }catch(e){ out.focus(); out.select(); document.execCommand('copy'); status.textContent = "Copié ✅"; } });