import { state } from './state.js';
import { fetchTMDB, fetchAllStreamsFromPlex } from './api.js';
import { showNotification, getRelativeTime, fetchWithTimeout, _ } from './utils.js';
import { getFromDB, addItemsToStore } from './db.js';
let charts = {};
export async function loadInitialContent() {
await Promise.all([loadGenres(), loadYears()]);
await Promise.all([loadContent(), initializeHeroSection()]);
setupScrollEffects();
}
export function initializeUserData() {
try {
const savedHistory = localStorage.getItem('cineplex_userHistory');
state.userHistory = savedHistory ? JSON.parse(savedHistory) : [];
if (!Array.isArray(state.userHistory)) state.userHistory = [];
} catch {
state.userHistory = [];
}
try {
const savedPrefs = localStorage.getItem('cineplex_userPreferences');
state.userPreferences = savedPrefs ? JSON.parse(savedPrefs) : { genres: {}, keywords: {}, ratings: [], cast: {}, crew: {} };
} catch {
state.userPreferences = { genres: {}, keywords: {}, ratings: [], cast: {}, crew: {} };
}
}
export async function loadLocalContent() {
if (!state.db) return;
try {
const [movies, series, artists, photos] = await Promise.all([getFromDB('movies'), getFromDB('series'), getFromDB('artists'), getFromDB('photos')]);
state.localMovies = movies;
state.localSeries = series;
state.localArtists = artists;
state.localPhotos = photos;
} catch (error) {
showNotification(_("errorLoadingLocalContent"), "error");
}
}
export function resetView() {
switchView('movies');
}
export function switchView(viewType) {
if (state.isLoading) return;
const sidebar = document.getElementById('sidebar-nav');
if (sidebar.classList.contains('open')) {
sidebar.classList.remove('open');
document.getElementById('main-container').classList.remove('sidebar-open');
}
const mainContent = document.querySelector('.main-content');
const topBarHeight = document.querySelector('.top-bar')?.offsetHeight || 60;
const targetScrollTop = mainContent ? mainContent.offsetTop - topBarHeight : 0;
if (state.currentView === viewType && viewType !== 'search') {
if (window.scrollY > targetScrollTop) {
window.scrollTo({
top: targetScrollTop,
behavior: 'smooth'
});
}
return;
}
state.currentView = viewType;
state.currentPage = 1;
state.currentParams.query = '';
document.getElementById('search-input').value = '';
state.lastScrollPosition = 0;
const allSections = ['content-section', 'stats-section', 'history-section', 'recommendations-section', 'photos-section'];
allSections.forEach(id => {
const el = document.getElementById(id);
if (el) el.style.display = 'none';
});
const filters = document.querySelector('.filters');
if (filters) filters.style.display = 'none';
switch(viewType) {
case 'movies':
case 'series':
case 'search':
document.getElementById('content-section').style.display = 'block';
filters.style.display = 'flex';
if (viewType !== 'search') {
state.currentParams.contentType = viewType === 'movies' ? 'movie' : 'tv';
resetFilters(false);
updateSortOptions();
loadGenres();
}
break;
case 'favorites':
document.getElementById('content-section').style.display = 'block';
break;
case 'history':
document.getElementById('history-section').style.display = 'block';
break;
case 'recommendations':
document.getElementById('recommendations-section').style.display = 'block';
break;
case 'stats':
document.getElementById('stats-section').style.display = 'block';
document.getElementById('stats-filters').style.display = 'flex';
break;
case 'photos':
document.getElementById('photos-section').style.display = 'block';
break;
}
updateActiveNav(viewType);
updateSectionTitle();
window.scrollTo({ top: targetScrollTop, behavior: 'auto' });
switch(viewType) {
case 'movies':
case 'series':
case 'search':
loadContent();
break;
case 'favorites':
loadFavorites();
break;
case 'history':
displayHistory();
break;
case 'recommendations':
loadRecommendations();
break;
case 'stats':
generateStatistics();
break;
case 'photos':
initPhotosView();
break;
}
if (document.getElementById('item-details-view').classList.contains('active')) {
showMainView();
}
}
function updateActiveNav(activeView) {
document.querySelectorAll('.nav-link, .footer-link').forEach(link => link.classList.remove('active'));
let navId = (activeView === 'search') ? (state.currentParams.contentType === 'movie' ? 'movies' : 'series') : activeView;
const activeLink = document.getElementById(`nav-${navId}`);
const activeFooterLink = document.getElementById(`footer-${navId}`);
if (activeLink) activeLink.classList.add('active');
if (activeFooterLink) activeFooterLink.classList.add('active');
}
export function updateSectionTitle() {
let title = "";
const mainTitleElement = document.getElementById('main-section-title');
switch (state.currentView) {
case 'movies': case 'series':
const sortMap = {
'popularity.desc': _('popularSort'),
'vote_average.desc': _('topRatedSort'),
'release_date.desc': _('recentSort'),
'first_air_date.desc': _('recentSort')
};
const typeTitle = state.currentView === 'movies' ? _('moviesSectionTitle') : _('seriesSectionTitle');
title = `${typeTitle} ${sortMap[state.currentParams.sort] || ''}`;
break;
case 'stats': title = _('statsTitle'); break;
case 'favorites': title = _('navFavorites'); break;
case 'history': title = _('historyTitle'); break;
case 'recommendations': title = _('recommendationsTitle'); break;
case 'photos': title = _('navPhotos'); break;
case 'search':
title = state.currentParams.query.startsWith('actor:')
? _('contentFrom', state.currentParams.query.split(':')[1])
: _('searchResultsFor', state.currentParams.query);
break;
default: title = _('explore');
}
let targetTitleElement;
if (['stats', 'history', 'recommendations'].includes(state.currentView)) {
targetTitleElement = document.querySelector(`#${state.currentView}-section .section-title`);
} else {
targetTitleElement = mainTitleElement;
}
if(targetTitleElement && state.currentView !== 'photos') {
targetTitleElement.textContent = title;
}
}
export function applyFilters() {
state.currentParams.genre = document.getElementById('genre-filter').value;
state.currentParams.year = document.getElementById('year-filter').value;
state.currentParams.sort = document.getElementById('sort-filter').value;
state.currentPage = 1;
loadContent();
updateSectionTitle();
}
function resetFilters(triggerLoad = true) {
state.currentParams.genre = '';
state.currentParams.year = '';
state.currentParams.sort = 'popularity.desc';
document.getElementById('genre-filter').value = '';
document.getElementById('year-filter').value = '';
document.getElementById('sort-filter').value = 'popularity.desc';
if (triggerLoad) {
state.currentPage = 1;
loadContent();
}
}
function updateSortOptions() {
const sortOption = document.getElementById('sort-release-date');
const isMovie = state.currentParams.contentType === 'movie';
sortOption.textContent = _('sortRecent');
sortOption.value = isMovie ? 'release_date.desc' : 'first_air_date.desc';
}
async function loadGenres() {
const type = state.currentParams.contentType;
const select = document.getElementById('genre-filter');
select.innerHTML = ``;
try {
const data = await fetchTMDB(`genre/${type}/list`);
select.innerHTML = ``;
data.genres.forEach(genre => {
const option = document.createElement('option');
option.value = genre.id;
option.textContent = genre.name;
select.appendChild(option);
});
select.value = state.currentParams.genre || "";
} catch (error) {
select.innerHTML = ``;
}
}
function loadYears() {
const select = document.getElementById('year-filter');
select.innerHTML = ``;
const currentYear = new Date().getFullYear();
for (let year = currentYear; year >= 1900; year--) {
const option = document.createElement('option');
option.value = year;
option.textContent = year;
select.appendChild(option);
}
select.value = state.currentParams.year || "";
}
export async function loadContent(append = false) {
if (state.currentContentFetchController) state.currentContentFetchController.abort();
state.currentContentFetchController = new AbortController();
const signal = state.currentContentFetchController.signal;
if (state.isLoading) return;
state.isLoading = true;
const grid = document.getElementById('content-grid');
const loadMoreButton = document.getElementById('load-more');
if (!append) {
grid.innerHTML = '
';
loadMoreButton.style.display = 'none';
} else {
loadMoreButton.disabled = true;
loadMoreButton.innerHTML = ` ${_('loading')}`;
}
try {
let endpoint = '';
const type = state.currentParams.contentType;
if (state.currentView === 'search' && state.currentParams.query) {
endpoint = `search/${type}?query=${encodeURIComponent(state.currentParams.query)}&page=${state.currentPage}&include_adult=false`;
} else {
endpoint = `discover/${type}?page=${state.currentPage}&sort_by=${state.currentParams.sort}&include_adult=false&vote_count.gte=50`;
if (state.currentParams.genre) endpoint += `&with_genres=${state.currentParams.genre}`;
const yearParam = type === 'movie' ? 'primary_release_year' : 'first_air_date_year';
if (state.currentParams.year) endpoint += `&${yearParam}=${state.currentParams.year}`;
}
const data = await fetchTMDB(endpoint, signal);
renderGrid(data.results, append);
loadMoreButton.style.display = (data.page < data.total_pages) ? 'block' : 'none';
if (!append) setupScrollEffects();
} catch (error) {
if (error.name !== 'AbortError') {
if (!append) grid.innerHTML = `${_('couldNotLoadContent')}
`;
}
} finally {
state.isLoading = false;
if (append) {
loadMoreButton.disabled = false;
loadMoreButton.textContent = _('loadMore');
}
}
}
function buscarContenidoLocal(title, type) {
if (!title || !type) return null;
const normalizedTitle = title.toLowerCase().trim();
const source = type === 'movie' ? state.localMovies : state.localSeries;
if (!Array.isArray(source)) return null;
return source.find(server =>
server && Array.isArray(server.titulos) &&
server.titulos.some(t => t && typeof t.title === 'string' && t.title.toLowerCase().trim() === normalizedTitle)
) || null;
}
function renderGrid(items, append = false) {
const grid = document.getElementById('content-grid');
if (!append) grid.innerHTML = '';
if (!items || items.length === 0) {
if (!append) {
const emptyStateTarget = document.getElementById('recommendations-grid') || grid;
const emptyIcon = state.currentView === 'recommendations' ? 'fa-user-astronaut' : 'fa-film';
const emptyText = state.currentView === 'recommendations' ? _('noRecommendations') : _('noContentFound');
emptyStateTarget.innerHTML = ``;
}
document.getElementById('load-more').style.display = 'none';
return;
}
const fragment = document.createDocumentFragment();
items.forEach((item, index) => {
if (!item || !item.id) return;
const isMovie = item.media_type === 'movie' || (item.hasOwnProperty('title') && !item.hasOwnProperty('name'));
const itemType = isMovie ? 'movie' : 'tv';
const title = isMovie ? item.title : item.name;
const releaseDate = isMovie ? item.release_date : item.first_air_date;
const year = releaseDate ? releaseDate.slice(0, 4) : 'N/A';
const posterPath = item.poster_path ? `https://image.tmdb.org/t/p/w500${item.poster_path}` : 'img/no-poster.png';
const isAvailable = !!buscarContenidoLocal(title, itemType);
const isFavorite = state.favorites.some(fav => fav.id === item.id && fav.type === itemType);
const voteAvg = item.vote_average ? item.vote_average.toFixed(1) : 'N/A';
const ratingClass = voteAvg >= 7.5 ? 'rating-good' : (voteAvg >= 5.0 ? 'rating-ok' : 'rating-bad');
const card = document.createElement('div');
card.className = `item-card`;
card.dataset.id = item.id;
card.dataset.type = itemType;
card.innerHTML = `
${voteAvg >= 7.8 ? 'TOP' : ''}
${isAvailable ? ` ${_('local')}` : ''}
${isAvailable ? `
` : ``}
${title}
${year}
${voteAvg !== 'N/A' ? ` ${voteAvg}` : ''}
`;
fragment.appendChild(card);
});
const targetGrid = state.currentView === 'recommendations' ? document.getElementById('recommendations-grid') : grid;
if(!append) targetGrid.innerHTML = '';
targetGrid.appendChild(fragment);
if (items.length > 0) setupScrollEffects();
}
export function showMainView() {
const detailsView = document.getElementById('item-details-view');
const mainView = document.getElementById('main-view');
const detailsContent = document.getElementById('item-details-content');
if (!state.lastClickedCardElement) {
detailsView.classList.remove('active');
document.body.classList.remove('details-view-active');
mainView.style.display = 'block';
mainView.style.opacity = '1';
window.scrollTo({ top: state.lastScrollPosition, behavior: 'auto' });
state.currentItemId = null;
state.currentItemType = null;
return;
}
const targetRect = state.lastClickedCardElement.getBoundingClientRect();
mainView.style.display = 'block';
const tl = gsap.timeline({
onComplete: () => {
detailsView.classList.remove('active');
document.body.classList.remove('details-view-active');
state.currentItemId = null;
state.currentItemType = null;
state.lastClickedCardElement = null;
window.scrollTo({ top: state.lastScrollPosition, behavior: 'auto' });
gsap.set(mainView, { clearProps: 'all' });
gsap.set(detailsView, { clearProps: 'all', display: 'none' });
}
});
tl.set(detailsView, { overflow: 'hidden' })
.to(detailsContent, { opacity: 0, duration: 0.3, ease: 'power2.in' })
.to(detailsView, {
top: targetRect.top,
left: targetRect.left,
width: targetRect.width,
height: targetRect.height,
borderRadius: '18px',
duration: 0.6,
ease: 'power3.inOut'
}, 0)
.to(mainView, { opacity: 1, scale: 1, duration: 0.5, ease: 'power3.out' }, "-=0.4");
}
export async function showItemDetails(itemId, contentType) {
if (!itemId || !contentType) return;
state.currentItemId = itemId;
state.currentItemType = contentType;
const detailsView = document.getElementById('item-details-view');
const mainView = document.getElementById('main-view');
const detailsContent = document.getElementById('item-details-content');
if (mainView.style.display !== 'none') state.lastScrollPosition = window.scrollY;
document.body.classList.add('details-view-active');
detailsContent.innerHTML = '';
detailsView.classList.add('active');
const startRect = state.lastClickedCardElement
? state.lastClickedCardElement.getBoundingClientRect()
: { top: window.innerHeight / 2, left: window.innerWidth / 2, width: 0, height: 0 };
const tl = gsap.timeline({
onComplete: () => {
mainView.style.display = 'none';
gsap.set(detailsView, { overflowY: 'auto' });
}
});
tl.set(detailsView, {
top: startRect.top,
left: startRect.left,
width: startRect.width,
height: startRect.height,
borderRadius: '18px',
overflow: 'hidden',
display: 'block'
})
.to(detailsView, {
top: 0,
left: 0,
width: '100vw',
height: '100vh',
borderRadius: '0px',
duration: 0.7,
ease: 'power3.inOut'
})
.to(mainView, {
opacity: 0,
scale: 0.98,
duration: 0.6,
ease: 'power2.inOut'
}, 0);
gsap.fromTo(detailsContent,
{ opacity: 0 },
{ opacity: 1, duration: 0.6, ease: 'power2.out', delay: 0.5 }
);
state.isLoading = true;
try {
const isMovie = contentType === 'movie';
let appendToResponse = 'videos,credits,keywords,release_dates,external_ids,similar';
if (!isMovie) {
appendToResponse += ',content_ratings,aggregate_credits';
}
const item = await fetchTMDB(`${contentType}/${itemId}?append_to_response=${appendToResponse}`);
if (!isMovie && item.seasons && item.seasons.length > 0) {
const seasonPromises = item.seasons
.filter(s => s.season_number > 0)
.map(s => fetchTMDB(`${contentType}/${itemId}/season/${s.season_number}`).catch(() => null));
const seasonsData = await Promise.all(seasonPromises);
item.seasons_with_episodes = seasonsData.filter(s => s !== null);
}
updateUserData(item);
await renderItemDetails(item);
} catch (error) {
detailsContent.innerHTML = `${_('errorLoadingDetails')}
${error.message}
`;
} finally {
state.isLoading = false;
}
}
async function renderItemDetails(item) {
const detailsContent = document.getElementById('item-details-content');
const isMovie = state.currentItemType === 'movie';
const title = isMovie ? item.title : item.name;
const backdropPath = item.backdrop_path ? `https://image.tmdb.org/t/p/original${item.backdrop_path}` : '';
const posterPath = item.poster_path ? `https://image.tmdb.org/t/p/w500${item.poster_path}` : 'img/no-poster.png';
const tagline = item.tagline || '';
const overview = item.overview || _('noSynopsis');
const releaseDate = isMovie ? item.release_date : item.first_air_date;
const year = releaseDate ? releaseDate.slice(0, 4) : 'N/A';
const voteAverage = item.vote_average ? item.vote_average.toFixed(1) : 'N/A';
const genres = item.genres || [];
const trailer = item.videos?.results?.find(v => v.site === 'YouTube' && v.type === 'Trailer');
const isAvailable = !!buscarContenidoLocal(title, state.currentItemType);
const isFavorite = state.favorites.some(fav => fav.id === item.id && fav.type === state.currentItemType);
const imdbId = item.external_ids?.imdb_id;
const crew = (isMovie ? item.credits?.crew : item.aggregate_credits?.crew) || [];
const director = crew.find(c => c.job === 'Director');
const writer = crew.find(c => c.job === 'Screenplay' || c.job === 'Writer' || c.job === 'Story');
detailsContent.innerHTML = `
${backdropPath ? `` : ''}
${(isMovie ? item.credits?.cast?.length > 0 : item.aggregate_credits?.cast?.length > 0) ? `
${_('mainCast')}
${((isMovie ? item.credits.cast : item.aggregate_credits.cast) || []).slice(0, 12).map(actor => `
${actor.name}
${actor.roles ? actor.roles.map(r=>r.character).slice(0,2).join(', ') : actor.character}
`).join('')}
` : ''}
${!isMovie && item.seasons_with_episodes && item.seasons_with_episodes.length > 0 ? `
${_('seasonsAndEpisodes')}
${item.seasons_with_episodes.map((season, index) => `
${season.episodes.map(ep => `
${ep.episode_number}
${ep.name}
${ep.air_date || 'N/A'}
${ep.vote_average.toFixed(1)}/10
${ep.overview || _('noSynopsis')}
`).join('')}
`).join('')}
` : ''}
${item.similar?.results?.length > 0 ? `
${_('similarContent')}
${item.similar.results.slice(0, 8).map(similar => `
${similar.title || similar.name}
`).join('')}
` : ''}
`;
}
function updateUserData(item) {
if (!item || !item.id || !state.currentItemType) return;
const { userHistory, userPreferences } = state;
const contentType = state.currentItemType;
const historyItem = { id: item.id, type: contentType, title: contentType === 'movie' ? item.title : item.name, poster: item.poster_path, timestamp: Date.now() };
state.userHistory = [historyItem, ...userHistory.filter(h => !(h.id === item.id && h.type === contentType))].slice(0, 100);
item.genres?.forEach(g => { userPreferences.genres[g.id] = (userPreferences.genres[g.id] || 0) + 1; });
(item.keywords?.keywords || item.keywords?.results || []).slice(0, 10).forEach(kw => { userPreferences.keywords[kw.id] = (userPreferences.keywords[kw.id] || 0) + 1; });
if (item.vote_average > 0) userPreferences.ratings.push(item.vote_average);
if (userPreferences.ratings.length > 100) userPreferences.ratings.shift();
const credits = contentType === 'tv' && item.aggregate_credits ? item.aggregate_credits : item.credits;
credits?.cast?.slice(0, 15).forEach(a => { userPreferences.cast[a.id] = (userPreferences.cast[a.id] || 0) + 1; });
credits?.crew?.slice(0, 15).forEach(c => { userPreferences.crew[c.id] = (userPreferences.crew[c.id] || 0) + 1; });
localStorage.setItem('cineplex_userHistory', JSON.stringify(state.userHistory));
localStorage.setItem('cineplex_userPreferences', JSON.stringify(userPreferences));
}
export function initializeFavorites() {
try {
const saved = localStorage.getItem('cineplex_favorites');
state.favorites = saved ? JSON.parse(saved) : [];
if (!Array.isArray(state.favorites)) state.favorites = [];
} catch {
state.favorites = [];
}
}
export function toggleFavorite(itemId, itemType) {
if (!itemId || !itemType) return;
const index = state.favorites.findIndex(fav => fav.id === itemId && fav.type === itemType);
let isFavoriteNow = false;
if (index === -1) {
state.favorites.push({ id: itemId, type: itemType });
showNotification(_('addedToFavorites'), 'success');
isFavoriteNow = true;
} else {
state.favorites.splice(index, 1);
showNotification(_('removedFromFavorites'), 'info');
}
localStorage.setItem('cineplex_favorites', JSON.stringify(state.favorites));
updateFavoriteButtonVisuals(itemId, itemType, isFavoriteNow);
if (state.currentView === 'favorites') loadFavorites();
}
function updateFavoriteButtonVisuals(itemId, itemType, isFavorite) {
document.querySelectorAll(`.item-card[data-id="${itemId}"][data-type="${itemType}"] .favorites-btn`).forEach(btn => {
btn.classList.toggle('active', isFavorite);
btn.querySelector('i').className = `fas ${isFavorite ? 'fa-heart-broken' : 'fa-heart'} fa-lg`;
});
if (document.getElementById('item-details-view').classList.contains('active') && state.currentItemId === itemId && state.currentItemType === itemType) {
const detailsBtn = document.querySelector('#item-details-content .favorites-btn');
if(detailsBtn) {
detailsBtn.classList.toggle('btn-danger', isFavorite);
detailsBtn.classList.toggle('btn-outline-danger', !isFavorite);
detailsBtn.querySelector('i').className = `fas ${isFavorite ? 'fa-heart-broken' : 'fa-heart'} me-1`;
detailsBtn.lastChild.textContent = ` ${isFavorite ? _('removeFromFavorites') : _('addToFavorites')}`;
}
}
}
export async function loadFavorites() {
const grid = document.getElementById('content-grid');
grid.innerHTML = '';
if (state.favorites.length === 0) {
grid.innerHTML = ``;
return;
}
try {
const favoritePromises = state.favorites.map(fav => fetchTMDB(`${fav.type}/${fav.id}`).catch(()=>null));
const favoriteItems = (await Promise.all(favoritePromises)).filter(item => item !== null);
renderGrid(favoriteItems, false);
} catch (error) {
grid.innerHTML = `${_('errorLoadingFavorites')}
`;
}
}
export function displayHistory() {
const listContainer = document.getElementById('history-list');
listContainer.innerHTML = "";
if (state.userHistory.length === 0) {
listContainer.innerHTML = `${_('historyEmpty')}
${_('historyEmptySub')}
`;
document.getElementById('clear-history-btn').style.display = 'none';
return;
}
document.getElementById('clear-history-btn').style.display = 'inline-flex';
const fragment = document.createDocumentFragment();
[...state.userHistory].sort((a,b) => b.timestamp - a.timestamp).forEach(item => {
const posterUrl = item.poster ? `https://image.tmdb.org/t/p/w92${item.poster}` : 'img/no-poster.png';
const isAvailable = !!buscarContenidoLocal(item.title, item.type);
const historyItem = document.createElement('div');
historyItem.className = 'history-item';
historyItem.dataset.id = item.id;
historyItem.dataset.type = item.type;
historyItem.dataset.title = item.title;
historyItem.innerHTML = `
${item.title}
${isAvailable ? `
${_('local')}` : ''}
${_('viewed')} ${getRelativeTime(item.timestamp)}
`;
fragment.appendChild(historyItem);
});
listContainer.appendChild(fragment);
}
export function deleteHistoryItem(id, type, element) {
const numericId = Number(id);
state.userHistory = state.userHistory.filter(item => !(item.id === numericId && item.type === type));
localStorage.setItem('cineplex_userHistory', JSON.stringify(state.userHistory));
gsap.to(element, {
height: 0,
opacity: 0,
paddingTop: 0,
paddingBottom: 0,
marginTop: 0,
marginBottom: 0,
duration: 0.5,
ease: "power3.inOut",
onComplete: () => {
element.remove();
if (state.userHistory.length === 0) {
displayHistory();
}
}
});
showNotification(_('historyItemDeleted'), "info");
}
export function clearAllHistory() {
if (confirm(_('confirmClearHistory'))) {
state.userHistory = [];
localStorage.setItem('cineplex_userHistory', JSON.stringify(state.userHistory));
displayHistory();
showNotification(_('historyCleared'), "success");
}
}
export async function getTrailerKey(id, type) {
try {
const data = await fetchTMDB(`${type}/${id}/videos`);
const trailer = data.results.find(v => v.site === 'YouTube' && v.type === 'Trailer') || data.results.find(v => v.site === 'YouTube');
return trailer ? trailer.key : null;
} catch (error) {
return null;
}
}
export async function loadRecommendations() {
const grid = document.getElementById('recommendations-grid');
grid.innerHTML = '';
const cachedRecs = sessionStorage.getItem('cineplex_recommendations');
if (cachedRecs) {
const { timestamp, data } = JSON.parse(cachedRecs);
if (Date.now() - timestamp < 1000 * 60 * 30) {
renderGrid(data);
return;
}
}
const seedPool = [...state.favorites, ...state.userHistory.slice(0, 10)];
if (seedPool.length === 0) {
renderGrid([]);
return;
}
try {
const seed = seedPool[Math.floor(Math.random() * seedPool.length)];
const endpoint = `${seed.type}/${seed.id}/recommendations?page=1`;
const data = await fetchTMDB(endpoint);
const seenIds = new Set(state.userHistory.map(item => item.id));
const recommendations = data.results.filter(rec => !seenIds.has(rec.id)).slice(0, 12);
if (recommendations.length > 0) {
renderGrid(recommendations);
sessionStorage.setItem('cineplex_recommendations', JSON.stringify({ timestamp: Date.now(), data: recommendations }));
} else {
renderGrid([]);
}
} catch(error) {
grid.innerHTML = `${_('errorGeneratingRecommendations')}
`;
}
}
async function populateStatsTokenFilter() {
const select = document.getElementById('stats-token-filter');
const currentValue = select.value;
select.innerHTML = ``;
try {
const tokensData = await getFromDB('tokens');
const primaryTokens = [...new Set(tokensData.map(t => t.token))];
const connections = await getFromDB('conexiones_locales');
const tokenNames = {};
connections.forEach(conn => {
if (conn.tokenPrincipal && !tokenNames[conn.tokenPrincipal]) {
tokenNames[conn.tokenPrincipal] = conn.nombre || `Token...${conn.tokenPrincipal.slice(-4)}`;
}
});
primaryTokens.forEach(token => {
const option = document.createElement('option');
option.value = token;
option.textContent = tokenNames[token] || `Token...${token.slice(-4)}`;
select.appendChild(option);
});
if (currentValue) {
select.value = currentValue;
}
} catch(e) {
select.innerHTML = ``;
}
}
export async function generateStatistics() {
const loader = document.getElementById('stats-loader');
const content = document.getElementById('stats-content');
loader.style.display = 'block';
content.style.display = 'none';
Object.values(charts).forEach(chart => chart.destroy());
charts = {};
await populateStatsTokenFilter();
try {
const selectedToken = document.getElementById('stats-token-filter').value;
const filterByToken = (data) => {
if (selectedToken === 'all') return data;
return data.filter(server => server.tokenPrincipal === selectedToken);
};
const filteredMovies = filterByToken(state.localMovies);
const filteredSeries = filterByToken(state.localSeries);
const filteredArtists = filterByToken(state.localArtists);
const allMovieItems = filteredMovies.flatMap(s => s.titulos);
const allSeriesItems = filteredSeries.flatMap(s => s.titulos);
const uniqueMovieTitles = new Set(allMovieItems.map(item => item.title));
const uniqueSeriesTitles = new Set(allSeriesItems.map(item => item.title));
const uniqueArtists = new Set(filteredArtists.flatMap(s => s.titulos.map(t => t.title)));
animateValue('total-movies', 0, uniqueMovieTitles.size, 1000);
animateValue('total-series', 0, uniqueSeriesTitles.size, 1000);
animateValue('total-artists', 0, uniqueArtists.size, 1000);
const allTokens = await getFromDB('tokens');
const allConnections = await getFromDB('conexiones_locales');
const filteredTokens = selectedToken === 'all'
? allTokens
: allTokens.filter(t => t.token === selectedToken);
const filteredConnections = selectedToken === 'all'
? allConnections
: allConnections.filter(c => c.tokenPrincipal === selectedToken);
animateValue('total-tokens', 0, filteredTokens.length, 1000);
animateValue('total-servers', 0, filteredConnections.length, 1000);
updateTokenDetailsCard(selectedToken, filteredConnections);
const movieGenres = processLocalGenres(allMovieItems);
const seriesGenres = processLocalGenres(allSeriesItems);
const allDecades = processLocalDecades([...allMovieItems, ...allSeriesItems]);
createGenreChart('movie-genres-chart', movieGenres, _('statsChartMoviesByGenre'));
createGenreChart('series-genres-chart', seriesGenres, _('statsChartSeriesByGenre'));
createDecadeChart('decade-chart', allDecades, _('statsChartByDecade'));
loader.style.display = 'none';
content.style.display = 'grid';
gsap.from(".stats-card, .chart-container", {
duration: 0.7,
y: 30,
opacity: 0,
stagger: 0.1,
ease: "power3.out"
});
} catch (error) {
loader.innerHTML = `${_('errorGeneratingStats')}
`;
}
}
function updateTokenDetailsCard(selectedToken, allConnections) {
const card = document.getElementById('token-details-card');
const serverList = document.getElementById('token-server-list');
if (selectedToken === 'all') {
card.style.display = 'none';
return;
}
const associatedServers = allConnections.filter(c => c.tokenPrincipal === selectedToken);
if (associatedServers.length === 0) {
serverList.innerHTML = `${_('noServersForToken')}`;
} else {
serverList.innerHTML = associatedServers.map(s =>
`${s.nombre}: ${s.token.slice(0, 5)}...${s.token.slice(-5)}
`
).join('');
}
card.style.display = 'block';
}
function processLocalGenres(items) {
const genreCounts = {};
items.forEach(item => {
const genre = item.genre || _('noGenre');
genreCounts[genre] = (genreCounts[genre] || 0) + 1;
});
return genreCounts;
}
function processLocalDecades(items) {
const decadeCounts = {};
items.forEach(item => {
if (item.year && !isNaN(item.year)) {
const decade = Math.floor(parseInt(item.year, 10) / 10) * 10;
decadeCounts[decade] = (decadeCounts[decade] || 0) + 1;
}
});
return decadeCounts;
}
async function createGenreChart(canvasId, genreData, label) {
const sortedGenres = Object.entries(genreData)
.sort(([, a], [, b]) => b - a)
.slice(0, 15);
const labels = sortedGenres.map(([name]) => name);
const data = sortedGenres.map(([, count]) => count);
const ctx = document.getElementById(canvasId).getContext('2d');
charts[canvasId] = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: label,
data: data,
backgroundColor: 'rgba(0, 224, 255, 0.6)',
borderColor: 'rgba(0, 224, 255, 1)',
borderWidth: 1,
borderRadius: 5,
}]
},
options: {
responsive: true,
indexAxis: 'y',
scales: { y: { ticks: { color: 'rgba(240, 240, 245, 0.8)' } }, x: { ticks: { color: 'rgba(240, 240, 245, 0.8)' } } },
plugins: { legend: { display: false }, tooltip: { bodyFont: { size: 14 }, titleFont: { size: 16 } } }
}
});
}
function createDecadeChart(canvasId, decadeData, label) {
const sortedDecades = Object.entries(decadeData).sort(([a], [b]) => a - b);
const labels = sortedDecades.map(([decade]) => `${decade}s`);
const data = sortedDecades.map(([, count]) => count);
const ctx = document.getElementById(canvasId).getContext('2d');
charts[canvasId] = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: label,
data: data,
fill: true,
backgroundColor: 'rgba(0, 114, 255, 0.2)',
borderColor: 'rgba(0, 114, 255, 1)',
tension: 0.3,
pointBackgroundColor: 'rgba(0, 114, 255, 1)',
pointBorderColor: '#fff',
pointHoverRadius: 7,
}]
},
options: {
responsive: true,
scales: { y: { ticks: { color: 'rgba(240, 240, 245, 0.8)', beginAtZero: true } }, x: { ticks: { color: 'rgba(240, 240, 245, 0.8)' } } },
plugins: { legend: { display: false } }
}
});
}
function animateValue(id, start, end, duration) {
const obj = document.getElementById(id);
if (!obj) return;
let startTimestamp = null;
const step = (timestamp) => {
if (!startTimestamp) startTimestamp = timestamp;
const progress = Math.min((timestamp - startTimestamp) / duration, 1);
obj.innerHTML = Math.floor(progress * (end - start) + start).toLocaleString(state.settings.language);
if (progress < 1) {
window.requestAnimationFrame(step);
}
};
window.requestAnimationFrame(step);
}
export async function searchByActor(actorId, actorName) {
if (state.isLoading) return;
showNotification(_('searchingActorContent', actorName), 'info');
if (document.getElementById('item-details-view').classList.contains('active')) {
const mainContent = document.querySelector('.main-content');
const topBarHeight = document.querySelector('.top-bar')?.offsetHeight || 60;
const targetScrollTop = mainContent ? mainContent.offsetTop - topBarHeight : 0;
state.lastScrollPosition = targetScrollTop;
showMainView();
}
state.currentView = 'search';
document.getElementById('search-input').value = ``;
state.currentParams.query = `actor:${actorName}`;
state.currentPage = 1;
updateSectionTitle();
updateActiveNav(state.currentParams.contentType);
const grid = document.getElementById('content-grid');
grid.innerHTML = '';
document.querySelector('.filters').style.display = 'none';
try {
const endpoint = `discover/${state.currentParams.contentType}?with_cast=${actorId}&sort_by=popularity.desc&page=1`;
const data = await fetchTMDB(endpoint);
renderGrid(data.results, false);
document.getElementById('load-more').style.display = (data.page < data.total_pages) ? 'block' : 'none';
} catch (error) {
grid.innerHTML = `${_('errorLoadingActorContent', actorName)}
`;
}
}
export async function initializeHeroSection() {
const heroSection = document.getElementById('hero-section');
if (heroSection.style.display === 'none') return;
try {
const type = Math.random() > 0.5 ? 'movie' : 'tv';
const data = await fetchTMDB(`${type}/popular?page=1`);
const popularItems = data.results.filter(i => i.backdrop_path && i.overview).slice(0, 8);
if (popularItems.length === 0) {
heroSection.style.display = 'none';
return;
}
const bg1 = document.querySelector('.hero-background-1');
const bg2 = document.querySelector('.hero-background-2');
const content = document.querySelector('.hero-content');
let currentBg = bg1;
let nextBg = bg2;
let currentIndex = -1;
function changeHeroSlide(isFirst = false) {
currentIndex = (currentIndex + 1) % popularItems.length;
const item = popularItems[currentIndex];
const nextImage = new Image();
nextImage.src = `https://image.tmdb.org/t/p/original${item.backdrop_path}`;
nextImage.onload = () => {
updateHeroContent(item);
const heroElements = [
content.querySelector('.hero-title'),
content.querySelector('.hero-subtitle'),
...content.querySelectorAll('.hero-meta-item'),
content.querySelector('.hero-buttons')
];
if (isFirst) {
gsap.set(currentBg, { backgroundImage: `url(${nextImage.src})` });
gsap.to(currentBg, { autoAlpha: 1, duration: 1.5, ease: 'power2.out' });
gsap.to(content, { autoAlpha: 1, duration: 1, delay: 0.5 });
gsap.fromTo(currentBg, { scale: 1.15, transformOrigin: 'center center' }, { scale: 1, duration: 12, ease: 'none' });
} else {
const tl = gsap.timeline({
onComplete: () => {
gsap.set(currentBg, { autoAlpha: 0 });
const temp = currentBg;
currentBg = nextBg;
nextBg = temp;
}
});
tl.to(heroElements, {
autoAlpha: 0,
y: 30,
stagger: 0.08,
duration: 0.6,
ease: 'power3.in'
});
gsap.set(nextBg, { backgroundImage: `url(${nextImage.src})` });
tl.to(nextBg, { autoAlpha: 1, duration: 1.5, ease: 'power2.inOut' }, '-=0.5');
gsap.fromTo(nextBg, { scale: 1.15, transformOrigin: 'center center' }, { scale: 1, duration: 12, ease: 'none' });
tl.fromTo(heroElements, {
y: -30,
autoAlpha: 0
}, {
y: 0,
autoAlpha: 1,
stagger: 0.1,
duration: 0.8,
ease: 'power3.out'
}, '>-1');
}
};
}
gsap.set(content, { autoAlpha: 0 });
gsap.set([bg1, bg2], { autoAlpha: 0 });
changeHeroSlide(true);
setInterval(() => changeHeroSlide(false), 12000);
} catch (error) {
console.error("Error initializing hero section:", error);
heroSection.style.display = 'none';
}
}
function updateHeroContent(item) {
const heroTitle = document.querySelector('.hero-title');
const heroSubtitle = document.querySelector('.hero-subtitle');
const heroRating = document.querySelector('#hero-rating');
const heroYear = document.querySelector('#hero-year');
const heroExtra = document.querySelector('#hero-extra');
const heroPlayBtn = document.getElementById('hero-play-btn');
const heroInfoBtn = document.getElementById('hero-info-btn');
const type = item.title ? 'movie' : 'tv';
const title = item.title || item.name;
const isAvailable = !!buscarContenidoLocal(title, type);
if (heroTitle) heroTitle.textContent = title;
if (heroSubtitle) heroSubtitle.textContent = item.overview.substring(0, 200) + (item.overview.length > 200 ? '...' : '');
if (heroRating) heroRating.innerHTML = ` ${item.vote_average.toFixed(1)}/10`;
if (heroYear) heroYear.innerHTML = ` ${(item.release_date || item.first_air_date).slice(0, 4)}`;
if (heroExtra) heroExtra.innerHTML = ` ${type === 'movie' ? _('moviesSectionTitle') : _('seriesSectionTitle')}`;
if (heroPlayBtn) {
heroPlayBtn.onclick = () => addStreamToList(title, type, heroPlayBtn);
heroPlayBtn.disabled = !isAvailable;
}
if (heroInfoBtn) {
heroInfoBtn.onclick = () => showItemDetails(item.id, type);
heroInfoBtn.disabled = false;
}
}
export async function addStreamToList(title, type, buttonElement = null) {
if (state.isAddingStream) return;
state.isAddingStream = true;
let originalButtonContent = null;
if (buttonElement) {
originalButtonContent = buttonElement.innerHTML;
buttonElement.disabled = true;
buttonElement.innerHTML = ``;
}
if (!state.settings.phpScriptUrl || !state.settings.phpScriptUrl.trim().startsWith('http')) {
showNotification(_('phpUrlNotConfigured'), 'warning');
state.isAddingStream = false;
if (buttonElement && originalButtonContent) {
buttonElement.innerHTML = originalButtonContent;
buttonElement.disabled = false;
}
return;
}
showNotification(_('searchingStreams', title), 'info');
try {
const streamData = await fetchAllStreamsFromPlex(title, type);
if (!streamData.success || streamData.streams.length === 0) throw new Error(streamData.message);
showNotification(_('sendingStreams', String(streamData.streams.length)), 'info');
const payload = { streams: streamData.streams.map(s => ({ url: s.url, extinf: s.extinf.replace(/[\r\n]+/g, '') })) };
const response = await fetch(state.settings.phpScriptUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const result = await response.json();
if (!response.ok || !result.success) throw new Error(result.message || _('errorServerResponse'));
showNotification(result.message || _('streamAddedSuccess'), 'success');
} catch (error) {
showNotification(_('errorAddingStream', error.message), "error");
} finally {
state.isAddingStream = false;
if (buttonElement && originalButtonContent) {
buttonElement.innerHTML = originalButtonContent;
buttonElement.disabled = false;
}
}
}
export async function downloadM3U(title, type, buttonElement = null) {
if (state.isDownloadingM3U) return;
state.isDownloadingM3U = true;
let originalButtonContent = null;
if (buttonElement) {
originalButtonContent = buttonElement.innerHTML;
buttonElement.disabled = true;
buttonElement.innerHTML = ``;
}
showNotification(_('generatingM3U', title), "info");
try {
const streamData = await fetchAllStreamsFromPlex(title, type);
if (!streamData.success || streamData.streams.length === 0) throw new Error(streamData.message);
let m3uContent = "#EXTM3U\n";
streamData.streams.forEach(stream => {
m3uContent += `${stream.extinf}\n${stream.url}\n`;
});
const blob = new Blob([m3uContent], { type: "audio/x-mpegurl;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${title.replace(/[^a-z0-9]/gi, '_')}.m3u`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showNotification(_('m3uDownloaded', title), 'success');
} catch (error) {
showNotification(_('errorGeneratingM3U', error.message), "error");
} finally {
state.isDownloadingM3U = false;
if (buttonElement && originalButtonContent) {
buttonElement.innerHTML = originalButtonContent;
buttonElement.disabled = false;
}
}
}
export function showTrailer(key) {
const lightbox = document.getElementById('video-lightbox');
const iframe = document.getElementById('video-iframe');
iframe.src = `https://www.youtube.com/embed/${key}?autoplay=1&modestbranding=1&rel=0`;
lightbox.classList.add('active');
document.body.style.overflow = 'hidden';
}
export function closeTrailer() {
const lightbox = document.getElementById('video-lightbox');
const iframe = document.getElementById('video-iframe');
iframe.src = '';
lightbox.classList.remove('active');
document.body.style.overflow = '';
}
function setupScrollEffects() {
gsap.utils.toArray('.item-card:not(.gsap-anim), .photo-card:not(.gsap-anim), .album-card:not(.gsap-anim)').forEach((el) => {
el.classList.add('gsap-anim');
gsap.from(el, {
autoAlpha: 0,
y: 50,
duration: 0.6,
ease: "power2.out",
scrollTrigger: {
trigger: el,
start: "top 95%",
toggleActions: "play none none none",
},
});
});
}
export function activateSettingsTab(tabId) {
const tabButtons = document.querySelectorAll('#settingsTabs .nav-link');
const tabPanes = document.querySelectorAll('#settingsTabsContent .tab-pane');
tabButtons.forEach(button => {
if (button.id === `${tabId}-tab`) {
button.classList.add('active');
button.setAttribute('aria-selected', 'true');
} else {
button.classList.remove('active');
button.setAttribute('aria-selected', 'false');
}
});
tabPanes.forEach(pane => {
if (pane.id === tabId) {
pane.classList.add('show', 'active');
} else {
pane.classList.remove('show', 'active');
}
});
}
export function openSettingsModal() {
document.getElementById('tmdbApiKey').value = state.settings.apiKey;
document.getElementById('appLanguage').value = state.settings.language;
document.getElementById('phpScriptUrl').value = state.settings.phpScriptUrl || '';
document.getElementById('lightModeToggle').checked = state.settings.theme === 'light';
document.getElementById('showHeroToggle').checked = state.settings.showHero;
document.getElementById('phpSecretKeyCheck').checked = state.settings.phpUseSecretKey;
document.getElementById('phpSecretKey').value = state.settings.phpSecretKey || '';
document.getElementById('phpSavePath').value = state.settings.phpSavePath || '';
document.getElementById('phpFilename').value = state.settings.phpFilename || 'CinePlex_Playlist.m3u';
document.getElementById('phpFileActionAppend').checked = state.settings.phpFileAction === 'append';
document.getElementById('phpFileActionOverwrite').checked = state.settings.phpFileAction === 'overwrite';
activateSettingsTab('general');
const modal = new bootstrap.Modal(document.getElementById('settingsModal'));
modal.show();
}
export async function saveSettings() {
const oldLanguage = state.settings.language;
const newSettings = {
id: 'user_settings',
apiKey: document.getElementById('tmdbApiKey').value.trim(),
language: document.getElementById('appLanguage').value,
theme: document.getElementById('lightModeToggle').checked ? 'light' : 'dark',
showHero: document.getElementById('showHeroToggle').checked,
phpScriptUrl: document.getElementById('phpScriptUrl').value.trim(),
phpUseSecretKey: document.getElementById('phpSecretKeyCheck').checked,
phpSecretKey: document.getElementById('phpSecretKey').value.trim(),
phpSavePath: document.getElementById('phpSavePath').value.trim(),
phpFilename: document.getElementById('phpFilename').value.trim(),
phpFileAction: document.getElementById('phpFileActionAppend').checked ? 'append' : 'overwrite'
};
state.settings = { ...state.settings, ...newSettings };
try {
await addItemsToStore('settings', [state.settings]);
showNotification(_('settingsSavedSuccess'), 'success');
applyTheme(state.settings.theme);
applyHeroVisibility(state.settings.showHero);
bootstrap.Modal.getInstance(document.getElementById('settingsModal'))?.hide();
if (newSettings.language !== oldLanguage) {
showNotification(_('languageChangeReload'), 'info', 4000);
setTimeout(() => window.location.reload(), 4000);
}
} catch (error) {
showNotification(_('errorSavingSettings'), 'error');
}
}
export function applyTheme(theme) {
document.body.classList.toggle('light-theme', theme === 'light');
}
export function applyHeroVisibility(show) {
const hero = document.getElementById('hero-section');
if(hero) hero.style.display = show ? 'flex' : 'none';
}
export const phpScriptGenerator = (() => {
let dom = {};
function cacheDom() {
const settingsModal = document.getElementById('settingsModal');
if (!settingsModal) return false;
dom.secretKeyCheck = settingsModal.querySelector('#phpSecretKeyCheck');
dom.secretKey = settingsModal.querySelector('#phpSecretKey');
dom.savePath = settingsModal.querySelector('#phpSavePath');
dom.filename = settingsModal.querySelector('#phpFilename');
dom.fileActionAppendRadio = settingsModal.querySelector('#phpFileActionAppend');
dom.generatedCode = settingsModal.querySelector('#generatedPhpCode');
dom.generateBtn = settingsModal.querySelector('#generatePhpScriptBtn');
dom.copyBtn = settingsModal.querySelector('#copyPhpScriptBtn');
return dom.generateBtn && dom.copyBtn;
}
function init() {
if (!cacheDom()) {
return;
}
dom.generateBtn.addEventListener('click', generatePhpScript);
dom.copyBtn.addEventListener('click', copyScript);
}
function generatePhpScript() {
const useSecretKey = dom.secretKeyCheck.checked;
const secretKey = dom.secretKey.value.trim();
const savePath = dom.savePath.value.trim();
const filename = dom.filename.value.trim() || 'CinePlex_Playlist.m3u';
const appendToFile = dom.fileActionAppendRadio.checked;
let script = ` $success, 'message' => $message, 'filename' => $filename]);
exit;
}
`;
if (useSecretKey) {
script += `
$auth_key = isset($_SERVER['HTTP_X_SECRET_KEY']) ? $_SERVER['HTTP_X_SECRET_KEY'] : '';
if (!defined('SECRET_KEY') || SECRET_KEY === '' || $auth_key !== SECRET_KEY) {
sendResponse(false, 'Acceso no autorizado. Clave secreta inválida o no proporcionada.', '', 403);
}
`;
}
script += `
$json_data = file_get_contents('php://input');
$data = json_decode($json_data, true);
if (json_last_error() !== JSON_ERROR_NONE) {
sendResponse(false, 'Error: Datos JSON inválidos.');
}
if (!isset($data['streams']) || !is_array($data['streams']) || empty($data['streams'])) {
sendResponse(false, 'Error: El JSON debe contener un array "streams" no vacío.');
}
$save_dir = SAVE_DIRECTORY !== '' ? rtrim(SAVE_DIRECTORY, '/\\\\') : __DIR__;
if (!is_dir($save_dir) || !is_writable($save_dir)) {
sendResponse(false, 'Error del servidor: El directorio de destino no existe o no tiene permisos de escritura.', '', 500);
}
$safe_filename = preg_replace('/[^\\w\\s._-]/', '', basename(FILENAME));
$safe_filename = preg_replace('/\\s+/', '_', $safe_filename);
$target_path = $save_dir . DIRECTORY_SEPARATOR . $safe_filename;
$content_to_write = "";
if (FILE_ACTION_APPEND) {
$file_exists = file_exists($target_path);
if (!$file_exists) {
$content_to_write .= "#EXTM3U\\n";
}
foreach ($data['streams'] as $stream) {
if (isset($stream['extinf'], $stream['url'])) {
$content_to_write .= trim($stream['extinf']) . "\\n";
$content_to_write .= trim($stream['url']) . "\\n";
}
}
if (file_put_contents($target_path, $content_to_write, FILE_APPEND | LOCK_EX) !== false) {
sendResponse(true, 'Streams añadidos correctamente al archivo.', $safe_filename, 200);
} else {
sendResponse(false, 'Error del servidor: No se pudo añadir contenido al archivo.', '', 500);
}
} else { // Overwrite mode
$content_to_write = "#EXTM3U\\n";
foreach ($data['streams'] as $stream) {
if (isset($stream['extinf'], $stream['url'])) {
$content_to_write .= trim($stream['extinf']) . "\\n";
$content_to_write .= trim($stream['url']) . "\\n";
}
}
if (file_put_contents($target_path, $content_to_write, LOCK_EX) !== false) {
sendResponse(true, 'Archivo de streams sobrescrito correctamente.', $safe_filename, 201);
} else {
sendResponse(false, 'Error del servidor: No se pudo escribir el archivo.', '', 500);
}
}
?>`;
dom.generatedCode.value = script;
showNotification(_("scriptGenerated"), "success");
}
function copyScript() {
if (!dom.generatedCode.value || dom.generatedCode.value.trim() === '') {
showNotification(_("errorGeneratingScript"), "warning");
return;
}
navigator.clipboard.writeText(dom.generatedCode.value).then(() => {
showNotification(_("scriptCopied"), "success");
}).catch(err => {
showNotification(_("errorCopyingScript"), "error");
});
}
return { init };
})();
export function initPhotosView() {
const select = document.getElementById('photos-token-select');
select.innerHTML = ``;
const photoServers = [...new Map(state.localPhotos.map(item => [item.tokenPrincipal, item])).values()];
if (photoServers.length === 0) {
select.innerHTML = ``;
document.getElementById('photos-empty-state').style.display = 'block';
document.getElementById('photos-grid').innerHTML = '';
return;
}
document.getElementById('photos-empty-state').style.display = 'none';
select.innerHTML = ``;
photoServers.forEach(server => {
const option = document.createElement('option');
option.value = server.tokenPrincipal;
option.textContent = server.serverName || `Servidor ${server.tokenPrincipal.slice(-4)}`;
select.appendChild(option);
});
if (state.currentPhotoToken && photoServers.some(s => s.tokenPrincipal === state.currentPhotoToken)) {
select.value = state.currentPhotoToken;
} else {
select.value = photoServers[0].tokenPrincipal;
}
handlePhotoTokenChange();
}
export function handlePhotoTokenChange() {
const select = document.getElementById('photos-token-select');
const selectedToken = select.value;
if (!selectedToken) {
document.getElementById('photos-grid').innerHTML = '';
document.getElementById('photos-empty-state').style.display = 'block';
return;
}
state.currentPhotoToken = selectedToken;
state.currentPhotoServer = state.localPhotos.find(s => s.tokenPrincipal === state.currentPhotoToken);
state.photoStack = [];
renderPhotoBreadcrumb();
renderPhotoGrid(state.currentPhotoServer.titulos, 'album');
}
async function fetchPhotoAlbumContent(albumData) {
const { id, token, ip, puerto, protocolo } = albumData;
const url = `${protocolo}://${ip}:${puerto}/library/metadata/${id}/children?X-Plex-Token=${token}`;
try {
const response = await fetchWithTimeout(url, {}, 10000);
if (!response.ok) throw new Error(_('errorPlexApi', String(response.status)));
const data = await response.text();
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(data, "text/xml");
if (xmlDoc.querySelector('parsererror')) throw new Error(_('errorParsingPlexXml'));
const photos = Array.from(xmlDoc.querySelectorAll('Photo')).map(el => {
const media = el.querySelector('Media Part');
if (!media) return null;
const key = media.getAttribute('key');
return {
type: 'photo',
id: el.getAttribute('ratingKey'),
title: el.getAttribute('title') || _('untitled'),
thumbUrl: `${protocolo}://${ip}:${puerto}/photo/:/transcode?width=400&height=400&minSize=1&upscale=1&url=${encodeURIComponent(key)}&X-Plex-Token=${token}`,
fullUrl: `${protocolo}://${ip}:${puerto}/photo/:/transcode?width=2000&height=2000&minSize=1&upscale=1&url=${encodeURIComponent(key)}&X-Plex-Token=${token}`
};
}).filter(p => p !== null);
const subAlbums = Array.from(xmlDoc.querySelectorAll('Directory[type="photo"]')).map(el => ({
type: 'album',
id: el.getAttribute('ratingKey'),
title: el.getAttribute('title'),
count: el.getAttribute('leafCount') || el.getAttribute('childCount'),
}));
return [...subAlbums, ...photos];
} catch (error) {
showNotification(_('errorLoadingAlbum', error.message), 'error');
return [];
}
}
async function loadPhotoAlbum(albumData) {
const loader = document.getElementById('photos-loader');
const grid = document.getElementById('photos-grid');
grid.innerHTML = '';
loader.style.display = 'block';
document.getElementById('photos-empty-state').style.display = 'none';
const items = await fetchPhotoAlbumContent(albumData);
loader.style.display = 'none';
if (items.length > 0) {
renderPhotoGrid(items);
} else {
document.getElementById('photos-empty-state').style.display = 'block';
}
}
export async function handlePhotoGridClick(card) {
const { type, id, title } = card.dataset;
if (type === 'album') {
if (!state.currentPhotoServer) {
showNotification(_('noPhotoServerSelected'), "error");
return;
}
const albumData = {
...state.currentPhotoServer,
id: id,
title: title
};
state.photoStack.push({ id, title });
renderPhotoBreadcrumb();
await loadPhotoAlbum(albumData);
} else if (type === 'photo') {
const photoIndex = state.currentPhotoItems.findIndex(p => p.id === id);
if (photoIndex > -1) {
openPhotoLightbox(photoIndex);
}
}
}
function renderPhotoGrid(items, forceType = null) {
const grid = document.getElementById('photos-grid');
grid.innerHTML = '';
document.getElementById('photos-empty-state').style.display = 'none';
state.currentPhotoItems = [];
if (!items || items.length === 0) {
document.getElementById('photos-empty-state').style.display = 'block';
return;
}
const fragment = document.createDocumentFragment();
items.forEach(item => {
const type = forceType || item.type;
const card = document.createElement('div');
if (type === 'album') {
card.className = 'album-card';
card.innerHTML = `
${item.title}
${item.count ? `${_('itemCount', String(item.count))}
` : ''}
`;
card.dataset.type = 'album';
} else if (type === 'photo') {
state.currentPhotoItems.push(item);
card.className = 'photo-card';
card.innerHTML = `
${item.title}
`;
card.dataset.type = 'photo';
}
card.dataset.id = item.id;
card.dataset.title = item.title;
fragment.appendChild(card);
});
grid.appendChild(fragment);
setupScrollEffects();
}
function renderPhotoBreadcrumb() {
const breadcrumb = document.getElementById('photos-breadcrumb');
breadcrumb.innerHTML = '';
const rootItem = document.createElement('li');
rootItem.className = 'breadcrumb-item';
const rootLink = document.createElement('a');
rootLink.href = '#';
rootLink.innerHTML = ` ${_('photosBreadcrumbHome')}`;
rootLink.onclick = (e) => {
e.preventDefault();
handlePhotoTokenChange();
};
rootItem.appendChild(rootLink);
breadcrumb.appendChild(rootItem);
state.photoStack.forEach((item, index) => {
const divider = document.createElement('li');
divider.className = 'breadcrumb-divider';
divider.innerHTML = '';
breadcrumb.appendChild(divider);
const breadcrumbItem = document.createElement('li');
breadcrumbItem.className = 'breadcrumb-item';
if (index === state.photoStack.length - 1) {
breadcrumbItem.classList.add('active');
breadcrumbItem.textContent = item.title;
} else {
const link = document.createElement('a');
link.href = '#';
link.textContent = item.title;
link.onclick = async (e) => {
e.preventDefault();
state.photoStack = state.photoStack.slice(0, index + 1);
renderPhotoBreadcrumb();
if (!state.currentPhotoServer) return;
const albumData = { ...state.currentPhotoServer, id: item.id, title: item.title };
await loadPhotoAlbum(albumData);
};
breadcrumbItem.appendChild(link);
}
breadcrumb.appendChild(breadcrumbItem);
});
}
function openPhotoLightbox(startIndex) {
if (state.currentPhotoItems.length === 0 || startIndex < 0) return;
state.currentPhotoLightboxIndex = startIndex;
const lightbox = document.getElementById('photo-lightbox');
document.body.style.overflow = 'hidden';
updatePhotoLightbox();
gsap.fromTo(lightbox, { autoAlpha: 0 }, { display: 'flex', autoAlpha: 1, duration: 0.3 });
}
export function closePhotoLightbox() {
const lightbox = document.getElementById('photo-lightbox');
document.body.style.overflow = '';
gsap.to(lightbox, {
autoAlpha: 0,
duration: 0.3,
onComplete: () => lightbox.style.display = 'none'
});
}
function updatePhotoLightbox() {
const photo = state.currentPhotoItems[state.currentPhotoLightboxIndex];
if (!photo) return;
const img = document.getElementById('photo-lightbox-img');
const caption = document.getElementById('photo-lightbox-caption');
gsap.to([img, caption], {
autoAlpha: 0,
duration: 0.15,
onComplete: () => {
img.src = photo.fullUrl;
caption.textContent = photo.title;
gsap.to([img, caption], { autoAlpha: 1, duration: 0.15 });
}
});
}
export function showNextPhoto() {
state.currentPhotoLightboxIndex = (state.currentPhotoLightboxIndex + 1) % state.currentPhotoItems.length;
updatePhotoLightbox();
}
export function showPrevPhoto() {
state.currentPhotoLightboxIndex = (state.currentPhotoLightboxIndex - 1 + state.currentPhotoItems.length) % state.currentPhotoItems.length;
updatePhotoLightbox();
}