/** * Lycostorrent - Admin Panel Unifié * Gestion des catégories, tags et flux RSS */ // ============================================================ // ÉTAT GLOBAL // ============================================================ let trackers = []; let selectedTracker = null; let currentTags = []; let rssFeeds = []; let latestConfig = {}; let filtersConfig = {}; // Nouveau: config des filtres let editingFilter = null; // Filtre en cours d'édition // ============================================================ // INITIALISATION // ============================================================ document.addEventListener('DOMContentLoaded', () => { initTabs(); loadAllData(); setupEventListeners(); }); function initTabs() { document.querySelectorAll('.tab-btn').forEach(btn => { btn.addEventListener('click', () => { const tabId = btn.dataset.tab; switchTab(tabId); }); }); // Collapsibles document.querySelectorAll('.collapsible').forEach(el => { el.querySelector('.collapsible-header')?.addEventListener('click', () => { el.classList.toggle('collapsed'); }); el.classList.add('collapsed'); // Fermé par défaut }); } function switchTab(tabId) { // Boutons document.querySelectorAll('.tab-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.tab === tabId); }); // Contenus document.querySelectorAll('.tab-content').forEach(content => { content.classList.toggle('active', content.id === `tab-${tabId}`); }); } function loadAllData() { loadModulesConfig(); // Modules en premier loadTrackers(); loadTags(); loadRSSFeeds(); loadLatestConfig(); loadFiltersConfig(); loadTorrentClientConfig(); loadDiscoverTrackers(); // Trackers pour Discover } function setupEventListeners() { // === MODULES === document.getElementById('saveModulesBtn')?.addEventListener('click', saveModulesConfig); // === CATÉGORIES === document.getElementById('saveConfigBtn')?.addEventListener('click', saveConfig); document.getElementById('resetConfigBtn')?.addEventListener('click', resetConfig); // === TAGS === document.getElementById('addTagBtn')?.addEventListener('click', addTag); document.getElementById('newTagInput')?.addEventListener('keypress', e => { if (e.key === 'Enter') addTag(); }); document.getElementById('saveTagsBtn')?.addEventListener('click', saveTags); document.getElementById('resetTagsBtn')?.addEventListener('click', resetTags); document.getElementById('testParsingBtn')?.addEventListener('click', testParsing); // Présets document.querySelectorAll('.preset-btn').forEach(btn => { btn.addEventListener('click', () => addPreset(btn.dataset.preset)); }); // === FILTRES === document.getElementById('saveFiltersBtn')?.addEventListener('click', saveFilters); document.getElementById('resetFiltersBtn')?.addEventListener('click', resetFilters); document.getElementById('addFilterBtn')?.addEventListener('click', addNewFilter); document.getElementById('testFilterBtn')?.addEventListener('click', testFilters); // === RSS === document.getElementById('add-feed-form')?.addEventListener('submit', addRSSFeed); document.getElementById('test-feed-btn')?.addEventListener('click', testRSSFeed); // === CLIENT TORRENT === document.getElementById('testTorrentClientBtn')?.addEventListener('click', testTorrentClient); document.getElementById('saveTorrentClientBtn')?.addEventListener('click', saveTorrentClient); } // ============================================================ // ONGLET CATÉGORIES // ============================================================ async function loadTrackers() { try { const response = await fetch('/api/trackers'); const data = await response.json(); if (data.success) { trackers = data.trackers.filter(t => !t.id.startsWith('rss:')); renderTrackers(); } } catch (error) { console.error('Erreur chargement trackers:', error); document.getElementById('trackerSelector').innerHTML = '

Erreur de chargement

'; } } function renderTrackers() { const container = document.getElementById('trackerSelector'); if (!container) return; if (trackers.length === 0) { container.innerHTML = '

Aucun tracker configuré

'; return; } container.innerHTML = trackers.map(tracker => ` `).join(''); container.querySelectorAll('.tracker-btn').forEach(btn => { btn.addEventListener('click', () => selectTracker(btn.dataset.id)); }); } async function selectTracker(trackerId) { selectedTracker = trackers.find(t => t.id === trackerId); if (!selectedTracker) return; // Highlight document.querySelectorAll('.tracker-btn').forEach(btn => { btn.classList.toggle('selected', btn.dataset.id === trackerId); }); // Afficher sections document.getElementById('categoriesSection')?.classList.remove('hidden'); document.getElementById('configSection')?.classList.remove('hidden'); // Nom du tracker document.querySelectorAll('#selectedTrackerName, #configTrackerName').forEach(el => { if (el) el.textContent = selectedTracker.name; }); // Charger les catégories await loadTrackerCategories(trackerId); loadTrackerConfig(trackerId); } async function loadTrackerCategories(trackerId) { const container = document.getElementById('availableCategories'); if (!container) return; container.innerHTML = '

Chargement...

'; try { const response = await fetch(`/api/admin/tracker-categories?tracker=${encodeURIComponent(trackerId)}`); const data = await response.json(); if (data.success && data.categories?.length > 0) { container.innerHTML = data.categories.map(cat => ` `).join(''); container.querySelectorAll('.category-chip').forEach(chip => { chip.addEventListener('click', () => addCategoryToConfig(chip.dataset.id)); }); } else { container.innerHTML = '

