(function () { 'use strict'; // Delete the class 'preload' from the body when the page is loaded window.addEventListener('DOMContentLoaded', event => { document.body.classList.remove('preload'); }); const buttons = document.querySelectorAll('[data-outside]'); const ACTIVE_CLASS = 'is-active'; function outsideClick(button) { if (!button) return; const target = document.getElementById(button.dataset.outside); if (!target) return; function toggleClasses() { button.classList.toggle(ACTIVE_CLASS); target.classList.toggle(ACTIVE_CLASS); if (button.classList.contains(ACTIVE_CLASS)) { document.addEventListener('click', clickOutside); return; } document.removeEventListener('click', clickOutside); } button.addEventListener('click', toggleClasses); function clickOutside(event) { if (!target.contains(event.target) && !button.contains(event.target)) { toggleClasses(); document.removeEventListener('click', clickOutside); } } const closeButton = target.querySelector('[data-close]'); if (closeButton) { closeButton.addEventListener('click', () => { button.classList.remove(ACTIVE_CLASS); target.classList.remove(ACTIVE_CLASS); document.removeEventListener('click', clickOutside); }); } } buttons.forEach(button => { outsideClick(button); }); // Función para inicializar el lienzo (canvas) function initCanvas(container) { const canvas = document.createElement('canvas'); canvas.setAttribute('id', 'visualizerCanvas'); canvas.setAttribute('class', 'visualizer-item'); container.appendChild(canvas); canvas.width = container.clientWidth; canvas.height = container.clientHeight; return canvas; } // Función para cambiar el lienzo según el tamaño del contenedor function resizeCanvas(canvas, container) { canvas.width = container.clientWidth; canvas.height = container.clientHeight; } // Visualizer const visualizer = (audio, container) => { if (!audio || !container) { return; } const options = { fftSize: container.dataset.fftSize || 2048, numBars: container.dataset.bars || 40, maxHeight: container.dataset.maxHeight || 255 }; const ctx = new AudioContext(); const audioSource = ctx.createMediaElementSource(audio); const analyzer = ctx.createAnalyser(); audioSource.connect(analyzer); audioSource.connect(ctx.destination); const frequencyData = new Uint8Array(analyzer.frequencyBinCount); const canvas = initCanvas(container); const canvasCtx = canvas.getContext('2d'); // Crear barras const renderBars = () => { resizeCanvas(canvas, container); analyzer.getByteFrequencyData(frequencyData); if (options.fftSize) { analyzer.fftSize = options.fftSize; } canvasCtx.clearRect(0, 0, canvas.width, canvas.height); for (let i = 0; i < options.numBars; i++) { const index = Math.floor((i + 10) * (i < options.numBars / 2 ? 2 : 1)); const fd = frequencyData[index]; const barHeight = Math.max(4, fd || 0) + options.maxHeight / 255; const barWidth = canvas.width / options.numBars; const x = i * barWidth; const y = canvas.height - barHeight; canvasCtx.fillStyle = 'white'; canvasCtx.fillRect(x, y, barWidth - 2, barHeight); } requestAnimationFrame(renderBars); }; renderBars(); // Listener del cambio de espacio en la ventana window.addEventListener('resize', () => { resizeCanvas(canvas, container); }); }; const API_KEY_LYRICS = '1637b78dc3b129e6843ed674489a92d0'; const cache = {}; // Iconos de Meteor Icons: https://meteoricons.com/ const icons = { play: '', pause: '', facebook: '', twitter: '', instagram: '', youtube: '', tiktok: '', whatsapp: '', telegram: '', tv: '', ios: '', android: '' }; const pixel = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdj+P//PxcACQYDCF0ysWYAAAAASUVORK5CYII='; const changeImageSize = (url, size) => url.replace(/100x100/, size); // Obtener Datos desde Stream Africa const getDataFromStreamAfrica = async (artist, title, defaultArt, defaultCover) => { let text; if (artist === null || artist === title) { text = `${title} - ${title}`; } else { text = `${artist} - ${title}`; } const cacheKey = text.toLowerCase(); if (cache[cacheKey]) { return cache[cacheKey]; } const API_URL = `https://api.streamafrica.net/new.search.php?query=${encodeURIComponent(text)}&service=itunes`; const response = await fetch(API_URL); if (title === 'MIXX SHOW RADIO' || response.status === 403) { const results = { title, artist, art: defaultArt, cover: defaultCover, stream_url: '#not-found' }; cache[cacheKey] = results; return results; } const data = response.ok ? await response.json() : {}; if (!data.results || data.results.length === 0) { const results = { title, artist, art: defaultArt, cover: defaultCover, stream_url: '#not-found' }; cache[cacheKey] = results; return results; } const stream = data.results; const results = { title: stream.title || title, artist: stream.artist || artist, thumbnail: stream.artwork || defaultArt, art: stream.artwork || defaultArt, cover: stream.artwork || defaultCover, stream_url: stream.stream_url || '#not-found' }; cache[cacheKey] = results; return results; }; // Obtener Datos desde iTunes const getDataFromITunes = async (artist, title, defaultArt, defaultCover) => { let text; if (artist === title) { text = `${title}`; } else { text = `${artist} - ${title}`; } const cacheKey = text.toLowerCase(); if (cache[cacheKey]) { return cache[cacheKey]; } // SyntaxError: Unexpected end of JSON input const response = await fetch(`https://itunes.apple.com/search?limit=1&term=${encodeURIComponent(text)}`); if (response.status === 403) { const results = { title, artist, art: defaultArt, cover: defaultCover, stream_url: '#not-found' }; return results; } const data = response.ok ? await response.json() : {}; if (!data.results || data.results.length === 0) { const results = { title, artist, art: defaultArt, cover: defaultCover, stream_url: '#not-found' }; return results; } const itunes = data.results[0]; const results = { title: itunes.trackName || title, artist: itunes.artistName || artist, thumbnail: itunes.artworkUrl100 || defaultArt, art: itunes.artworkUrl100 ? changeImageSize(itunes.artworkUrl100, '1500x1500') : defaultArt, cover: itunes.artworkUrl100 ? changeImageSize(itunes.artworkUrl100, '1500x1500') : defaultCover, stream_url: '#not-found' }; cache[cacheKey] = results; return results; }; // Determinar de donde obtener los datos async function getDataFrom({ artist, title, art, cover, server }) { let dataFrom = {}; if (server.toLowerCase() === 'africa') { dataFrom = await getDataFromStreamAfrica(artist, title, art, cover); } else { dataFrom = await getDataFromITunes(artist, title, art, cover); } return dataFrom; } // Obtener letras de canciones const getLyrics = async (artist, name) => { try { const response = await fetch(`https://api.vagalume.com.br/search.php?apikey=${API_KEY_LYRICS}&art=${encodeURIComponent(artist)}&mus=${encodeURIComponent(name)}`); const data = await response.json(); if (data.type === 'exact' || data.type === 'aprox') { const lyrics = data.mus[0].text; return lyrics; } else { return 'Not found lyrics'; } } catch (error) { console.error('Error fetching lyrics:', error); return 'Not found lyrics'; } }; // Crear un elemento HTML a partir de una cadena de texto function createElementFromHTML(htmlString) { const div = document.createElement('div'); div.innerHTML = htmlString.trim(); return div.firstChild; } // Eliminar elementos innecesarios del texto function sanitizeText(text) { return text.replace(/^\d+\.\)\s/, '').replace(/
$/, ''); } // Normalizar historial function normalizeHistory(api) { let artist; let song; let history = api.song_history || api.history || api.songHistory || []; history = history.slice(0, 4); const historyNormalized = history.map(item => { if (api.song_history) { artist = item.song.artist; song = item.song.title; } else if (api.history) { artist = sanitizeText(item.split(' - ')[0] || item); song = sanitizeText(item.split(' - ')[1] || item); } else if (api.songHistory) { artist = item.artist; song = item.title; } return { artist, song }; }); // limitar a 4 elementos return historyNormalized; } function createTempImage(src) { return new Promise((resolve, reject) => { const img = document.createElement('img'); img.crossOrigin = 'Anonymous'; img.src = `https://images.weserv.nl/?url=${src}`; img.onload = () => resolve(img); img.onerror = reject; }); } function normalizeTitle(api) { let title; let artist; if (api.songtitle && api.songtitle.includes(' - ')) { title = api.songtitle.split(' - ')[0]; artist = api.songtitle.split(' - ')[1]; } else if (api.now_playing) { title = api.now_playing.song.title; artist = api.now_playing.song.artist; } else if (api.artist && api.title) { title = api.title; artist = api.artist; } else if (api.currenttrack_title) { title = api.currenttrack_title; artist = api.currenttrack_artist; } else if (api.title && api.djprofile && api.djusername) { title = api.title.split(' - ')[1]; artist = api.title.split(' - ')[0]; } else { title = api.currentSong; artist = api.currentArtist; } return { title, artist }; } const playButton = document.querySelector('.player-button-play'); const visualizerContainer = document.querySelector('.visualizer'); const audio = new Audio(); audio.crossOrigin = 'anonymous'; let hasVisualizer = false; function play(audio, newSource = null) { if (newSource) { audio.src = newSource; } // Visualizer if (!hasVisualizer) { visualizer(audio, visualizerContainer); hasVisualizer = true; } audio.load(); audio.play(); playButton.innerHTML = icons.pause; playButton.classList.add('is-active'); document.body.classList.add('is-playing'); } function pause(audio) { audio.pause(); playButton.innerHTML = icons.play; playButton.classList.remove('is-active'); document.body.classList.remove('is-playing'); } // Botón play/pause, al pausar detener el stream, al reproducir iniciar el stream de nuevo // playButton, play, pause son funciones exportadas que se usaran en otros archivos if (playButton !== null) { playButton.addEventListener('click', async () => { if (audio.paused) { play(audio); } else { pause(audio); } }); } const range = document.querySelector('.player-volume'); const rangeFill = document.querySelector('.player-range-fill'); const rangeWrapper = document.querySelector('.player-range-wrapper'); const rangeThumb = document.querySelector('.player-range-thumb'); const currentVolume = localStorage.getItem('volume') || 100; // Rango recorrido function setRangeWidth(percent) { { rangeFill.style.height = `${percent}%`; } } // Posición del thumb function setThumbPosition(percent) { const compensatedWidth = rangeWrapper.offsetHeight - rangeThumb.offsetHeight ; const thumbPosition = percent / 100 * compensatedWidth; { rangeThumb.style.bottom = `${thumbPosition}px`; } } // Actualiza el volumen al cambiar el rango function updateVolume(value) { range.value = value; setRangeWidth(value); setThumbPosition(value); localStorage.setItem('volume', value); audio.volume = value / 100; } // Valor inicial if (range !== null) { updateVolume(currentVolume); // Escucha el cambio del rango range.addEventListener('input', event => { updateVolume(event.target.value); }); // Escucha el movimiento del mouse rangeThumb.addEventListener('mousedown', () => { document.addEventListener('mousemove', handleThumbDrag); }); } // Mueve el thumb y actualiza el volumen function handleThumbDrag(event) { const rangeRect = range.getBoundingClientRect(); const click = event.clientY - rangeRect.top; let percent = click / range.offsetWidth * 100; percent = 100 - percent; percent = Math.max(0, Math.min(100, percent)); const value = Math.round((range.max - range.min) * (percent / 100)) + parseInt(range.min); updateVolume(value); } // Deja de escuchar el movimiento del mouse document.addEventListener('mouseup', () => { document.removeEventListener('mousemove', handleThumbDrag); }); window.addEventListener('resize', () => { const currentPercent = range.value; setRangeWidth(currentPercent); setThumbPosition(currentPercent); }); const songNow = document.querySelector('.song-now'); const stationsList = document.getElementById('stations'); const stationName = document.querySelector('.station-name'); const stationDescription = document.querySelector('.station-description'); const headerLogoImg = document.querySelector('.header-logo-img'); const playerArtwork = document.querySelector('.player-artwork img:first-child'); const playerCoverImg = document.querySelector('.player-cover-image'); const playerSocial = document.querySelectorAll('.player-social'); const playerApps = document.querySelector('.footer-app'); const playerTv = document.querySelector('.online-tv'); const playerTvHeader = document.querySelector('.online-tv-header'); const playerTvModal = document.getElementById('modal-tv'); const playerProgram = document.querySelector('.player-program'); const lyricsContent = document.getElementById('lyrics'); const history = document.getElementById('history'); const historyTemplate = `
{{song}} {{artist}}
`; const API_URL = 'https://api.streamafrica.net/metadata/index.php?z='; const TIME_TO_REFRESH = window?.streams?.timeRefresh || 10000; let currentStation; let activeButton; function setAssetsInPage(station) { if (playerSocial) { playerSocial.forEach(item => { item.innerHTML = ''; }); } playerApps && (playerApps.innerHTML = ''); playerProgram && (playerProgram.innerHTML = ''); playerTv && (playerTv.innerHTML = ''); playerTvHeader && (playerTvHeader.innerHTML = ''); headerLogoImg.src = station.logo; playerArtwork.src = station.album; playerCoverImg.src = station.cover || station.album; stationName.textContent = station.name; stationDescription.textContent = station.description; if (station.social && playerSocial.length) { playerSocial.forEach(item => { Object.keys(station.social).forEach(key => { item.appendChild(createSocialItem(station.social[key], key)); }); }); } if (station.apps && playerApps) { Object.keys(station.apps).forEach(key => { playerApps.appendChild(createAppsItem(station.apps[key], key)); }); } if (station.program && playerProgram) { createProgram(station.program); } if (station.tv_url && playerTv) { createOpenTvButton(station.tv_url); } } function setAccentColor(image, colorThief) { const dom = document.documentElement; const metaThemeColor = document.querySelector('meta[name=theme-color]'); if (image.complete) { dom.setAttribute('style', `--accent: rgb(${colorThief.getColor(image)})`); metaThemeColor.setAttribute('content', `rgb(${colorThief.getColor(image)})`); } else { image.addEventListener('load', function () { dom.setAttribute('style', `--accent: rgb(${colorThief.getColor(image)})`); metaThemeColor.setAttribute('content', `rgb(${colorThief.getColor(image)})`); }); } } function createOpenTvButton(url) { const $button = document.createElement('button'); $button.classList.add('player-button-tv', 'player-button'); $button.innerHTML = icons.tv + 'Tv en vivo'; function openTv() { playerTvModal.classList.add('is-active'); pause(audio); const modalBody = playerTvModal.querySelector('.modal-body-video'); const closeButton = playerTvModal.querySelector('[data-close]'); const $iframe = document.createElement('iframe'); $iframe.src = url; $iframe.allowFullscreen = true; modalBody.appendChild($iframe); closeButton.addEventListener('click', () => { playerTvModal.classList.remove('is-active'); // al terminar de cerrar el modal, eliminar el iframe $iframe.remove(); }); } $button.addEventListener('click', openTv); playerTv.appendChild($button); const cloneButton = $button.cloneNode(true); cloneButton.classList.add('btn'); cloneButton.classList.remove('player-button'); cloneButton.addEventListener('click', openTv); playerTvHeader.appendChild(cloneButton); } function createProgram(program) { if (!program) return; if (program.time) { const $div = document.createElement('div'); const $span = document.createElement('span'); $div.classList.add('player-program-time-container'); $span.classList.add('player-program-badge'); $span.innerHTML = ' On Air'; $div.appendChild($span); const $time = document.createElement('span'); $time.classList.add('player-program-time'); $time.textContent = program.time; $div.appendChild($time); playerProgram.appendChild($div); } if (program.name) { const $name = document.createElement('span'); $name.classList.add('player-program-name'); $name.textContent = program.name; playerProgram.appendChild($name); } if (program.description) { const $description = document.createElement('span'); $description.classList.add('player-program-description'); $description.textContent = program.description; playerProgram.appendChild($description); } } function createSocialItem(url, icon) { const $a = document.createElement('a'); $a.classList.add('player-social-item'); $a.href = url; $a.target = '_blank'; $a.innerHTML = icons[icon]; return $a; } function createAppsItem(url, name) { const $a = document.createElement('a'); $a.classList.add('player-apps-item'); $a.href = url; $a.target = '_blank'; $a.innerHTML = `${name}`; return $a; } function createStations(stations, currentStation, callback) { if (!stationsList) return; stationsList.innerHTML = ''; stations.forEach(async (station, index) => { const $fragment = document.createDocumentFragment(); const $button = createStreamItem(station, index, currentStation, callback); $fragment.appendChild($button); stationsList.appendChild($fragment); }); } function createStreamItem(station, index, currentStation, callback) { const $button = document.createElement('button'); $button.classList.add('station'); $button.innerHTML = `station`; $button.dataset.index = index; $button.dataset.hash = station.hash; if (currentStation.stream_url === station.stream_url) { $button.classList.add('is-active'); activeButton = $button; } $button.addEventListener('click', () => { if ($button.classList.contains('is-active')) return; // Eliminar la clase "active" del botón activo anterior, si existe if (activeButton) { activeButton.classList.remove('is-active'); } const playerStation = document.querySelector('.player-station img:first-child'); if (playerStation) { playerStation.src = station.album; } // Agregar la clase "active" al botón actualmente presionado $button.classList.add('is-active'); activeButton = $button; // Actualizar el botón activo setAssetsInPage(station); play(audio, station.stream_url); if (history) { history.innerHTML = ''; } // Llamar a la función de devolución de llamada (callback) si se proporciona if (typeof callback === 'function') { callback(station); } }); return $button; } // Cargar datos de la canción actual al navegador function mediaSession(data) { const { title, artist, album, art } = data; if ('mediaSession' in navigator) { navigator.mediaSession.metadata = new MediaMetadata({ title, artist, album, artwork: [{ src: art, sizes: '512x512', type: 'image/png' }] }); navigator.mediaSession.setActionHandler('play', () => { play(); }); navigator.mediaSession.setActionHandler('pause', () => { pause(); }); } } // Establecer datos de la canción actual function currentSong(data) { const content = songNow; const songTitle = content.querySelector('.song-title'); const songName = document.querySelectorAll('.song-name'); const songArtist = document.querySelectorAll('.song-artist'); // const playerModalImage = document.querySelector('.player-modal-image') songName.forEach(item => { item.textContent = data.title; }); songArtist.forEach(item => { item.textContent = data.artist; }); songTitle.classList.remove('is-scrolling'); songTitle.removeAttribute('style'); if (songTitle.scrollWidth > songTitle.offsetWidth) { songTitle.classList.add('is-scrolling'); const scroll = songTitle.scrollWidth - songTitle.offsetWidth; const speed = scroll / 10; songTitle.setAttribute('style', `--text-scroll: -${scroll}px; --text-scroll-duration: ${speed}s`); } else { songTitle.classList.remove('is-scrolling'); songTitle.removeAttribute('style'); } const artwork = document.querySelector('.player-artwork'); const miniArtwork = document.querySelector('.player-artwork-mini'); if (artwork) { const $img = document.createElement('img'); $img.src = data.art; $img.width = 600; $img.height = 600; // playerModalImage.src = data.art // Cuando la imagen se haya cargado, insertarla en artwork $img.addEventListener('load', () => { artwork.appendChild($img); // eslint-disable-next-line no-undef const colorThief = new ColorThief(); // Ejecutar cada vez que cambie la imagen // Crear una imagen temporal para evitar errores de CORS createTempImage($img.src).then(img => { setAccentColor(img, colorThief); }); // Animar la imagen para desplazarla hacia la izquierda con transform setTimeout(() => { artwork.querySelectorAll('img').forEach(img => { // Establecer la transición img.style.transform = `translateX(${-img.width}px)`; // Esperar a que la animación termine img.addEventListener('transitionend', () => { // Eliminar todas las imágenes excepto la última artwork.querySelectorAll('img:not(:last-child)').forEach(img => { img.remove(); }); img.style.transition = 'none'; img.style.transform = 'none'; setTimeout(() => { img.removeAttribute('style'); }, 1000); }); }); }, 100); }); } if (miniArtwork) { miniArtwork.src = data.art; } if (playerCoverImg) { const tempImg = new Image(); tempImg.src = data.cover || data.art; tempImg.addEventListener('load', () => { playerCoverImg.style.opacity = 0; // Esperar a que la animación termine playerCoverImg.addEventListener('transitionend', () => { playerCoverImg.src = data.cover || data.art; playerCoverImg.style.opacity = 1; }); }); } } // Establecer las canciones que se han reproducido function setHistory(data, current, server) { if (!history) return; history.innerHTML = historyTemplate.replace('{{art}}', pixel).replace('{{song}}', 'Cargando historial...').replace('{{artist}}', 'Artista').replace('{{stream_url}}', '#not-found'); if (!data) return; // max 10 items data = data.slice(0, 4); const promises = data.map(async item => { const { artist, song } = item; const { album, cover } = current; const dataFrom = await getDataFrom({ artist, title: song, art: album, cover, server }); return historyTemplate.replace('{{art}}', dataFrom.thumbnail || dataFrom.art).replace('{{song}}', dataFrom.title).replace('{{artist}}', dataFrom.artist).replace('{{stream_url}}', dataFrom.stream_url); }); Promise.all(promises).then(itemsHTML => { const $fragment = document.createDocumentFragment(); itemsHTML.forEach(itemHTML => { $fragment.appendChild(createElementFromHTML(itemHTML)); }); history.innerHTML = ''; history.appendChild($fragment); }).catch(error => { console.error('Error:', error); }); } function setLyrics(artist, title) { if (!lyricsContent) return; getLyrics(artist, title).then(lyrics => { const $p = document.createElement('p'); $p.innerHTML = lyrics.replace(/\n/g, '
'); lyricsContent.innerHTML = ''; lyricsContent.appendChild($p); }).catch(error => { console.error('Error:', error); }); } // Iniciar la aplicación function initApp() { // Variables para almacenar información que se actualizará let currentSongPlaying; let timeoutId; const json = window.streams || {}; const stations = json.stations; currentStation = stations[0]; // Establecer los assets de la página setAssetsInPage(currentStation); // Establecer la fuente de audio audio.src = currentStation.stream_url; // Iniciar el stream function init(current) { // Cancelar el timeout anterior if (timeoutId) clearTimeout(timeoutId); // Si la url de la estación actual es diferente a la estación actual, se actualiza la información if (currentStation.stream_url !== current.stream_url) { currentStation = current; } const server = currentStation.server || 'itunes'; const jsonUri = currentStation.api || API_URL + encodeURIComponent(current.stream_url); fetch(jsonUri).then(response => response.json()).then(async res => { const current = normalizeTitle(res); // Si currentSong es diferente a la canción actual, se actualiza la información const title = current.title; if (currentSongPlaying !== title) { // Actualizar la canción actual currentSongPlaying = title; let artist = current.artist; const art = currentStation.album; const cover = currentStation.cover; const history = normalizeHistory(res); artist = title === artist ? null : artist; const dataFrom = await getDataFrom({ artist, title, art, cover, server: 'itunes' }); // Establecer datos de la canción actual currentSong(dataFrom); mediaSession(dataFrom); setLyrics(dataFrom.artist, dataFrom.title); setHistory(history, currentStation, server); } }).catch(error => console.log(error)); timeoutId = setTimeout(() => { init(current); }, TIME_TO_REFRESH); } init(currentStation); createStations(stations, currentStation, station => { init(station); }); const nextStation = document.querySelector('.player-button-forward-step'); const prevStation = document.querySelector('.player-button-backward-step'); if (nextStation) { nextStation.addEventListener('click', () => { const next = stationsList.querySelector('.is-active').nextElementSibling; if (next) { next.click(); } }); } if (prevStation) { prevStation.addEventListener('click', () => { const prev = stationsList.querySelector('.is-active').previousElementSibling; if (prev) { prev.click(); } }); } } initApp(); })();