/**
* 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 `
${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.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);
}
});