document.addEventListener('DOMContentLoaded', () => { // --- INIT ICONS --- if (typeof lucide !== 'undefined') { lucide.createIcons(); } // --- UTILS --- const dom = (id) => document.getElementById(id); const show = (el) => el && el.classList.remove('hidden'); const hide = (el) => el && el.classList.add('hidden'); const showToast = (msg) => { const t = dom('toast'); if (!t) return; t.textContent = msg; show(t); setTimeout(() => hide(t), 3000); }; window.showToast = showToast; const showConfirmModal = (title, message, options = {}) => { return new Promise((resolve) => { const confirmText = options.confirmText || 'Confirmer'; const cancelText = options.cancelText || 'Annuler'; const isDestructive = options.isDestructive !== false; const overlay = document.createElement('div'); overlay.className = 'modal-overlay'; overlay.style.zIndex = '30000'; overlay.innerHTML = ` `; document.body.appendChild(overlay); if (window.lucide) { window.lucide.createIcons({ root: overlay }); } const cleanUp = (value) => { overlay.style.opacity = '0'; overlay.style.transition = 'opacity 0.2s ease'; setTimeout(() => { overlay.remove(); }, 200); resolve(value); }; overlay.querySelector('#confirm-yes-btn').onclick = () => cleanUp(true); overlay.querySelector('#confirm-no-btn').onclick = () => cleanUp(false); overlay.onclick = (e) => { if (e.target === overlay) cleanUp(false); }; }); }; const updateSiteStatusUI = (isOffline, message) => { let banner = dom('offline-banner'); if (!banner) { banner = document.createElement('div'); banner.id = 'offline-banner'; banner.style = "background: #e50914; color: white; text-align: center; padding: 10px; font-weight: 700; position: sticky; top: 0; z-index: 9999; display: none; font-size: 0.9rem;"; document.body.prepend(banner); } if (isOffline) { banner.textContent = `⚠️ SITE SOURCE INDISPONIBLE : ${message || 'Vérification en cours...'}`; banner.style.display = 'block'; } else { banner.style.display = 'none'; } }; const apiCall = async (endpoint, method = 'GET', body = null) => { if (!endpoint.startsWith('/api')) { endpoint = endpoint.startsWith('/') ? `/api${endpoint}` : `/api/${endpoint}`; } try { const opts = { method, headers: { 'Content-Type': 'application/json' } }; if (body) opts.body = JSON.stringify(body); const res = await fetch(endpoint, opts); const text = await res.text(); if (!res.ok) { let err = `Erreur ${res.status}`; try { err = JSON.parse(text).error || err; } catch (e) { } throw new Error(err); } return text ? JSON.parse(text) : {}; } catch (e) { console.error(`API ${endpoint}:`, e); throw e; } }; // --- STATE & LOOPS MANAGER --- const state = { downloadInterval: null, trendingData: { films: [], series: [], recent: [] }, activeSources: [], availableSources: [], // Dynamically populated from /status sourceLabels: {}, }; // --- GESTION INTELLIGENTE DES BOUCLES --- const getPref = (key, defaultValue) => { if (state.currentUser && state.currentUser.preferences && state.currentUser.preferences[key] !== undefined) { return state.currentUser.preferences[key]; } const local = localStorage.getItem(key); if (local !== null) return local === 'true' ? true : (local === 'false' ? false : local); return defaultValue; }; const setPref = async (key, value) => { localStorage.setItem(key, value); if (state.currentUser) { if (!state.currentUser.preferences) state.currentUser.preferences = {}; state.currentUser.preferences[key] = value; } try { await apiCall('/api/preferences', 'POST', { key, value }); } catch (e) { console.error('Erreur sauvegarde pref', e); } }; // 1. Gestion Téléchargements (Actif seulement sur l'onglet) const startDownloadLoop = () => { if (state.downloadInterval) clearInterval(state.downloadInterval); loadDownloads(); // Appel immédiat state.downloadInterval = setInterval(loadDownloads, 5000); // Mise à jour toutes les 5s console.log("Flux Téléchargement : ACTIVÉ"); }; // La boucle des téléchargements a été déplacée à la fin du fichier. // --- LOGIN --- (handled in login.ejs inline script, not here) const checkSession = async () => { try { const res = await apiCall('/check-session'); if (res.isLoggedIn) { // Stocker les infos user pour usage dans l'app state.currentUser = res.user || null; if (dom('login-form')) { const allowedPages = ['/trending', '/recent', '/search', '/downloads', '/manual', '/settings']; const savedPage = localStorage.getItem('defaultPage') || '/trending'; window.location.href = allowedPages.includes(savedPage) ? savedPage : '/trending'; } else { initApp(); } } else { if (!dom('login-form')) { window.location.href = '/login'; } } } catch (e) { } }; const initApp = async () => { hide(dom('login-overlay')); show(dom('app-container')); const getCookie = (name) => { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) return parts.pop().split(';').shift(); return null; }; const showMustChangePasswordOverlay = () => { const existing = document.getElementById('change-password-overlay'); if (existing) return; const overlay = document.createElement('div'); overlay.id = 'change-password-overlay'; overlay.innerHTML = `

Sécurité requise

Votre compte utilise un mot de passe temporaire ou réinitialisé. Vous devez définir un nouveau mot de passe sécurisé pour continuer.

Exigences du mot de passe :
`; document.body.appendChild(overlay); if (window.lucide) { window.lucide.createIcons({ root: overlay }); } const newPwInput = overlay.querySelector('#force-new-pw'); const confirmPwInput = overlay.querySelector('#force-confirm-pw'); const form = overlay.querySelector('#force-change-pw-form'); const errorDiv = overlay.querySelector('#force-pw-error'); const submitBtn = overlay.querySelector('#btn-force-pw-submit'); const logoutBtn = overlay.querySelector('#btn-force-logout'); const validateInputs = () => { if (!window.AuthHelpers) return; const val = newPwInput.value; const confirmVal = confirmPwInput.value; const statuses = AuthHelpers.validateComplexity(val, confirmVal); AuthHelpers.updateRequirementsUI(overlay, statuses); submitBtn.disabled = !statuses.allValid; }; newPwInput.addEventListener('input', validateInputs); confirmPwInput.addEventListener('input', validateInputs); if (window.AuthHelpers) { AuthHelpers.initPasswordToggles(overlay); } logoutBtn.onclick = async () => { try { await apiCall('/logout', 'POST'); window.location.href = '/login'; } catch (e) { window.location.href = '/login'; } }; form.onsubmit = async (e) => { e.preventDefault(); errorDiv.style.display = 'none'; errorDiv.textContent = ''; try { const res = await apiCall('/change-password', 'POST', { newPassword: newPwInput.value }); if (res.success) { showToast('✓ Mot de passe mis à jour avec succès !'); overlay.remove(); if (state.currentUser) { state.currentUser.mustChangePassword = false; } } } catch (err) { errorDiv.textContent = err.message || "Une erreur est survenue."; errorDiv.style.display = 'block'; } }; }; // --- Forced Password Change Checker --- if (state.currentUser && state.currentUser.mustChangePassword) { showMustChangePasswordOverlay(); return; // Bloquer la suite de l'initialisation de l'application } // --- Auth Error Toast Checker --- const authError = getCookie('authError'); if (authError) { // Delete the cookie document.cookie = "authError=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; showToast(decodeURIComponent(authError).replace(/\+/g, ' ')); } // --- JDownloader Toggle --- const toggleJd = document.getElementById('toggle-jd'); if (toggleJd) { toggleJd.checked = getPref('useJD', false); toggleJd.addEventListener('change', (e) => setPref('useJD', e.target.checked)); } // --- Default Preferred Page --- const selectDefaultPage = document.getElementById('select-default-page'); if (selectDefaultPage) { const setCookie = (name, value, days = 365) => { const date = new Date(); date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); document.cookie = `${name}=${value}; expires=${date.toUTCString()}; path=/`; }; const savedPage = getPref('defaultPage', '/trending'); selectDefaultPage.value = savedPage; // Sync cookie setCookie('defaultPage', savedPage); selectDefaultPage.addEventListener('change', (e) => { const newPage = e.target.value; setCookie('defaultPage', newPage); setPref('defaultPage', newPage); showToast('Page par défaut mise à jour'); }); } // --- Source Detection & Dynamic Options --- try { const statusData = await apiCall('/status'); state.activeSources = statusData.activeSources || []; state.availableSources = statusData.availableSources || []; state.sourceLabels = statusData.sourceLabels || {}; renderSourcesUI(); updateLocalDbFiltersVisibility(); } catch(e) { console.error('Erreur détection source:', e); } loadTrending(); lucide.createIcons(); document.querySelectorAll('input[name="trending-type"]').forEach(radio => { radio.addEventListener('change', renderTrending); }); // --- HEARTBEAT : Détection source down + Refresh Tendances --- let wasOffline = false; const checkSourceStatus = async () => { try { const s = await apiCall('/status'); updateSiteStatusUI(s.isOffline, s.message); state.activeSources = s.activeSources || []; state.sourceLabels = s.sourceLabels || {}; const searchInput = dom('search-input'); const searchBtn = dom('btn-search-trigger'); if (searchInput) searchInput.disabled = s.isOffline; if (searchBtn) searchBtn.disabled = s.isOffline; if (!s.isOffline && (wasOffline || (!state.trendingData.films.length && !state.trendingData.series.length))) { console.log('[Heartbeat] Site source en ligne, rechargement des tendances...'); loadTrending(); } wasOffline = s.isOffline; updateLocalDbFiltersVisibility(); } catch (e) { wasOffline = true; updateSiteStatusUI(true, 'Connexion au serveur perdue...'); } }; setInterval(checkSourceStatus, 30000); // ========================= ADMIN USER MANAGEMENT ========================= const adminUsersList = dom('admin-users-list'); if (adminUsersList && state.currentUser && state.currentUser.role === 'admin') { const loadAdminUsers = async () => { try { const users = await apiCall('/admin/users'); adminUsersList.innerHTML = ''; users.forEach(user => { const isCurrentUser = state.currentUser && state.currentUser.username === user.username; const roleColor = user.role === 'admin' ? 'var(--primary)' : '#22c55e'; const roleLabel = user.role === 'admin' ? 'Admin' : 'Utilisateur'; const item = document.createElement('div'); item.className = 'admin-user-item'; item.style.cssText = 'display: flex; align-items: center; justify-content: space-between; padding: 12px 14px; background: var(--bg-main); border: 1px solid var(--border); border-radius: 10px;'; item.innerHTML = `
${user.username}${isCurrentUser ? ' (vous)' : ''}
${roleLabel}
${!isCurrentUser ? `` : ''}
`; adminUsersList.appendChild(item); }); // Re-init Lucide icons for the newly added elements if (typeof lucide !== 'undefined') lucide.createIcons(); // Bind delete buttons document.querySelectorAll('.admin-delete-user-btn').forEach(btn => { btn.addEventListener('click', async () => { const userId = btn.dataset.id; const userName = btn.dataset.name; const confirmed = await showConfirmModal( "Supprimer l'utilisateur", `Êtes-vous sûr de vouloir supprimer définitivement l'utilisateur "${userName}" ? Cette action est irréversible.`, { confirmText: 'Supprimer', cancelText: 'Annuler', isDestructive: true } ); if (!confirmed) return; try { await apiCall(`/admin/users/${userId}`, 'DELETE'); showToast(`Utilisateur "${userName}" supprimé.`); loadAdminUsers(); } catch (err) { showToast('Erreur: ' + err.message); } }); }); // Bind reset password buttons document.querySelectorAll('.admin-reset-pw-btn').forEach(btn => { btn.addEventListener('click', async () => { const userId = btn.dataset.id; const userName = btn.dataset.name; const confirmed = await showConfirmModal( "Réinitialiser le mot de passe", `Voulez-vous réinitialiser le mot de passe de l'utilisateur "${userName}" ? Un nouveau mot de passe aléatoire temporaire sera généré.`, { confirmText: 'Réinitialiser', cancelText: 'Annuler', isDestructive: false } ); if (!confirmed) return; try { const result = await apiCall(`/admin/users/${userId}/reset-password`, 'POST'); if (result.generatedPassword) { showPasswordModal(result.generatedPassword); } } catch (err) { showToast('Erreur: ' + err.message); } }); }); } catch (err) { console.error('[Admin] Erreur chargement users:', err); } }; // Show password modal helper const showPasswordModal = (password) => { const modal = dom('admin-password-modal'); const pwDisplay = dom('admin-generated-password'); if (!modal || !pwDisplay) return; pwDisplay.textContent = password; show(modal); modal.style.display = ''; // Clear inline styles if any const copyBtn = dom('admin-copy-password-btn'); const closeBtn = dom('admin-close-modal-btn'); const closeModal = () => { hide(modal); }; if (copyBtn) { copyBtn.onclick = () => { navigator.clipboard.writeText(password).then(() => { copyBtn.textContent = '✓ Copié !'; setTimeout(() => { copyBtn.textContent = 'Copier le mot de passe'; }, 2000); }).catch(() => { showToast('Erreur clipboard. Sélectionnez et copiez manuellement.'); }); }; } if (closeBtn) closeBtn.onclick = closeModal; }; // Add user button const addUserBtn = dom('admin-add-user-btn'); if (addUserBtn) { addUserBtn.addEventListener('click', async () => { const usernameInput = dom('admin-new-username'); const roleSelect = dom('admin-new-role'); if (!usernameInput) return; const username = usernameInput.value.trim(); if (!username) { showToast("Nom d'utilisateur requis."); return; } try { const result = await apiCall('/admin/users', 'POST', { username: username, role: roleSelect ? roleSelect.value : 'user' }); if (result.success && result.generatedPassword) { usernameInput.value = ''; showPasswordModal(result.generatedPassword); loadAdminUsers(); } } catch (err) { showToast('Erreur: ' + err.message); } }); } // Initial load loadAdminUsers(); } }; // --- SOURCE MANAGEMENT --- function renderSourcesUI() { const container = dom('sources-sortable-container'); if (!container) return; container.innerHTML = ''; // Sort available sources so that active ones appear first in their priority order const allSources = [...state.activeSources]; state.availableSources.forEach(sourceName => { if (!allSources.includes(sourceName)) { allSources.push(sourceName); } }); const updateWarning = () => { const warningEl = dom('primary-source-warning'); if (warningEl) { // Find first active source const firstActive = state.activeSources[0]; if (firstActive === 'hydracker') { warningEl.style.display = 'flex'; } else { warningEl.style.display = 'none'; } } }; allSources.forEach(sourceName => { const item = document.createElement('div'); item.className = 'sortable-source-item'; item.setAttribute('draggable', 'true'); item.dataset.name = sourceName; const isActive = state.activeSources.includes(sourceName); const isPrimary = state.activeSources[0] === sourceName; const displayName = state.sourceLabels[sourceName] || sourceName.toUpperCase(); item.innerHTML = `
${displayName} ${isPrimary ? 'Principale' : ''}
`; // Drag and drop event listeners item.addEventListener('dragstart', (e) => { item.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; }); item.addEventListener('dragend', async () => { item.classList.remove('dragging'); // Rebuild the priority order from the DOM const items = Array.from(container.querySelectorAll('.sortable-source-item')); const newOrderedSources = items.map(el => el.dataset.name); // Extract active sources in their new order const newActiveSources = newOrderedSources.filter(name => { const checkbox = container.querySelector(`[data-name="${name}"] input[type="checkbox"]`); return checkbox && checkbox.checked; }); await updateActiveSources(newActiveSources); }); // Toggle switch logic const checkbox = item.querySelector('input[type="checkbox"]'); checkbox.addEventListener('change', async () => { // To maintain order, we rebuild the list order from the DOM const items = Array.from(container.querySelectorAll('.sortable-source-item')); const newOrderedSources = items.map(el => el.dataset.name); const newActiveSources = newOrderedSources.filter(name => { const cb = container.querySelector(`[data-name="${name}"] input[type="checkbox"]`); return cb && cb.checked; }); await updateActiveSources(newActiveSources); }); container.appendChild(item); }); // Dragover logic on container to allow real-time visual sorting container.addEventListener('dragover', (e) => { e.preventDefault(); const draggingEl = container.querySelector('.dragging'); if (!draggingEl) return; const siblings = Array.from(container.querySelectorAll('.sortable-source-item:not(.dragging)')); const nextSibling = siblings.find(sibling => { const rect = sibling.getBoundingClientRect(); const boxCenter = rect.top + rect.height / 2; return e.clientY < boxCenter; }); if (nextSibling) { container.insertBefore(draggingEl, nextSibling); } else { container.appendChild(draggingEl); } }); updateWarning(); lucide.createIcons(); // Reactivate icons for newly injected HTML } // --- UI UPDATES --- function updateLocalDbFiltersVisibility() { const hasLocalDb = state.activeSources.includes('localdb'); document.querySelectorAll('.localdb-filter').forEach(el => { if (hasLocalDb) { el.classList.remove('hidden'); } else { el.classList.add('hidden'); // If a hidden radio is checked, default back to 'film' const radio = el.querySelector('input[type="radio"]'); if (radio && radio.checked) { const defaultRadio = document.querySelector('input[name="search-type"][value="film"]'); if (defaultRadio) defaultRadio.checked = true; } } }); } async function updateActiveSources(sources) { try { const res = await apiCall('/set-sources', 'POST', { sources }); state.activeSources = res.activeSources; renderSourcesUI(); updateLocalDbFiltersVisibility(); showToast('Sources mises à jour'); loadTrending(); } catch(e) { showToast('Erreur sources: ' + e.message); renderSourcesUI(); // Revert UI } } // --- DOWNLOADS (LIST) --- const loadDownloads = async () => { const list = dom('downloads-list'); if (!list) return; try { const data = await apiCall('/download-status'); list.innerHTML = ''; if (!data || !data.length) { list.innerHTML = `

Aucun téléchargement actif

`; lucide.createIcons(); return; } data.forEach(dl => { const item = document.createElement('div'); item.className = 'dl-card'; const isDone = dl.percent >= 100; const barColor = isDone ? 'var(--success)' : 'var(--accent)'; item.innerHTML = `
${dl.name} ${Math.round(dl.percent)}%
${isDone ? 'Terminé' : 'Téléchargement en cours...'}
`; // Attach delete handler const deleteBtn = item.querySelector('.btn-jd-delete'); deleteBtn.onmouseover = () => { deleteBtn.style.color = '#ef4444'; deleteBtn.style.background = 'rgba(239,68,68,0.1)'; }; deleteBtn.onmouseout = () => { deleteBtn.style.color = 'var(--text-sec)'; deleteBtn.style.background = 'none'; }; deleteBtn.onclick = async (e) => { e.stopPropagation(); const confirmed = await showConfirmModal( "Supprimer de JDownloader", `Êtes-vous sûr de vouloir supprimer "${dl.name}" de JDownloader ?`, { confirmText: 'Supprimer', cancelText: 'Annuler', isDestructive: true } ); if (!confirmed) return; try { await apiCall('/jd/remove-link', 'POST', { linkIds: [dl.uuid] }); showToast(`🗑️ ${dl.name} supprimé`); loadDownloads(); } catch (err) { showToast('Erreur : ' + err.message); } }; list.appendChild(item); }); lucide.createIcons(); } catch (e) { } }; const btnRefreshDownloads = dom('btn-refresh-downloads'); if (btnRefreshDownloads) { btnRefreshDownloads.onclick = loadDownloads; } // --- UTILS: BLOCKING LOADER & STATE HELPERS --- const toggleBlockingLoader = (show, msg = "Traitement en cours...") => { let loader = document.getElementById('blocking-loader'); if (!loader) { loader = document.createElement('div'); loader.id = 'blocking-loader'; loader.className = 'hidden'; loader.innerHTML = `

`; document.body.appendChild(loader); } if (show) { document.getElementById('blocking-msg').textContent = msg; loader.classList.remove('hidden'); } else { loader.classList.add('hidden'); } }; // --- PROXY IMAGE HELPER --- // Passe les URLs HTTPS des posters par le proxy serveur pour éviter le blocage mixed-content const proxyImageUrl = (url) => { if (!url) return ''; // Si l'image est déjà locale ou en HTTP sur notre domaine, pas besoin de proxy if (url.startsWith('/') || url.startsWith('data:')) return url; // Proxy les URLs HTTPS via le serveur if (url.startsWith('https://')) { return `/api/proxy-image?url=${encodeURIComponent(url)}`; } return url; }; // --- TRENDING & CARDS --- const createCard = (movie) => { const div = document.createElement('div'); div.className = 'card'; const posterSrc = proxyImageUrl(movie.image); // Build subtitle: quality + lang for ZT, year for Hydracker let subtitle = movie.year || ''; let cleanedTitle = movie.title; // Clean ZT titles: "Show Name - Saison X [Quality]" -> "Show Name - Saison X" if (movie.source === 'zt') { const parts = []; // Extract quality info from title if present: "Title [1080p]" -> "Title" const titleQualityMatch = cleanedTitle.match(/(.*)\s+\[([^\]]+)\]$/); if (titleQualityMatch) { cleanedTitle = titleQualityMatch[1].trim(); const qualityFromTitle = titleQualityMatch[2].trim(); if (!movie.quality) movie.quality = qualityFromTitle; } if (movie.quality) parts.push(movie.quality); if (movie.lang) parts.push(movie.lang); subtitle = parts.join(' — ') || ''; } const typeBadge = movie.type === 'series' || movie.type === 'anime' ? `${movie.type === 'anime' ? 'Anime' : 'Série'}` : ''; div.innerHTML = `
${cleanedTitle}
${movie.source}
${cleanedTitle}
${typeBadge} ${subtitle}
`; div.addEventListener('click', () => handleSelection(movie)); return div; }; const renderTrending = () => { const grid = dom('trending-grid'); if (!grid) return; const type = document.querySelector('input[name="trending-type"]:checked').value; const itemsToDisplay = type === 'film' ? state.trendingData.films : state.trendingData.series; grid.innerHTML = ''; if (!itemsToDisplay || !itemsToDisplay.length) { if (dom('offline-banner') && dom('offline-banner').textContent.includes('Aucune source configurée')) { grid.innerHTML = `

Aucune source configurée

Pour afficher du contenu, activez au moins une source dans les paramètres. ZT est recommandé par défaut.

`; lucide.createIcons(); } else { grid.innerHTML = '

Aucune tendance trouvée.

'; } return; } itemsToDisplay.forEach(m => grid.appendChild(createCard(m))); }; const renderRecent = () => { const grid = dom('recent-grid'); if (!grid) return; const itemsToDisplay = state.trendingData.recent || []; grid.innerHTML = ''; if (!itemsToDisplay || !itemsToDisplay.length) { grid.innerHTML = '

Aucun ajout récent trouvé.

'; return; } itemsToDisplay.forEach(m => grid.appendChild(createCard(m))); }; const loadTrending = async () => { const trendingGrid = dom('trending-grid'); const recentGrid = dom('recent-grid'); if (!trendingGrid && !recentGrid) return; try { const data = await apiCall('/api/trending'); // Mise à jour de la bannière si le site est down updateSiteStatusUI(data.isSiteOffline, data.siteOfflineMessage); state.trendingData = data; // Si le serveur n'a pas encore fini de scrapper au démarrage if (data.films.length === 0 && data.series.length === 0 && (!data.recent || data.recent.length === 0)) { if (trendingGrid) { trendingGrid.innerHTML = `

Le serveur prépare les tendances, un instant...

`; } if (recentGrid) { recentGrid.innerHTML = `

Le serveur prépare les ajouts récents, un instant...

`; } setTimeout(loadTrending, 3000); // On réessaye dans 3 secondes return; } renderTrending(); renderRecent(); } catch (e) { console.error("Erreur tendances/récents:", e); if (trendingGrid) { trendingGrid.innerHTML = '

⚠️ Erreur de liaison avec le serveur.

'; } if (recentGrid) { recentGrid.innerHTML = '

⚠️ Erreur de liaison avec le serveur.

'; } } }; document.querySelectorAll('input[name="trending-type"]').forEach(radio => { radio.addEventListener('change', renderTrending); }); // --- SEARCH --- const searchInput = dom('search-input'); const searchBtn = dom('btn-search-trigger'); const searchRadios = document.querySelectorAll('input[name="search-type"]'); let searchTimeout = null; const performSearch = async () => { const q = searchInput ? searchInput.value.trim() : ''; const grid = dom('search-results'); if (!q) { if (grid) grid.innerHTML = ''; return; } // ZT requires min 4 characters if (state.activeSources.includes('zt') && state.activeSources.length === 1 && q.length < 4) { if (grid) grid.innerHTML = '

Minimum 4 caractères pour la recherche sur ZT.

'; return; } const typeEl = document.querySelector('input[name="search-type"]:checked'); const type = typeEl ? typeEl.value : 'film'; if (grid) grid.innerHTML = '
'; try { const res = await apiCall('/search', 'POST', { title: q, mediaType: type }); // Vérification si la recherche n'a pas changé entre temps if (searchInput.value.trim() !== q) return; if (grid) { grid.innerHTML = ''; if (!res || !res.length) { if (dom('offline-banner') && dom('offline-banner').textContent.includes('Aucune source configurée')) { grid.innerHTML = '

Veuillez configurer une source dans les paramètres.

'; } else { grid.innerHTML = '

Aucun résultat.

'; } } else res.forEach(m => grid.appendChild(createCard(m))); } } catch (e) { if (grid) grid.innerHTML = `

Erreur: ${e.message}

`; } }; if (searchInput) { // Recherche instantanée avec debounce searchInput.addEventListener('input', () => { if (searchTimeout) clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { performSearch(); }, 500); // 500ms d'attente }); // Entrée pour valider immédiatement searchInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { e.preventDefault(); if (searchTimeout) clearTimeout(searchTimeout); performSearch(); } }); } if (searchBtn) { searchBtn.addEventListener('click', (e) => { e.preventDefault(); if (searchTimeout) clearTimeout(searchTimeout); performSearch(); }); } // Relancer la recherche si on change de filtre (Film/Série) searchRadios.forEach(radio => { radio.addEventListener('change', () => { if (searchInput && searchInput.value.trim().length > 0) { if (searchTimeout) clearTimeout(searchTimeout); performSearch(); } }); }); // --- MODAL --- const handleSelection = async (movie) => { showModal(movie.title, '
'); try { let ep = '/select-movie'; if (movie.source === 'hydracker' && movie.hrefPath && movie.hrefPath.includes('download')) { ep = '/select-trending'; } const data = await apiCall(ep, 'POST', { hrefPath: movie.hrefPath || '', title: movie.title, type: movie.type, source: movie.source }); // Ajout du 3ème argument : movie.source renderModalOptions(data, movie.title, movie.source); } catch (e) { dom('modal-body').innerHTML = `

${e.message}

`; } }; function parseSizeToMB(sizeStr) { if (!sizeStr || sizeStr === 'N/A') return 0; const match = sizeStr.match(/([\d.,]+)\s*(gb|go|mb|mo|ko|kb|tb|to)/i); if (!match) return 0; let size = parseFloat(match[1].replace(',', '.')); const unit = match[2].toLowerCase(); if (unit.includes('gb') || unit.includes('go')) size *= 1024; else if (unit.includes('tb')) size *= 1024 * 1024; else if (unit.includes('kb') || unit.includes('ko')) size /= 1024; return size; } const getQualityRank = (qualityString) => { const lower = qualityString.toLowerCase(); if (lower.includes("ultra hdlight") && lower.includes("x265")) return 1; if (lower.includes("1080p") && lower.includes("x265") || lower.includes("1080p light") || lower.includes("x265")) return 2; return 3; }; const renderModalOptions = (data, currentTitle = '', source = '') => { const body = dom('modal-body'); body.innerHTML = ''; const selectedIds = new Set(); const updateBatchUI = () => { const count = selectedIds.size; const container = document.getElementById('batch-download-container'); if (container) { container.style.display = count > 0 ? 'block' : 'none'; const countSpan = document.getElementById('batch-count'); if (countSpan) countSpan.textContent = count; } }; // --- 0. TYPE DETECTION --- const hasSaisonLabel = data.seasons && data.seasons.some(s => s.label.toLowerCase().includes('saison')); const hasNumericEpisodes = data.clientOptions && data.clientOptions.some(q => { if (!q.episode) return false; const lowEp = q.episode.toLowerCase(); if (lowEp.includes('saison complète') || lowEp.includes('intégrale') || lowEp.includes('pack')) return false; // Si c'est un nombre ou contient "Ep", c'est une série return /^\d+$/.test(q.episode) || lowEp.includes('ep'); }); const isActuallySeries = hasSaisonLabel || hasNumericEpisodes; // --- 1. SAISONS / VERSIONS --- if (data.seasons && data.seasons.length > 0) { const h4 = document.createElement('h4'); h4.textContent = isActuallySeries ? "Saisons disponibles" : "Qualités & Versions"; h4.className = "modal-subtitle"; body.appendChild(h4); const seasonsWrapper = document.createElement('div'); seasonsWrapper.className = 'seasons-wrapper'; data.seasons.forEach(s => { const btn = document.createElement('button'); btn.className = 'version-tab'; // using version-tab for a clean look btn.innerHTML = `${s.label}`; btn.title = s.label; btn.onclick = async () => { body.innerHTML = '
'; try { const res = await apiCall('/select-season', 'POST', { seasonValue: s.value }); res.seasons = data.seasons; const baseTitle = currentTitle.split(' - ')[0]; const newTitle = (s.label.toLowerCase().includes('saison') || s.label.toLowerCase().includes('intégrale')) ? `${baseTitle} - ${s.label}` : currentTitle; const titleEl = dom('modal-title'); if (titleEl) titleEl.textContent = newTitle; console.log(`[Modal] Titre mis à jour : ${newTitle}`); renderModalOptions(res, newTitle, source); } catch (e) { body.innerHTML = `

Erreur: ${e.message}

`; } }; seasonsWrapper.appendChild(btn); }); body.appendChild(seasonsWrapper); const sep = document.createElement('hr'); sep.className = 'modal-sep'; body.appendChild(sep); } // --- 2. FILES --- if (!data.clientOptions || !data.clientOptions.length) { body.innerHTML += '

Aucun fichier disponible.

'; return; } const MAX_FILM_SIZE_MB = 45360; const enriched = data.clientOptions.map(q => { const lowEp = q.episode ? q.episode.toLowerCase() : ''; const isFullSeason = lowEp.includes('saison complète') || lowEp.includes('intégrale') || lowEp.includes('pack') ? 1 : 0; return { ...q, sizeVal: parseSizeToMB(q.size), rank: getQualityRank(q.quality), isFullSeason }; }) .filter(q => { if (!isActuallySeries) return q.sizeVal <= MAX_FILM_SIZE_MB; return true; }).sort((a, b) => { if (a.isFullSeason !== b.isFullSeason) return b.isFullSeason - a.isFullSeason; if (isActuallySeries) { const epA = parseInt(String(a.episode || '').replace(/\D/g, '')) || 0; const epB = parseInt(String(b.episode || '').replace(/\D/g, '')) || 0; if (epA !== epB) return epA - epB; } if (a.rank !== b.rank) return a.rank - b.rank; return a.sizeVal - b.sizeVal; }); if (enriched.length === 0) { body.innerHTML += '

Aucun fichier disponible.

'; return; } // --- 2a. BUILD VERSION TABS --- // Gather unique versions from quality field const allVersions = [...new Set(enriched.map(q => q.quality || 'Inconnu'))]; let activeVersion = allVersions[0]; const h4files = document.createElement('h4'); h4files.textContent = "Fichiers Disponibles"; h4files.className = "modal-subtitle"; body.appendChild(h4files); // Version filter tabs (only if >1 version) const versionTabsWrap = document.createElement('div'); versionTabsWrap.className = 'version-tabs'; body.appendChild(versionTabsWrap); const filesContainer = document.createElement('div'); filesContainer.className = 'files-list'; body.appendChild(filesContainer); const renderFiles = (version) => { filesContainer.innerHTML = ''; const toShow = allVersions.length > 1 ? enriched.filter(q => (q.quality || 'Inconnu') === version) : enriched; if (toShow.length === 0) { filesContainer.innerHTML = '

Aucun fichier pour cette version.

'; return; } // Group by host const hostGroups = new Map(); toShow.forEach(q => { let hostKey = q.host || 'inconnu'; if (!hostGroups.has(hostKey)) hostGroups.set(hostKey, []); hostGroups.get(hostKey).push(q); }); hostGroups.forEach((items, hostName) => { // Host Header const hostHeader = document.createElement('div'); hostHeader.className = "season-group-title"; hostHeader.style.marginTop = "1rem"; // Friendly host name const hostDisplay = hostName .replace(/\.(png|jpg|webp|gif)$/i, '') .replace(/[-_]/g, ' ') .toUpperCase(); hostHeader.textContent = hostDisplay; filesContainer.appendChild(hostHeader); // Sort items by episode number items.sort((a, b) => { if (a.isFullSeason && !b.isFullSeason) return -1; if (!a.isFullSeason && b.isFullSeason) return 1; const numA = a.episode ? parseFloat(a.episode.toString().replace(/[^0-9.]/g, '')) || 0 : 0; const numB = b.episode ? parseFloat(b.episode.toString().replace(/[^0-9.]/g, '')) || 0 : 0; return numA - numB; }); const itemsGrid = document.createElement('div'); itemsGrid.className = isActuallySeries ? "seasons-grid" : "files-list"; filesContainer.appendChild(itemsGrid); items.forEach((q, idx) => { const row = document.createElement('div'); let specialClass = ''; let rankIcon = ''; if (q.isFullSeason) { specialClass = 'quality-gold'; rankIcon = '📦'; } else if (q.rank === 1) { specialClass = 'quality-gold'; rankIcon = '⭐'; } else if (q.rank === 2) { specialClass = 'quality-blue'; rankIcon = '✨'; } const episodeLabel = q.episode ? (q.isFullSeason || !/^\d+$/.test(q.episode) ? q.episode : `Ep. ${q.episode}`) : "Télécharger"; // On vérifie si la source est Hydracker const isHydracker = source === 'hydracker'; let badgesHtml = '
'; if (isActuallySeries && q.size && q.size !== 'N/A') { badgesHtml += `💾 ${q.size}`; } if (q.langs && q.langs.length) { q.langs.forEach(l => { if (l && l !== 'unknown') badgesHtml += `🎧 ${l.replace('Subs:', '💬').replace('Audio:', '🎧')}`; }); } if (q.subs && q.subs.length) { q.subs.forEach(s => { if (s && s !== 'Inconnu') badgesHtml += `💬 ${s}`; }); } badgesHtml += '
'; if (q.releaseName) { badgesHtml += `
${q.releaseName}
`; } if (isActuallySeries) { row.className = `quality-pill ${specialClass}`; row.style.display = 'flex'; row.style.justifyContent = 'space-between'; row.style.alignItems = 'center'; row.style.padding = "8px 14px"; row.style.cursor = "pointer"; row.style.gap = "20px"; let buttonsHtml = ''; if (isHydracker) { buttonsHtml = `
`; } else { buttonsHtml = ` `; } const isChecked = selectedIds.has(q.id); row.innerHTML = `
${rankIcon ? `${rankIcon}` : ''} ${episodeLabel}
${badgesHtml}
${buttonsHtml} `; } else { row.className = `file-btn ${specialClass}`; row.style.cursor = "pointer"; row.style.display = 'flex'; row.style.justifyContent = 'space-between'; row.style.alignItems = 'center'; const mirrorLabel = items.length > 1 ? ` Miroir ${idx + 1}` : ''; let buttonsHtml = ''; if (isHydracker) { buttonsHtml = `
`; } else { buttonsHtml = `
`; } const isChecked = selectedIds.has(q.id); row.innerHTML = `
${rankIcon ? `${rankIcon}` : ''} ${hostDisplay} ${mirrorLabel} ${q.episode ? `${q.episode}` : ''}
${q.size && q.size !== 'N/A' ? q.size : 'Taille inconnue'}
${badgesHtml}
${buttonsHtml} `; } const chk = row.querySelector('.batch-checkbox'); if (chk) { chk.onclick = (e) => { e.stopPropagation(); if (selectedIds.has(q.id)) { selectedIds.delete(q.id); chk.innerHTML = ''; chk.style.color = 'var(--text-sec)'; } else { selectedIds.add(q.id); chk.innerHTML = ''; chk.style.color = 'var(--primary)'; } lucide.createIcons({ root: chk }); updateBatchUI(); }; } // On attache l'event listener Movix UNIQUEMENT si on est sur Hydracker if (isHydracker) { row.querySelector('.btn-movix').onclick = async (e) => { e.stopPropagation(); toggleBlockingLoader(true, "Débridage Movix en cours..."); try { const result = await apiCall(`/movix-decode/${q.id}`, 'GET'); toggleBlockingLoader(false); if (result.link) { let toggleEl = document.getElementById('toggle-jd'); let useJD = toggleEl ? toggleEl.checked : getPref('useJD', false); if (useJD) { showToast('Lien Movix envoyé à JDownloader !'); await apiCall('/jd/add', 'POST', { link: result.link, packageName: q.releaseName || 'Movix Download', isSeries: isActuallySeries }); } else { showDirectLinkModal('Lien Direct Movix', ` `); } } } catch (err) { toggleBlockingLoader(false); showToast("Erreur Movix: " + err.message); show(dom('modal-overlay')); } }; } // L'event listener classique fonctionne pour toutes les sources row.onclick = async (e) => { e.stopPropagation(); let toggleEl = document.getElementById('toggle-jd'); let useJD = toggleEl ? toggleEl.checked : getPref('useJD', false); let forcedDirect = (source === 'ztnews'); if (forcedDirect) { useJD = false; } toggleBlockingLoader(true, "Récupération du lien..."); try { const result = await apiCall('/get-link', 'POST', { chosenId: q.id, useJD }); toggleBlockingLoader(false); if (useJD) { showToast('Lien envoyé à JDownloader !'); } else { if (forcedDirect) { showToast("⚠️ Cloudflare Turnstile actif. Résolution manuelle requise."); } const titleText = forcedDirect ? 'Lien Direct (Protection Cloudflare)' : 'Lien Direct'; const descText = forcedDirect ? 'Ce lien est protégé par Cloudflare. Cliquez sur "Ouvrir" pour le déverrouiller manuellement :' : 'Voici votre lien direct rapide :'; showDirectLinkModal(titleText, ` `); } } catch (err) { toggleBlockingLoader(false); showToast("Erreur: " + err.message); show(dom('modal-overlay')); } }; row.querySelectorAll('.btn-action').forEach(b => { b.addEventListener('mouseover', () => b.style.transform = 'scale(1.05)'); b.addEventListener('mouseout', () => b.style.transform = 'scale(1)'); }); itemsGrid.appendChild(row); }); }); lucide.createIcons(); }; if (allVersions.length > 1) { allVersions.forEach(version => { const tab = document.createElement('button'); tab.className = 'version-tab'; tab.textContent = version; if (version === activeVersion) tab.classList.add('active'); tab.onclick = () => { activeVersion = version; versionTabsWrap.querySelectorAll('.version-tab').forEach(t => t.classList.remove('active')); tab.classList.add('active'); renderFiles(version); }; versionTabsWrap.appendChild(tab); }); } renderFiles(activeVersion); const batchDownloadContainer = document.createElement('div'); batchDownloadContainer.id = 'batch-download-container'; batchDownloadContainer.style = 'display: none; padding-top: 1rem; border-top: 1px solid var(--border); margin-top: 1rem;'; batchDownloadContainer.innerHTML = ` `; body.appendChild(batchDownloadContainer); const btnBatch = batchDownloadContainer.querySelector('#btn-batch-download'); btnBatch.onclick = async () => { if (selectedIds.size === 0) return; let toggleEl = document.getElementById('toggle-jd'); let useJD = toggleEl ? toggleEl.checked : getPref('useJD', false); let forcedDirect = (source === 'ztnews'); if (forcedDirect) { useJD = false; } toggleBlockingLoader(true, "Récupération des liens..."); try { const result = await apiCall('/get-links-batch', 'POST', { chosenIds: Array.from(selectedIds), useJD }); toggleBlockingLoader(false); if (useJD) { showToast(result.message || 'Liens envoyés à JDownloader !'); } else { if (forcedDirect) { showToast("⚠️ Cloudflare Turnstile actif. Résolution manuelle requise."); } const titleText = forcedDirect ? 'Liens Directs (Protection Cloudflare)' : 'Liens Directs'; const descText = forcedDirect ? 'Ces liens sont protégés par Cloudflare. Copiez-les pour les ouvrir manuellement :' : 'Voici vos liens directs :'; showDirectLinkModal(titleText, ` `); } } catch (err) { toggleBlockingLoader(false); showToast("Erreur: " + err.message); show(dom('modal-overlay')); } }; updateBatchUI(); lucide.createIcons({ root: batchDownloadContainer }); }; const showModal = (title, content) => { dom('modal-title').textContent = title; dom('modal-body').innerHTML = content; show(dom('modal-overlay')); }; const showDirectLinkModal = (title, content) => { const existing = document.getElementById('direct-link-modal-overlay'); if (existing) existing.remove(); const overlay = document.createElement('div'); overlay.id = 'direct-link-modal-overlay'; overlay.className = 'modal-overlay'; overlay.style.zIndex = '10001'; overlay.innerHTML = ` `; document.body.appendChild(overlay); lucide.createIcons({ root: overlay }); show(overlay); // CSP Safe Event Listeners const selects = overlay.querySelectorAll('.select-on-click'); selects.forEach(el => el.addEventListener('click', function() { this.select(); })); const copyBtns = overlay.querySelectorAll('.copy-btn-target'); copyBtns.forEach(btn => { btn.addEventListener('click', () => { const targetId = btn.getAttribute('data-copy-target'); const targetEl = document.getElementById(targetId); if (targetEl) { navigator.clipboard.writeText(targetEl.value) .then(() => showToast('Copié !')) .catch(() => showToast('Erreur de copie !')); } }); }); const closeBtn = overlay.querySelector('#direct-modal-close'); if (closeBtn) { closeBtn.addEventListener('click', () => { hide(overlay); setTimeout(() => overlay.remove(), 300); }); } }; const modalClose = dom('modal-close'); if (modalClose) { modalClose.onclick = () => { hide(dom('modal-overlay')); }; } const btnLogout = dom('btn-logout'); if (btnLogout) { btnLogout.onclick = async () => { await apiCall('/logout', 'POST'); window.location.href = '/login'; }; } // --- MANUAL ADD TO JD --- const btnManualSubmit = dom('btn-manual-submit'); if (btnManualSubmit) { const typeOptions = document.querySelectorAll('.manual-type-selector .type-option'); const manualTypeInput = dom('manual-type'); typeOptions.forEach(opt => { opt.onclick = () => { typeOptions.forEach(o => o.classList.remove('active')); opt.classList.add('active'); if (manualTypeInput) { manualTypeInput.value = opt.dataset.type; } }; }); btnManualSubmit.onclick = async () => { const title = dom('manual-title').value.trim(); const linksText = dom('manual-links').value.trim(); const typeValue = manualTypeInput ? manualTypeInput.value : 'film'; const isSeries = typeValue === 'series'; if (!title) { showToast("⚠️ Veuillez entrer un titre."); return; } if (!linksText) { showToast("⚠️ Veuillez ajouter au moins un lien."); return; } toggleBlockingLoader(true, "Envoi des liens à JDownloader..."); try { const res = await apiCall('/jd/add', 'POST', { links: linksText, packageName: title, isSeries: isSeries }); toggleBlockingLoader(false); if (res.success) { showToast(`✅ ${res.count} lien(s) envoyé(s) à JDownloader !`); dom('manual-title').value = ''; dom('manual-links').value = ''; // Reset selector style typeOptions.forEach(o => o.classList.remove('active')); if (typeOptions[0]) typeOptions[0].classList.add('active'); if (manualTypeInput) manualTypeInput.value = 'film'; setTimeout(() => { window.location.href = '/downloads'; }, 1000); } else { showToast("⚠️ Échec de l'envoi."); } } catch (err) { toggleBlockingLoader(false); showToast("Erreur: " + err.message); } }; } // Start checkSession(); // --- DOWNLOAD AUTOMATION --- if (dom('downloads-list')) { startDownloadLoop(); } });