Aucune catégorie disponible

'; } } catch (error) { container.innerHTML = '

Erreur de chargement

'; } } function loadTrackerConfig(trackerId) { const config = latestConfig[trackerId] || {}; document.getElementById('config-movies').value = config.movies || ''; document.getElementById('config-tv').value = config.tv || ''; document.getElementById('config-anime').value = config.anime || ''; document.getElementById('config-music').value = config.music || ''; } function addCategoryToConfig(catId) { // Ajouter à tous les champs qui sont focusés ou au premier const inputs = ['config-movies', 'config-tv', 'config-anime', 'config-music']; const focused = document.activeElement; let targetInput; if (inputs.includes(focused?.id)) { targetInput = focused; } else { targetInput = document.getElementById('config-movies'); } if (targetInput) { const current = targetInput.value.split(',').map(s => s.trim()).filter(Boolean); if (!current.includes(catId)) { current.push(catId); targetInput.value = current.join(','); } } } async function saveConfig() { if (!selectedTracker) { showToast('Sélectionnez un tracker', 'error'); return; } const config = { tracker: selectedTracker.id, categories: { movies: document.getElementById('config-movies').value, tv: document.getElementById('config-tv').value, anime: document.getElementById('config-anime').value, music: document.getElementById('config-music').value } }; try { const response = await fetch('/api/admin/latest-config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ config: config }) }); const data = await response.json(); if (data.success) { showToast('Configuration sauvegardée', 'success'); loadLatestConfig(); } else { showToast(data.error || 'Erreur', 'error'); } } catch (error) { showToast('Erreur de connexion', 'error'); } } function resetConfig() { document.getElementById('config-movies').value = ''; document.getElementById('config-tv').value = ''; document.getElementById('config-anime').value = ''; document.getElementById('config-music').value = ''; } async function loadLatestConfig() { try { const response = await fetch('/api/admin/latest-config'); const data = await response.json(); if (data.success) { latestConfig = data.config || {}; renderConfigSummary(); } } catch (error) { console.error('Erreur chargement config:', error); } } function renderConfigSummary() { const container = document.getElementById('configSummary'); if (!container) return; const entries = Object.entries(latestConfig); if (entries.length === 0) { container.innerHTML = '

Aucune configuration. Les catégories par défaut seront utilisées.

'; return; } container.innerHTML = entries.map(([trackerId, cats]) => `
${escapeHtml(trackerId)}
${cats.movies ? `🎥 ${cats.movies}` : ''} ${cats.tv ? `📺 ${cats.tv}` : ''} ${cats.anime ? `🎌 ${cats.anime}` : ''} ${cats.music ? `🎵 ${cats.music}` : ''}
`).join(''); } // ============================================================ // ONGLET TAGS // ============================================================ async function loadTags() { try { const response = await fetch('/api/admin/parsing-tags'); const data = await response.json(); if (data.success) { currentTags = data.tags || []; renderTags(); } } catch (error) { console.error('Erreur chargement tags:', error); } } function renderTags() { const container = document.getElementById('tagsList'); if (!container) return; if (currentTags.length === 0) { container.innerHTML = '

Aucun tag configuré

'; return; } const sorted = [...currentTags].sort((a, b) => a.localeCompare(b)); container.innerHTML = sorted.map(tag => ` ${escapeHtml(tag)} `).join(''); container.querySelectorAll('.tag-remove').forEach(btn => { btn.addEventListener('click', () => removeTag(btn.dataset.tag)); }); } function addTag() { const input = document.getElementById('newTagInput'); const tag = input.value.trim().toUpperCase(); if (!tag) return; if (currentTags.includes(tag)) { showToast('Ce tag existe déjà', 'error'); return; } currentTags.push(tag); input.value = ''; renderTags(); } function removeTag(tag) { currentTags = currentTags.filter(t => t !== tag); renderTags(); } async function saveTags() { try { const response = await fetch('/api/admin/parsing-tags', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tags: currentTags }) }); const data = await response.json(); if (data.success) { showToast('Tags sauvegardés', 'success'); } else { showToast(data.error || 'Erreur', 'error'); } } catch (error) { showToast('Erreur de connexion', 'error'); } } async function resetTags() { if (!confirm('Réinitialiser aux tags par défaut ?')) return; try { const response = await fetch('/api/admin/parsing-tags/reset', { method: 'POST' }); const data = await response.json(); if (data.success) { currentTags = data.tags || []; renderTags(); showToast('Tags réinitialisés', 'success'); } } catch (error) { showToast('Erreur', 'error'); } } function addPreset(presetName) { const presets = { langues: ['FRENCH', 'MULTI', 'TRUEFRENCH', 'VFF', 'VFQ', 'VOSTFR', 'SUBFRENCH', 'FASTSUB'], resolutions: ['2160P', '1080P', '720P', '4K', 'UHD', 'HDTV', 'PDTV'], sources: ['BLURAY', 'BDRIP', 'BRRIP', 'DVDRIP', 'WEBRIP', 'WEB-DL', 'HDTV', 'REMUX'], codecs: ['X264', 'X265', 'H264', 'H265', 'HEVC', 'AVC', 'AV1', 'XVID', 'DIVX'], audio: ['DTS', 'AC3', 'AAC', 'FLAC', 'TRUEHD', 'ATMOS', 'EAC3'] }; const newTags = presets[presetName] || []; let added = 0; newTags.forEach(tag => { if (!currentTags.includes(tag)) { currentTags.push(tag); added++; } }); renderTags(); showToast(`${added} tags ajoutés`, 'success'); } async function testParsing() { const input = document.getElementById('testTitleInput'); const title = input.value.trim(); if (!title) { showToast('Entrez un titre', 'error'); return; } try { const response = await fetch('/api/admin/test-parsing', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title }) }); const data = await response.json(); if (data.success) { document.getElementById('testOriginal').textContent = data.original; document.getElementById('testCleaned').textContent = data.cleaned; document.getElementById('testResult').classList.remove('hidden'); } } catch (error) { showToast('Erreur', 'error'); } } // ============================================================ // ONGLET RSS // ============================================================ async function loadRSSFeeds() { try { const response = await fetch('/api/admin/rss'); const data = await response.json(); if (data.success) { rssFeeds = data.feeds || []; renderRSSFeeds(); } } catch (error) { console.error('Erreur chargement RSS:', error); } } function renderRSSFeeds() { const container = document.getElementById('feeds-list'); if (!container) return; if (rssFeeds.length === 0) { container.innerHTML = '

Aucun flux RSS configuré

'; return; } container.innerHTML = rssFeeds.map(feed => `
${escapeHtml(feed.name)}
${getCategoryLabel(feed.category)} ${feed.use_flaresolverr ? '🛡️' : ''} ${feed.has_cookies ? '🍪' : ''}
${maskUrl(feed.url)}
`).join(''); } async function addRSSFeed(e) { e.preventDefault(); const feed = { name: document.getElementById('feed-name').value.trim(), url: document.getElementById('feed-url').value.trim(), category: document.getElementById('feed-category').value, passkey: document.getElementById('feed-passkey').value.trim(), use_flaresolverr: document.getElementById('feed-flaresolverr').checked, cookies: document.getElementById('feed-cookies').value.trim() }; if (!feed.name || !feed.url || !feed.category) { showToast('Remplissez tous les champs obligatoires', 'error'); return; } try { const response = await fetch('/api/admin/rss', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(feed) }); const data = await response.json(); if (data.success) { document.getElementById('add-feed-form').reset(); document.getElementById('test-result').classList.add('hidden'); loadRSSFeeds(); showToast('Flux RSS ajouté', 'success'); } else { showToast(data.error || 'Erreur', 'error'); } } catch (error) { showToast('Erreur de connexion', 'error'); } } async function testRSSFeed() { const url = document.getElementById('feed-url').value.trim(); const passkey = document.getElementById('feed-passkey').value.trim(); const use_flaresolverr = document.getElementById('feed-flaresolverr').checked; const cookies = document.getElementById('feed-cookies').value.trim(); if (!url) { showToast('Entrez une URL', 'error'); return; } const resultDiv = document.getElementById('test-result'); resultDiv.classList.remove('hidden'); resultDiv.innerHTML = '

🔄 Test en cours...

'; try { const response = await fetch('/api/admin/rss/test', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url, passkey, use_flaresolverr, cookies }) }); const data = await response.json(); if (data.success && data.count > 0) { resultDiv.innerHTML = `
✅ Succès ! ${data.count} résultats trouvés
`; } else { resultDiv.innerHTML = `
❌ Échec - Vérifiez l'URL et les cookies
`; } } catch (error) { resultDiv.innerHTML = '
❌ Erreur de connexion
'; } } async function toggleRSSFeed(feedId) { try { const response = await fetch(`/api/admin/rss/${feedId}/toggle`, { method: 'POST' }); const data = await response.json(); if (data.success) { loadRSSFeeds(); } } catch (error) { showToast('Erreur', 'error'); } } async function deleteRSSFeed(feedId) { if (!confirm('Supprimer ce flux RSS ?')) return; try { const response = await fetch(`/api/admin/rss/${feedId}`, { method: 'DELETE' }); const data = await response.json(); if (data.success) { loadRSSFeeds(); showToast('Flux supprimé', 'success'); } } catch (error) { showToast('Erreur', 'error'); } } // ============================================================ // ONGLET FILTRES // ============================================================ async function loadFiltersConfig() { try { const response = await fetch('/api/admin/filters'); const data = await response.json(); if (data.success) { filtersConfig = data.filters || {}; renderFilters(); } } catch (error) { console.error('Erreur chargement filtres:', error); } } function renderFilters() { const container = document.getElementById('filtersList'); if (!container) return; const filterKeys = Object.keys(filtersConfig); if (filterKeys.length === 0) { container.innerHTML = '

Aucun filtre configuré

'; return; } container.innerHTML = filterKeys.map(key => { const filter = filtersConfig[key]; const values = filter.values || []; const isEditing = editingFilter === key; return `
${filter.icon || '🏷️'} ${escapeHtml(filter.name || key)} ${values.length} valeurs ${isEditing ? '▼' : '▶'}
${isEditing ? `
` : `
${values.slice(0, 10).map(v => `${escapeHtml(v)}`).join('')} ${values.length > 10 ? `+${values.length - 10}` : ''}
`}
`; }).join(''); } function toggleFilterEdit(key) { if (editingFilter === key) { editingFilter = null; } else { editingFilter = key; } renderFilters(); } function saveFilterEdit(key) { const item = document.querySelector(`.filter-editor-item[data-key="${key}"]`); if (!item) return; const nameInput = item.querySelector('.filter-name-input'); const iconInput = item.querySelector('.filter-icon-input'); const valuesTextarea = item.querySelector('.filter-values-textarea'); const name = nameInput?.value.trim() || key; const icon = iconInput?.value.trim() || '🏷️'; const values = valuesTextarea?.value.split('\n').map(v => v.trim()).filter(Boolean) || []; filtersConfig[key] = { name, icon, values }; editingFilter = null; renderFilters(); showToast('Modifications appliquées (non sauvegardées)', 'info'); } function cancelFilterEdit() { editingFilter = null; renderFilters(); } function deleteFilter(key) { if (!confirm(`Supprimer le filtre "${filtersConfig[key]?.name || key}" ?`)) return; delete filtersConfig[key]; editingFilter = null; renderFilters(); showToast('Filtre supprimé (non sauvegardé)', 'info'); } function addNewFilter() { // Créer une modale simple const name = prompt('Nom du filtre (ex: Genre Jeu, Format Vidéo):'); if (!name) return; // Générer automatiquement le nom technique const key = name.toLowerCase() .normalize('NFD').replace(/[\u0300-\u036f]/g, '') // Enlever accents .replace(/[^a-z0-9]+/g, '_') // Remplacer caractères spéciaux par _ .replace(/^_|_$/g, ''); // Enlever _ au début/fin if (!key) { showToast('Nom invalide', 'error'); return; } if (filtersConfig[key]) { showToast('Un filtre similaire existe déjà', 'error'); return; } const icon = prompt('Icône emoji (ex: 🎮, 📚, 🎵):', '🏷️') || '🏷️'; filtersConfig[key] = { name: name, icon: icon, values: [] }; editingFilter = key; renderFilters(); showToast(`Filtre "${name}" créé. Ajoutez des valeurs puis sauvegardez.`, 'success'); } async function saveFilters() { try { const response = await fetch('/api/admin/filters', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ filters: filtersConfig }) }); const data = await response.json(); if (data.success) { showToast('Filtres sauvegardés', 'success'); } else { showToast(data.error || 'Erreur', 'error'); } } catch (error) { showToast('Erreur de connexion', 'error'); } } async function resetFilters() { if (!confirm('Réinitialiser aux filtres par défaut ?')) return; try { const response = await fetch('/api/admin/filters/reset', { method: 'POST' }); const data = await response.json(); if (data.success) { filtersConfig = data.filters || {}; editingFilter = null; renderFilters(); showToast('Filtres réinitialisés', 'success'); } } catch (error) { showToast('Erreur', 'error'); } } async function testFilters() { const input = document.getElementById('testFilterInput'); const title = input?.value.trim(); if (!title) { showToast('Entrez un titre', 'error'); return; } try { const response = await fetch('/api/admin/filters/test', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title }) }); const data = await response.json(); const resultDiv = document.getElementById('filterTestResult'); if (data.success && resultDiv) { const parsed = data.parsed || {}; const entries = Object.entries(parsed).filter(([k, v]) => v && (Array.isArray(v) ? v.length > 0 : true)); if (entries.length === 0) { resultDiv.innerHTML = '

❌ Aucun filtre détecté

'; } else { resultDiv.innerHTML = `

✅ Filtres détectés :

${entries.map(([key, value]) => { const filter = filtersConfig[key]; const icon = filter?.icon || '🏷️'; const name = filter?.name || key; const values = Array.isArray(value) ? value.join(', ') : value; return `
${icon} ${escapeHtml(name)}: ${escapeHtml(values)}
`; }).join('')}
`; } resultDiv.classList.remove('hidden'); } } catch (error) { showToast('Erreur', 'error'); } } // ============================================================ // UTILITAIRES // ============================================================ function getCategoryLabel(cat) { const labels = { movies: '🎬 Films', tv: '📺 Séries', anime: '🎌 Anime', music: '🎵 Musique', all: '📦 Toutes' }; return labels[cat] || cat; } function maskUrl(url) { return url.replace(/passkey=[^&]+/gi, 'passkey=***') .replace(/apikey=[^&]+/gi, 'apikey=***'); } function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function showToast(message, type = 'info') { const toast = document.getElementById('toast'); if (!toast) return; toast.textContent = message; toast.className = `toast ${type}`; toast.classList.remove('hidden'); setTimeout(() => toast.classList.add('hidden'), 3000); } // ============================================================ // ONGLET CLIENT TORRENT // ============================================================ async function loadTorrentClientConfig() { try { // Charger les plugins disponibles const pluginsResponse = await fetch('/api/admin/torrent-client/plugins'); const pluginsData = await pluginsResponse.json(); if (pluginsData.success) { renderPluginsList(pluginsData.plugins); populatePluginSelect(pluginsData.plugins); } // Charger la config actuelle const configResponse = await fetch('/api/admin/torrent-client/config'); const configData = await configResponse.json(); if (configData.success) { fillTorrentClientForm(configData.config, configData.connected); updateTorrentClientStatus(configData.config, configData.connected); } // Charger les catégories personnalisées await loadCustomCategories(); // Event listeners pour les catégories document.getElementById('addCategoryBtn')?.addEventListener('click', addCategory); document.getElementById('saveCategoriesBtn')?.addEventListener('click', saveCustomCategories); document.getElementById('syncCategoriesBtn')?.addEventListener('click', syncCategoriesWithClient); } catch (error) { console.error('Erreur chargement config client torrent:', error); } } function renderPluginsList(plugins) { const container = document.getElementById('pluginsList'); if (!container) return; if (plugins.length === 0) { container.innerHTML = '

Aucun plugin installé

'; return; } container.innerHTML = plugins.map(p => `
${escapeHtml(p.name)} v${escapeHtml(p.version)}

${escapeHtml(p.description)}

Par ${escapeHtml(p.author)}
`).join(''); } function populatePluginSelect(plugins) { const select = document.getElementById('tcPlugin'); if (!select) return; select.innerHTML = ''; plugins.forEach(p => { const option = document.createElement('option'); option.value = p.id; option.textContent = p.name; select.appendChild(option); }); } function fillTorrentClientForm(config, connected) { if (!config) return; document.getElementById('tcEnabled').checked = config.enabled || false; document.getElementById('tcPlugin').value = config.plugin || ''; document.getElementById('tcHost').value = config.host || ''; document.getElementById('tcPort').value = config.port || ''; document.getElementById('tcPath').value = config.path || ''; document.getElementById('tcUsername').value = config.username || ''; // Ne pas remplir le mot de passe (sécurité) document.getElementById('tcSSL').checked = config.use_ssl || false; } function updateTorrentClientStatus(config, connected) { const container = document.getElementById('torrentClientStatus'); if (!container) return; if (!config || !config.enabled) { container.innerHTML = ` ⚫ Désactivé

Aucun client torrent configuré

`; return; } // Construire l'URL affichée let urlDisplay = config.host; if (config.port) urlDisplay += `:${config.port}`; if (config.path) urlDisplay += config.path; if (connected) { container.innerHTML = ` 🟢 Connecté

${escapeHtml(config.plugin)} sur ${escapeHtml(urlDisplay)}

`; } else { container.innerHTML = ` 🔴 Déconnecté

${escapeHtml(config.plugin)} - Vérifiez les paramètres

`; } } function getTorrentClientFormData() { return { enabled: document.getElementById('tcEnabled').checked, plugin: document.getElementById('tcPlugin').value, host: document.getElementById('tcHost').value, port: document.getElementById('tcPort').value, path: document.getElementById('tcPath').value, username: document.getElementById('tcUsername').value, password: document.getElementById('tcPassword').value, use_ssl: document.getElementById('tcSSL').checked }; } async function testTorrentClient() { const data = getTorrentClientFormData(); if (!data.plugin) { showToast('Sélectionnez un client', 'error'); return; } if (!data.host) { showToast('Entrez l\'adresse du serveur', 'error'); return; } const resultDiv = document.getElementById('tcTestResult'); resultDiv.innerHTML = '

Test en cours...

'; resultDiv.classList.remove('hidden'); try { const response = await fetch('/api/admin/torrent-client/test', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); const result = await response.json(); if (result.success) { resultDiv.innerHTML = `

✅ Connexion réussie !

${result.version ? `

Version: ${escapeHtml(result.version)}

` : ''} `; showToast('Connexion réussie', 'success'); } else { resultDiv.innerHTML = `

❌ ${escapeHtml(result.error || result.message)}

`; showToast('Échec de la connexion', 'error'); } } catch (error) { resultDiv.innerHTML = `

❌ Erreur: ${escapeHtml(error.message)}

`; showToast('Erreur de connexion', 'error'); } } async function saveTorrentClient() { const data = getTorrentClientFormData(); try { const response = await fetch('/api/admin/torrent-client/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); const result = await response.json(); if (result.success) { showToast(result.message || 'Configuration sauvegardée', 'success'); if (result.warning) { showToast(result.warning, 'warning'); } loadTorrentClientConfig(); } else { showToast(result.error || 'Erreur', 'error'); } } catch (error) { showToast('Erreur de connexion', 'error'); } } // ============================================================ // CATÉGORIES PERSONNALISÉES // ============================================================ let customCategories = {}; async function loadCustomCategories() { try { const response = await fetch('/api/admin/torrent-client/categories'); const data = await response.json(); if (data.success) { customCategories = data.categories || {}; renderCustomCategories(); } } catch (error) { console.error('Erreur chargement catégories:', error); } } function renderCustomCategories() { const container = document.getElementById('customCategoriesList'); if (!container) return; if (Object.keys(customCategories).length === 0) { container.innerHTML = '

Aucune catégorie configurée

'; return; } container.innerHTML = Object.entries(customCategories).map(([name, path]) => `
📁 ${escapeHtml(name)} ${escapeHtml(path) || '(pas de chemin)'}
`).join(''); } function addCategory() { const nameInput = document.getElementById('newCategoryName'); const pathInput = document.getElementById('newCategoryPath'); const name = nameInput.value.trim(); const path = pathInput.value.trim(); if (!name) { showToast('Nom de catégorie requis', 'error'); return; } customCategories[name] = path; renderCustomCategories(); // Reset form nameInput.value = ''; pathInput.value = ''; showToast(`Catégorie "${name}" ajoutée`, 'success'); } function editCategory(name) { const newName = prompt('Nouveau nom:', name); if (!newName || newName === name) return; const newPath = prompt('Chemin:', customCategories[name] || ''); // Supprimer l'ancienne et ajouter la nouvelle const oldPath = customCategories[name]; delete customCategories[name]; customCategories[newName] = newPath !== null ? newPath : oldPath; renderCustomCategories(); showToast('Catégorie modifiée', 'success'); } function deleteCategory(name) { if (!confirm(`Supprimer la catégorie "${name}" ?`)) return; delete customCategories[name]; renderCustomCategories(); showToast('Catégorie supprimée', 'success'); } async function saveCustomCategories() { try { const response = await fetch('/api/admin/torrent-client/categories', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ categories: customCategories }) }); const data = await response.json(); if (data.success) { showToast('Catégories sauvegardées', 'success'); } else { showToast(data.error || 'Erreur', 'error'); } } catch (error) { showToast('Erreur de connexion', 'error'); } } async function syncCategoriesWithClient() { try { const response = await fetch('/api/admin/torrent-client/sync-categories', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); const data = await response.json(); if (data.success) { showToast(data.message || 'Catégories synchronisées', 'success'); } else { showToast(data.error || 'Erreur', 'error'); } } catch (error) { showToast('Erreur de connexion', 'error'); } } // Exposer les fonctions pour les onclick inline window.toggleRSSFeed = toggleRSSFeed; window.deleteRSSFeed = deleteRSSFeed; window.toggleFilterEdit = toggleFilterEdit; window.saveFilterEdit = saveFilterEdit; window.cancelFilterEdit = cancelFilterEdit; window.deleteFilter = deleteFilter; window.editCategory = editCategory; window.deleteCategory = deleteCategory; // ============================================================ // GESTION DES THÈMES // ============================================================ function initThemes() { // Charger le thème sauvegardé const savedTheme = localStorage.getItem('lycostorrent-theme') || 'dark'; applyTheme(savedTheme); // Marquer le thème actif document.querySelectorAll('.theme-card').forEach(card => { card.classList.toggle('active', card.dataset.theme === savedTheme); // Ajouter l'événement click card.addEventListener('click', () => { const theme = card.dataset.theme; applyTheme(theme); // Mettre à jour l'UI document.querySelectorAll('.theme-card').forEach(c => c.classList.remove('active')); card.classList.add('active'); // Sauvegarder localStorage.setItem('lycostorrent-theme', theme); showToast(`Thème "${getThemeName(theme)}" appliqué`, 'success'); }); }); } function applyTheme(theme) { document.documentElement.setAttribute('data-theme', theme); } function getThemeName(theme) { const names = { 'dark': 'Sombre', 'light': 'Clair', 'ocean': 'Océan', 'purple': 'Violet', 'nature': 'Nature', 'sunset': 'Sunset', 'cyberpunk': 'Cyberpunk', 'nord': 'Nord' }; return names[theme] || theme; } // Initialiser les thèmes au chargement de la page document.addEventListener('DOMContentLoaded', () => { initThemes(); }); // ============================================================ // GESTION DES MODULES // ============================================================ async function loadModulesConfig() { try { const response = await fetch('/api/admin/modules'); const data = await response.json(); if (data.success && data.modules) { document.getElementById('module-search').checked = data.modules.search !== false; document.getElementById('module-latest').checked = data.modules.latest !== false; document.getElementById('module-discover').checked = data.modules.discover === true; } } catch (error) { console.error('Erreur chargement modules:', error); } } async function saveModulesConfig() { try { const modules = { search: document.getElementById('module-search').checked, latest: document.getElementById('module-latest').checked, discover: document.getElementById('module-discover').checked }; const response = await fetch('/api/admin/modules', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ modules }) }); const data = await response.json(); if (data.success) { showToast('Modules sauvegardés ! Rechargez la page pour voir les changements.', 'success'); } else { showToast(data.error || 'Erreur', 'error'); } } catch (error) { showToast('Erreur de connexion', 'error'); } } // ============================================================ // TRACKERS POUR DISCOVER // ============================================================ let allTrackers = []; let selectedDiscoverTrackers = []; async function loadDiscoverTrackers() { const container = document.getElementById('discoverTrackersList'); if (!container) return; try { // Charger tous les trackers const trackersResponse = await fetch('/api/trackers'); const trackersData = await trackersResponse.json(); if (trackersData.success) { allTrackers = trackersData.trackers || []; } // Charger la config des trackers sélectionnés const configResponse = await fetch('/api/admin/discover-trackers'); const configData = await configResponse.json(); if (configData.success && configData.trackers) { selectedDiscoverTrackers = configData.trackers; } else { // Par défaut, tous les trackers sont sélectionnés selectedDiscoverTrackers = allTrackers.map(t => t.id); } renderDiscoverTrackers(); } catch (error) { container.innerHTML = '

Erreur de chargement des trackers

'; } } function renderDiscoverTrackers() { const container = document.getElementById('discoverTrackersList'); if (!container || !allTrackers.length) { container.innerHTML = '

Aucun tracker disponible

'; return; } container.innerHTML = allTrackers.map(tracker => { const isChecked = selectedDiscoverTrackers.includes(tracker.id); const source = tracker.id.startsWith('jackett:') ? 'jackett' : tracker.id.startsWith('prowlarr:') ? 'prowlarr' : ''; const sourceLabel = source.charAt(0).toUpperCase() + source.slice(1); return `
`; }).join(''); } function toggleDiscoverTracker(trackerId) { const index = selectedDiscoverTrackers.indexOf(trackerId); if (index > -1) { selectedDiscoverTrackers.splice(index, 1); } else { selectedDiscoverTrackers.push(trackerId); } } function selectAllDiscoverTrackers() { selectedDiscoverTrackers = allTrackers.map(t => t.id); renderDiscoverTrackers(); } function selectNoneDiscoverTrackers() { selectedDiscoverTrackers = []; renderDiscoverTrackers(); } async function saveDiscoverTrackers() { try { const response = await fetch('/api/admin/discover-trackers', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ trackers: selectedDiscoverTrackers }) }); const data = await response.json(); if (data.success) { showToast('Trackers Discover sauvegardés !', 'success'); } else { showToast(data.error || 'Erreur', 'error'); } } catch (error) { showToast('Erreur de connexion', 'error'); } } // Event listeners pour les boutons document.getElementById('selectAllDiscoverTrackers')?.addEventListener('click', selectAllDiscoverTrackers); document.getElementById('selectNoneDiscoverTrackers')?.addEventListener('click', selectNoneDiscoverTrackers); document.getElementById('saveDiscoverTrackers')?.addEventListener('click', saveDiscoverTrackers); // Exposer les fonctions window.toggleDiscoverTracker = toggleDiscoverTracker; // ============================================================ // GESTION DU CACHE // ============================================================ let cacheConfig = {}; async function loadCacheStatus() { try { const response = await fetch('/api/cache/status'); const data = await response.json(); if (data.success) { updateCacheStatusDisplay(data); } } catch (error) { console.error('Erreur chargement statut cache:', error); } } function updateCacheStatusDisplay(status) { const badge = document.getElementById('cacheStatusBadge'); const lastRefresh = document.getElementById('cacheLastRefresh'); const nextRefresh = document.getElementById('cacheNextRefresh'); const sizeDisplay = document.getElementById('cacheSizeDisplay'); if (!badge) return; // Badge statut if (!status.enabled) { badge.textContent = '⚫ Désactivé'; badge.className = 'status-badge status-disabled'; } else if (status.is_refreshing) { badge.textContent = '🔄 Refresh en cours...'; badge.className = 'status-badge status-refreshing'; } else if (status.status === 'success') { badge.textContent = '🟢 Actif'; badge.className = 'status-badge status-success'; } else if (status.status === 'error') { badge.textContent = '🔴 Erreur'; badge.className = 'status-badge status-error'; } else { badge.textContent = '⚪ Jamais exécuté'; badge.className = 'status-badge status-never'; } // Dernier refresh if (status.last_refresh) { const date = new Date(status.last_refresh); lastRefresh.textContent = `${date.toLocaleDateString('fr-FR')} à ${date.toLocaleTimeString('fr-FR', {hour: '2-digit', minute: '2-digit'})} (${status.last_refresh_ago || ''})`; } else { lastRefresh.textContent = 'Jamais'; } // Prochain refresh if (status.next_refresh && status.enabled) { const date = new Date(status.next_refresh); nextRefresh.textContent = date.toLocaleTimeString('fr-FR', {hour: '2-digit', minute: '2-digit'}); } else { nextRefresh.textContent = '-'; } // Taille if (status.cache_size_mb > 0) { sizeDisplay.textContent = `${status.cache_size_mb} Mo`; } else { sizeDisplay.textContent = 'Vide'; } } async function loadCacheConfig() { try { const response = await fetch('/api/cache/config'); const data = await response.json(); if (data.success && data.config) { cacheConfig = data.config; fillCacheForm(data.config); } } catch (error) { console.error('Erreur chargement config cache:', error); } } function fillCacheForm(config) { document.getElementById('cacheEnabled').checked = config.enabled || false; document.getElementById('cacheInterval').value = config.interval_minutes || 60; // Latest const latest = config.latest || {}; document.getElementById('cacheLatestEnabled').checked = latest.enabled !== false; document.getElementById('cacheLatestMovies').checked = (latest.categories || []).includes('movies'); document.getElementById('cacheLatestTv').checked = (latest.categories || []).includes('tv'); document.getElementById('cacheLatestAnime').checked = (latest.categories || []).includes('anime'); document.getElementById('cacheLatestMusic').checked = (latest.categories || []).includes('music'); document.getElementById('cacheLatestLimit').value = latest.limit || 50; // Discover const discover = config.discover || {}; document.getElementById('cacheDiscoverEnabled').checked = discover.enabled !== false; document.getElementById('cacheDiscoverLimit').value = discover.limit || 30; // Charger les trackers loadCacheTrackers(latest.trackers || []); } async function loadCacheTrackers(selectedTrackers) { try { const response = await fetch('/api/trackers'); const data = await response.json(); if (data.success && data.trackers) { const container = document.getElementById('cacheTrackersList'); container.innerHTML = ''; data.trackers.forEach(tracker => { const label = document.createElement('label'); const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.value = tracker.id; checkbox.checked = selectedTrackers.length === 0 || selectedTrackers.includes(tracker.id); checkbox.className = 'cache-tracker-checkbox'; label.appendChild(checkbox); label.appendChild(document.createTextNode(' ' + tracker.name)); container.appendChild(label); }); } } catch (error) { console.error('Erreur chargement trackers cache:', error); } } async function saveCacheConfig() { try { // Récupérer les catégories cochées const categories = []; if (document.getElementById('cacheLatestMovies').checked) categories.push('movies'); if (document.getElementById('cacheLatestTv').checked) categories.push('tv'); if (document.getElementById('cacheLatestAnime').checked) categories.push('anime'); if (document.getElementById('cacheLatestMusic').checked) categories.push('music'); // Récupérer les trackers cochés const trackers = []; document.querySelectorAll('.cache-tracker-checkbox:checked').forEach(cb => { trackers.push(cb.value); }); const config = { enabled: document.getElementById('cacheEnabled').checked, interval_minutes: parseInt(document.getElementById('cacheInterval').value), latest_enabled: document.getElementById('cacheLatestEnabled').checked, latest_categories: categories, latest_trackers: trackers, latest_limit: parseInt(document.getElementById('cacheLatestLimit').value), discover_enabled: document.getElementById('cacheDiscoverEnabled').checked, discover_limit: parseInt(document.getElementById('cacheDiscoverLimit').value) }; const response = await fetch('/api/cache/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(config) }); const data = await response.json(); if (data.success) { showToast('Configuration du cache sauvegardée !', 'success'); loadCacheStatus(); } else { showToast(data.error || 'Erreur', 'error'); } } catch (error) { showToast('Erreur de connexion', 'error'); } } async function refreshCache() { const btn = document.getElementById('refreshCacheBtn'); const originalText = btn.textContent; btn.textContent = '⏳ Refresh en cours...'; btn.disabled = true; try { const response = await fetch('/api/cache/refresh', { method: 'POST' }); const data = await response.json(); if (data.success) { showToast('Refresh du cache lancé !', 'success'); // Mettre à jour le statut périodiquement setTimeout(loadCacheStatus, 2000); setTimeout(loadCacheStatus, 5000); setTimeout(loadCacheStatus, 10000); } else { showToast(data.error || 'Erreur', 'error'); } } catch (error) { showToast('Erreur de connexion', 'error'); } finally { btn.textContent = originalText; btn.disabled = false; } } async function clearCache() { if (!confirm('Voulez-vous vraiment vider le cache ?')) return; try { const response = await fetch('/api/cache/clear', { method: 'POST' }); const data = await response.json(); if (data.success) { showToast('Cache vidé !', 'success'); loadCacheStatus(); } else { showToast(data.error || 'Erreur', 'error'); } } catch (error) { showToast('Erreur de connexion', 'error'); } } // Event listeners Cache document.getElementById('refreshCacheBtn')?.addEventListener('click', refreshCache); document.getElementById('clearCacheBtn')?.addEventListener('click', clearCache); document.getElementById('saveCacheConfigBtn')?.addEventListener('click', saveCacheConfig); // Charger le cache au chargement de la page document.addEventListener('DOMContentLoaded', function() { // Charger le statut et la config du cache si l'onglet existe if (document.getElementById('tab-cache')) { loadCacheStatus(); loadCacheConfig(); // Rafraîchir le statut toutes les 30 secondes setInterval(loadCacheStatus, 30000); } });