1673 lines
57 KiB
JavaScript
1673 lines
57 KiB
JavaScript
/**
|
||
* 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);
|
||
}
|
||
});
|