CinePlex/js/music.js
2025-08-16 10:53:11 +02:00

514 lines
19 KiB
JavaScript

import { state } from './state.js';
import { debounce, _, showNotification } from './utils.js';
import { fetchArtistGenresFromLastFM } from './api.js';
import { getFromDB, addItemsToStore } from './db.js';
let musicSection, genreGrid, artistGrid, songListView, searchInput, serverFilter, loadMoreBtn, backBtn, genreSearchInput;
let isInitialized = false;
let isClassifying = false;
let currentPage = 0;
const ARTISTS_PER_PAGE = 20;
let currentMusicView = 'genres';
let selectedGenre = null;
let genreScrollPosition = 0;
export function initMusicView() {
if (typeof chrome === 'undefined' || !chrome.storage) {
console.warn('Running outside of Chrome extension, skipping initMusicView.');
return;
}
musicSection = document.getElementById('music-section');
if (!musicSection) return;
if (state.musicPlayer && state.musicPlayer.isReady) {
setupMusicView();
} else {
const initialGrid = document.getElementById('music-section-genre-grid');
if (initialGrid) initialGrid.innerHTML = `<div class="col-12 text-center mt-5"><div class="spinner"></div><p class="mt-2">${_('loading')}</p></div>`;
const checkReady = setInterval(() => {
if (state.musicPlayer && state.musicPlayer.isReady) {
clearInterval(checkReady);
setupMusicView();
}
}, 150);
}
}
async function setupMusicView() {
genreGrid = document.getElementById('music-section-genre-grid');
artistGrid = document.getElementById('music-section-artist-grid');
songListView = document.getElementById('music-section-song-list');
searchInput = document.getElementById('music-section-search-input');
serverFilter = document.getElementById('music-server-filter');
loadMoreBtn = document.getElementById('music-load-more-btn');
backBtn = document.getElementById('music-back-btn');
genreSearchInput = document.getElementById('genre-search-input');
if (!genreGrid || !artistGrid || !songListView || !searchInput || !serverFilter || !loadMoreBtn || !backBtn || !genreSearchInput) return;
if (!isInitialized) {
serverFilter.addEventListener('change', () => {
currentPage = 0;
renderArtists();
});
searchInput.addEventListener('input', debounce(() => {
currentPage = 0;
renderArtists();
}, 300));
genreSearchInput.addEventListener('input', debounce(() => {
renderGenres();
}, 300));
loadMoreBtn.addEventListener('click', () => {
currentPage++;
renderArtists(true);
});
backBtn.addEventListener('click', navigateBack);
genreGrid.addEventListener('click', handleGenreClick);
artistGrid.addEventListener('click', handleArtistClick);
isInitialized = true;
}
if (isClassifying) {
const overlay = document.getElementById('music-classification-overlay');
overlay.style.display = 'flex';
} else {
await checkAndClassifyArtists();
}
currentMusicView = 'genres';
selectedGenre = null;
await renderMusicView();
}
async function checkAndClassifyArtists() {
if (isClassifying) {
const overlay = document.getElementById('music-classification-overlay');
overlay.style.display = 'flex';
return;
}
const allServers = await getFromDB('artists');
let artistsToClassify = [];
allServers.forEach(server => {
if (server.titulos) {
server.titulos.forEach(artist => {
if (!artist.hasOwnProperty('genres')) {
artistsToClassify.push(artist);
}
});
}
});
if (artistsToClassify.length > 0) {
isClassifying = true;
const overlay = document.getElementById('music-classification-overlay');
overlay.style.display = 'flex';
const totalArtists = artistsToClassify.length;
try {
for (let i = 0; i < artistsToClassify.length; i += 5) {
const batch = artistsToClassify.slice(i, i + 5);
const promises = batch.map(artist =>
fetchArtistGenresFromLastFM(artist.title).then(genres => ({ artist, genres }))
);
await Promise.all(promises.map(p => p.then(({ artist, genres }) => {
artist.genres = genres.length > 0 ? genres : ['Varios'];
})));
const processedCount = Math.min(i + batch.length, totalArtists);
const percentage = Math.round((processedCount / totalArtists) * 100);
const nextArtist = artistsToClassify[processedCount];
updateClassificationProgress(percentage, processedCount, totalArtists, nextArtist ? nextArtist.title : null);
}
await addItemsToStore('artists', allServers);
await state.musicPlayer.loadMusicData();
showNotification('Clasificación de música completada.', 'success');
} catch (error) {
showNotification('Ocurrió un error durante la clasificación de artistas.', 'error');
} finally {
isClassifying = false;
gsap.to(overlay, {
autoAlpha: 0,
duration: 0.5,
onComplete: () => {
overlay.style.display = 'none';
}
});
}
}
}
function updateClassificationProgress(percentage, processedCount, totalArtists, nextArtistTitle) {
const progressFill = document.getElementById('classification-progress-fill');
const percentageText = document.getElementById('classification-percentage');
const progressDetails = document.getElementById('classification-progress-details');
const statusText = document.getElementById('classification-status-text');
if (progressFill) progressFill.style.width = `${percentage}%`;
if (percentageText) percentageText.textContent = `${percentage}%`;
if (progressDetails) progressDetails.textContent = `${processedCount} / ${totalArtists} artistas`;
if (statusText) {
if (nextArtistTitle) {
statusText.textContent = `Analizando: ${nextArtistTitle}...`;
} else {
statusText.textContent = `Finalizando...`;
}
}
}
async function renderMusicView() {
genreGrid.style.display = 'none';
artistGrid.style.display = 'none';
songListView.style.display = 'none';
serverFilter.style.display = 'none';
searchInput.parentElement.style.display = 'none';
genreSearchInput.parentElement.style.display = 'none';
backBtn.style.display = 'none';
loadMoreBtn.style.display = 'none';
switch (currentMusicView) {
case 'genres':
genreSearchInput.parentElement.style.display = 'flex';
await renderGenres();
break;
case 'artists':
backBtn.style.display = 'inline-flex';
serverFilter.style.display = 'block';
searchInput.parentElement.style.display = 'flex';
renderArtists();
break;
case 'songs':
break;
}
}
async function renderGenres() {
currentMusicView = 'genres';
genreGrid.style.display = 'grid';
genreGrid.innerHTML = '';
const allArtists = state.musicPlayer._generateFullArtistListForToken('all');
const genreSet = new Set();
allArtists.forEach(artist => {
if (artist.genres && artist.genres.length > 0) {
artist.genres.forEach(g => genreSet.add(g));
}
});
let sortedGenres = Array.from(genreSet).sort((a, b) => a.localeCompare(b));
const filter = genreSearchInput.value.toLowerCase();
if (filter) {
sortedGenres = sortedGenres.filter(genre => genre.toLowerCase().includes(filter));
}
if (sortedGenres.length === 0) {
genreGrid.innerHTML = `<div class="empty-state"><i class="fas fa-microphone-slash"></i><p>${_('noArtistsFound')}</p></div>`;
return;
}
const fragment = document.createDocumentFragment();
sortedGenres.forEach(genre => {
const card = document.createElement('div');
card.className = 'genre-card';
card.textContent = genre;
card.dataset.genre = genre;
fragment.appendChild(card);
});
genreGrid.appendChild(fragment);
}
function handleGenreClick(e) {
const card = e.target.closest('.genre-card');
if (card && card.dataset.genre) {
genreScrollPosition = window.scrollY;
window.scrollTo(0, 0);
selectedGenre = card.dataset.genre;
currentMusicView = 'artists';
currentPage = 0;
populateServerFilter();
renderMusicView();
}
}
async function navigateBack() {
if (currentMusicView === 'songs') {
currentMusicView = 'artists';
await renderMusicView();
} else if (currentMusicView === 'artists') {
selectedGenre = null;
currentMusicView = 'genres';
await renderMusicView();
requestAnimationFrame(() => {
window.scrollTo(0, genreScrollPosition);
});
}
}
function populateServerFilter() {
if (!serverFilter || !state.musicPlayer) return;
const currentVal = serverFilter.value;
serverFilter.innerHTML = `<option value="all">${_('musicAllServers')}</option>`;
state.musicPlayer.tokens.forEach(token => {
const option = document.createElement('option');
option.value = token.value;
option.textContent = token.name;
serverFilter.appendChild(option);
});
if (currentVal) serverFilter.value = currentVal;
}
function renderArtists(append = false) {
if (!artistGrid || !state.musicPlayer) return;
artistGrid.style.display = 'grid';
const musicPlayer = state.musicPlayer;
const selectedServer = serverFilter.value;
const filter = searchInput.value.toLowerCase();
let allArtists = musicPlayer._generateFullArtistListForToken(selectedServer);
if (selectedGenre) {
allArtists = allArtists.filter(artist => artist.genres && artist.genres.map(g => g.toLowerCase()).includes(selectedGenre.toLowerCase()));
}
const filteredArtists = filter
? allArtists.filter(artist => artist.title.toLowerCase().includes(filter))
: allArtists;
if (!append) {
artistGrid.innerHTML = '';
currentPage = 0;
}
if (filteredArtists.length === 0 && !append) {
artistGrid.innerHTML = `<div class="empty-state" style="grid-column: 1 / -1;"><i class="fas fa-microphone-slash"></i><p>${_('noArtistsFound')}</p></div>`;
loadMoreBtn.style.display = 'none';
return;
}
const startIndex = currentPage * ARTISTS_PER_PAGE;
const endIndex = startIndex + ARTISTS_PER_PAGE;
const artistsToRender = filteredArtists.slice(startIndex, endIndex);
const fragment = document.createDocumentFragment();
artistsToRender.forEach(artist => {
const card = createMusicCard(artist.title, artist.serverName, artist.thumb, artist.id, artist.isJellyfin, artist.token, artist.protocolo, artist.ip, artist.puerto);
fragment.appendChild(card);
});
artistGrid.appendChild(fragment);
if (endIndex < filteredArtists.length) {
loadMoreBtn.style.display = 'block';
} else {
loadMoreBtn.style.display = 'none';
}
if (musicPlayer) {
musicPlayer.markCurrentArtist();
}
}
async function handleArtistClick(e) {
const card = e.target.closest('.artist-card-spotify');
if (card && card.dataset.id) {
currentMusicView = 'songs';
const artistId = card.dataset.id;
const musicPlayer = state.musicPlayer;
if (musicPlayer) {
const fullList = musicPlayer._generateFullArtistListForToken(serverFilter.value);
const artistData = fullList.find(a => a.id == artistId);
if (artistData) {
const songs = await musicPlayer.getArtistSongs(artistData);
renderSongList(artistData, songs);
}
}
}
}
async function renderSongList(artistData, songs) {
artistGrid.style.display = 'none';
genreGrid.style.display = 'none';
searchInput.parentElement.style.display = 'none';
serverFilter.style.display = 'none';
loadMoreBtn.style.display = 'none';
backBtn.style.display = 'none';
songListView.style.display = 'block';
songListView.innerHTML = '';
let thumbUrl = 'img/no-profile.png';
if (artistData.thumb) {
if (artistData.isJellyfin) {
thumbUrl = artistData.thumb;
} else {
thumbUrl = `${artistData.protocolo}://${artistData.ip}:${artistData.puerto}${artistData.thumb}?X-Plex-Token=${artistData.token}`;
}
}
const header = document.createElement('div');
header.className = 'song-list-header-spotify';
header.innerHTML = `
<button id="backToArtistsViewBtn" class="btn-icon"><i class="fas fa-arrow-left"></i></button>
<img src="${thumbUrl}" class="artist-header-thumb-spotify" crossorigin="anonymous" onerror="this.onerror=null;this.src='img/no-profile.png';">
<div class="artist-header-info-spotify">
<span class="artist-header-type">${_('artist')}</span>
<h1>${artistData.title}</h1>
</div>
`;
songListView.appendChild(header);
const img = header.querySelector('.artist-header-thumb-spotify');
img.onload = () => {
if(img.src.includes('no-profile.png')) return;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
const data = ctx.getImageData(0, 0, 1, 1).data;
const color = `rgba(${data[0]}, ${data[1]}, ${data[2]}, 0.5)`;
const darkColor = `rgba(${Math.round(data[0] * 0.3)}, ${Math.round(data[1] * 0.3)}, ${Math.round(data[2] * 0.3)}, 0.8)`;
header.style.background = `linear-gradient(135deg, ${color} 0%, ${darkColor} 100%)`;
};
const songListContainer = document.createElement('div');
songListContainer.className = 'song-list-container-spotify';
songListView.appendChild(songListContainer);
songListContainer.addEventListener('click', e => {
const row = e.target.closest('.song-grid-row');
if(row) {
const songId = row.dataset.id;
const originalIndex = songs.findIndex(s => s.id === songId);
if(originalIndex !== -1) {
state.musicPlayer.cancionesActuales = songs;
state.musicPlayer.playSong(originalIndex);
}
}
const albumPlayBtn = e.target.closest('.album-play-btn');
if(albumPlayBtn) {
const albumTitle = albumPlayBtn.dataset.album;
const albumSongs = songs.filter(s => s.album === albumTitle);
if(albumSongs.length > 0) {
state.musicPlayer.cancionesActuales = albumSongs;
state.musicPlayer.playSong(0);
}
}
});
if (songs.length === 0) {
songListContainer.innerHTML = `<div class="empty-state"><i class="fas fa-music"></i><p>${_('noSongsFound')}</p></div>`;
return;
}
const albums = songs.reduce((acc, song) => {
const albumTitle = song.album || 'Unknown Album';
if (!acc[albumTitle]) {
acc[albumTitle] = {
cover: song.cover,
songs: []
};
}
acc[albumTitle].songs.push(song);
return acc;
}, {});
for (const albumTitle in albums) {
const album = albums[albumTitle];
const albumContainer = document.createElement('div');
albumContainer.className = 'album-group-container-spotify';
albumContainer.innerHTML = `
<div class="album-group-header-spotify">
<img src="${album.cover || 'img/no-poster.png'}" class="album-group-cover-art-spotify" onerror="this.onerror=null;this.src='img/no-poster.png';">
<div class="album-info-spotify">
<h3 class="album-group-title-spotify">${albumTitle}</h3>
<span class="album-track-count">${album.songs.length} ${_('tracks')}</span>
</div>
<button class="album-play-btn" data-album="${albumTitle}">
<i class="fas fa-play"></i>
</button>
</div>
<div class="song-list-grid">
<div class="song-list-grid-header">
<div>#</div>
<div>${_('infoModalFieldTitle')}</div>
<div class="song-duration-header"><i class="far fa-clock"></i></div>
</div>
</div>
`;
const songsGrid = albumContainer.querySelector('.song-list-grid');
album.songs.forEach(song => {
const songRow = document.createElement('div');
songRow.className = 'song-grid-row';
songRow.dataset.id = song.id;
const originalIndex = songs.findIndex(s => s.id === song.id);
songRow.innerHTML = `
<div class="song-index">
<span class="track-number">${song.index || originalIndex + 1}</span>
<i class="fas fa-play play-icon-spotify"></i>
<div class="playing-indicator"><div></div><div></div><div></div></div>
</div>
<div class="song-title-artist">
<span class="song-title-spotify">${song.titulo}</span>
</div>
<div class="song-duration">${song.duration ? state.musicPlayer.formatTime(song.duration / 1000) : '--:--'}</div>
`;
songsGrid.appendChild(songRow);
});
songListContainer.appendChild(albumContainer);
}
document.getElementById('backToArtistsViewBtn').addEventListener('click', navigateBack);
if (state.musicPlayer) {
state.musicPlayer.markCurrentSong();
}
}
function createMusicCard(title, subtitle, imageUrl, artistId, isJellyfin, token, protocolo, ip, puerto) {
const card = document.createElement('div');
card.className = 'artist-card-spotify';
card.dataset.id = artistId;
let thumbUrl = 'img/no-profile.png';
if (imageUrl) {
if (isJellyfin === 'true' || isJellyfin === true) {
thumbUrl = imageUrl;
} else {
thumbUrl = `${protocolo}://${ip}:${puerto}${imageUrl}?X-Plex-Token=${token}`;
}
}
card.innerHTML = `
<div class="artist-card-img-container">
<img src="${thumbUrl}" class="artist-card-img" alt="${title}" loading="lazy" onerror="this.onerror=null;this.src='img/no-profile.png';">
<div class="artist-card-play-btn">
<i class="fas fa-play"></i>
</div>
</div>
<div class="artist-card-info">
<p class="artist-card-title-spotify">${title}</p>
<p class="artist-card-subtitle">${_('artist')}</p>
</div>
`;
return card;
}