2025-07-02 14:16:25 +02:00
|
|
|
import { getFromDB } from './db.js';
|
|
|
|
import { debounce, showNotification, _ } from './utils.js';
|
|
|
|
import { getMusicUrlsFromPlex } from './api.js';
|
|
|
|
|
|
|
|
export class MusicPlayer {
|
|
|
|
constructor() {
|
|
|
|
this.cancionesActuales = [];
|
|
|
|
this.indiceActual = -1;
|
|
|
|
this.isPlaying = false;
|
|
|
|
this.audioPlayer = document.getElementById("audioPlayer");
|
|
|
|
this.currentArtist = "";
|
|
|
|
this.currentAlbumId = null;
|
|
|
|
this.currentSongId = null;
|
|
|
|
this.currentArtistId = null;
|
|
|
|
this.currentSongArtistId = null;
|
|
|
|
this.db = null;
|
|
|
|
this.artistListScrollPosition = 0;
|
|
|
|
this.artistsData = [];
|
|
|
|
this.artistsPageSize = 20;
|
|
|
|
this.currentPage = 0;
|
|
|
|
this.shuffleMode = false;
|
|
|
|
this.tokens = [];
|
|
|
|
this.isPlayerVisible = false;
|
|
|
|
this.isReady = false;
|
|
|
|
this.isInitializing = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
setDB(databaseInstance) {
|
|
|
|
if (databaseInstance && this.db !== databaseInstance) {
|
|
|
|
this.db = databaseInstance;
|
|
|
|
if (!this.isReady && !this.isInitializing) {
|
|
|
|
this.asyncInitialize();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async asyncInitialize() {
|
|
|
|
if (this.isReady || this.isInitializing || !this.db) return;
|
|
|
|
this.isInitializing = true;
|
|
|
|
try {
|
|
|
|
this.setupEventHandlers();
|
|
|
|
await this.loadMusicData();
|
|
|
|
this.updateArtistCounter();
|
|
|
|
this.markCurrentArtist();
|
|
|
|
this.setupIndexedDBUpdateListener();
|
|
|
|
this.isReady = true;
|
|
|
|
} catch (error) {
|
|
|
|
showNotification(_('errorInitializingMusicPlayer'), "error");
|
|
|
|
this.isReady = false;
|
|
|
|
} finally {
|
|
|
|
this.isInitializing = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async loadMusicData() {
|
|
|
|
if (!this.db) {
|
|
|
|
document.getElementById('artistList').innerHTML = `<div class="list-item-empty">${_('dbUnavailableError')}</div>`;
|
|
|
|
this.artistsData = []; this.tokens = []; this.updateArtistCounter();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
const rawArtistData = await getFromDB('artists');
|
|
|
|
this.artistsData = rawArtistData || [];
|
|
|
|
this.loadTokens();
|
|
|
|
const initialArtistList = this._generateFullArtistListForToken('all');
|
|
|
|
this.loadArtists(initialArtistList, this.currentPage);
|
|
|
|
} catch (error) {
|
|
|
|
showNotification(_('criticalErrorLoadingMusic'), "error");
|
|
|
|
this.artistsData = []; this.tokens = [];
|
|
|
|
document.getElementById('artistList').innerHTML = `<div class="list-item-empty">${_('errorLoadingArtists')}</div>`;
|
|
|
|
this.updateTokenSelectorUI();
|
|
|
|
this.updateArtistCounter();
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
setupEventHandlers() {
|
|
|
|
document.querySelectorAll('#openMusicPlayerMobile, #openMusicPlayerDesktop, #openMusicPlayerFromMiniplayer').forEach(btn => {
|
|
|
|
btn.addEventListener('click', () => this.togglePlayerVisibility());
|
|
|
|
});
|
|
|
|
document.getElementById('closeSideNavBtn').addEventListener('click', () => this.hidePlayer());
|
|
|
|
document.getElementById('searchArtist').addEventListener("input", debounce(() => this.filterArtists(), 300));
|
|
|
|
document.getElementById('searchSong').addEventListener("input", debounce(() => this.filterSongs(), 300));
|
|
|
|
document.getElementById('backBtn').addEventListener('click', () => this.showArtistList());
|
|
|
|
document.getElementById('playPauseBtn').addEventListener('click', () => this.togglePlayPause());
|
|
|
|
document.getElementById('nextBtn').addEventListener('click', () => this.playNext());
|
|
|
|
document.getElementById('prevBtn').addEventListener('click', () => this.playPrevious());
|
|
|
|
document.getElementById('shuffleBtn').addEventListener('click', () => this.toggleShuffle());
|
|
|
|
|
|
|
|
document.getElementById('volumeSlider').addEventListener("input", (event) => this.updateVolume(event));
|
|
|
|
document.getElementById('volume-icon-btn').addEventListener('click', (e) => {
|
|
|
|
e.stopPropagation();
|
|
|
|
document.querySelector('.volume-slider-wrapper').classList.toggle('active');
|
|
|
|
this.audioPlayer.muted = false;
|
|
|
|
if (this.audioPlayer.volume === 0) {
|
|
|
|
this.audioPlayer.volume = 0.5;
|
|
|
|
this.updateVolumeIcon(0.5);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
document.addEventListener('click', (e) => {
|
|
|
|
if (!document.getElementById('volumeControl').contains(e.target)) {
|
|
|
|
document.querySelector('.volume-slider-wrapper').classList.remove('active');
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('downloadAlbumBtn').addEventListener('click', () => this.downloadAlbum());
|
|
|
|
document.getElementById('downloadBtn').addEventListener('click', () => {
|
|
|
|
if (this.indiceActual >= 0 && this.cancionesActuales[this.indiceActual]) {
|
|
|
|
const currentSong = this.cancionesActuales[this.indiceActual];
|
|
|
|
this.downloadSong(currentSong.url, currentSong.titulo, currentSong.extension);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
this.audioPlayer.addEventListener("ended", () => this.handleAudioEnded());
|
|
|
|
this.audioPlayer.addEventListener("timeupdate", () => this.updateProgressBar());
|
|
|
|
this.audioPlayer.addEventListener("error", () => this.handleAudioErrorEvent());
|
|
|
|
|
|
|
|
const progressBarContainer = document.getElementById('progressBarContainer');
|
|
|
|
progressBarContainer.addEventListener('click', (event) => this.seek(event));
|
|
|
|
progressBarContainer.addEventListener('mousemove', (event) => this.updateSeekHover(event));
|
|
|
|
progressBarContainer.addEventListener('mouseleave', () => this.hideSeekHover());
|
|
|
|
|
|
|
|
document.getElementById('artistList').addEventListener("click", (event) => {
|
|
|
|
const card = event.target.closest('.artist-card');
|
|
|
|
if(card) this.loadArtistSongs(card);
|
|
|
|
});
|
|
|
|
document.getElementById('listaCanciones').addEventListener("click", (event) => {
|
|
|
|
const item = event.target.closest('.song-item');
|
|
|
|
if(item) {
|
|
|
|
const index = parseInt(item.dataset.index, 10);
|
|
|
|
if (!isNaN(index) && this.cancionesActuales[index]) {
|
|
|
|
this.playSong(this.cancionesActuales[index], index);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
document.getElementById('prevArtistsBtn').addEventListener('click', () => this.loadPreviousArtists());
|
|
|
|
document.getElementById('nextArtistsBtn').addEventListener('click', () => this.loadNextArtists());
|
|
|
|
|
|
|
|
const tokenSelectorContainer = document.getElementById('tokenSelectorContainer');
|
|
|
|
tokenSelectorContainer.addEventListener('click', e => {
|
|
|
|
const target = e.target;
|
|
|
|
const selectItems = tokenSelectorContainer.querySelector('.select-items');
|
|
|
|
if (target.closest('.select-selected')) {
|
|
|
|
selectItems.classList.toggle('select-hide');
|
|
|
|
target.closest('.select-selected').classList.toggle('select-arrow-active');
|
|
|
|
} else if (target.classList.contains('select-option')) {
|
|
|
|
const selectedText = tokenSelectorContainer.querySelector('.select-selected span');
|
|
|
|
selectedText.textContent = target.textContent;
|
|
|
|
selectedText.dataset.value = target.dataset.value;
|
|
|
|
selectItems.classList.add('select-hide');
|
|
|
|
tokenSelectorContainer.querySelector('.select-selected').classList.remove('select-arrow-active');
|
|
|
|
|
|
|
|
this.currentPage = 0;
|
|
|
|
this.loadArtistsByToken(target.dataset.value);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
document.addEventListener('click', (e) => {
|
|
|
|
if (!tokenSelectorContainer.contains(e.target)) {
|
|
|
|
tokenSelectorContainer.querySelector('.select-items').classList.add('select-hide');
|
|
|
|
tokenSelectorContainer.querySelector('.select-selected').classList.remove('select-arrow-active');
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
document.getElementById('trackInfo').addEventListener('click', (event) => {
|
|
|
|
if (event.target.closest('#albumCover')) {
|
|
|
|
if (this.indiceActual >= 0 && this.cancionesActuales[this.indiceActual]) {
|
|
|
|
this.showInfoModal(this.cancionesActuales[this.indiceActual]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
_getCurrentTokenFilter() {
|
|
|
|
return document.querySelector("#tokenSelectorContainer .select-selected span").dataset.value;
|
|
|
|
}
|
|
|
|
|
|
|
|
handleAudioEnded() { this.playNext(); }
|
|
|
|
handleAudioErrorEvent() { this.handleAudioError(_('playbackError')); }
|
|
|
|
|
|
|
|
togglePlayerVisibility() {
|
|
|
|
if (this.isPlayerVisible) this.hidePlayer();
|
|
|
|
else this.showPlayer();
|
|
|
|
}
|
|
|
|
|
|
|
|
async showPlayer() {
|
|
|
|
if (this.isPlayerVisible) return;
|
|
|
|
|
|
|
|
gsap.to('#musicPlayerContainer', { x: 0, duration: 0.5, ease: 'power3.out' });
|
|
|
|
this.isPlayerVisible = true;
|
|
|
|
|
|
|
|
if (!this.isReady) await this.asyncInitialize();
|
|
|
|
if (!this.isReady) {
|
|
|
|
document.getElementById('artistList').innerHTML = `<div class="list-item-empty">${_('errorInitializingMusicPlayer')}</div>`;
|
|
|
|
this.updateArtistCounter();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (this.artistsData.length === 0 && this.isReady) {
|
|
|
|
try { await this.loadMusicData(); } catch (error) {
|
|
|
|
document.getElementById('artistList').innerHTML = `<div class="list-item-empty">${_('errorLoadingArtists')}</div>`;
|
|
|
|
this.updateArtistCounter();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.markCurrentArtist();
|
|
|
|
this.markCurrentSong();
|
|
|
|
}
|
|
|
|
|
|
|
|
hidePlayer() {
|
|
|
|
if (!this.isPlayerVisible) return;
|
|
|
|
gsap.to('#musicPlayerContainer', { x: '-100%', duration: 0.4, ease: 'power3.in' });
|
|
|
|
this.isPlayerVisible = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
async handleDatabaseUpdate() {
|
|
|
|
if (!this.isReady) await this.asyncInitialize();
|
|
|
|
if (!this.isReady) return;
|
|
|
|
showNotification(_('updatingMusicData'), "info", 1500);
|
|
|
|
const currentTokenFilter = this._getCurrentTokenFilter();
|
|
|
|
const songListPanel = document.getElementById("songListContainer");
|
|
|
|
const wasSongListVisible = songListPanel.style.visibility === 'visible' && songListPanel.style.opacity === '1';
|
|
|
|
const wasArtistId = this.currentArtistId;
|
|
|
|
|
|
|
|
await this.loadMusicData();
|
|
|
|
|
|
|
|
this.loadArtistsByToken(currentTokenFilter);
|
|
|
|
|
|
|
|
if (wasSongListVisible && wasArtistId) {
|
|
|
|
const artistCard = document.querySelector(`.artist-card[data-id='${wasArtistId}']`);
|
|
|
|
if (artistCard) {
|
|
|
|
try {
|
|
|
|
const canciones = await getMusicUrlsFromPlex(artistCard.dataset.token, artistCard.dataset.protocolo, artistCard.dataset.ip, artistCard.dataset.puerto, wasArtistId);
|
|
|
|
this.handleSongsLoaded(canciones, wasArtistId);
|
|
|
|
this.markCurrentSong();
|
|
|
|
|
|
|
|
const tl = gsap.timeline();
|
|
|
|
tl.set("#artistListContainer", { x: "-100%", autoAlpha: 0 });
|
|
|
|
tl.set("#songListContainer", { x: "0%", autoAlpha: 1 });
|
|
|
|
|
|
|
|
} catch (error) { this.showArtistList(); }
|
|
|
|
} else this.showArtistList();
|
|
|
|
} else {
|
|
|
|
this.loadArtistsByToken(currentTokenFilter);
|
|
|
|
this.markCurrentArtist();
|
|
|
|
}
|
|
|
|
showNotification(_('musicDataUpdated'), "success", 1500);
|
|
|
|
}
|
|
|
|
|
|
|
|
setupIndexedDBUpdateListener() {
|
|
|
|
window.removeEventListener('indexedDBUpdated', this.boundHandleDatabaseUpdate);
|
|
|
|
this.boundHandleDatabaseUpdate = this.handleDatabaseUpdate.bind(this);
|
|
|
|
window.addEventListener('indexedDBUpdated', this.boundHandleDatabaseUpdate);
|
|
|
|
}
|
|
|
|
|
|
|
|
loadTokens() {
|
|
|
|
const tokenMap = new Map();
|
|
|
|
(this.artistsData || []).forEach(servidor => {
|
|
|
|
if (!servidor) return;
|
|
|
|
const primaryToken = servidor.tokenPrincipal;
|
|
|
|
const serverToken = servidor.token;
|
|
|
|
const serverName = servidor.serverName || servidor.ip || `Token Desconocido`;
|
|
|
|
if (primaryToken && !tokenMap.has(primaryToken)) tokenMap.set(primaryToken, serverName);
|
|
|
|
else if (serverToken && !tokenMap.has(serverToken) && !primaryToken) tokenMap.set(serverToken, serverName);
|
|
|
|
});
|
|
|
|
this.tokens = Array.from(tokenMap, ([value, name]) => ({ value, name }));
|
|
|
|
this.updateTokenSelectorUI();
|
|
|
|
}
|
|
|
|
|
|
|
|
updateTokenSelectorUI() {
|
|
|
|
const selectItems = document.querySelector('#tokenSelectorContainer .select-items');
|
|
|
|
const selectedText = document.querySelector('#tokenSelectorContainer .select-selected span');
|
|
|
|
const previousValue = selectedText.dataset.value;
|
|
|
|
|
|
|
|
selectItems.innerHTML = `<div class="select-option" data-value="all">${_('musicAllServers')}</div>`;
|
|
|
|
|
|
|
|
this.tokens.forEach(token => {
|
|
|
|
const optionDiv = document.createElement('div');
|
|
|
|
optionDiv.classList.add('select-option');
|
|
|
|
optionDiv.dataset.value = token.value;
|
|
|
|
optionDiv.textContent = token.name;
|
|
|
|
selectItems.appendChild(optionDiv);
|
|
|
|
});
|
|
|
|
|
|
|
|
const currentSelection = this.tokens.find(t => t.value === previousValue);
|
|
|
|
if (currentSelection) {
|
|
|
|
selectedText.textContent = currentSelection.name;
|
|
|
|
selectedText.dataset.value = currentSelection.value;
|
|
|
|
} else {
|
|
|
|
selectedText.textContent = _('musicAllServers');
|
|
|
|
selectedText.dataset.value = 'all';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_generateFullArtistListForToken(token) {
|
|
|
|
let filteredSourceData = (token === "all" || !token) ? this.artistsData : (this.artistsData || []).filter(s => s && (s.tokenPrincipal === token || (!s.tokenPrincipal && s.token === token)));
|
|
|
|
const artistMap = new Map();
|
|
|
|
(filteredSourceData || []).forEach(servidor => {
|
|
|
|
if (servidor && Array.isArray(servidor.titulos)) {
|
|
|
|
servidor.titulos.forEach(artista => {
|
|
|
|
if (artista && typeof artista.id !== 'undefined' && artista.id !== null && !artistMap.has(artista.id)) {
|
|
|
|
artistMap.set(artista.id, {
|
|
|
|
id: artista.id, title: artista.title || 'Artista Desconocido',
|
|
|
|
token: servidor.token, ip: servidor.ip, puerto: servidor.puerto,
|
|
|
|
protocolo: servidor.protocolo, serverName: servidor.serverName || servidor.ip || 'Servidor Desconocido',
|
|
|
|
thumb: artista.thumb
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return Array.from(artistMap.values()).sort((a, b) => (a.title || '').localeCompare(b.title || ''));
|
|
|
|
}
|
|
|
|
|
|
|
|
loadArtists(fullArtistListForFilter, page) {
|
|
|
|
if (!this.isReady) return;
|
|
|
|
const totalArtists = fullArtistListForFilter.length;
|
|
|
|
const start = page * this.artistsPageSize;
|
|
|
|
const end = Math.min(start + this.artistsPageSize, totalArtists);
|
|
|
|
const artistGrid = document.getElementById("artistList");
|
|
|
|
artistGrid.innerHTML = '';
|
|
|
|
|
|
|
|
if (totalArtists === 0) {
|
|
|
|
artistGrid.innerHTML = `<div class="list-item-empty" style="grid-column: 1 / -1;">${_('noArtistsFound')}</div>`;
|
|
|
|
document.getElementById('prevArtistsBtn').style.display = 'none';
|
|
|
|
document.getElementById('nextArtistsBtn').style.display = 'none';
|
|
|
|
this.updateArtistCounter(0, 0, 0); return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const artistsToDisplay = fullArtistListForFilter.slice(start, end);
|
|
|
|
artistsToDisplay.forEach((artista) => {
|
|
|
|
if (artista && typeof artista.id !== 'undefined' && artista.id !== null && artista.title) {
|
|
|
|
const card = document.createElement('div');
|
|
|
|
card.className = 'artist-card';
|
|
|
|
|
|
|
|
const thumbUrl = artista.thumb
|
|
|
|
? `${artista.protocolo}://${artista.ip}:${artista.puerto}${artista.thumb}?X-Plex-Token=${artista.token}`
|
|
|
|
: null;
|
|
|
|
|
|
|
|
card.innerHTML = `
|
|
|
|
<div class="artist-thumb-wrapper">
|
|
|
|
${thumbUrl
|
|
|
|
? `<img src="${thumbUrl}" class="artist-thumb" alt="${artista.title}" loading="lazy">`
|
|
|
|
: '<i class="fas fa-user-music artist-thumb-placeholder"></i>'}
|
|
|
|
</div>
|
|
|
|
<div class="artist-card-title">${artista.title}</div>
|
|
|
|
`;
|
|
|
|
card.dataset.id = artista.id;
|
|
|
|
card.dataset.token = artista.token;
|
|
|
|
card.dataset.ip = artista.ip;
|
|
|
|
card.dataset.puerto = artista.puerto;
|
|
|
|
card.dataset.protocolo = artista.protocolo;
|
|
|
|
card.dataset.thumb = artista.thumb || '';
|
|
|
|
card.title = `${artista.title} en ${artista.serverName || 'Servidor Desconocido'}`;
|
|
|
|
artistGrid.appendChild(card);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
gsap.from(".artist-card", {
|
|
|
|
duration: 0.5,
|
|
|
|
opacity: 0,
|
|
|
|
y: 20,
|
|
|
|
stagger: 0.05,
|
|
|
|
ease: "power3.out"
|
|
|
|
});
|
|
|
|
document.getElementById('prevArtistsBtn').style.display = page > 0 ? 'inline-flex' : 'none';
|
|
|
|
document.getElementById('nextArtistsBtn').style.display = end < totalArtists ? 'inline-flex' : 'none';
|
|
|
|
this.markCurrentArtist();
|
|
|
|
this.updateArtistCounter(start + 1, end, totalArtists);
|
|
|
|
}
|
|
|
|
|
|
|
|
loadArtistsByToken(token) {
|
|
|
|
if (!this.isReady) return;
|
|
|
|
this.currentPage = 0;
|
|
|
|
this.loadArtists(this._generateFullArtistListForToken(token), this.currentPage);
|
|
|
|
}
|
|
|
|
|
|
|
|
loadPreviousArtists() {
|
|
|
|
if (!this.isReady || this.currentPage <= 0) return;
|
|
|
|
this.currentPage--;
|
|
|
|
this.loadArtists(this._generateFullArtistListForToken(this._getCurrentTokenFilter()), this.currentPage);
|
|
|
|
}
|
|
|
|
|
|
|
|
loadNextArtists() {
|
|
|
|
if (!this.isReady) return;
|
|
|
|
const fullList = this._generateFullArtistListForToken(this._getCurrentTokenFilter());
|
|
|
|
if ((this.currentPage + 1) * this.artistsPageSize < fullList.length) {
|
|
|
|
this.currentPage++;
|
|
|
|
this.loadArtists(fullList, this.currentPage);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
updateArtistCounter(start = 0, end = 0, total = 0) {
|
|
|
|
const counter = document.getElementById('artistCounter');
|
|
|
|
if (!this.isReady) counter.textContent = _('artistsCounterLoading');
|
|
|
|
else if (total === 0) counter.textContent = _('artistsCounterSingle', '0');
|
|
|
|
else counter.textContent = _('artistsCounter', [start, end, total]);
|
|
|
|
}
|
|
|
|
|
|
|
|
async loadArtistSongs(selectedCard) {
|
|
|
|
if (!this.isReady) return;
|
|
|
|
const { token, protocolo, ip, puerto, id: artistaId, thumb } = selectedCard.dataset;
|
|
|
|
this.currentArtist = selectedCard.querySelector('.artist-card-title').textContent;
|
|
|
|
this.currentArtistId = artistaId;
|
|
|
|
if (!artistaId) return;
|
|
|
|
|
|
|
|
const thumbUrl = thumb ? `${protocolo}://${ip}:${puerto}${thumb}?X-Plex-Token=${token}` : 'img/no-profile.png';
|
|
|
|
document.getElementById('artist-header-thumb').src = thumbUrl;
|
|
|
|
document.getElementById('artist-header-title').textContent = this.currentArtist;
|
|
|
|
|
|
|
|
const tl = gsap.timeline();
|
|
|
|
tl.to("#artistListContainer", { x: "-100%", autoAlpha: 0, duration: 0.5, ease: "power3.inOut" })
|
|
|
|
.fromTo("#songListContainer", { x: "100%", autoAlpha: 0 }, { x: "0%", autoAlpha: 1, duration: 0.5, ease: "power3.inOut" }, "-=0.5");
|
|
|
|
|
|
|
|
document.getElementById('loader').style.display = 'block';
|
|
|
|
document.getElementById('listaCanciones').innerHTML = '';
|
|
|
|
|
|
|
|
try {
|
|
|
|
let canciones = await getMusicUrlsFromPlex(token, protocolo, ip, puerto, artistaId);
|
|
|
|
this.handleSongsLoaded(canciones, artistaId);
|
|
|
|
} catch (error) {
|
|
|
|
showNotification(_('errorFetchingArtistSongs'), "error");
|
|
|
|
document.getElementById('listaCanciones').innerHTML = `<div class="list-item-empty">${_('errorLoadingSongs')}</div>`;
|
|
|
|
this.showArtistList();
|
|
|
|
} finally {
|
|
|
|
document.getElementById('loader').style.display = 'none';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
handleSongsLoaded(canciones, artistId) {
|
|
|
|
if (!this.isReady) return;
|
|
|
|
if (!Array.isArray(canciones)) canciones = [];
|
|
|
|
|
|
|
|
if (canciones.length > 0) {
|
|
|
|
if (!this.shuffleMode) {
|
|
|
|
canciones.sort((a, b) => {
|
|
|
|
const albumCompare = (a.album || '').localeCompare(b.album || '');
|
|
|
|
if (albumCompare !== 0) return albumCompare;
|
|
|
|
const trackIndexA = a.trackIndex !== undefined ? a.trackIndex : (a.title || '');
|
|
|
|
const trackIndexB = b.trackIndex !== undefined ? b.trackIndex : (b.title || '');
|
|
|
|
return trackIndexA - trackIndexB;
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
this.shuffleArray(canciones);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.cancionesActuales = canciones;
|
|
|
|
this.currentAlbumId = artistId;
|
|
|
|
this.displaySongList(canciones);
|
|
|
|
this.markCurrentSong();
|
|
|
|
this.markCurrentArtist(artistId);
|
|
|
|
}
|
|
|
|
|
|
|
|
shuffleArray(array) {
|
|
|
|
for (let i = array.length - 1; i > 0; i--) {
|
|
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
|
|
[array[i], array[j]] = [array[j], array[i]];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
displaySongList(canciones) {
|
|
|
|
if (!this.isReady) return;
|
|
|
|
const lista = document.getElementById("listaCanciones");
|
|
|
|
lista.innerHTML = '';
|
|
|
|
if (!Array.isArray(canciones) || canciones.length === 0) {
|
|
|
|
lista.innerHTML = `<div class="list-item-empty">${_('noSongsFound')}</div>`;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const albums = canciones.reduce((acc, cancion) => {
|
|
|
|
const albumTitle = cancion.album || 'Otras Canciones';
|
|
|
|
if (!acc[albumTitle]) acc[albumTitle] = [];
|
|
|
|
acc[albumTitle].push(cancion);
|
|
|
|
return acc;
|
|
|
|
}, {});
|
|
|
|
|
|
|
|
const fragment = document.createDocumentFragment();
|
|
|
|
for (const albumTitle in albums) {
|
|
|
|
const albumWrapper = document.createElement('div');
|
|
|
|
albumWrapper.className = 'album-group';
|
|
|
|
const albumHeader = document.createElement('h6');
|
|
|
|
albumHeader.className = 'album-group-title';
|
|
|
|
albumHeader.textContent = albumTitle;
|
|
|
|
albumWrapper.appendChild(albumHeader);
|
|
|
|
|
|
|
|
albums[albumTitle].forEach(cancion => {
|
|
|
|
const originalIndex = this.cancionesActuales.findIndex(c => c.id === cancion.id);
|
|
|
|
if (cancion && cancion.titulo) {
|
|
|
|
const item = document.createElement('div');
|
|
|
|
item.className = 'song-item';
|
|
|
|
item.innerHTML = `
|
|
|
|
<span class="song-number">${cancion.index || originalIndex + 1}</span>
|
|
|
|
<div class="song-details">
|
|
|
|
<div class="item-title">${cancion.titulo}</div>
|
|
|
|
</div>
|
|
|
|
<i class="fas fa-play play-icon"></i>
|
|
|
|
`;
|
|
|
|
item.dataset.index = originalIndex;
|
|
|
|
item.dataset.id = cancion.id;
|
|
|
|
item.dataset.artistId = cancion.artistId;
|
|
|
|
item.title = `${cancion.titulo} - ${cancion.album}`;
|
|
|
|
albumWrapper.appendChild(item);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
fragment.appendChild(albumWrapper);
|
|
|
|
}
|
|
|
|
lista.appendChild(fragment);
|
|
|
|
this.markCurrentSong();
|
|
|
|
gsap.from(".album-group", { duration: 0.5, opacity: 0, y: 20, stagger: 0.1, ease: "power3.out" });
|
|
|
|
}
|
|
|
|
|
|
|
|
playSong(cancion, index) {
|
|
|
|
if (!this.isReady || !this.audioPlayer || !cancion || !cancion.url) return;
|
|
|
|
|
2025-07-04 10:04:57 +02:00
|
|
|
const songItemElement = document.querySelector(`.song-item[data-index='${index}']`);
|
|
|
|
const playIconElement = songItemElement ? songItemElement.querySelector('.play-icon') : null;
|
|
|
|
|
|
|
|
if (playIconElement) {
|
|
|
|
playIconElement.className = 'loading-spinner';
|
|
|
|
}
|
|
|
|
|
2025-07-02 14:16:25 +02:00
|
|
|
const miniplayer = document.getElementById('miniplayer');
|
|
|
|
if (miniplayer.style.display === 'none') {
|
|
|
|
gsap.fromTo(miniplayer, { y: '100%' }, { display: 'grid', y: '0%', duration: 0.5, ease: 'power3.out' });
|
|
|
|
}
|
|
|
|
document.body.classList.add('miniplayer-active');
|
|
|
|
|
|
|
|
this.audioPlayer.innerHTML = '';
|
|
|
|
const source = document.createElement('source');
|
|
|
|
source.src = cancion.url;
|
|
|
|
source.type = this.getMimeType(cancion.extension);
|
|
|
|
this.audioPlayer.appendChild(source);
|
|
|
|
|
|
|
|
const tl = gsap.timeline();
|
|
|
|
tl.to(['#albumCover', '#trackTitle', '#trackArtist'], { opacity: 0, y: -10, duration: 0.2, ease: "power2.in", stagger: 0.05 })
|
|
|
|
.add(() => {
|
|
|
|
document.getElementById('albumCover').src = cancion.cover || 'img/no-poster.png';
|
|
|
|
document.getElementById('trackTitle').textContent = cancion.titulo;
|
|
|
|
document.getElementById('trackArtist').textContent = cancion.artista;
|
|
|
|
})
|
|
|
|
.to(['#albumCover', '#trackTitle', '#trackArtist'], { opacity: 1, y: 0, duration: 0.4, ease: "power2.out", stagger: 0.07 });
|
|
|
|
|
|
|
|
this.audioPlayer.load();
|
|
|
|
this.audioPlayer.play().then(() => {
|
|
|
|
this.isPlaying = true;
|
|
|
|
document.getElementById('playPauseBtn').innerHTML = '<i class="fas fa-pause"></i>';
|
|
|
|
this.indiceActual = index;
|
|
|
|
this.currentSongId = cancion.id;
|
|
|
|
this.currentSongArtistId = cancion.artistId;
|
|
|
|
this.markCurrentSong();
|
|
|
|
this.markCurrentArtist(cancion.artistId);
|
2025-07-04 10:04:57 +02:00
|
|
|
if (playIconElement) {
|
|
|
|
playIconElement.className = 'fas fa-play play-icon';
|
|
|
|
}
|
2025-07-02 14:16:25 +02:00
|
|
|
}).catch((error) => {
|
|
|
|
this.handleAudioError(_('playbackError'));
|
2025-07-04 10:04:57 +02:00
|
|
|
if (playIconElement) {
|
|
|
|
playIconElement.className = 'fas fa-play play-icon';
|
|
|
|
}
|
2025-07-02 14:16:25 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
getMimeType(extension) {
|
|
|
|
const mimeTypes = { 'mp3': 'audio/mpeg', 'wav': 'audio/wav', 'ogg': 'audio/ogg', 'oga': 'audio/ogg', 'm4a': 'audio/mp4', 'mp4': 'audio/mp4', 'aac': 'audio/aac', 'flac': 'audio/flac', 'opus': 'audio/opus', 'weba': 'audio/webm', 'webm': 'audio/webm' };
|
|
|
|
return mimeTypes[extension?.toLowerCase()] || undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
togglePlayPause() {
|
|
|
|
if (!this.isReady || !this.audioPlayer || this.indiceActual < 0) return;
|
|
|
|
const btn = document.getElementById('playPauseBtn');
|
|
|
|
if (this.isPlaying) {
|
|
|
|
this.audioPlayer.pause();
|
|
|
|
btn.innerHTML = '<i class="fas fa-play"></i>';
|
|
|
|
} else {
|
|
|
|
this.audioPlayer.play().then(() => { btn.innerHTML = '<i class="fas fa-pause"></i>'; })
|
|
|
|
.catch(err => { this.isPlaying = false; btn.innerHTML = '<i class="fas fa-play"></i>'; });
|
|
|
|
}
|
|
|
|
this.isPlaying = !this.isPlaying;
|
|
|
|
}
|
|
|
|
|
|
|
|
playNext() {
|
|
|
|
if (!this.isReady || this.cancionesActuales.length === 0) return;
|
|
|
|
let nextIndex;
|
|
|
|
if (this.shuffleMode) {
|
|
|
|
if (this.cancionesActuales.length <= 1) {
|
|
|
|
nextIndex = 0;
|
|
|
|
} else {
|
|
|
|
do {
|
|
|
|
nextIndex = Math.floor(Math.random() * this.cancionesActuales.length);
|
|
|
|
} while (nextIndex === this.indiceActual);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
nextIndex = (this.indiceActual + 1) % this.cancionesActuales.length;
|
|
|
|
}
|
|
|
|
if (this.cancionesActuales[nextIndex]) this.playSong(this.cancionesActuales[nextIndex], nextIndex);
|
|
|
|
}
|
|
|
|
|
|
|
|
playPrevious() {
|
|
|
|
if (!this.isReady || this.cancionesActuales.length === 0) return;
|
|
|
|
let prevIndex;
|
|
|
|
if (this.shuffleMode) {
|
|
|
|
if (this.cancionesActuales.length <= 1) {
|
|
|
|
prevIndex = 0;
|
|
|
|
} else {
|
|
|
|
do {
|
|
|
|
prevIndex = Math.floor(Math.random() * this.cancionesActuales.length);
|
|
|
|
} while (prevIndex === this.indiceActual);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
prevIndex = (this.indiceActual - 1 + this.cancionesActuales.length) % this.cancionesActuales.length;
|
|
|
|
}
|
|
|
|
if (this.cancionesActuales[prevIndex]) this.playSong(this.cancionesActuales[prevIndex], prevIndex);
|
|
|
|
}
|
|
|
|
|
|
|
|
toggleShuffle() {
|
|
|
|
if (!this.isReady) return;
|
|
|
|
this.shuffleMode = !this.shuffleMode;
|
|
|
|
document.getElementById('shuffleBtn').classList.toggle("active", this.shuffleMode);
|
|
|
|
showNotification(this.shuffleMode ? _('shuffleOn') : _('shuffleOff'), 'info', 1500);
|
|
|
|
}
|
|
|
|
|
|
|
|
markCurrentSong() {
|
|
|
|
if (!this.isReady) return;
|
|
|
|
document.querySelectorAll(".song-item").forEach(item => {
|
|
|
|
item.classList.remove("current-song");
|
|
|
|
});
|
|
|
|
if (this.currentSongId !== null) {
|
|
|
|
const songItem = document.querySelector(`.song-item[data-id='${this.currentSongId}']`);
|
|
|
|
if (songItem) {
|
|
|
|
songItem.classList.add("current-song");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
markCurrentArtist(artistIdToMark = null) {
|
|
|
|
if (!this.isReady) return;
|
|
|
|
const targetArtistId = artistIdToMark !== null ? artistIdToMark : this.currentArtistId;
|
|
|
|
document.querySelectorAll(".artist-card").forEach(card => card.classList.remove("current-artist"));
|
|
|
|
if (targetArtistId !== null) {
|
|
|
|
const artistCard = document.querySelector(`.artist-card[data-id='${targetArtistId}']`);
|
|
|
|
if (artistCard) artistCard.classList.add("current-artist");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
updateProgressBar() {
|
|
|
|
if (!this.isReady || !this.audioPlayer) return;
|
|
|
|
const { currentTime, duration } = this.audioPlayer;
|
|
|
|
if (!isNaN(duration) && duration > 0) {
|
|
|
|
const progressPercent = (currentTime / duration) * 100;
|
|
|
|
document.getElementById("played-bar").style.width = `${progressPercent}%`;
|
|
|
|
document.getElementById("progress-handle").style.left = `${progressPercent}%`;
|
|
|
|
document.getElementById("elapsedTime").textContent = this.formatTime(currentTime);
|
|
|
|
document.getElementById("remainingTime").textContent = this.formatTime(duration);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
updateSeekHover(event) {
|
|
|
|
const progressBarContainer = document.getElementById("progressBarContainer");
|
|
|
|
const rect = progressBarContainer.getBoundingClientRect();
|
|
|
|
const hoverWidth = ((event.clientX - rect.left) / rect.width) * 100;
|
|
|
|
document.getElementById('seek-hover-bar').style.width = `${Math.max(0, Math.min(hoverWidth, 100))}%`;
|
|
|
|
}
|
|
|
|
hideSeekHover() {
|
|
|
|
document.getElementById('seek-hover-bar').style.width = `0%`;
|
|
|
|
}
|
|
|
|
|
|
|
|
handleAudioError(message = _('playbackError')) {
|
|
|
|
if (!this.isReady || !this.audioPlayer) return;
|
|
|
|
showNotification(message, "error");
|
|
|
|
document.getElementById("remainingTime").textContent = _('errorLabel');
|
|
|
|
document.getElementById('playPauseBtn').innerHTML = '<i class="fas fa-play"></i>';
|
|
|
|
this.isPlaying = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
seek(event) {
|
|
|
|
if (!this.isReady || !this.audioPlayer || isNaN(this.audioPlayer.duration) || this.audioPlayer.duration <= 0) return;
|
|
|
|
const progressBarContainer = document.getElementById("progressBarContainer");
|
|
|
|
const rect = progressBarContainer.getBoundingClientRect();
|
|
|
|
const seekTime = ((event.clientX - rect.left) / rect.width) * this.audioPlayer.duration;
|
|
|
|
this.audioPlayer.currentTime = Math.max(0, Math.min(seekTime, this.audioPlayer.duration));
|
|
|
|
}
|
|
|
|
|
|
|
|
formatTime(seconds) {
|
|
|
|
if (isNaN(seconds) || seconds < 0) return "0:00";
|
|
|
|
const minutes = Math.floor(seconds / 60);
|
|
|
|
const remainingSeconds = Math.floor(seconds % 60);
|
|
|
|
return `${minutes}:${remainingSeconds < 10 ? "0" : ""}${remainingSeconds}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
downloadSong(url, titulo, extension) {
|
|
|
|
if (!this.isReady || !url || !titulo) return;
|
|
|
|
showNotification(_('downloadingSong', titulo), "info");
|
|
|
|
fetch(url).then(response => response.blob()).then(blob => {
|
|
|
|
const urlBlob = window.URL.createObjectURL(blob);
|
|
|
|
const a = document.createElement("a");
|
|
|
|
a.style.display = "none";
|
|
|
|
a.href = urlBlob;
|
|
|
|
a.download = `${titulo.replace(/[\\/:*?"<>|]/g, "_").replace(/\s+/g, '_')}.${extension || 'mp3'}`;
|
|
|
|
document.body.appendChild(a);
|
|
|
|
a.click();
|
|
|
|
window.URL.revokeObjectURL(urlBlob);
|
|
|
|
a.remove();
|
|
|
|
showNotification(_('songDownloaded', titulo), "success");
|
|
|
|
}).catch(err => showNotification(_('errorDownloadingSong', titulo), "error"));
|
|
|
|
}
|
|
|
|
|
|
|
|
downloadAlbum() {
|
|
|
|
if (!this.isReady || this.cancionesActuales.length === 0 || !this.currentArtist) return;
|
|
|
|
showNotification(_('generatingAlbumM3U', this.currentArtist), "info");
|
|
|
|
const m3uContent = ["#EXTM3U", ...this.cancionesActuales.map(c => `#EXTINF:-1 tvg-name="${c.artista} - ${c.titulo}",${c.artista} - ${c.titulo}\n${c.url}`)];
|
|
|
|
const blob = new Blob([m3uContent.join("\n")], { type: "audio/x-mpegurl;charset=utf-8" });
|
|
|
|
const urlBlob = window.URL.createObjectURL(blob);
|
|
|
|
const a = document.createElement("a");
|
|
|
|
a.href = urlBlob;
|
|
|
|
a.download = `${this.currentArtist.replace(/[\\/:*?"<>|]/g, "_").replace(/\s+/g, '_')}_Album.m3u`;
|
|
|
|
document.body.appendChild(a);
|
|
|
|
a.click();
|
|
|
|
window.URL.revokeObjectURL(urlBlob);
|
|
|
|
a.remove();
|
|
|
|
showNotification(_('albumM3UGenerated', this.currentArtist), "success");
|
|
|
|
}
|
|
|
|
|
|
|
|
updateVolume(event) {
|
|
|
|
if (!this.isReady || !this.audioPlayer) return;
|
|
|
|
const volume = event.currentTarget.value;
|
|
|
|
this.audioPlayer.volume = volume;
|
|
|
|
this.updateVolumeIcon(volume);
|
|
|
|
}
|
|
|
|
|
|
|
|
updateVolumeIcon(volume) {
|
|
|
|
const vol = parseFloat(volume);
|
|
|
|
const icon = document.querySelector('#volume-icon-btn i');
|
|
|
|
if (vol === 0) {
|
|
|
|
icon.className = 'fas fa-volume-mute';
|
|
|
|
} else if (vol < 0.5) {
|
|
|
|
icon.className = 'fas fa-volume-down';
|
|
|
|
} else {
|
|
|
|
icon.className = 'fas fa-volume-up';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
filterArtists() {
|
|
|
|
if (!this.isReady) return;
|
|
|
|
const value = document.getElementById("searchArtist").value.toLowerCase();
|
|
|
|
const tokenFilter = this._getCurrentTokenFilter();
|
|
|
|
const fullList = this._generateFullArtistListForToken(tokenFilter);
|
|
|
|
const filteredArtists = fullList.filter(a => a.title?.toLowerCase().includes(value));
|
|
|
|
const artistGrid = document.getElementById("artistList");
|
|
|
|
artistGrid.innerHTML = '';
|
|
|
|
if (filteredArtists.length === 0) {
|
|
|
|
artistGrid.innerHTML = `<div class="list-item-empty" style="grid-column: 1 / -1;">${_('noArtistsFound')}</div>`;
|
|
|
|
} else {
|
|
|
|
filteredArtists.forEach((artista) => {
|
|
|
|
const card = document.createElement('div');
|
|
|
|
card.className = 'artist-card';
|
|
|
|
const thumbUrl = artista.thumb ? `${artista.protocolo}://${artista.ip}:${artista.puerto}${artista.thumb}?X-Plex-Token=${artista.token}` : null;
|
|
|
|
card.innerHTML = `
|
|
|
|
<div class="artist-thumb-wrapper">
|
|
|
|
${thumbUrl ? `<img src="${thumbUrl}" class="artist-thumb" alt="${artista.title}" loading="lazy">` : '<i class="fas fa-user-music artist-thumb-placeholder"></i>'}
|
|
|
|
</div>
|
|
|
|
<div class="artist-card-title">${artista.title}</div>
|
|
|
|
`;
|
|
|
|
card.dataset.id = artista.id;
|
|
|
|
card.dataset.token = artista.token;
|
|
|
|
card.dataset.ip = artista.ip;
|
|
|
|
card.dataset.puerto = artista.puerto;
|
|
|
|
card.dataset.protocolo = artista.protocolo;
|
|
|
|
card.dataset.thumb = artista.thumb || '';
|
|
|
|
card.title = `${artista.title} en ${artista.serverName || 'Servidor Desconocido'}`;
|
|
|
|
artistGrid.appendChild(card);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
if (value === '') {
|
|
|
|
this.loadArtists(fullList, this.currentPage);
|
|
|
|
} else {
|
|
|
|
document.getElementById('prevArtistsBtn').style.display = 'none';
|
|
|
|
document.getElementById('nextArtistsBtn').style.display = 'none';
|
|
|
|
document.getElementById('artistCounter').style.display = 'none';
|
|
|
|
this.markCurrentArtist();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
filterSongs() {
|
|
|
|
if (!this.isReady) return;
|
|
|
|
const value = document.getElementById("searchSong").value.toLowerCase();
|
|
|
|
const filteredSongs = this.cancionesActuales.filter(c => c.titulo?.toLowerCase().includes(value) || c.album?.toLowerCase().includes(value));
|
|
|
|
this.displaySongList(filteredSongs);
|
|
|
|
}
|
|
|
|
|
|
|
|
showArtistList() {
|
|
|
|
if (!this.isReady) return;
|
|
|
|
|
|
|
|
const tl = gsap.timeline();
|
|
|
|
tl.to("#songListContainer", { x: "100%", autoAlpha: 0, duration: 0.5, ease: "power3.inOut" })
|
|
|
|
.to("#artistListContainer", { x: "0%", autoAlpha: 1, duration: 0.5, ease: "power3.inOut" }, "-=0.5");
|
|
|
|
|
|
|
|
if (document.getElementById("searchArtist").value === '') {
|
|
|
|
this.loadArtists(this._generateFullArtistListForToken(this._getCurrentTokenFilter()), this.currentPage);
|
|
|
|
} else {
|
|
|
|
this.filterArtists();
|
|
|
|
}
|
|
|
|
this.markCurrentArtist();
|
|
|
|
}
|
|
|
|
|
|
|
|
showInfoModal(cancion) {
|
|
|
|
if (!this.isReady || !cancion) return;
|
|
|
|
document.querySelector('#infoModalLabel').textContent = `${_('infoModalTitle')}: ${cancion.titulo}`;
|
|
|
|
document.querySelector('#modalTitle span').textContent = cancion.titulo || 'N/A';
|
|
|
|
document.querySelector('#modalArtist span').textContent = cancion.artista || 'N/A';
|
|
|
|
document.querySelector('#modalAlbum span').textContent = cancion.album || 'N/A';
|
|
|
|
document.querySelector('#modalSong span').textContent = cancion.titulo || 'N/A';
|
|
|
|
document.querySelector('#modalYear span').textContent = cancion.year || 'N/A';
|
|
|
|
document.querySelector('#modalGenre span').textContent = cancion.genre || 'N/A';
|
|
|
|
const infoModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('infoModal'));
|
|
|
|
infoModal.show();
|
|
|
|
}
|
|
|
|
}
|