import { getFromDB } from './db.js'; import { debounce, showNotification, _ } from './utils.js'; import { getMusicUrlsFromPlex, getMusicUrlsFromJellyfin } from './api.js'; import { state } from './state.js'; export class MusicPlayer { constructor() { this.cancionesActuales = []; this.displayedSongs = []; this.indiceActual = -1; this.isPlaying = false; this.audioPlayer = document.getElementById("audioPlayer"); this.preloaderAudio = document.createElement('audio'); 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; this.miniplayerManuallyClosed = false; this.isPreloading = 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 = `
${_('dbUnavailableError')}
`; 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 = `
${_('errorLoadingArtists')}
`; 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('closeMiniplayerBtn').addEventListener('click', () => this.closeMiniplayer()); document.getElementById('fab-music-player').addEventListener('click', () => this.openMiniplayer()); 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.handleTimeUpdate()); this.audioPlayer.addEventListener("error", () => this.handleAudioErrorEvent()); this.audioPlayer.addEventListener("volumechange", () => { this.preloaderAudio.volume = this.audioPlayer.volume; }); this.preloaderAudio.addEventListener("error", (e) => { this.isPreloading = false; }); this.preloaderAudio.addEventListener("canplaythrough", () => { this.isPreloading = true; }); 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.displayedSongs[index]) { this.cancionesActuales = [...this.displayedSongs]; if (this.shuffleMode) { this.shuffleArray(this.cancionesActuales); const newIndex = this.cancionesActuales.findIndex(s => s.id === this.displayedSongs[index].id); this.playSong(newIndex); } else { this.playSong(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(true); } 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 = `
${_('errorInitializingMusicPlayer')}
`; this.updateArtistCounter(); return; } if (this.artistsData.length === 0 && this.isReady) { try { await this.loadMusicData(); } catch (error) { document.getElementById('artistList').innerHTML = `
${_('errorLoadingArtists')}
`; 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; } closeMiniplayer() { this.miniplayerManuallyClosed = true; const miniplayer = document.getElementById('miniplayer'); gsap.to(miniplayer, { y: '110%', duration: 0.5, ease: 'power3.in', onComplete: () => { miniplayer.style.display = 'none'; document.body.classList.remove('miniplayer-active'); if (this.indiceActual >= 0) { document.getElementById('fab-music-player').style.display = 'flex'; gsap.fromTo('#fab-music-player', { scale: 0, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.3, ease: 'back.out(1.7)' }); } }}); } openMiniplayer() { this.miniplayerManuallyClosed = false; const miniplayer = document.getElementById('miniplayer'); const fab = document.getElementById('fab-music-player'); gsap.to(fab, { scale: 0, opacity: 0, duration: 0.3, ease: 'back.in(1.7)', onComplete: () => { fab.style.display = 'none'; miniplayer.style.display = 'grid'; document.body.classList.add('miniplayer-active'); gsap.fromTo(miniplayer, { y: '110%' }, { y: '0%', duration: 0.5, ease: 'power3.out' }); }}); } 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 { token, protocolo, ip, puerto, id: artistaId, isjellyfin, serverurl, userid } = artistCard.dataset; let canciones; if (isjellyfin === 'true') { canciones = await getMusicUrlsFromJellyfin(serverurl, userid, token, artistaId); } else { canciones = await getMusicUrlsFromPlex(token, protocolo, ip, puerto, artistaId); } 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 = `
${_('musicAllServers')}
`; 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)) { const artistEntry = { id: artista.id, title: artista.title || 'Artista Desconocido', thumb: artista.thumb, isJellyfin: servidor.isJellyfin || false, serverName: servidor.serverName || 'Servidor Desconocido', }; if (servidor.isJellyfin) { artistEntry.serverUrl = servidor.serverUrl; artistEntry.userId = servidor.userId; artistEntry.token = servidor.token; } else { artistEntry.token = servidor.token; artistEntry.ip = servidor.ip; artistEntry.puerto = servidor.puerto; artistEntry.protocolo = servidor.protocolo; } artistMap.set(artista.id, artistEntry); } }); } }); 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 = `
${_('noArtistsFound')}
`; 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'; let thumbUrl = null; if (artista.thumb) { if (artista.isJellyfin) { thumbUrl = artista.thumb; } else { thumbUrl = `${artista.protocolo}://${artista.ip}:${artista.puerto}${artista.thumb}?X-Plex-Token=${artista.token}`; } } card.innerHTML = `
${thumbUrl ? `${artista.title}` : ''}
${artista.title}
`; card.dataset.id = artista.id; card.dataset.thumb = artista.thumb || ''; card.dataset.isjellyfin = artista.isJellyfin; card.title = `${artista.title} en ${artista.serverName || 'Servidor Desconocido'}`; if (artista.isJellyfin) { card.dataset.serverurl = artista.serverUrl; card.dataset.userid = artista.userId; card.dataset.token = artista.token; } else { card.dataset.token = artista.token; card.dataset.ip = artista.ip; card.dataset.puerto = artista.puerto; card.dataset.protocolo = artista.protocolo; } 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 { id: artistaId, thumb, isjellyfin, serverurl, userid, token, protocolo, ip, puerto } = selectedCard.dataset; this.currentArtist = selectedCard.querySelector('.artist-card-title').textContent; this.currentArtistId = artistaId; if (!artistaId) return; let thumbUrl = 'img/no-profile.png'; if (thumb) { if (isjellyfin === 'true') { thumbUrl = thumb; } else { thumbUrl = `${protocolo}://${ip}:${puerto}${thumb}?X-Plex-Token=${token}`; } } 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; if (isjellyfin === 'true') { canciones = await getMusicUrlsFromJellyfin(serverurl, userid, token, artistaId); } else { canciones = await getMusicUrlsFromPlex(token, protocolo, ip, puerto, artistaId); } this.handleSongsLoaded(canciones, artistaId); } catch (error) { showNotification(_('errorFetchingArtistSongs'), "error"); document.getElementById('listaCanciones').innerHTML = `
${_('errorLoadingSongs')}
`; this.showArtistList(); } finally { document.getElementById('loader').style.display = 'none'; } } async getArtistSongs(artist) { if (!this.isReady) return; const { id: artistaId, isJellyfin, serverUrl, userId, token, protocolo, ip, puerto } = artist; try { let canciones; if (isJellyfin) { canciones = await getMusicUrlsFromJellyfin(serverUrl, userId, token, artistaId); } else { canciones = await getMusicUrlsFromPlex(token, protocolo, ip, puerto, artistaId); } return canciones; } catch (error) { showNotification(_('errorFetchingArtistSongs'), "error"); return []; } } handleSongsLoaded(canciones, artistId) { if (!this.isReady) return; if (!Array.isArray(canciones)) canciones = []; 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; }); this.displayedSongs = canciones; this.currentAlbumId = artistId; this.displaySongList(this.displayedSongs); this.markCurrentSong(); } 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(songsToDisplay) { if (!this.isReady) return; const lista = document.getElementById("listaCanciones"); lista.innerHTML = ''; if (!Array.isArray(songsToDisplay) || songsToDisplay.length === 0) { lista.innerHTML = `
${_('noSongsFound')}
`; return; } const albums = songsToDisplay.reduce((acc, cancion) => { const albumTitle = cancion.album || 'Otras Canciones'; if (!acc[albumTitle]) acc[albumTitle] = []; acc[albumTitle].push(cancion); return acc; }, {}); const fragment = document.createDocumentFragment(); let songCounter = 0; 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 => { if (cancion && cancion.titulo) { const item = document.createElement('div'); item.className = 'song-item'; item.innerHTML = ` ${cancion.index || songCounter + 1}
${cancion.titulo}
`; item.dataset.index = songCounter; item.dataset.id = cancion.id; item.dataset.artistId = cancion.artistId; item.title = `${cancion.titulo} - ${cancion.album}`; albumWrapper.appendChild(item); songCounter++; } }); 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" }); } _getNextSongIndex() { if (this.cancionesActuales.length === 0) return -1; if (this.shuffleMode) { if (this.cancionesActuales.length <= 1) return 0; let nextIndex; do { nextIndex = Math.floor(Math.random() * this.cancionesActuales.length); } while (nextIndex === this.indiceActual); return nextIndex; } return (this.indiceActual + 1) % this.cancionesActuales.length; } _getPreviousSongIndex() { if (this.cancionesActuales.length === 0) return -1; if (this.shuffleMode) { return this._getNextSongIndex(); } return (this.indiceActual - 1 + this.cancionesActuales.length) % this.cancionesActuales.length; } preloadNextSong() { this.isPreloading = false; const nextIndex = this._getNextSongIndex(); if (nextIndex === -1 || nextIndex === this.indiceActual) return; const nextSong = this.cancionesActuales[nextIndex]; if (nextSong && nextSong.url) { this.preloaderAudio.innerHTML = ''; const source = document.createElement('source'); source.src = nextSong.url; source.type = this.getMimeType(nextSong.extension); this.preloaderAudio.appendChild(source); this.preloaderAudio.load(); } } handleTimeUpdate() { this.updateProgressBar(); if (this.audioPlayer.duration && this.audioPlayer.currentTime > this.audioPlayer.duration - 15) { if (!this.isPreloading && this.preloaderAudio.currentSrc === '') { this.preloadNextSong(); } } } playSong(index, fromNext = false) { if (!this.isReady || !this.audioPlayer || index < 0 || !this.cancionesActuales[index]) return; const cancion = this.cancionesActuales[index]; this.indiceActual = index; const songItemElement = document.querySelector(`.song-item[data-id='${cancion.id}']`); const playIconElement = songItemElement ? songItemElement.querySelector('.play-icon') : null; if (playIconElement) { playIconElement.className = 'loading-spinner'; } const miniplayer = document.getElementById('miniplayer'); if (miniplayer.style.display === 'none' && !this.miniplayerManuallyClosed) { gsap.fromTo(miniplayer, { y: '100%' }, { display: 'grid', y: '0%', duration: 0.5, ease: 'power3.out' }); } document.body.classList.add('miniplayer-active'); 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 }); if (fromNext && this.isPreloading) { this.audioPlayer.innerHTML = this.preloaderAudio.innerHTML; this.preloaderAudio.innerHTML = ''; this.isPreloading = false; } else { this.audioPlayer.innerHTML = ''; const source = document.createElement('source'); source.src = cancion.url; source.type = this.getMimeType(cancion.extension); this.audioPlayer.appendChild(source); } this.audioPlayer.load(); const playPromise = this.audioPlayer.play(); if (playPromise !== undefined) { playPromise.then(() => { this.isPlaying = true; document.getElementById('playPauseBtn').innerHTML = ''; this.currentSongId = cancion.id; this.currentSongArtistId = cancion.artistId; this.markCurrentSong(); this.ensureArtistVisible(cancion.artistId); if (playIconElement) { playIconElement.className = 'fas fa-play play-icon'; } if (!this.miniplayerManuallyClosed) { document.getElementById('fab-music-player').style.display = 'none'; } this.preloadNextSong(); }).catch(error => { if (error.name === 'NotAllowedError') { this.isPlaying = false; document.getElementById('playPauseBtn').innerHTML = ''; showNotification(_('autoplayBlocked'), 'warning'); } else { this.handleAudioError(_('playbackError')); } if (playIconElement) { playIconElement.className = 'fas fa-play play-icon'; } }); } } 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 = ''; } else { this.audioPlayer.play().then(() => { btn.innerHTML = ''; }) .catch(err => { this.isPlaying = false; btn.innerHTML = ''; }); } this.isPlaying = !this.isPlaying; if (this.isPlaying) { document.getElementById('fab-music-player').style.display = 'none'; } } playNext(fromEnded = false) { if (!this.isReady || this.cancionesActuales.length === 0) return; const nextIndex = this._getNextSongIndex(); if (nextIndex !== -1) { this.playSong(nextIndex, fromEnded); } } playPrevious() { if (!this.isReady || this.cancionesActuales.length === 0) return; const prevIndex = this._getPreviousSongIndex(); if (prevIndex !== -1) { this.playSong(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); this.preloadNextSong(); } 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() { if (!this.isReady) return; const targetArtistId = this.currentSongArtistId; document.querySelectorAll(".artist-card").forEach(card => { if (targetArtistId != null && card.dataset.id == targetArtistId) { card.classList.add("current-artist"); } else { card.classList.remove("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 = ''; 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 = `
${_('noArtistsFound')}
`; } else { filteredArtists.forEach((artista) => { const card = document.createElement('div'); card.className = 'artist-card'; let thumbUrl = null; if (artista.thumb) { if (artista.isJellyfin) { thumbUrl = artista.thumb; } else { thumbUrl = `${artista.protocolo}://${artista.ip}:${artista.puerto}${artista.thumb}?X-Plex-Token=${artista.token}`; } } card.innerHTML = `
${thumbUrl ? `${artista.title}` : ''}
${artista.title}
`; card.dataset.id = artista.id; card.dataset.thumb = artista.thumb || ''; card.dataset.isjellyfin = artista.isJellyfin; card.title = `${artista.title} en ${artista.serverName || 'Servidor Desconocido'}`; if (artista.isJellyfin) { card.dataset.serverurl = artista.serverUrl; card.dataset.userid = artista.userId; card.dataset.token = artista.token; } else { card.dataset.token = artista.token; card.dataset.ip = artista.ip; card.dataset.puerto = artista.puerto; card.dataset.protocolo = artista.protocolo; } 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(); } ensureArtistVisible(artistId) { if (!this.isReady || artistId === null) return; const artistCard = document.querySelector(`.artist-card[data-id='${artistId}']`); if (artistCard) { this.markCurrentArtist(artistId); return; } const fullList = this._generateFullArtistListForToken(this._getCurrentTokenFilter()); const artistIndex = fullList.findIndex(a => a.id == artistId); if (artistIndex !== -1) { const targetPage = Math.floor(artistIndex / this.artistsPageSize); if (this.currentPage !== targetPage) { this.currentPage = targetPage; this.loadArtists(fullList, this.currentPage); } } this.markCurrentArtist(artistId); } }