Files
Lycostorrent/app/static/js/admin.js
2026-03-23 20:59:26 +01:00

1673 lines
57 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 = '<p class="error">Erreur de chargement</p>';
}
}
function renderTrackers() {
const container = document.getElementById('trackerSelector');
if (!container) return;
if (trackers.length === 0) {
container.innerHTML = '<p class="empty-state">Aucun tracker configuré</p>';
return;
}
container.innerHTML = trackers.map(tracker => `
<button class="tracker-btn" data-id="${escapeHtml(tracker.id)}">
<span class="tracker-name">${escapeHtml(tracker.name)}</span>
<span class="tracker-source">${tracker.sources?.join(' + ') || tracker.source || ''}</span>
</button>
`).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 = '<p class="loading">Chargement...</p>';
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 => `
<button class="category-chip" data-id="${cat.id}" data-name="${escapeHtml(cat.name)}">
${escapeHtml(cat.name)} <span class="cat-id">(${cat.id})</span>
</button>
`).join('');
container.querySelectorAll('.category-chip').forEach(chip => {
chip.addEventListener('click', () => addCategoryToConfig(chip.dataset.id));
});
} else {
container.innerHTML = '<p class="empty-state">Aucune catégorie disponible</p>';
}
} catch (error) {
container.innerHTML = '<p class="error">Erreur de chargement</p>';
}
}
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 = '<p class="empty-state">Aucune configuration. Les catégories par défaut seront utilisées.</p>';
return;
}
container.innerHTML = entries.map(([trackerId, cats]) => `
<div class="summary-item">
<span class="summary-tracker">${escapeHtml(trackerId)}</span>
<div class="summary-cats">
${cats.movies ? `<span class="summary-cat">🎥 <span>${cats.movies}</span></span>` : ''}
${cats.tv ? `<span class="summary-cat">📺 <span>${cats.tv}</span></span>` : ''}
${cats.anime ? `<span class="summary-cat">🎌 <span>${cats.anime}</span></span>` : ''}
${cats.music ? `<span class="summary-cat">🎵 <span>${cats.music}</span></span>` : ''}
</div>
</div>
`).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 = '<p class="empty-state">Aucun tag configuré</p>';
return;
}
const sorted = [...currentTags].sort((a, b) => a.localeCompare(b));
container.innerHTML = sorted.map(tag => `
<span class="tag-item">
${escapeHtml(tag)}
<button class="tag-remove" data-tag="${escapeHtml(tag)}">×</button>
</span>
`).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 = '<p class="empty-state">Aucun flux RSS configuré</p>';
return;
}
container.innerHTML = rssFeeds.map(feed => `
<div class="feed-card ${feed.enabled ? '' : 'disabled'}" data-id="${feed.id}">
<div class="feed-info">
<div class="feed-name">${escapeHtml(feed.name)}</div>
<div class="feed-meta">
<span class="feed-badge ${feed.category}">${getCategoryLabel(feed.category)}</span>
${feed.use_flaresolverr ? '<span class="feed-badge flaresolverr">🛡️</span>' : ''}
${feed.has_cookies ? '<span class="feed-badge cookies">🍪</span>' : ''}
</div>
<div class="feed-url">${maskUrl(feed.url)}</div>
</div>
<div class="feed-actions">
<button class="btn-icon" onclick="toggleRSSFeed('${feed.id}')" title="${feed.enabled ? 'Désactiver' : 'Activer'}">
${feed.enabled ? '✅' : '⏸️'}
</button>
<button class="btn-icon" onclick="deleteRSSFeed('${feed.id}')" title="Supprimer">🗑️</button>
</div>
</div>
`).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 = '<p class="loading">🔄 Test en cours...</p>';
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 = `
<div class="test-success">
<strong>✅ Succès !</strong> ${data.count} résultats trouvés
</div>
`;
} else {
resultDiv.innerHTML = `
<div class="test-error">
<strong>❌ Échec</strong> - Vérifiez l'URL et les cookies
</div>
`;
}
} catch (error) {
resultDiv.innerHTML = '<div class="test-error">❌ Erreur de connexion</div>';
}
}
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 = '<p class="empty-state">Aucun filtre configuré</p>';
return;
}
container.innerHTML = filterKeys.map(key => {
const filter = filtersConfig[key];
const values = filter.values || [];
const isEditing = editingFilter === key;
return `
<div class="filter-editor-item ${isEditing ? 'editing' : ''}" data-key="${escapeHtml(key)}">
<div class="filter-header" onclick="toggleFilterEdit('${escapeHtml(key)}')">
<span class="filter-icon">${filter.icon || '🏷️'}</span>
<span class="filter-name">${escapeHtml(filter.name || key)}</span>
<span class="filter-count">${values.length} valeurs</span>
<span class="filter-expand">${isEditing ? '▼' : '▶'}</span>
</div>
${isEditing ? `
<div class="filter-edit-content">
<div class="filter-meta-edit">
<input type="text" class="filter-name-input" value="${escapeHtml(filter.name || '')}" placeholder="Nom du filtre">
<input type="text" class="filter-icon-input" value="${escapeHtml(filter.icon || '')}" placeholder="Icône" maxlength="4">
<button class="btn-icon btn-delete" onclick="deleteFilter('${escapeHtml(key)}')" title="Supprimer">🗑️</button>
</div>
<div class="filter-values-edit">
<textarea class="filter-values-textarea" rows="4" placeholder="Une valeur par ligne">${values.join('\n')}</textarea>
</div>
<div class="filter-edit-actions">
<button class="btn btn-primary btn-sm" onclick="saveFilterEdit('${escapeHtml(key)}')">✓ Appliquer</button>
<button class="btn btn-secondary btn-sm" onclick="cancelFilterEdit()">✕ Annuler</button>
</div>
</div>
` : `
<div class="filter-values-preview">
${values.slice(0, 10).map(v => `<span class="value-chip">${escapeHtml(v)}</span>`).join('')}
${values.length > 10 ? `<span class="value-more">+${values.length - 10}</span>` : ''}
</div>
`}
</div>
`;
}).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 = '<p class="test-error">❌ Aucun filtre détecté</p>';
} else {
resultDiv.innerHTML = `
<p class="test-success">✅ Filtres détectés :</p>
<div class="parsed-results">
${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 `<div class="parsed-item"><strong>${icon} ${escapeHtml(name)}:</strong> ${escapeHtml(values)}</div>`;
}).join('')}
</div>
`;
}
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 = '<p class="empty-state">Aucun plugin installé</p>';
return;
}
container.innerHTML = plugins.map(p => `
<div class="plugin-item">
<div class="plugin-header">
<strong>${escapeHtml(p.name)}</strong>
<span class="plugin-version">v${escapeHtml(p.version)}</span>
</div>
<p class="plugin-description">${escapeHtml(p.description)}</p>
<small class="plugin-author">Par ${escapeHtml(p.author)}</small>
</div>
`).join('');
}
function populatePluginSelect(plugins) {
const select = document.getElementById('tcPlugin');
if (!select) return;
select.innerHTML = '<option value="">-- Sélectionner --</option>';
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 = `
<span class="status-badge status-disabled">⚫ Désactivé</span>
<p>Aucun client torrent configuré</p>
`;
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 = `
<span class="status-badge status-connected">🟢 Connecté</span>
<p><strong>${escapeHtml(config.plugin)}</strong> sur ${escapeHtml(urlDisplay)}</p>
`;
} else {
container.innerHTML = `
<span class="status-badge status-disconnected">🔴 Déconnecté</span>
<p><strong>${escapeHtml(config.plugin)}</strong> - Vérifiez les paramètres</p>
`;
}
}
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 = '<p class="loading">Test en cours...</p>';
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 = `
<p class="test-success">✅ Connexion réussie !</p>
${result.version ? `<p>Version: ${escapeHtml(result.version)}</p>` : ''}
`;
showToast('Connexion réussie', 'success');
} else {
resultDiv.innerHTML = `<p class="test-error">❌ ${escapeHtml(result.error || result.message)}</p>`;
showToast('Échec de la connexion', 'error');
}
} catch (error) {
resultDiv.innerHTML = `<p class="test-error">❌ Erreur: ${escapeHtml(error.message)}</p>`;
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 = '<p class="no-categories">Aucune catégorie configurée</p>';
return;
}
container.innerHTML = Object.entries(customCategories).map(([name, path]) => `
<div class="custom-category-item" data-name="${escapeHtml(name)}">
<span class="category-name">📁 ${escapeHtml(name)}</span>
<span class="category-path">${escapeHtml(path) || '(pas de chemin)'}</span>
<div class="category-actions">
<button class="btn-edit" onclick="editCategory('${escapeHtml(name)}')" title="Modifier">✏️</button>
<button class="btn-delete" onclick="deleteCategory('${escapeHtml(name)}')" title="Supprimer">🗑️</button>
</div>
</div>
`).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 = '<p class="error">Erreur de chargement des trackers</p>';
}
}
function renderDiscoverTrackers() {
const container = document.getElementById('discoverTrackersList');
if (!container || !allTrackers.length) {
container.innerHTML = '<p>Aucun tracker disponible</p>';
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 `
<div class="discover-tracker-item">
<input type="checkbox"
id="discover-tracker-${tracker.id}"
value="${tracker.id}"
${isChecked ? 'checked' : ''}
onchange="toggleDiscoverTracker('${tracker.id}')">
<label for="discover-tracker-${tracker.id}">
<span class="tracker-name">${tracker.name}</span>
${source ? `<span class="tracker-source ${source}">${sourceLabel}</span>` : ''}
</label>
</div>
`;
}).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);
}
});