902 lines
35 KiB
JavaScript
902 lines
35 KiB
JavaScript
/**
|
||
* 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();
|