985 lines
34 KiB
JavaScript
985 lines
34 KiB
JavaScript
/**
|
|
* Lycostorrent - Search & Filter
|
|
* Filtrage, tri et pagination 100% côté client
|
|
*/
|
|
|
|
// État global
|
|
let allResults = []; // Tous les résultats de la recherche
|
|
let filteredResults = []; // Résultats après filtrage
|
|
let activeFilters = {}; // Filtres actifs { quality: ['1080p'], language: ['FRENCH', 'MULTI'], ... }
|
|
let availableFilters = {}; // Filtres disponibles extraits des résultats
|
|
|
|
// Pagination
|
|
let currentPage = 1;
|
|
const RESULTS_PER_PAGE = 50;
|
|
|
|
// Tri
|
|
let currentSort = { field: 'Seeders', order: 'desc' };
|
|
|
|
// Configuration des filtres (chargée dynamiquement)
|
|
let FILTER_CONFIG = {
|
|
// Fallback si l'API ne répond pas
|
|
Tracker: { name: 'Tracker', icon: '🌐', order: 999, fromRoot: true },
|
|
};
|
|
|
|
// ============================================================
|
|
// INITIALISATION
|
|
// ============================================================
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadFiltersConfig(); // Charger les filtres depuis l'API
|
|
loadTrackers();
|
|
setupEventListeners();
|
|
|
|
// Re-render lors du changement de taille de fenêtre
|
|
let resizeTimeout;
|
|
window.addEventListener('resize', () => {
|
|
clearTimeout(resizeTimeout);
|
|
resizeTimeout = setTimeout(() => {
|
|
if (filteredResults.length > 0) {
|
|
renderResults();
|
|
}
|
|
}, 250);
|
|
});
|
|
});
|
|
|
|
function setupEventListeners() {
|
|
// Recherche au clic ou Entrée
|
|
document.getElementById('search-btn').addEventListener('click', performSearch);
|
|
document.getElementById('search-input').addEventListener('keypress', (e) => {
|
|
if (e.key === 'Enter') performSearch();
|
|
});
|
|
|
|
// Effacer les filtres
|
|
document.getElementById('clear-filters').addEventListener('click', clearAllFilters);
|
|
|
|
// Toggle panel trackers
|
|
document.getElementById('toggleTrackers').addEventListener('click', () => {
|
|
const panel = document.getElementById('trackersPanel');
|
|
panel.classList.toggle('hidden');
|
|
});
|
|
|
|
// Tout sélectionner / désélectionner
|
|
document.getElementById('selectAllTrackers').addEventListener('click', () => {
|
|
document.querySelectorAll('#trackers-list input[type="checkbox"]').forEach(cb => cb.checked = true);
|
|
});
|
|
|
|
document.getElementById('deselectAllTrackers').addEventListener('click', () => {
|
|
document.querySelectorAll('#trackers-list input[type="checkbox"]').forEach(cb => cb.checked = false);
|
|
});
|
|
|
|
// Toggle filtres
|
|
document.getElementById('toggle-filters')?.addEventListener('click', () => {
|
|
const btn = document.getElementById('toggle-filters');
|
|
const content = document.getElementById('filters-content');
|
|
btn.classList.toggle('collapsed');
|
|
content.classList.toggle('collapsed');
|
|
btn.textContent = content.classList.contains('collapsed') ? '▶' : '▼';
|
|
});
|
|
}
|
|
|
|
// ============================================================
|
|
// CHARGEMENT DE LA CONFIG DES FILTRES
|
|
// ============================================================
|
|
|
|
async function loadFiltersConfig() {
|
|
try {
|
|
const response = await fetch('/api/filters');
|
|
const data = await response.json();
|
|
|
|
if (data.success && data.filters) {
|
|
// Construire FILTER_CONFIG depuis l'API
|
|
FILTER_CONFIG = {};
|
|
let order = 1;
|
|
|
|
for (const [key, filter] of Object.entries(data.filters)) {
|
|
FILTER_CONFIG[key] = {
|
|
name: filter.name || key,
|
|
icon: filter.icon || '🏷️',
|
|
order: order++
|
|
};
|
|
}
|
|
|
|
// Toujours ajouter Tracker à la fin
|
|
FILTER_CONFIG['Tracker'] = { name: 'Tracker', icon: '🌐', order: 999, fromRoot: true };
|
|
|
|
console.log('✅ Filtres chargés:', Object.keys(FILTER_CONFIG).length);
|
|
}
|
|
} catch (error) {
|
|
console.error('Erreur chargement config filtres:', error);
|
|
// Garder le fallback par défaut
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// CHARGEMENT DES TRACKERS
|
|
// ============================================================
|
|
|
|
async function loadTrackers() {
|
|
try {
|
|
const response = await fetch('/api/trackers');
|
|
const data = await response.json();
|
|
|
|
if (data.success && data.trackers) {
|
|
renderTrackers(data.trackers);
|
|
} else {
|
|
showError('Impossible de charger les trackers');
|
|
}
|
|
} catch (error) {
|
|
console.error('Erreur chargement trackers:', error);
|
|
showError('Erreur de connexion au serveur');
|
|
}
|
|
}
|
|
|
|
function renderTrackers(trackers) {
|
|
const container = document.getElementById('trackers-list');
|
|
|
|
if (trackers.length === 0) {
|
|
container.innerHTML = '<p class="no-trackers">Aucun tracker configuré</p>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = trackers.map(tracker => {
|
|
// Créer le badge de source
|
|
let sourceBadge = '';
|
|
if (tracker.sources && tracker.sources.length > 0) {
|
|
if (tracker.sources.includes('jackett') && tracker.sources.includes('prowlarr')) {
|
|
sourceBadge = '<span class="source-badge source-both" title="Jackett + Prowlarr">J+P</span>';
|
|
} else if (tracker.sources.includes('jackett')) {
|
|
sourceBadge = '<span class="source-badge source-jackett" title="Jackett">J</span>';
|
|
} else if (tracker.sources.includes('prowlarr')) {
|
|
sourceBadge = '<span class="source-badge source-prowlarr" title="Prowlarr">P</span>';
|
|
} else if (tracker.sources.includes('rss')) {
|
|
sourceBadge = '<span class="source-badge source-rss" title="RSS">RSS</span>';
|
|
}
|
|
} else {
|
|
if (tracker.source === 'jackett') {
|
|
sourceBadge = '<span class="source-badge source-jackett" title="Jackett">J</span>';
|
|
} else if (tracker.source === 'prowlarr') {
|
|
sourceBadge = '<span class="source-badge source-prowlarr" title="Prowlarr">P</span>';
|
|
}
|
|
}
|
|
|
|
return `
|
|
<div class="tracker-item">
|
|
<input type="checkbox" id="tracker-${escapeHtml(tracker.id)}" value="${escapeHtml(tracker.id)}" checked>
|
|
<label for="tracker-${escapeHtml(tracker.id)}">${escapeHtml(tracker.name)}</label>
|
|
${sourceBadge}
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
function getSelectedTrackers() {
|
|
const checkboxes = document.querySelectorAll('#trackers-list input[type="checkbox"]:checked');
|
|
return Array.from(checkboxes).map(cb => cb.value);
|
|
}
|
|
|
|
// ============================================================
|
|
// RECHERCHE
|
|
// ============================================================
|
|
|
|
async function performSearch() {
|
|
const query = document.getElementById('search-input').value.trim();
|
|
const category = document.getElementById('category-select').value;
|
|
const trackers = getSelectedTrackers();
|
|
|
|
// Validation
|
|
if (!query) {
|
|
showError('Veuillez entrer une recherche');
|
|
return;
|
|
}
|
|
|
|
if (trackers.length === 0) {
|
|
showError('Veuillez sélectionner au moins un tracker');
|
|
return;
|
|
}
|
|
|
|
// Afficher le loading
|
|
showLoading(true);
|
|
|
|
// Reset
|
|
activeFilters = {};
|
|
availableFilters = {};
|
|
currentPage = 1;
|
|
currentSort = { field: 'Seeders', order: 'desc' };
|
|
|
|
try {
|
|
const response = await fetch('/api/search', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ query, category, trackers })
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
allResults = data.results;
|
|
filteredResults = [...allResults];
|
|
|
|
// Trier par seeders par défaut
|
|
sortResults();
|
|
|
|
// Extraire les filtres disponibles depuis les résultats
|
|
extractAvailableFilters();
|
|
|
|
// Afficher les filtres et les résultats
|
|
renderFilters();
|
|
renderResults();
|
|
|
|
// Afficher la section des filtres
|
|
document.getElementById('filters-section').classList.remove('hidden');
|
|
|
|
} else {
|
|
showError(data.error || 'Erreur lors de la recherche');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Erreur recherche:', error);
|
|
showError('Erreur de connexion au serveur');
|
|
} finally {
|
|
showLoading(false);
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// TRI
|
|
// ============================================================
|
|
|
|
function sortResults() {
|
|
const { field, order } = currentSort;
|
|
|
|
filteredResults.sort((a, b) => {
|
|
let valA, valB;
|
|
|
|
switch (field) {
|
|
case 'Title':
|
|
valA = (a.Title || '').toLowerCase();
|
|
valB = (b.Title || '').toLowerCase();
|
|
break;
|
|
case 'Tracker':
|
|
valA = (a.Tracker || '').toLowerCase();
|
|
valB = (b.Tracker || '').toLowerCase();
|
|
break;
|
|
case 'Size':
|
|
valA = a.Size || 0;
|
|
valB = b.Size || 0;
|
|
break;
|
|
case 'Seeders':
|
|
valA = a.Seeders || 0;
|
|
valB = b.Seeders || 0;
|
|
break;
|
|
case 'PublishDate':
|
|
valA = a.PublishDateRaw || '';
|
|
valB = b.PublishDateRaw || '';
|
|
break;
|
|
default:
|
|
valA = a[field] || 0;
|
|
valB = b[field] || 0;
|
|
}
|
|
|
|
if (valA < valB) return order === 'asc' ? -1 : 1;
|
|
if (valA > valB) return order === 'asc' ? 1 : -1;
|
|
return 0;
|
|
});
|
|
}
|
|
|
|
function onSortChange(field) {
|
|
if (currentSort.field === field) {
|
|
// Inverser l'ordre si on clique sur la même colonne
|
|
currentSort.order = currentSort.order === 'asc' ? 'desc' : 'asc';
|
|
} else {
|
|
currentSort.field = field;
|
|
// Ordre par défaut selon le champ
|
|
currentSort.order = (field === 'Title' || field === 'Tracker') ? 'asc' : 'desc';
|
|
}
|
|
|
|
sortResults();
|
|
renderResults();
|
|
}
|
|
|
|
// ============================================================
|
|
// EXTRACTION DES FILTRES DISPONIBLES
|
|
// ============================================================
|
|
|
|
function extractAvailableFilters() {
|
|
availableFilters = {};
|
|
|
|
for (const torrent of allResults) {
|
|
const parsed = torrent.parsed || {};
|
|
|
|
for (const [key, config] of Object.entries(FILTER_CONFIG)) {
|
|
if (!availableFilters[key]) {
|
|
availableFilters[key] = {};
|
|
}
|
|
|
|
let values;
|
|
if (config.fromRoot) {
|
|
// Valeur directement sur le torrent (ex: Tracker)
|
|
values = torrent[key] ? [torrent[key]] : [];
|
|
} else {
|
|
// Valeur dans parsed
|
|
values = parsed[key] || [];
|
|
}
|
|
|
|
const valueArray = Array.isArray(values) ? values : [values];
|
|
for (const value of valueArray) {
|
|
if (value) {
|
|
availableFilters[key][value] = (availableFilters[key][value] || 0) + 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log('Filtres disponibles:', availableFilters);
|
|
}
|
|
|
|
// ============================================================
|
|
// RENDU DES FILTRES
|
|
// ============================================================
|
|
|
|
function renderFilters() {
|
|
const container = document.getElementById('filters-container');
|
|
container.innerHTML = '';
|
|
|
|
// Trier les filtres par ordre défini
|
|
const sortedFilters = Object.keys(availableFilters)
|
|
.filter(key => Object.keys(availableFilters[key]).length > 0)
|
|
.sort((a, b) => (FILTER_CONFIG[a]?.order || 99) - (FILTER_CONFIG[b]?.order || 99));
|
|
|
|
for (const filterKey of sortedFilters) {
|
|
const filterConfig = FILTER_CONFIG[filterKey];
|
|
const values = availableFilters[filterKey];
|
|
|
|
// Trier les valeurs par nombre d'occurrences
|
|
const sortedValues = Object.entries(values)
|
|
.sort((a, b) => b[1] - a[1]);
|
|
|
|
const filterHTML = `
|
|
<div class="filter-group" data-filter="${filterKey}">
|
|
<h4>${filterConfig.icon} ${filterConfig.name}</h4>
|
|
<div class="filter-values">
|
|
${sortedValues.map(([value, count]) => `
|
|
<label class="filter-checkbox">
|
|
<input
|
|
type="checkbox"
|
|
data-filter="${filterKey}"
|
|
data-value="${escapeHtml(value)}"
|
|
${isFilterActive(filterKey, value) ? 'checked' : ''}
|
|
>
|
|
<span class="filter-label">${escapeHtml(value)}</span>
|
|
<span class="filter-count">(${count})</span>
|
|
</label>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
container.innerHTML += filterHTML;
|
|
}
|
|
|
|
// Ajouter les event listeners sur les checkboxes
|
|
container.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
|
|
checkbox.addEventListener('change', onFilterChange);
|
|
});
|
|
|
|
updateResultsCount();
|
|
}
|
|
|
|
function isFilterActive(filterKey, value) {
|
|
return activeFilters[filterKey]?.includes(value) || false;
|
|
}
|
|
|
|
// ============================================================
|
|
// GESTION DES FILTRES
|
|
// ============================================================
|
|
|
|
function onFilterChange(event) {
|
|
const checkbox = event.target;
|
|
const filterKey = checkbox.dataset.filter;
|
|
const value = checkbox.dataset.value;
|
|
|
|
if (!activeFilters[filterKey]) {
|
|
activeFilters[filterKey] = [];
|
|
}
|
|
|
|
if (checkbox.checked) {
|
|
if (!activeFilters[filterKey].includes(value)) {
|
|
activeFilters[filterKey].push(value);
|
|
}
|
|
} else {
|
|
activeFilters[filterKey] = activeFilters[filterKey].filter(v => v !== value);
|
|
if (activeFilters[filterKey].length === 0) {
|
|
delete activeFilters[filterKey];
|
|
}
|
|
}
|
|
|
|
console.log('Filtres actifs:', activeFilters);
|
|
|
|
// Appliquer les filtres
|
|
applyFilters();
|
|
}
|
|
|
|
function applyFilters() {
|
|
// Reset pagination
|
|
currentPage = 1;
|
|
|
|
if (Object.keys(activeFilters).length === 0) {
|
|
filteredResults = [...allResults];
|
|
} else {
|
|
filteredResults = allResults.filter(torrent => {
|
|
const parsed = torrent.parsed || {};
|
|
|
|
for (const [filterKey, selectedValues] of Object.entries(activeFilters)) {
|
|
if (selectedValues.length === 0) continue;
|
|
|
|
let torrentValues;
|
|
if (FILTER_CONFIG[filterKey]?.fromRoot) {
|
|
torrentValues = torrent[filterKey] ? [torrent[filterKey]] : [];
|
|
} else {
|
|
torrentValues = parsed[filterKey] || [];
|
|
}
|
|
|
|
const torrentValuesArray = Array.isArray(torrentValues) ? torrentValues : [torrentValues];
|
|
const hasMatch = selectedValues.some(val => torrentValuesArray.includes(val));
|
|
|
|
if (!hasMatch) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
// Réappliquer le tri
|
|
sortResults();
|
|
renderResults();
|
|
updateResultsCount();
|
|
}
|
|
|
|
function clearAllFilters() {
|
|
activeFilters = {};
|
|
currentPage = 1;
|
|
|
|
document.querySelectorAll('#filters-container input[type="checkbox"]').forEach(cb => {
|
|
cb.checked = false;
|
|
});
|
|
|
|
filteredResults = [...allResults];
|
|
sortResults();
|
|
renderResults();
|
|
updateResultsCount();
|
|
}
|
|
|
|
function updateResultsCount() {
|
|
const countEl = document.getElementById('results-count');
|
|
const total = allResults.length;
|
|
const filtered = filteredResults.length;
|
|
|
|
if (total === filtered) {
|
|
countEl.textContent = `(${total} résultats)`;
|
|
} else {
|
|
countEl.textContent = `(${filtered} / ${total} résultats)`;
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// PAGINATION
|
|
// ============================================================
|
|
|
|
function getTotalPages() {
|
|
return Math.ceil(filteredResults.length / RESULTS_PER_PAGE);
|
|
}
|
|
|
|
function getPageResults() {
|
|
const start = (currentPage - 1) * RESULTS_PER_PAGE;
|
|
const end = start + RESULTS_PER_PAGE;
|
|
return filteredResults.slice(start, end);
|
|
}
|
|
|
|
function goToPage(page) {
|
|
const totalPages = getTotalPages();
|
|
if (page < 1 || page > totalPages) return;
|
|
|
|
currentPage = page;
|
|
renderResults();
|
|
|
|
// Scroll vers le haut des résultats
|
|
document.getElementById('results-section').scrollIntoView({ behavior: 'smooth' });
|
|
}
|
|
|
|
function renderPagination() {
|
|
const totalPages = getTotalPages();
|
|
if (totalPages <= 1) return '';
|
|
|
|
const pages = [];
|
|
const maxVisiblePages = 7;
|
|
|
|
// Toujours afficher la première page
|
|
pages.push(1);
|
|
|
|
if (totalPages <= maxVisiblePages) {
|
|
for (let i = 2; i <= totalPages; i++) {
|
|
pages.push(i);
|
|
}
|
|
} else {
|
|
// Logique pour afficher les pages autour de la page courante
|
|
let start = Math.max(2, currentPage - 2);
|
|
let end = Math.min(totalPages - 1, currentPage + 2);
|
|
|
|
if (currentPage <= 3) {
|
|
end = 5;
|
|
}
|
|
if (currentPage >= totalPages - 2) {
|
|
start = totalPages - 4;
|
|
}
|
|
|
|
if (start > 2) {
|
|
pages.push('...');
|
|
}
|
|
|
|
for (let i = start; i <= end; i++) {
|
|
pages.push(i);
|
|
}
|
|
|
|
if (end < totalPages - 1) {
|
|
pages.push('...');
|
|
}
|
|
|
|
pages.push(totalPages);
|
|
}
|
|
|
|
const startResult = (currentPage - 1) * RESULTS_PER_PAGE + 1;
|
|
const endResult = Math.min(currentPage * RESULTS_PER_PAGE, filteredResults.length);
|
|
|
|
return `
|
|
<div class="pagination">
|
|
<div class="pagination-info">
|
|
Résultats ${startResult} - ${endResult} sur ${filteredResults.length}
|
|
</div>
|
|
<div class="pagination-controls">
|
|
<button class="pagination-btn" onclick="goToPage(${currentPage - 1})" ${currentPage === 1 ? 'disabled' : ''}>
|
|
← Précédent
|
|
</button>
|
|
${pages.map(page => {
|
|
if (page === '...') {
|
|
return '<span class="pagination-ellipsis">...</span>';
|
|
}
|
|
return `<button class="pagination-btn ${page === currentPage ? 'active' : ''}" onclick="goToPage(${page})">${page}</button>`;
|
|
}).join('')}
|
|
<button class="pagination-btn" onclick="goToPage(${currentPage + 1})" ${currentPage === totalPages ? 'disabled' : ''}>
|
|
Suivant →
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// ============================================================
|
|
// RENDU DES RÉSULTATS
|
|
// ============================================================
|
|
|
|
function renderResults() {
|
|
const container = document.getElementById('results-container');
|
|
|
|
if (filteredResults.length === 0) {
|
|
if (allResults.length === 0) {
|
|
container.innerHTML = '<p class="no-results">Aucun résultat trouvé</p>';
|
|
} else {
|
|
container.innerHTML = '<p class="no-results">Aucun résultat ne correspond aux filtres sélectionnés</p>';
|
|
}
|
|
return;
|
|
}
|
|
|
|
const pageResults = getPageResults();
|
|
const isMobile = window.innerWidth <= 768;
|
|
|
|
if (isMobile) {
|
|
// Mode cards pour mobile
|
|
container.innerHTML = `
|
|
${renderPagination()}
|
|
<div class="results-cards">
|
|
${pageResults.map(torrent => renderTorrentCard(torrent)).join('')}
|
|
</div>
|
|
${renderPagination()}
|
|
`;
|
|
} else {
|
|
// Mode table pour desktop
|
|
container.innerHTML = `
|
|
${renderPagination()}
|
|
<table class="results-table">
|
|
<thead>
|
|
<tr>
|
|
<th class="col-name sortable" onclick="onSortChange('Title')">
|
|
Nom ${getSortIcon('Title')}
|
|
</th>
|
|
<th class="col-tracker sortable" onclick="onSortChange('Tracker')">
|
|
Tracker ${getSortIcon('Tracker')}
|
|
</th>
|
|
<th class="col-size sortable" onclick="onSortChange('Size')">
|
|
Taille ${getSortIcon('Size')}
|
|
</th>
|
|
<th class="col-seeders sortable" onclick="onSortChange('Seeders')">
|
|
Seeders ${getSortIcon('Seeders')}
|
|
</th>
|
|
<th class="col-date sortable" onclick="onSortChange('PublishDate')">
|
|
Date ${getSortIcon('PublishDate')}
|
|
</th>
|
|
<th class="col-actions">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${pageResults.map(torrent => renderTorrentRow(torrent)).join('')}
|
|
</tbody>
|
|
</table>
|
|
${renderPagination()}
|
|
`;
|
|
}
|
|
}
|
|
|
|
function renderTorrentCard(torrent) {
|
|
const parsed = torrent.parsed || {};
|
|
|
|
const badges = [];
|
|
if (parsed.quality?.length) {
|
|
badges.push(...parsed.quality.map(q => `<span class="badge badge-quality">${escapeHtml(q)}</span>`));
|
|
}
|
|
if (parsed.source?.length) {
|
|
badges.push(...parsed.source.map(s => `<span class="badge badge-source">${escapeHtml(s)}</span>`));
|
|
}
|
|
if (parsed.language?.length) {
|
|
badges.push(...parsed.language.map(l => `<span class="badge badge-language">${escapeHtml(l)}</span>`));
|
|
}
|
|
|
|
const seedersClass = getSeedersClass(torrent.Seeders);
|
|
|
|
// Sanitize URLs
|
|
const magnetUrl = sanitizeUrl(torrent.MagnetUri);
|
|
const downloadUrl = sanitizeUrl(torrent.Link);
|
|
const detailsUrl = sanitizeUrl(torrent.Details);
|
|
|
|
return `
|
|
<div class="result-card-mobile">
|
|
<div class="torrent-title">${escapeHtml(torrent.Title)}</div>
|
|
<div class="torrent-badges">${badges.join('')}</div>
|
|
<div class="result-meta">
|
|
<span>📁 ${escapeHtml(torrent.SizeFormatted || 'N/A')}</span>
|
|
<span class="${seedersClass}">🌱 ${parseInt(torrent.Seeders) || 0}</span>
|
|
<span>🏷️ ${escapeHtml(torrent.Tracker)}</span>
|
|
</div>
|
|
<div class="result-actions">
|
|
${magnetUrl ? `<a href="${magnetUrl}" class="btn-magnet-mobile" title="Magnet">🧲</a>` : ''}
|
|
${downloadUrl ? `<a href="${downloadUrl}" class="btn-download-mobile" title="Télécharger" target="_blank">⬇️</a>` : ''}
|
|
${detailsUrl ? `<a href="${detailsUrl}" class="btn-details-mobile" title="Détails" target="_blank">🔗</a>` : ''}
|
|
${torrentClientEnabled && (magnetUrl || (torrentClientSupportsTorrentFiles && downloadUrl)) ? `<button class="btn-send-client-mobile" title="Envoyer au client" onclick="sendToTorrentClient('${magnetUrl || downloadUrl}', this)">📥</button>` : ''}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function getSortIcon(field) {
|
|
if (currentSort.field !== field) {
|
|
return '<span class="sort-icon">⇅</span>';
|
|
}
|
|
return currentSort.order === 'asc'
|
|
? '<span class="sort-icon active">↑</span>'
|
|
: '<span class="sort-icon active">↓</span>';
|
|
}
|
|
|
|
function renderTorrentRow(torrent) {
|
|
const parsed = torrent.parsed || {};
|
|
|
|
const badges = [];
|
|
|
|
if (parsed.quality?.length) {
|
|
badges.push(...parsed.quality.map(q => `<span class="badge badge-quality">${escapeHtml(q)}</span>`));
|
|
}
|
|
if (parsed.source?.length) {
|
|
badges.push(...parsed.source.map(s => `<span class="badge badge-source">${escapeHtml(s)}</span>`));
|
|
}
|
|
if (parsed.video_codec?.length) {
|
|
badges.push(...parsed.video_codec.map(c => `<span class="badge badge-codec">${escapeHtml(c)}</span>`));
|
|
}
|
|
if (parsed.language?.length) {
|
|
badges.push(...parsed.language.map(l => `<span class="badge badge-language">${escapeHtml(l)}</span>`));
|
|
}
|
|
if (parsed.hdr?.length) {
|
|
badges.push(...parsed.hdr.map(h => `<span class="badge badge-hdr">${escapeHtml(h)}</span>`));
|
|
}
|
|
|
|
const seedersClass = getSeedersClass(torrent.Seeders);
|
|
|
|
// Sanitize URLs
|
|
const magnetUrl = sanitizeUrl(torrent.MagnetUri);
|
|
const downloadUrl = sanitizeUrl(torrent.Link);
|
|
const detailsUrl = sanitizeUrl(torrent.Details);
|
|
|
|
return `
|
|
<tr>
|
|
<td class="col-name">
|
|
<div class="torrent-title">${escapeHtml(torrent.Title)}</div>
|
|
<div class="torrent-badges">${badges.join('')}</div>
|
|
</td>
|
|
<td class="col-tracker">${escapeHtml(torrent.Tracker)}</td>
|
|
<td class="col-size">${escapeHtml(torrent.SizeFormatted || 'N/A')}</td>
|
|
<td class="col-seeders ${seedersClass}">${parseInt(torrent.Seeders) || 0}</td>
|
|
<td class="col-date">${escapeHtml(torrent.PublishDate || 'N/A')}</td>
|
|
<td class="col-actions">
|
|
${magnetUrl ? `<a href="${magnetUrl}" class="btn-magnet" title="Magnet">🧲</a>` : ''}
|
|
${downloadUrl ? `<a href="${downloadUrl}" class="btn-download" title="Télécharger" target="_blank">⬇️</a>` : ''}
|
|
${detailsUrl ? `<a href="${detailsUrl}" class="btn-details" title="Détails" target="_blank">🔗</a>` : ''}
|
|
${torrentClientEnabled && (magnetUrl || (torrentClientSupportsTorrentFiles && downloadUrl)) ? `<button class="btn-send-client" title="Envoyer au client" onclick="sendToTorrentClient('${magnetUrl || downloadUrl}', this)">📥</button>` : ''}
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
|
|
function getSeedersClass(seeders) {
|
|
if (!seeders || seeders === 0) return 'seeders-none';
|
|
if (seeders < 5) return 'seeders-low';
|
|
if (seeders < 20) return 'seeders-medium';
|
|
return 'seeders-high';
|
|
}
|
|
|
|
// ============================================================
|
|
// UTILITAIRES
|
|
// ============================================================
|
|
|
|
function escapeHtml(text) {
|
|
if (!text) return '';
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function sanitizeUrl(url) {
|
|
if (!url) return '';
|
|
|
|
// Autoriser uniquement http, https, et magnet
|
|
const allowedProtocols = ['http:', 'https:', 'magnet:'];
|
|
|
|
try {
|
|
// Pour les URLs magnet, vérifier le préfixe
|
|
if (url.startsWith('magnet:')) {
|
|
return url;
|
|
}
|
|
|
|
const parsed = new URL(url);
|
|
if (!allowedProtocols.includes(parsed.protocol)) {
|
|
console.warn('URL avec protocole non autorisé:', parsed.protocol);
|
|
return '';
|
|
}
|
|
return url;
|
|
} catch (e) {
|
|
// Si ce n'est pas une URL valide, retourner vide
|
|
console.warn('URL invalide:', url);
|
|
return '';
|
|
}
|
|
}
|
|
|
|
function showLoading(show) {
|
|
const overlay = document.getElementById('loading-overlay');
|
|
if (show) {
|
|
overlay.classList.remove('hidden');
|
|
} else {
|
|
overlay.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
function showError(message) {
|
|
alert(message);
|
|
}
|
|
|
|
// ============================================================
|
|
// CLIENT TORRENT
|
|
// ============================================================
|
|
|
|
let torrentClientEnabled = false;
|
|
let torrentClientSupportsTorrentFiles = false;
|
|
|
|
async function checkTorrentClient() {
|
|
try {
|
|
const response = await fetch('/api/torrent-client/status');
|
|
const data = await response.json();
|
|
torrentClientEnabled = data.success && data.enabled && data.connected;
|
|
// Par défaut true si non spécifié (qBittorrent supporte les .torrent)
|
|
torrentClientSupportsTorrentFiles = data.supportsTorrentFiles !== false;
|
|
console.log('🔌 Client torrent:', torrentClientEnabled ? 'connecté' : 'non connecté',
|
|
'| Supporte .torrent:', torrentClientSupportsTorrentFiles);
|
|
} catch (error) {
|
|
torrentClientEnabled = false;
|
|
torrentClientSupportsTorrentFiles = false;
|
|
console.log('🔌 Client torrent: erreur de connexion');
|
|
}
|
|
}
|
|
|
|
async function sendToTorrentClient(url, button) {
|
|
if (!url) {
|
|
showToast('Aucun lien disponible', 'error');
|
|
return;
|
|
}
|
|
|
|
// Afficher le modal de sélection
|
|
showTorrentOptionsModal(url, button);
|
|
}
|
|
|
|
async function showTorrentOptionsModal(url, button) {
|
|
// Créer le modal s'il n'existe pas
|
|
let modal = document.getElementById('torrentOptionsModal');
|
|
if (!modal) {
|
|
modal = document.createElement('div');
|
|
modal.id = 'torrentOptionsModal';
|
|
modal.className = 'torrent-options-modal';
|
|
modal.innerHTML = `
|
|
<div class="torrent-options-content">
|
|
<h3>📥 Options de téléchargement</h3>
|
|
<div class="torrent-option-group">
|
|
<label for="torrentCategory">Catégorie</label>
|
|
<select id="torrentCategory">
|
|
<option value="">-- Aucune --</option>
|
|
</select>
|
|
</div>
|
|
<div class="torrent-option-group">
|
|
<label for="torrentSavePath">Dossier (optionnel)</label>
|
|
<input type="text" id="torrentSavePath" placeholder="/chemin/vers/dossier">
|
|
</div>
|
|
<div class="torrent-option-group checkbox-group">
|
|
<input type="checkbox" id="torrentPaused">
|
|
<label for="torrentPaused">Démarrer en pause</label>
|
|
</div>
|
|
<div class="torrent-options-buttons">
|
|
<button class="btn-cancel" onclick="closeTorrentOptionsModal()">Annuler</button>
|
|
<button class="btn-confirm" id="confirmTorrentAdd">Envoyer</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(modal);
|
|
|
|
// Fermer en cliquant à l'extérieur
|
|
modal.addEventListener('click', (e) => {
|
|
if (e.target === modal) closeTorrentOptionsModal();
|
|
});
|
|
}
|
|
|
|
// Charger les catégories
|
|
const categorySelect = document.getElementById('torrentCategory');
|
|
const savePathInput = document.getElementById('torrentSavePath');
|
|
categorySelect.innerHTML = '<option value="">-- Chargement... --</option>';
|
|
|
|
let categoriesWithPaths = {};
|
|
|
|
try {
|
|
const response = await fetch('/api/torrent-client/categories');
|
|
const data = await response.json();
|
|
|
|
categorySelect.innerHTML = '<option value="">-- Aucune --</option>';
|
|
if (data.success && data.categories) {
|
|
data.categories.forEach(cat => {
|
|
categorySelect.innerHTML += `<option value="${escapeHtml(cat)}">${escapeHtml(cat)}</option>`;
|
|
});
|
|
// Stocker les chemins personnalisés
|
|
categoriesWithPaths = data.custom_categories || {};
|
|
}
|
|
} catch (error) {
|
|
categorySelect.innerHTML = '<option value="">-- Aucune --</option>';
|
|
}
|
|
|
|
// Auto-remplir le chemin quand on sélectionne une catégorie
|
|
categorySelect.onchange = () => {
|
|
const selectedCat = categorySelect.value;
|
|
if (selectedCat && categoriesWithPaths[selectedCat]) {
|
|
savePathInput.value = categoriesWithPaths[selectedCat];
|
|
} else {
|
|
savePathInput.value = '';
|
|
}
|
|
};
|
|
|
|
// Reset les champs
|
|
savePathInput.value = '';
|
|
document.getElementById('torrentPaused').checked = false;
|
|
|
|
// Configurer le bouton de confirmation
|
|
const confirmBtn = document.getElementById('confirmTorrentAdd');
|
|
confirmBtn.onclick = async () => {
|
|
const category = document.getElementById('torrentCategory').value;
|
|
const savePath = document.getElementById('torrentSavePath').value.trim();
|
|
const paused = document.getElementById('torrentPaused').checked;
|
|
|
|
closeTorrentOptionsModal();
|
|
await doSendToTorrentClient(url, button, category, savePath, paused);
|
|
};
|
|
|
|
// Afficher le modal
|
|
modal.classList.add('visible');
|
|
}
|
|
|
|
function closeTorrentOptionsModal() {
|
|
const modal = document.getElementById('torrentOptionsModal');
|
|
if (modal) {
|
|
modal.classList.remove('visible');
|
|
}
|
|
}
|
|
|
|
async function doSendToTorrentClient(url, button, category, savePath, paused) {
|
|
const originalText = button.textContent;
|
|
button.textContent = '⏳';
|
|
button.disabled = true;
|
|
|
|
try {
|
|
const body = { url: url };
|
|
if (category) body.category = category;
|
|
if (savePath) body.save_path = savePath;
|
|
if (paused) body.paused = paused;
|
|
|
|
const response = await fetch('/api/torrent-client/add', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body)
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
button.textContent = '✅';
|
|
showToast('Torrent envoyé !', 'success');
|
|
setTimeout(() => {
|
|
button.textContent = '📥';
|
|
button.disabled = false;
|
|
}, 2000);
|
|
} else {
|
|
button.textContent = '❌';
|
|
showToast(data.error || 'Erreur', 'error');
|
|
setTimeout(() => {
|
|
button.textContent = '📥';
|
|
button.disabled = false;
|
|
}, 2000);
|
|
}
|
|
} catch (error) {
|
|
button.textContent = '❌';
|
|
showToast('Erreur de connexion', 'error');
|
|
setTimeout(() => {
|
|
button.textContent = '📥';
|
|
button.disabled = false;
|
|
}, 2000);
|
|
}
|
|
}
|
|
|
|
function showToast(message, type = 'info') {
|
|
// Créer le toast s'il n'existe pas
|
|
let toast = document.getElementById('toast');
|
|
if (!toast) {
|
|
toast = document.createElement('div');
|
|
toast.id = 'toast';
|
|
toast.className = 'toast hidden';
|
|
document.body.appendChild(toast);
|
|
}
|
|
|
|
toast.textContent = message;
|
|
toast.className = `toast ${type}`;
|
|
|
|
setTimeout(() => toast.classList.add('hidden'), 3000);
|
|
}
|
|
|
|
// Vérifier le client torrent au chargement
|
|
checkTorrentClient(); |