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

902 lines
35 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Lycostorrent - Latest Releases
* Page des nouveautés avec enrichissement TMDb/Last.fm
*/
// Variables globales
let selectedCategory = 'movies';
let selectedTrackers = [];
let availableTrackers = [];
let allResults = [];
let selectedYears = ['all']; // Par défaut: tous
// Images par défaut en base64 (évite les problèmes d'échappement)
const DEFAULT_POSTER_B64 = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyODAiIGhlaWdodD0iNDIwIj48cmVjdCB3aWR0aD0iMjgwIiBoZWlnaHQ9IjQyMCIgZmlsbD0iIzMzMyIvPjx0ZXh0IHg9IjE0MCIgeT0iMjAwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNjY2IiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iNDAiPvCfjqw8L3RleHQ+PHRleHQgeD0iMTQwIiB5PSIyNDAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM2NjYiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxNCI+Tm8gSW1hZ2U8L3RleHQ+PC9zdmc+';
const DEFAULT_COVER_B64 = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MDAiIGhlaWdodD0iNDAwIj48cmVjdCB3aWR0aD0iNDAwIiBoZWlnaHQ9IjQwMCIgZmlsbD0iIzMzMyIvPjx0ZXh0IHg9IjIwMCIgeT0iMTkwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNjY2IiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iNjAiPvCfjrU8L3RleHQ+PHRleHQgeD0iMjAwIiB5PSIyNDAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM2NjYiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxOCI+Tm8gQ292ZXI8L3RleHQ+PC9zdmc+';
const DEFAULT_BACKDROP_B64 = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI5MDAiIGhlaWdodD0iNDAwIj48cmVjdCB3aWR0aD0iOTAwIiBoZWlnaHQ9IjQwMCIgZmlsbD0iIzIyMiIvPjwvc3ZnPg==';
function getDefaultPosterUrl() {
return DEFAULT_POSTER_B64;
}
function getDefaultCoverUrl() {
return DEFAULT_COVER_B64;
}
function getDefaultBackdropUrl() {
return DEFAULT_BACKDROP_B64;
}
// Initialisation
document.addEventListener('DOMContentLoaded', function() {
console.log('🚀 Page latest.js chargée');
initializeApp();
});
function initializeApp() {
// Vérifier le client torrent en premier
checkTorrentClient();
loadTrackers();
// Event listeners
document.getElementById('toggleTrackers').addEventListener('click', toggleTrackersPanel);
document.getElementById('selectAllTrackers').addEventListener('click', selectAllTrackers);
document.getElementById('deselectAllTrackers').addEventListener('click', deselectAllTrackers);
document.getElementById('loadLatestBtn').addEventListener('click', () => loadLatestReleases(true));
// Bouton refresh live (dans le header des résultats)
document.getElementById('refreshLiveBtn')?.addEventListener('click', () => loadLatestReleases(true));
// Pastilles d'années
document.querySelectorAll('.year-pill').forEach(pill => {
pill.addEventListener('click', function() {
handleYearPillClick(this);
});
});
// Catégories
document.querySelectorAll('.category-btn').forEach(btn => {
btn.addEventListener('click', function() {
selectCategory(this.dataset.category);
});
});
// Modal
document.querySelector('.modal-close').addEventListener('click', closeModal);
document.getElementById('detailsModal').addEventListener('click', function(e) {
if (e.target === this) closeModal();
});
// Gestion erreurs images
document.addEventListener('error', function(e) {
if (e.target.tagName === 'IMG') {
const fallback = e.target.dataset.fallback;
if (fallback === 'poster') e.target.src = getDefaultPosterUrl();
else if (fallback === 'cover') e.target.src = getDefaultCoverUrl();
else if (fallback === 'backdrop') e.target.src = getDefaultBackdropUrl();
}
}, true);
// Charger depuis le cache au démarrage (après chargement des trackers)
setTimeout(() => {
loadFromCacheOrLive();
}, 500);
}
// Gestion des pastilles d'années
function handleYearPillClick(pill) {
const year = pill.dataset.year;
if (year === 'all') {
// Clic sur "Tous" -> désactive tout le reste
selectedYears = ['all'];
document.querySelectorAll('.year-pill').forEach(p => p.classList.remove('active'));
pill.classList.add('active');
} else {
// Clic sur une année spécifique
// Retirer "all" s'il était sélectionné
if (selectedYears.includes('all')) {
selectedYears = [];
document.querySelector('.year-pill[data-year="all"]').classList.remove('active');
}
// Toggle l'année cliquée
if (selectedYears.includes(year)) {
// Désactiver
selectedYears = selectedYears.filter(y => y !== year);
pill.classList.remove('active');
// Si plus rien de sélectionné, réactiver "Tous"
if (selectedYears.length === 0) {
selectedYears = ['all'];
document.querySelector('.year-pill[data-year="all"]').classList.add('active');
}
} else {
// Activer
selectedYears.push(year);
pill.classList.add('active');
}
}
// Re-filtrer les résultats
if (allResults.length > 0) {
displayResults(allResults);
}
}
// Chargement des trackers (inclut les RSS pour les nouveautés)
async function loadTrackers() {
try {
showLoader(true);
const response = await fetch('/api/trackers?include_rss=true');
const data = await response.json();
if (data.success) {
availableTrackers = data.trackers;
displayTrackers(availableTrackers);
} else {
showMessage('Erreur lors du chargement des trackers', 'error');
}
} catch (error) {
console.error('Erreur chargement trackers:', error);
showMessage('Impossible de charger les trackers', 'error');
} finally {
showLoader(false);
}
}
function displayTrackers(trackers) {
const trackersList = document.getElementById('trackersList');
if (trackers.length === 0) {
trackersList.innerHTML = '<p class="no-trackers">Aucun tracker configuré</p>';
return;
}
// Trackers sélectionnés par défaut
const defaultTrackers = ['yggtorrent', 'sharewood-api'];
trackersList.innerHTML = '';
trackers.forEach(tracker => {
const trackerItem = document.createElement('div');
trackerItem.className = 'tracker-item';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = `tracker-${tracker.id}`;
checkbox.value = tracker.id;
checkbox.checked = defaultTrackers.includes(tracker.id.toLowerCase().replace(/\s+/g, '-'));
checkbox.addEventListener('change', updateSelectedTrackers);
const label = document.createElement('label');
label.htmlFor = `tracker-${tracker.id}`;
label.textContent = tracker.name;
// Badge de source
let sourceBadge = '';
if (tracker.sources && tracker.sources.length > 0) {
if (tracker.sources.includes('rss')) {
sourceBadge = '<span class="source-badge source-rss" title="Flux RSS">RSS</span>';
} else 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.source) {
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>';
}
}
trackerItem.appendChild(checkbox);
trackerItem.appendChild(label);
if (sourceBadge) {
const badgeSpan = document.createElement('span');
badgeSpan.innerHTML = sourceBadge;
trackerItem.appendChild(badgeSpan.firstChild);
}
trackersList.appendChild(trackerItem);
});
updateSelectedTrackers();
}
function updateSelectedTrackers() {
selectedTrackers = Array.from(document.querySelectorAll('#trackersList input[type="checkbox"]:checked'))
.map(cb => cb.value);
}
function toggleTrackersPanel() {
document.getElementById('trackersPanel').classList.toggle('hidden');
}
function selectAllTrackers() {
document.querySelectorAll('#trackersList input[type="checkbox"]').forEach(cb => cb.checked = true);
updateSelectedTrackers();
}
function deselectAllTrackers() {
document.querySelectorAll('#trackersList input[type="checkbox"]').forEach(cb => cb.checked = false);
updateSelectedTrackers();
}
function selectCategory(category) {
selectedCategory = category;
document.querySelectorAll('.category-btn').forEach(btn => btn.classList.remove('active'));
event.target.classList.add('active');
// Charger depuis le cache si disponible
loadFromCacheOrLive();
}
// Variable pour savoir si on utilise le cache
let usingCache = false;
// Vérifier et charger depuis le cache au démarrage
async function loadFromCacheOrLive() {
try {
// Vérifier si le cache existe pour cette catégorie
const response = await fetch(`/api/cache/data/latest/${selectedCategory}`);
const data = await response.json();
if (data.success && data.cached && data.data && data.data.length > 0) {
// Afficher les données du cache
usingCache = true;
allResults = data.data;
displayResults(allResults);
showCacheInfo(data.timestamp);
console.log(`📦 Chargé depuis le cache: ${data.data.length} résultats`);
}
} catch (error) {
console.log('Pas de cache disponible');
}
}
// Afficher les infos du cache
function showCacheInfo(timestamp) {
const cacheInfo = document.getElementById('cacheInfo');
const cacheTimestamp = document.getElementById('cacheTimestamp');
if (cacheInfo && timestamp) {
const date = new Date(timestamp);
const now = new Date();
const diffMinutes = Math.floor((now - date) / 60000);
let timeAgo;
if (diffMinutes < 1) {
timeAgo = "à l'instant";
} else if (diffMinutes < 60) {
timeAgo = `il y a ${diffMinutes} min`;
} else {
const hours = Math.floor(diffMinutes / 60);
timeAgo = `il y a ${hours}h`;
}
cacheTimestamp.textContent = timeAgo;
cacheInfo.classList.remove('hidden');
}
}
// Masquer les infos du cache
function hideCacheInfo() {
const cacheInfo = document.getElementById('cacheInfo');
if (cacheInfo) {
cacheInfo.classList.add('hidden');
}
usingCache = false;
}
// Chargement des dernières sorties (en direct)
async function loadLatestReleases(forceRefresh = true) {
if (selectedTrackers.length === 0) {
showMessage('Veuillez sélectionner au moins un tracker', 'error');
return;
}
const limit = parseInt(document.getElementById('limitSelect').value);
try {
showLoader(true);
hideCacheInfo();
const response = await fetch('/api/latest', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
trackers: selectedTrackers,
category: selectedCategory,
limit: limit
})
});
const data = await response.json();
if (data.success) {
allResults = data.results;
displayResults(allResults);
if (allResults.length > 0) {
showMessage(`${allResults.length} nouveautés trouvées`, 'success');
} else {
showMessage('Aucune nouveauté trouvée', 'info');
}
} else {
showMessage(data.error || 'Erreur lors de la récupération', 'error');
}
} catch (error) {
console.error('Erreur:', error);
showMessage('Erreur lors de la récupération des nouveautés', 'error');
} finally {
showLoader(false);
}
}
function displayResults(results) {
const resultsSection = document.getElementById('latestResults');
const resultsGrid = document.getElementById('resultsGrid');
const resultsCount = document.getElementById('resultsCount');
const yearFiltersSection = document.getElementById('yearFilters');
const filterCountSpan = document.getElementById('filterCount');
// Afficher la section des filtres
yearFiltersSection.classList.remove('hidden');
// Filtrer par années sélectionnées
let filteredResults = results;
if (!selectedYears.includes('all')) {
filteredResults = results.filter(result => {
const tmdb = result.tmdb || {};
const year = tmdb.year ? parseInt(tmdb.year) : null;
// Si pas d'année TMDb, on garde le résultat (on ne peut pas filtrer)
if (!year) return true;
// Vérifier si l'année correspond à une des années sélectionnées
for (const selectedYear of selectedYears) {
if (selectedYear === 'old') {
// ≤2022
if (year <= 2022) return true;
} else {
// Année spécifique
if (year === parseInt(selectedYear)) return true;
}
}
return false;
});
}
// Mettre à jour le compteur de filtre
if (!selectedYears.includes('all')) {
const yearsText = selectedYears.map(y => y === 'old' ? '≤2022' : y).join(', ');
filterCountSpan.textContent = `(${filteredResults.length}/${results.length})`;
} else {
filterCountSpan.textContent = '';
}
if (filteredResults.length === 0) {
resultsSection.classList.remove('hidden');
resultsCount.textContent = `0 nouveauté (${results.length} total)`;
resultsGrid.innerHTML = '<p class="no-results">Aucun résultat pour les années sélectionnées</p>';
return;
}
resultsSection.classList.remove('hidden');
if (!selectedYears.includes('all')) {
resultsCount.textContent = `${filteredResults.length} nouveauté${filteredResults.length > 1 ? 's' : ''} sur ${results.length}`;
} else {
resultsCount.textContent = `${filteredResults.length} nouveauté${filteredResults.length > 1 ? 's' : ''}`;
}
resultsGrid.innerHTML = '';
filteredResults.forEach(result => {
const card = createCard(result);
resultsGrid.appendChild(card);
});
}
function createCard(group) {
const card = document.createElement('div');
card.className = 'release-card';
const mainTorrent = group.torrents[0];
const tmdb = group.tmdb || {};
const music = group.music || {};
const isMusic = group.is_music || false;
const isAnime = group.is_anime || false;
let title = tmdb.title || music.album || mainTorrent.Title || 'Sans titre';
let year = tmdb.year || '';
let overview = escapeHtml(tmdb.overview || '');
let posterUrl = sanitizeUrl(tmdb.poster_url || music.cover_url) || getDefaultPosterUrl();
let torrentUrl = sanitizeUrl(mainTorrent.Details || mainTorrent.Guid) || '';
let uniqueId = `result-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
let variantsCount = group.torrents.length;
let contentType = '🎬';
if (isMusic && music.artist) {
contentType = '🎵';
title = `${music.artist} - ${music.album}`;
overview = `
<strong>Artiste:</strong> ${escapeHtml(music.artist)}<br>
<strong>Album:</strong> ${escapeHtml(music.album)}<br>
${music.tags?.length ? `<strong>Genres:</strong> ${escapeHtml(music.tags.join(', '))}` : ''}
`;
} else if (isAnime) {
contentType = '🎌';
} else if (tmdb.type === 'tv') {
contentType = '📺';
}
card.innerHTML = `
<div class="card-poster">
<img src="${posterUrl}" alt="${escapeHtml(title)}" class="card-image" data-fallback="poster">
<div class="card-type">${contentType} ${isMusic ? 'Musique' : (isAnime ? 'Anime' : (tmdb.type === 'tv' ? 'Série' : 'Film'))}</div>
${!isMusic && tmdb.vote_average ? `<div class="card-rating">⭐ ${tmdb.vote_average.toFixed(1)}</div>` : ''}
${isMusic && music.listeners ? `<div class="card-rating">👥 ${formatNumber(music.listeners)}</div>` : ''}
${variantsCount > 1 ? `<div class="card-variants">📦 ${variantsCount} versions</div>` : ''}
<div class="card-seeders">🌱 ${mainTorrent.Seeders || 0}</div>
</div>
<div class="card-content">
<div class="card-title">${escapeHtml(title)}</div>
<div class="card-meta">
<span class="card-year">${year}</span>
<a href="${torrentUrl}" target="_blank" class="card-tracker-link" title="Voir sur ${escapeHtml(mainTorrent.Tracker)}">${escapeHtml(mainTorrent.Tracker)} 🔗</a>
</div>
<div class="card-overview">${overview}</div>
<div class="card-actions">
<button class="btn-details" data-result-id="${uniqueId}"> Détails ${variantsCount > 1 ? '(' + variantsCount + ')' : ''}</button>
${torrentUrl ? `<a href="${torrentUrl}" target="_blank" class="btn-tracker" title="Page du torrent">🔗</a>` : ''}
${mainTorrent.MagnetUri ? `<a href="${mainTorrent.MagnetUri}" class="btn-download-card" title="Magnet">🧲</a>` : ''}
</div>
</div>
`;
card.dataset.resultId = uniqueId;
card.dataset.resultData = JSON.stringify(group);
card.querySelector('.btn-details').addEventListener('click', function(e) {
e.preventDefault();
showDetails(this.getAttribute('data-result-id'));
});
return card;
}
function showDetails(resultId) {
const card = document.querySelector(`[data-result-id="${resultId}"]`);
if (!card) return;
const group = JSON.parse(card.dataset.resultData);
const isMusic = group.is_music || false;
const isAnime = group.is_anime || false;
const modal = document.getElementById('detailsModal');
const modalBody = document.getElementById('modalBody');
if (isMusic) {
showMusicDetails(group, modalBody);
} else {
showVideoDetails(group, modalBody, isAnime);
}
modal.classList.remove('hidden');
}
function showMusicDetails(group, modalBody) {
const mainTorrent = group.torrents[0];
const music = group.music || {};
const coverUrl = sanitizeUrl(music.cover_url) || '';
const artist = music.artist || mainTorrent.Title?.split(' - ')[0] || 'Artiste inconnu';
const album = music.album || mainTorrent.Title?.split(' - ')[1] || mainTorrent.Title || 'Album inconnu';
const listeners = formatNumber(music.listeners || 0);
const playcount = formatNumber(music.playcount || 0);
const tags = music.tags || [];
const url = music.url || '';
// Vérifier si on a des infos Last.fm
const hasLastFmData = music.artist && music.album;
// Si pas de cover, utiliser un placeholder
const displayCover = coverUrl || 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MDAiIGhlaWdodD0iNDAwIj48cmVjdCB3aWR0aD0iNDAwIiBoZWlnaHQ9IjQwMCIgZmlsbD0iIzMzMyIvPjx0ZXh0IHg9IjIwMCIgeT0iMTkwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNjY2IiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iNjAiPvCfjrU8L3RleHQ+PHRleHQgeD0iMjAwIiB5PSIyNDAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM2NjYiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxOCI+Tm8gQ292ZXI8L3RleHQ+PC9zdmc+';
modalBody.innerHTML = `
<div class="modal-header music-modal-header">
<img src="${displayCover}" alt="Album art" class="modal-album-art" data-fallback="cover">
<div class="modal-header-content music-modal-header-content">
<h2 class="modal-title">🎵 ${escapeHtml(album)}</h2>
<p class="modal-artist">${escapeHtml(artist)}</p>
${hasLastFmData ? `
<div class="modal-meta music-modal-meta">
<span>👥 ${listeners} auditeurs</span>
<span>▶️ ${playcount} lectures</span>
</div>
` : `
<div class="modal-meta music-modal-meta">
<span class="no-data"> Infos Last.fm non disponibles</span>
</div>
`}
</div>
</div>
<div class="modal-body-content">
${tags.length > 0 ? `
<div class="modal-section">
<h3>🏷️ Genres</h3>
<div class="tags-cloud">
${tags.map(tag => `<span class="tag-item">${escapeHtml(tag)}</span>`).join('')}
</div>
</div>
` : ''}
${url ? `<p><a href="${url}" target="_blank" class="external-link">🔗 Voir sur Last.fm</a></p>` : ''}
<div class="modal-section">
<h3>💾 Versions disponibles (${group.torrents.length})</h3>
<div class="torrents-list">
${createTorrentsTable(group.torrents, true)}
</div>
</div>
</div>
`;
}
function showVideoDetails(group, modalBody, isAnime) {
const mainTorrent = group.torrents[0];
const tmdb = group.tmdb || {};
const backdropUrl = tmdb.backdrop_url || tmdb.poster_url || getDefaultBackdropUrl();
const title = tmdb.title || mainTorrent.Title;
const originalTitle = tmdb.original_title || '';
const overview = tmdb.overview || 'Synopsis non disponible';
const year = tmdb.year || '';
const rating = tmdb.vote_average ? tmdb.vote_average.toFixed(1) : 'N/A';
const trailerUrl = tmdb.trailer_url || '';
let youtubeId = '';
if (trailerUrl) {
const match = trailerUrl.match(/[?&]v=([^&]+)/);
youtubeId = match ? match[1] : '';
}
let modalType = isAnime ? '🎌 Anime' : (tmdb.type === 'tv' ? '📺 Série' : '🎬 Film');
modalBody.innerHTML = `
<div class="modal-header">
<img src="${backdropUrl}" alt="" class="modal-backdrop" data-fallback="backdrop">
<div class="modal-header-content">
<h2 class="modal-title">${escapeHtml(title)}</h2>
${originalTitle && originalTitle !== title ? `<p style="opacity: 0.8;">${escapeHtml(originalTitle)}</p>` : ''}
<div class="modal-meta">
<span>${year}</span>
${tmdb.vote_average ? `<span class="modal-rating">⭐ ${rating}/10</span>` : ''}
<span>${modalType}</span>
<span>📦 ${group.torrents.length} version(s)</span>
</div>
</div>
</div>
<div class="modal-body-content">
<div class="modal-section">
<h3>📖 Synopsis</h3>
<p class="modal-overview">${escapeHtml(overview)}</p>
</div>
${youtubeId ? `
<div class="modal-section">
<h3>🎬 Bande-annonce</h3>
<div class="modal-trailer">
<iframe src="https://www.youtube.com/embed/${youtubeId}" allowfullscreen></iframe>
</div>
</div>
` : ''}
<div class="modal-section">
<h3>💾 Versions disponibles (${group.torrents.length})</h3>
<div class="torrents-list">
${createTorrentsTable(group.torrents, false)}
</div>
</div>
</div>
`;
}
function createTorrentsTable(torrents, isMusic) {
// Sur mobile on utilise la même structure que discover
// Sur desktop on garde la table pour l'alignement
// Version avec divs (comme discover) - fonctionne partout
let html = `<div class="torrents-list-items">`;
torrents.forEach((torrent, index) => {
const quality = extractQuality(torrent.Title);
const language = extractLanguage(torrent.Title);
const torrentUrl = torrent.Details || torrent.Guid || '';
html += `
<div class="torrent-item ${index === 0 ? 'best-torrent' : ''}">
<div class="torrent-info">
<div class="torrent-name" title="${escapeHtml(torrent.Title)}">
${torrentUrl
? `<a href="${torrentUrl}" target="_blank" class="torrent-name-link">${escapeHtml(torrent.Title)}</a>`
: escapeHtml(torrent.Title)
}
</div>
<div class="torrent-meta">
<span class="tracker">📡 ${escapeHtml(torrent.Tracker)}</span>
<span class="size">💾 ${torrent.SizeFormatted || 'N/A'}</span>
<span class="seeds">🌱 ${torrent.Seeders || 0}</span>
${quality ? `<span class="quality">${quality}</span>` : ''}
${language ? `<span class="language">${language}</span>` : ''}
${index === 0 ? '<span class="best">👑 Meilleur</span>' : ''}
</div>
</div>
<div class="torrent-actions">
${torrentUrl ? `<a href="${torrentUrl}" target="_blank" class="btn-link" title="Page du torrent">🔗</a>` : ''}
${torrent.MagnetUri ? `<a href="${torrent.MagnetUri}" class="btn-magnet" title="Magnet">🧲</a>` : ''}
${torrent.Link ? `<a href="${torrent.Link}" target="_blank" class="btn-download" title="Télécharger">⬇️</a>` : ''}
${torrentClientEnabled && (torrent.MagnetUri || (torrentClientSupportsTorrentFiles && torrent.Link)) ? `<button class="btn-send" title="Envoyer au client" onclick="sendToTorrentClient('${sanitizeUrl(torrent.MagnetUri || torrent.Link)}', this)">📥</button>` : ''}
</div>
</div>
`;
});
html += `</div>`;
return html;
}
function extractQuality(title) {
const qualities = ['2160p', '4K', '1080p', '720p', '480p'];
for (const q of qualities) {
if (title.toLowerCase().includes(q.toLowerCase())) return q;
}
return null;
}
function extractLanguage(title) {
const languages = { 'FRENCH': 'VF', 'TRUEFRENCH': 'VFF', 'VFF': 'VFF', 'VOSTFR': 'VOSTFR', 'MULTI': 'MULTI' };
const upper = title.toUpperCase();
for (const [key, val] of Object.entries(languages)) {
if (upper.includes(key)) return val;
}
return null;
}
function formatNumber(num) {
if (!num) return '0';
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
return num.toString();
}
function closeModal() {
document.getElementById('detailsModal').classList.add('hidden');
}
function showLoader(show) {
document.getElementById('loader').classList.toggle('hidden', !show);
}
function showMessage(message, type = 'info') {
const messageBox = document.getElementById('messageBox');
messageBox.textContent = message;
messageBox.className = `message-box ${type}`;
messageBox.classList.remove('hidden');
setTimeout(() => messageBox.classList.add('hidden'), 4000);
}
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 '';
}
}
// ============================================================
// 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) {
showMessage('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 = '✅';
showMessage('Torrent envoyé !', 'success');
setTimeout(() => {
button.textContent = '📥';
button.disabled = false;
}, 2000);
} else {
button.textContent = '❌';
showMessage(data.error || 'Erreur', 'error');
setTimeout(() => {
button.textContent = '📥';
button.disabled = false;
}, 2000);
}
} catch (error) {
button.textContent = '❌';
showMessage('Erreur de connexion', 'error');
setTimeout(() => {
button.textContent = '📥';
button.disabled = false;
}, 2000);
}
}
// Vérifier le client torrent au chargement
checkTorrentClient();