⚠️ Ne publie pas ce fichier en ligne : la clé API serait visible. Utilisation locale uniquement.
${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é ✅";
}
});