/**
* Lycostorrent - Page Découvrir (Version simplifiée)
* 2 catégories : Films récents / Séries en cours
* Avec pré-cache des torrents
*/
// ============================================================
// ÉTAT GLOBAL
// ============================================================
let currentCategory = 'movies';
let currentMedia = null;
let torrentClientEnabled = false;
let cachedData = {}; // Cache local des données
// ============================================================
// INITIALISATION
// ============================================================
document.addEventListener('DOMContentLoaded', () => {
initTabs();
checkTorrentClient();
loadCategory('movies');
});
function initTabs() {
document.querySelectorAll('.discover-tab').forEach(tab => {
tab.addEventListener('click', () => {
const category = tab.dataset.category;
// Mettre à jour l'UI
document.querySelectorAll('.discover-tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
// Charger la catégorie
loadCategory(category);
});
});
}
async function checkTorrentClient() {
try {
const response = await fetch('/api/torrent-client/status');
const data = await response.json();
torrentClientEnabled = data.success && data.enabled && data.connected;
} catch (error) {
torrentClientEnabled = false;
}
}
// ============================================================
// CHARGEMENT DES DONNÉES
// ============================================================
async function loadCategory(category) {
currentCategory = category;
const grid = document.getElementById('discoverGrid');
const loader = document.getElementById('discoverLoader');
const empty = document.getElementById('discoverEmpty');
// Afficher le loader
grid.innerHTML = '';
loader.classList.remove('hidden');
empty.classList.add('hidden');
hideCacheInfo();
// Essayer de charger depuis le cache d'abord
try {
const cacheResponse = await fetch(`/api/cache/data/discover/${category}`);
const cacheData = await cacheResponse.json();
if (cacheData.success && cacheData.cached && cacheData.data && cacheData.data.length > 0) {
loader.classList.add('hidden');
cachedData[category] = cacheData.data;
const mediaType = category === 'movies' ? 'movie' : 'tv';
renderGrid(cacheData.data, mediaType, true);
showCacheInfo(cacheData.timestamp);
console.log(`📦 Discover ${category} chargé depuis le cache: ${cacheData.data.length} résultats`);
return;
}
} catch (error) {
console.log('Pas de cache disponible, chargement en direct...');
}
// Si pas de cache, charger en direct
await loadCategoryLive(category);
}
async function loadCategoryLive(category) {
const grid = document.getElementById('discoverGrid');
const loader = document.getElementById('discoverLoader');
const empty = document.getElementById('discoverEmpty');
grid.innerHTML = '';
loader.classList.remove('hidden');
empty.classList.add('hidden');
hideCacheInfo();
try {
const response = await fetch(`/api/discover/${category}`);
const data = await response.json();
loader.classList.add('hidden');
if (data.success && data.results && data.results.length > 0) {
cachedData[category] = data.results;
renderGrid(data.results, data.media_type, false);
} else {
empty.classList.remove('hidden');
empty.querySelector('p').textContent = data.error || 'Aucun résultat trouvé';
}
} catch (error) {
loader.classList.add('hidden');
empty.classList.remove('hidden');
empty.querySelector('p').textContent = 'Erreur de chargement';
console.error('Erreur:', error);
}
}
function renderGrid(results, mediaType, fromCache) {
const grid = document.getElementById('discoverGrid');
grid.innerHTML = results.map((item, index) => {
const posterUrl = item.poster_path
? `https://image.tmdb.org/t/p/w300${item.poster_path}`
: null;
const title = item.title || item.name;
const year = (item.release_date || item.first_air_date || '').substring(0, 4);
const rating = item.vote_average ? item.vote_average.toFixed(1) : '--';
const type = mediaType === 'movie' ? '🎬' : '📺';
// Indicateur de torrents disponibles (si depuis le cache)
const torrentCount = item.torrent_count || 0;
const torrentBadge = fromCache && torrentCount > 0
? `🧲 ${torrentCount}`
: '';
return `
${posterUrl
? `

`
: `
${type}
`
}
⭐ ${rating}
${torrentBadge}
${escapeHtml(title)}
${year || 'N/A'}
${type}
`;
}).join('');
}
// Afficher les infos du cache
function showCacheInfo(timestamp) {
const cacheInfo = document.getElementById('cacheInfo');
const cacheTimestampEl = 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`;
}
cacheTimestampEl.textContent = timeAgo;
cacheInfo.classList.remove('hidden');
}
}
function hideCacheInfo() {
const cacheInfo = document.getElementById('cacheInfo');
if (cacheInfo) {
cacheInfo.classList.add('hidden');
}
}
function refreshLive() {
loadCategoryLive(currentCategory);
}
// ============================================================
// MODAL DÉTAILS
// ============================================================
async function openDetail(id, mediaType, index) {
const modal = document.getElementById('detailModal');
const listEl = document.getElementById('torrentsList');
const loadingEl = document.getElementById('torrentsLoading');
const emptyEl = document.getElementById('torrentsEmpty');
// Réinitialiser
listEl.innerHTML = '';
loadingEl.classList.add('hidden');
emptyEl.classList.add('hidden');
modal.classList.remove('hidden');
// Vérifier si on a les données en cache local (avec détails + torrents pré-chargés)
const category = mediaType === 'movie' ? 'movies' : 'tv';
const cachedItem = cachedData[category] ? cachedData[category][index] : null;
// Si les détails sont pré-cachés, on les utilise directement (INSTANTANÉ)
if (cachedItem && cachedItem.details_cached) {
console.log(`📦 Détails + torrents depuis le cache pour: ${cachedItem.title || cachedItem.name}`);
currentMedia = cachedItem;
currentMedia.media_type = mediaType;
// Afficher les détails depuis le cache
renderDetailFromCache(cachedItem, mediaType);
// Afficher les torrents depuis le cache
if (cachedItem.torrents && cachedItem.torrents.length > 0) {
renderTorrents(cachedItem.torrents);
} else {
emptyEl.classList.remove('hidden');
}
return;
}
// Sinon, fallback : charger depuis l'API (pour les items sans cache)
try {
const response = await fetch(`/api/discover/detail/${mediaType}/${id}`);
const data = await response.json();
if (data.success) {
currentMedia = data.detail;
currentMedia.media_type = mediaType;
renderDetail(data.detail, mediaType);
// Si on a des torrents pré-cachés, les afficher
if (cachedItem && cachedItem.torrents && cachedItem.torrents.length > 0) {
renderTorrents(cachedItem.torrents);
} else {
// Sinon, rechercher en direct
searchTorrents(data.detail, mediaType);
}
} else {
closeDetailModal();
alert('Erreur lors du chargement des détails');
}
} catch (error) {
console.error('Erreur:', error);
closeDetailModal();
}
}
// Afficher les détails depuis le cache (nouvelle fonction)
function renderDetailFromCache(item, mediaType) {
const title = item.title || item.name;
const year = (item.release_date || item.first_air_date || '').substring(0, 4);
const posterUrl = item.poster_path
? `https://image.tmdb.org/t/p/w300${item.poster_path}`
: '/static/icons/icon-192x192.png';
document.getElementById('detailPoster').src = posterUrl;
document.getElementById('detailPoster').alt = title;
document.getElementById('detailTitle').textContent = title;
document.getElementById('detailYear').textContent = year;
document.getElementById('detailRating').textContent = `⭐ ${item.vote_average ? item.vote_average.toFixed(1) : '--'}`;
document.getElementById('detailOverview').textContent = item.overview || 'Aucune description disponible.';
// Genres
const genresContainer = document.getElementById('detailGenres');
if (item.genres && item.genres.length > 0) {
genresContainer.innerHTML = item.genres.map(g => `${g.name}`).join('');
} else {
genresContainer.innerHTML = '';
}
// Bande-annonce YouTube
const trailerSection = document.getElementById('detailTrailer');
const trailerFrame = document.getElementById('trailerFrame');
if (item.trailer_url) {
trailerFrame.src = item.trailer_url;
trailerSection.classList.remove('hidden');
} else {
trailerFrame.src = '';
trailerSection.classList.add('hidden');
}
}
function renderDetail(detail, mediaType) {
const title = detail.title || detail.name;
const year = (detail.release_date || detail.first_air_date || '').substring(0, 4);
const posterUrl = detail.poster_path
? `https://image.tmdb.org/t/p/w300${detail.poster_path}`
: '/static/icons/icon-192x192.png';
document.getElementById('detailPoster').src = posterUrl;
document.getElementById('detailPoster').alt = title;
document.getElementById('detailTitle').textContent = title;
document.getElementById('detailYear').textContent = year;
document.getElementById('detailRating').textContent = `⭐ ${detail.vote_average ? detail.vote_average.toFixed(1) : '--'}`;
document.getElementById('detailOverview').textContent = detail.overview || 'Aucune description disponible.';
// Genres
const genresContainer = document.getElementById('detailGenres');
if (detail.genres && detail.genres.length > 0) {
genresContainer.innerHTML = detail.genres.map(g => `${g.name}`).join('');
} else {
genresContainer.innerHTML = '';
}
// Bande-annonce YouTube
const trailerSection = document.getElementById('detailTrailer');
const trailerFrame = document.getElementById('trailerFrame');
if (detail.trailer_url) {
trailerFrame.src = detail.trailer_url;
trailerSection.classList.remove('hidden');
} else {
trailerFrame.src = '';
trailerSection.classList.add('hidden');
}
}
function closeDetailModal() {
document.getElementById('detailModal').classList.add('hidden');
// Arrêter la vidéo YouTube
document.getElementById('trailerFrame').src = '';
currentMedia = null;
}
// Fermer le modal en cliquant à l'extérieur
document.getElementById('detailModal')?.addEventListener('click', (e) => {
if (e.target.id === 'detailModal') {
closeDetailModal();
}
});
// Fermer avec Escape
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeDetailModal();
}
});
// ============================================================
// RECHERCHE DE TORRENTS (fallback si pas en cache)
// ============================================================
async function searchTorrents(detail, mediaType) {
const loadingEl = document.getElementById('torrentsLoading');
const listEl = document.getElementById('torrentsList');
const emptyEl = document.getElementById('torrentsEmpty');
loadingEl.classList.remove('hidden');
listEl.innerHTML = '';
emptyEl.classList.add('hidden');
const title = detail.title || detail.name;
const originalTitle = detail.original_title || detail.original_name || '';
const year = (detail.release_date || detail.first_air_date || '').substring(0, 4);
try {
const response = await fetch('/api/discover/search-torrents', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: title,
original_title: originalTitle,
year: year,
media_type: mediaType,
tmdb_id: detail.id
})
});
const data = await response.json();
loadingEl.classList.add('hidden');
if (data.success && data.results && data.results.length > 0) {
renderTorrents(data.results);
} else {
emptyEl.classList.remove('hidden');
}
} catch (error) {
loadingEl.classList.add('hidden');
emptyEl.classList.remove('hidden');
console.error('Erreur recherche torrents:', error);
}
}
function renderTorrents(torrents) {
const listEl = document.getElementById('torrentsList');
listEl.innerHTML = torrents.slice(0, 20).map((torrent, index) => {
const size = torrent.Size ? formatSize(torrent.Size) : 'N/A';
const seeds = torrent.Seeders || 0;
const quality = torrent.parsed?.quality || '';
const tracker = torrent.Tracker || torrent.TrackerName || 'Unknown';
const magnetUrl = torrent.MagnetUri || '';
const downloadUrl = torrent.Link || '';
const detailsUrl = torrent.Details || torrent.Guid || '';
const torrentUrl = magnetUrl || downloadUrl;
return `
${escapeHtml(torrent.Title)}
📡 ${escapeHtml(tracker)}
💾 ${size}
🌱 ${seeds}
${quality ? `${escapeHtml(quality)}` : ''}
${detailsUrl ? `
🔗` : ''}
${magnetUrl ? `
🧲` : ''}
${downloadUrl ? `
⬇️` : ''}
${torrentClientEnabled && torrentUrl ?
`
`
: ''}
`;
}).join('');
}
function handleSendToClient(url, buttonId) {
const button = document.getElementById(buttonId);
sendToClient(url, button);
}
// ============================================================
// ENVOI AU CLIENT TORRENT
// ============================================================
async function sendToClient(url, buttonElement) {
if (!url) return;
showTorrentOptionsModal(url, buttonElement);
}
async function showTorrentOptionsModal(url, button) {
let modal = document.getElementById('torrentOptionsModal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'torrentOptionsModal';
modal.className = 'torrent-options-modal';
modal.innerHTML = `
`;
document.body.appendChild(modal);
modal.addEventListener('click', (e) => {
if (e.target === modal) closeTorrentOptionsModal();
});
}
const categorySelect = document.getElementById('torrentCategory');
const savePathInput = document.getElementById('torrentSavePath');
categorySelect.innerHTML = '';
let categoriesWithPaths = {};
try {
const response = await fetch('/api/torrent-client/categories');
const data = await response.json();
categorySelect.innerHTML = '';
if (data.success && data.categories) {
data.categories.forEach(cat => {
categorySelect.innerHTML += ``;
});
categoriesWithPaths = data.custom_categories || {};
}
} catch (error) {
categorySelect.innerHTML = '';
}
categorySelect.onchange = () => {
const selectedCat = categorySelect.value;
if (selectedCat && categoriesWithPaths[selectedCat]) {
savePathInput.value = categoriesWithPaths[selectedCat];
} else {
savePathInput.value = '';
}
};
savePathInput.value = '';
document.getElementById('torrentPaused').checked = false;
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);
};
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) {
if (button) {
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) {
if (button) {
button.textContent = '✅';
setTimeout(() => {
button.textContent = '📥';
button.disabled = false;
}, 2000);
}
showToast('Torrent envoyé !', 'success');
} else {
if (button) {
button.textContent = '❌';
setTimeout(() => {
button.textContent = '📥';
button.disabled = false;
}, 2000);
}
showToast(data.error || 'Erreur', 'error');
}
} catch (error) {
if (button) {
button.textContent = '❌';
setTimeout(() => {
button.textContent = '📥';
button.disabled = false;
}, 2000);
}
showToast('Erreur de connexion', 'error');
}
}
// ============================================================
// UTILITAIRES
// ============================================================
function formatSize(bytes) {
if (!bytes) return 'N/A';
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i];
}
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function showToast(message, type = 'info') {
let toast = document.getElementById('toast');
if (!toast) {
toast = document.createElement('div');
toast.id = 'toast';
toast.className = 'toast';
document.body.appendChild(toast);
}
toast.textContent = message;
toast.className = `toast ${type}`;
toast.classList.remove('hidden');
setTimeout(() => {
toast.classList.add('hidden');
}, 3000);
}