PageSourceSearch

https://www.maray.cl/wp-content/plugins/mediaweb_player_v3/assets/js/player.js?ver=1791066621

js maray.cl collected 2026-10-03 22:30:46 UTC 45,202 bytes, 872 lines download raw bytes

1function initPlayerJS() {
2    if (window.self !== window.top) return;
3
4    const playerDiv = document.getElementById('player');
5    if (!playerDiv) {
6        setTimeout(initPlayerJS, 500);
7        return;
8    }
9
10    const settings = window.mwPlayerSettings || {
11        radio_id: '60bfe4d2ca7d157a8c3a9423',
12        stream_url: 'https://mediaweb.stream:8000/',
13        rest_url: '/wp-json/player/v1/proxy-image'
14    };
15
16    let isPlaying = false;
17    let audio = null;
18    let updateInterval = null;
19    let currentStreamUrl = null;
20    let currentRadioName = "Radio";
21    let currentRadioUrl = "https://aricanetwork.cl";
22    let currentSong = "Cargando...";
23    let currentArtist = "Conectando...";
24
25    // Audio Context vars
26    let audioCtx = null;
27    let analyser = null;
28    let sourceNode = null;
29    let dataArray = null;
30    let animationFrameId = null;
31
32    // Elementos Minimizados
33    const coverMin = document.getElementById('mw-cover-min');
34    const songMin = document.getElementById('mw-song-min');
35    const artistMin = document.getElementById('mw-artist-min');
36    const playBtnMin = document.getElementById('mw-play-btn-min');
37    const iconPlayMin = document.getElementById('mw-icon-play-min');
38    const iconPauseMin = document.getElementById('mw-icon-pause-min');
39    const clickExpand = document.getElementById('mw-click-expand');
40    const expandIcon = document.getElementById('mw-btn-expand-icon');
41
42    // Elementos Expandidos
43    const coverMax = document.getElementById('mw-cover-max');
44    const songMax = document.getElementById('mw-song-max');
45    const artistMax = document.getElementById('mw-artist-max');
46    const playBtnMax = document.getElementById('mw-play-btn-max');
47    const iconPlayMax = document.getElementById('mw-icon-play-max');
48    const iconPauseMax = document.getElementById('mw-icon-pause-max');
49    const bgBlur = document.getElementById('mw-bg-blur');
50    const btnCollapse = document.getElementById('mw-btn-collapse');
51    const radioLogoMax = document.getElementById('mw-radio-logo-max');
52    
53    // Controles Nuevos
54    const volBtns = document.querySelectorAll('.mw-vol-btn');
55    const volPopups = document.querySelectorAll('.mw-volume-popup');
56    const volumeSliders = document.querySelectorAll('.mw-vol');
57    const shareBtns = document.querySelectorAll('.mw-share-btn');
58    
59    // Barritas Espectro
60    const mwBar1 = document.querySelectorAll('.mw-bar-1');
61    const mwBar2 = document.querySelectorAll('.mw-bar-2');
62    const mwBar3 = document.querySelectorAll('.mw-bar-3');
63
64    // Historial
65    const historyList = document.getElementById('player_history_list');
66    const historyPane = document.getElementById('mw-history-pane');
67
68    // Estado
69    let isExpanded = false;
70
71    // Toggle Expand
72    clickExpand.addEventListener('click', function(e) {
73        if (e.target.closest('.mw-play-circle, .mw-btn-icon, .mw-volume-slider, .mw-volume-wrapper')) return;
74        if (!isExpanded) {
75            isExpanded = true;
76            playerDiv.classList.remove('mediaweb-minimized');
77            playerDiv.classList.add('mediaweb-expanded');
78            if (historyPane && window.matchMedia("(min-width: 769px)").matches) {
79                historyPane.classList.add('open');
80            }
81        }
82    });
83
84    const btnHistoryToggle = document.getElementById('mw-btn-history-toggle');
85    const btnHistoryClose = document.getElementById('mw-btn-history-close');
86    
87    if (btnHistoryToggle) {
88        btnHistoryToggle.addEventListener('click', function(e) {
89            e.stopPropagation();
90            if (historyPane) historyPane.classList.toggle('open');
91        });
92    }
93    
94    if (btnHistoryClose) {
95        btnHistoryClose.addEventListener('click', function(e) {
96            e.stopPropagation();
97            if (historyPane) historyPane.classList.remove('open');
98        });
99    }
100
101    if(expandIcon) {
102        expandIcon.addEventListener('click', function(e) {
103            e.stopPropagation();
104            if (!isExpanded) {
105                isExpanded = true;
106                playerDiv.classList.remove('mediaweb-minimized');
107                playerDiv.classList.add('mediaweb-expanded');
108            }
109        });
110    }
111
112    btnCollapse.addEventListener('click', function(e) {
113        e.stopPropagation();
114        if (isExpanded) {
115            isExpanded = false;
116            playerDiv.classList.remove('mediaweb-expanded');
117            playerDiv.classList.add('mediaweb-minimized');
118        }
119    });
120
121    // Toggle Volume Popup
122    volBtns.forEach((btn, index) => {
123        btn.addEventListener('click', function(e) {
124            e.stopPropagation();
125            const popup = volPopups[index];
126            const isShowing = popup.classList.contains('show');
127            volPopups.forEach(p => p.classList.remove('show'));
128            if(!isShowing) {
129                popup.classList.add('show');
130            }
131        });
132    });
133
134    // Cerrar popup al hacer clic fuera
135    document.addEventListener('click', function(e) {
136        if (!e.target.closest('.mw-volume-wrapper')) {
137            volPopups.forEach(p => p.classList.remove('show'));
138        }
139    });
140
141    // Inicializar Web Audio API sincronizadamente para Safari
142    function setupWebAudio() {
143        if (!audioCtx) {
144            try {
145                const AudioContext = window.AudioContext || window.webkitAudioContext;
146                audioCtx = new AudioContext();
147                analyser = audioCtx.createAnalyser();
148                analyser.fftSize = 64;
149                const bufferLength = analyser.frequencyBinCount;
150                dataArray = new Uint8Array(bufferLength);
151                drawSpectrum();
152            } catch(e) {
153                console.error("Web Audio API falló (posible problema de CORS):", e);
154            }
155        }
156        if (audioCtx && audioCtx.state === 'suspended') {
157            audioCtx.resume();
158        }
159    }
160
161    let zeroDataCount = 0;
162
163    function drawSpectrum() {
164        if(!analyser) return;
165        animationFrameId = requestAnimationFrame(drawSpectrum);
166        
167        if (isPlaying) {
168            analyser.getByteFrequencyData(dataArray);
169            
170            // Promediar en 3 zonas (Bajos, Medios, Agudos)
171            let sum1 = 0, sum2 = 0, sum3 = 0;
172            for(let i=0; i<8; i++) sum1 += dataArray[i];
173            for(let i=8; i<16; i++) sum2 += dataArray[i];
174            for(let i=16; i<32; i++) sum3 += dataArray[i];
175
176            let s1 = 1, s2 = 1, s3 = 1;
177
178            // Bug de WebKit/Safari con Icecast CORS: bloquea los datos del analizador devolviendo puro cero.
179            if (sum1 + sum2 + sum3 === 0) {
180                zeroDataCount++;
181                if (zeroDataCount > 15) { 
182                    // Fallback visual fluido si está reproduciendo pero no hay datos del espectro
183                    s1 = 1 + (Math.random() * 4);
184                    s2 = 1 + (Math.random() * 5);
185                    s3 = 1 + (Math.random() * 3);
186                }
187            } else {
188                zeroDataCount = 0;
189                let avg1 = sum1 / 8;
190                let avg2 = sum2 / 8;
191                let avg3 = (sum3 / 16) * 1.5; // Compensar caída de agudos
192
193                // Mapear de 0-255 a escala 1 - 7
194                s1 = 1 + (Math.min(255, avg1) / 255) * 6;
195                s2 = 1 + (Math.min(255, avg2) / 255) * 6;
196                s3 = 1 + (Math.min(255, avg3) / 255) * 6;
197            }
198
199            mwBar1.forEach(b => b.style.transform = `scaleY(${s1})`);
200            mwBar2.forEach(b => b.style.transform = `scaleY(${s2})`);
201            mwBar3.forEach(b => b.style.transform = `scaleY(${s3})`);
202        } else {
203            zeroDataCount = 0;
204            mwBar1.forEach(b => b.style.transform = 'scaleY(1)');
205            mwBar2.forEach(b => b.style.transform = 'scaleY(1)');
206            mwBar3.forEach(b => b.style.transform = 'scaleY(1)');
207        }
208    }
209
210    // Reproducción
211    function togglePlay(e) {
212        if (e) e.stopPropagation(); 
213        
214        // REQUERIDO EN SAFARI/IOS: Crear y reanudar el AudioContext síncronamente en el evento de usuario
215        setupWebAudio();
216
217        if (!audio) {
218            const streamUrl = currentStreamUrl || (settings.stream_url + 'stream/' + settings.radio_id);
219            audio = new Audio();
220            // audio.crossOrigin = "anonymous"; // REMOVIDO: Provoca Error de Audio si el servidor Icecast no tiene CORS
221            audio.src = streamUrl;
222            
223            if(volumeSliders.length > 0) {
224                audio.volume = volumeSliders[0].value;
225            }
226
227            // NOTA IMPORTANTE: Si la radio no tiene CORS, conectarlo al AudioContext silenciará el audio por seguridad del navegador.
228            // Por lo tanto, deshabilitamos el ruteo real y usaremos el ecualizador fluido de respaldo para garantizar que SIEMPRE suene.
229            /*
230            if (audioCtx && analyser) {
231                try {
232                    sourceNode = audioCtx.createMediaElementSource(audio);
233                    sourceNode.connect(analyser);
234                    analyser.connect(audioCtx.destination);
235                } catch(e) {
236                    console.error("Fallo al conectar MediaElementSource:", e);
237                }
238            }
239            */
240
241            // --- Failsafe: Reconexión Inteligente ---
242            let reconnectTimeout = null;
243            let reconnectAttempts = 0;
244            const MAX_RECONNECT_ATTEMPTS = 10;
245
246            function attemptReconnect() {
247                if (!isPlaying || window.isDvrMode) return; // Si el usuario pausó o está en modo DVR, no reconectar
248                if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
249                    console.error("Failsafe: Máximos intentos de reconexión alcanzados.");
250                    isPlaying = false;
251                    iconPlayMin.style.display = 'block';
252                    iconPauseMin.style.display = 'none';
253                    iconPlayMax.style.display = 'block';
254                    iconPauseMax.style.display = 'none';
255                    playBtnMin.classList.remove('mw-is-loading');
256                    playBtnMax.classList.remove('mw-is-loading');
257                    return;
258                }
259                
260                reconnectAttempts++;
261                console.log(`Failsafe: Intentando reconectar... (Intento ${reconnectAttempts})`);
262                
263                // Forzar recarga del stream ignorando la caché del navegador
264                audio.src = streamUrl + (streamUrl.includes('?') ? '&' : '?') + 'nocache=' + new Date().getTime();
265                audio.load();
266                audio.play().catch(e => console.error("Failsafe play error:", e));
267            }
268
269            function scheduleReconnect() {
270                if (!isPlaying || window.isDvrMode) return;
271                clearTimeout(reconnectTimeout);
272                // Si en 8 segundos no se recupera el audio, forzamos reinicio de la conexión
273                reconnectTimeout = setTimeout(attemptReconnect, 8000);
274            }
275
276            function clearReconnect() {
277                clearTimeout(reconnectTimeout);
278                reconnectAttempts = 0;
279            }
280
281            // Event Listeners Failsafe y UI
282            audio.addEventListener('error', (e) => {
283                if (window.isDvrMode) return; // Ignorar errores de reconexión si estamos en DVR
284                console.error("Audio error:", e);
285                scheduleReconnect();
286            });
287            audio.addEventListener('ended', () => {
288                if (window.isDvrMode && typeof window.playNextDvrTrack === 'function') {
289                    window.playNextDvrTrack();
290                } else {
291                    scheduleReconnect(); // Un stream de radio no debería terminar
292                }
293            });
294            audio.addEventListener('waiting', () => {
295                playBtnMin.classList.add('mw-is-loading');
296                playBtnMax.classList.add('mw-is-loading');
297                scheduleReconnect();
298            });
299            audio.addEventListener('stalled', () => {
300                playBtnMin.classList.add('mw-is-loading');
301                playBtnMax.classList.add('mw-is-loading');
302                scheduleReconnect();
303            });
304            audio.addEventListener('playing', () => {
305                playBtnMin.classList.remove('mw-is-loading');
306                playBtnMax.classList.remove('mw-is-loading');
307                clearReconnect();
308            });
309            audio.addEventListener('canplay', () => {
310                playBtnMin.classList.remove('mw-is-loading');
311                playBtnMax.classList.remove('mw-is-loading');
312            });
313        }
314
315        // --- Offline Resilience ---
316        window.addEventListener('offline', () => {
317            console.warn("Failsafe: Conexión a Internet perdida.");
318            if (isPlaying) {
319                audio.pause();
320                playBtnMin.classList.add('mw-is-loading');
321                playBtnMax.classList.add('mw-is-loading');
322                // Indicador visual de offline
323                songMin.textContent = "Sin conexión a Internet";
324                songMax.textContent = "Sin conexión a Internet";
325                artistMin.textContent = "Esperando red...";
326                artistMax.textContent = "Esperando red...";
327            }
328        });
329
330        window.addEventListener('online', () => {
331            console.info("Failsafe: Conexión a Internet recuperada.");
332            if (isPlaying) { // Si estaba reproduciendo antes de cortarse
333                songMin.textContent = currentSong;
334                songMax.textContent = currentSong;
335                artistMin.textContent = currentArtist;
336                artistMax.textContent = currentArtist;
337                attemptReconnect();
338            }
339        });
340
341        if (isPlaying) {
342            audio.pause();
343            isPlaying = false;
344            iconPlayMin.style.display = 'block';
345            iconPauseMin.style.display = 'none';
346            iconPlayMax.style.display = 'block';
347            iconPauseMax.style.display = 'none';
348        } else {
349            audio.play().then(() => {
350                isPlaying = true;
351                iconPlayMin.style.display = 'none';
352                iconPauseMin.style.display = 'block';
353                iconPlayMax.style.display = 'none';
354                iconPauseMax.style.display = 'block';
355            }).catch(function(err) { 
356                console.error("Play error:", err); 
357            });
358        }
359    }
360
361    playBtnMin.addEventListener('click', togglePlay);
362    playBtnMax.addEventListener('click', togglePlay);
363
364    // Función auxiliar para actualizar el estilo visual del slider
365    function updateSliderBackground(slider, vol) {
366        const accent = document.documentElement.style.getPropertyValue('--mw-accent') || '#7cb5d8';
367        slider.style.setProperty('background', `linear-gradient(to right, ${accent} ${vol * 100}%, rgba(255,255,255,0.2) ${vol * 100}%)`, 'important');
368    }
369
370    // Volumen
371    volumeSliders.forEach(slider => {
372        // Inicializar
373        updateSliderBackground(slider, slider.value);
374        
375        slider.addEventListener('input', function(e) {
376            const vol = e.target.value;
377            if (audio) audio.volume = vol;
378            volumeSliders.forEach(s => { 
379                s.value = vol; 
380                updateSliderBackground(s, vol);
381            });
382        });
383    });
384
385    // Compartir
386    shareBtns.forEach(btn => {
387        btn.addEventListener('click', function(e) {
388            e.stopPropagation();
389            const text = `Estoy escuchando "${currentSong}" de "${currentArtist}" en "${currentRadioName}". Escúchala en "${currentRadioUrl}"`;
390            window.open(`https://api.whatsapp.com/send?text=${encodeURIComponent(text)}`, '_blank');
391        });
392    });
393
394    function updateData() {
395        const urlNow = settings.stream_url + 'getRadio/' + settings.radio_id;
396        fetch(urlNow)
397            .then(r => r.json())
398            .then(data => {
399                if (data && data[0]) {
400                    const info = data[0];
401                    
402                    if (info.primario) {
403                        // Eliminamos setProperty('background') para mantener el glassmorphism en el historial
404                    }
405
406                    const accentColor = info.colorplayer || info.primario;
407                    if (accentColor) {
408                        document.documentElement.style.setProperty('--mw-accent', accentColor);
409                        playBtnMin.style.setProperty('background', accentColor, 'important');
410                        playBtnMax.style.setProperty('background', accentColor, 'important');
411                        const badges = document.querySelectorAll('.mw-badge-pill');
412                        badges.forEach(b => b.style.setProperty('background', accentColor, 'important'));
413                        
414                        // Refrescar color del slider actual
415                        volumeSliders.forEach(s => updateSliderBackground(s, s.value));
416                    }
417
418                    if (info.background) {
419                        // Eliminamos la anulación de colores sólidos para mantener el fondo translúcido borroso (Glassmorphism)
420                        // en ambas vistas (minimizada y maximizada).
421                    }
422                    
423                    if (info.colortexto) {
424                        playerDiv.style.setProperty('color', info.colortexto, 'important');
425                        const texts = document.querySelectorAll('.mw-min-info h3, .mw-min-info p, .mw-song-large, .mw-artist-large, .mw-btn-icon');
426                        texts.forEach(t => t.style.setProperty('color', info.colortexto, 'important'));
427                    }
428                    
429                    if (info.streamurl) currentStreamUrl = info.streamurl;
430                    if (info.nombre) currentRadioName = info.nombre;
431                    if (info.url) currentRadioUrl = info.url;
432                    
433                    if (info.defaultimage) {
434                        if(radioLogoMax) radioLogoMax.src = info.defaultimage;
435                    }
436
437                    // Función de fallback para obtener datos directamente del servidor si la BD falla
438                    function fetchFallbackApi() {
439                        if (info.apiurl) {
440                            fetch(info.apiurl)
441                                .then(r => r.text())
442                                .then(text => {
443                                    try {
444                                        const apiData = JSON.parse(text);
445                                        updatePlayerFromRaw(info, info.defaultimage);
446                                    } catch (e) {
447                                        console.error("MediaCP API Error (Invalid JSON):", e);
448                                        updatePlayerFromRaw({ nowplaying: info.nombre || 'Radio en Vivo', coverart: info.defaultimage }, info.defaultimage);
449                                    }
450                                })
451                                .catch(e => {
452                                    console.error("Network Error fetching apiurl:", e);
453                                    updatePlayerFromRaw({ nowplaying: info.nombre || 'Radio en Vivo', coverart: info.defaultimage }, info.defaultimage);
454                                });
455                        } else {
456                            updatePlayerFromRaw({ nowplaying: info.nombre || 'Radio en Vivo', coverart: info.defaultimage }, info.defaultimage);
457                        }
458                    }
459
460                    // Función para procesar y actualizar la UI principal (usada en DB y Fallback)
461                    function applyPlayerUI(songText, artistText, coverImg, defaultImg) {
462                        currentSong = songText;
463                        currentArtist = artistText;
464                        songMin.textContent = currentSong;
465                        songMax.textContent = currentSong;
466                        artistMin.textContent = currentArtist;
467                        artistMax.textContent = currentArtist;
468
469                        let coverOriginal = coverImg || defaultImg || '';
470                        let coverHD = coverOriginal;
471                        
472                        if (coverHD.includes('mzstatic.com')) {
473                            coverHD = coverHD.replace(/100x100bb\.jpg/ig, '600x600bb.jpg')
474                                             .replace(/150x150bb\.jpg/ig, '600x600bb.jpg')
475                                             .replace(/200x200bb\.jpg/ig, '600x600bb.jpg');
476                        }
477
478                        // Actualizar Media Session API (Pantalla de Bloqueo Móvil)
479                        if ('mediaSession' in navigator) {
480                            navigator.mediaSession.metadata = new MediaMetadata({
481                                title: currentSong,
482                                artist: currentArtist,
483                                album: currentRadioName || 'Radio en Vivo',
484                                artwork: [
485                                    { src: coverHD || coverOriginal || 'https://dummyimage.com/512x512', sizes: '512x512', type: 'image/jpeg' }
486                                ]
487                            });
488                            navigator.mediaSession.setActionHandler('play', () => { if (!isPlaying) togglePlay(); });
489                            navigator.mediaSession.setActionHandler('pause', () => { if (isPlaying) togglePlay(); });
490                        }
491
492                        // Solo actualizar si la imagen es diferente para evitar parpadeos
493                        if (!coverMax.src.includes(coverHD) && coverHD !== '') {
494                            coverMin.style.opacity = 0;
495                            coverMax.style.opacity = 0;
496                            setTimeout(() => {
497                                coverMin.src = coverOriginal;
498                                coverMax.src = coverHD;
499                                if (bgBlur) bgBlur.style.backgroundImage = `url(${coverHD})`;
500                                setTimeout(() => {
501                                    coverMin.style.opacity = 1;
502                                    coverMax.style.opacity = 1;
503                                }, 50);
504                            }, 500);
505                        }
506                    }
507
508                    // Función auxiliar si usamos el fallback de la API en bruto
509                    function updatePlayerFromRaw(apiData, defaultImage) {
510                        if (!apiData) return;
511                        let rawNow = apiData.nowplaying || apiData.nowPlaying || apiData.current || 'Desconocido';
512                        let nowplaying = String(rawNow).split('~')[0].trim();
513                        let parts = nowplaying.split(' - ');
514                        let aText = parts.length > 1 ? parts[0].trim() : 'Desconocido';
515                        let sText = parts.length > 1 ? parts.slice(1).join(' - ').trim() : nowplaying.trim();
516                        
517                        applyPlayerUI(sText, aText, apiData.coverart, defaultImage);
518                        renderHistory([{ canciones: sText, artista: aText, portada_api: apiData.coverart || defaultImage, is_al_aire: true }]);
519                    }
520                    // 1. CONEXIÓN EN TIEMPO REAL (SSE) EN VEZ DE POLLING
521                    function setupSSE() {
522                        if (window.mwEventSource) {
523                            window.mwEventSource.close();
524                        }
525                        const sseUrl = 'https://mediaweb.stream:8000/stream/metadata?id=' + settings.radio_id;
526                        const sse = new EventSource(sseUrl);
527                        window.mwEventSource = sse;
528
529                        sse.onmessage = function(e) {
530                            try {
531                                if (window.lastSseDataString === e.data) return; // Evitar parpadeos si la historia no ha cambiado
532                                window.lastSseDataString = e.data;
533                                
534                                const dbHistory = JSON.parse(e.data);
535                                if (dbHistory && Array.isArray(dbHistory) && dbHistory.length > 0) {
536                                    // Usar el primer item (AL AIRE) como la canción principal
537                                    let firstItem = dbHistory[0];
538                                    let cleanNombre = (firstItem.nombre || '').split('~')[0].trim();
539                                    let tParts = cleanNombre.split(' - ');
540                                    let aText = tParts.length > 1 ? tParts[0].trim() : 'Desconocido';
541                                    let sText = tParts.length > 1 ? tParts.slice(1).join(' - ').trim() : (cleanNombre || 'Desconocido').trim();
542                                    // Guardar la última info en vivo
543                                    window.lastSseInfo = { sText, aText, cover: firstItem.url || info.defaultimage };
544                                    
545                                    if (!window.isDvrMode) {
546                                        applyPlayerUI(sText, aText, firstItem.url, info.defaultimage);
547                                    }
548
549                                    // Construir el historial completo
550                                    let fullHistory = [];
551                                    dbHistory.forEach((track, idx) => {
552                                        let cNombre = (track.nombre || '').split('~')[0].trim();
553                                        let p = cNombre.split(' - ');
554                                        fullHistory.push({
555                                            canciones: p.length > 1 ? p.slice(1).join(' - ').trim() : (cNombre || 'Desconocido').trim(),
556                                            artista: p.length > 1 ? p[0].trim() : 'Desconocido',
557                                            portada_api: track.url || info.defaultimage || '',
558                                            is_al_aire: idx === 0,
559                                            inicio: track.inicio
560                                        });
561                                    });
562                                    
563                                    if (info.dvr_enabled) {
564                                        fetch(`https://mediaweb.stream:8000/api/dvr/list?id=${settings.radio_id}`)
565                                            .then(r => r.json())
566                                            .then(files => {
567                                                window.dvrFiles = files;
568                                                renderHistory(fullHistory);
569                                            })
570                                            .catch(err => {
571                                                console.error("Error actualizando dvrFiles", err);
572                                                renderHistory(fullHistory);
573                                            });
574                                    } else {
575                                        renderHistory(fullHistory);
576                                    }
577                                } else {
578                                    // Si la base de datos está vacía, usar API en bruto
579                                    fetchFallbackApi();
580                                }
581                            } catch(err) {
582                                console.error("Error parseando SSE:", err);
583                                fetchFallbackApi();
584                            }
585                        };
586
587                        sse.onerror = function() {
588                            console.warn("SSE error, reconectando silenciosamente...");
589                            sse.close();
590                            setTimeout(setupSSE, 5000);
591                        };
592                    }
593
594                    function renderHistory(items) {
595                        window.mwFullHistory = items;
596                        historyList.innerHTML = '';
597                        items.forEach((item, index) => {
598                            const div = document.createElement('div');
599                            div.className = 'mw-history-item';
600                            if (index === 0) {
601                                div.classList.add('active');
602                            }
603
604                            const img = document.createElement('img');
605                            img.src = item.portada_api || '';
606                            div.appendChild(img);
607
608                            const infoDiv = document.createElement('div');
609                            infoDiv.className = 'mw-history-item-info';
610                            
611                            const pSong = document.createElement('h4');
612                            pSong.textContent = item.canciones || '';
613                            infoDiv.appendChild(pSong);
614                            
615                            const pArtist = document.createElement('p');
616                            pArtist.textContent = item.artista || '';
617                            infoDiv.appendChild(pArtist);
618
619                            div.appendChild(infoDiv);
620
621                            const timeSpan = document.createElement('span');
622                            timeSpan.className = 'mw-history-time';
623                            
624                            let timeText = 'Hace un momento';
625                            if (item.inicio) {
626                                try {
627                                    const date = new Date(item.inicio);
628                                    if (!isNaN(date.getTime())) {
629                                        const h = date.getHours().toString().padStart(2, '0');
630                                        const m = date.getMinutes().toString().padStart(2, '0');
631                                        timeText = `${h}:${m}`;
632                                    }
633                                } catch(e) {}
634                            }
635                            
636                            const clockSvg = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:14px;
636height:14px;opacity:0.7;vertical-align:middle;margin-top:-2px;"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>`;
637                            const barsHtml = `<span class="mw-bars"><span class="mw-bar mw-bar-1"></span><span class="mw-bar mw-bar-2"></span><span class="mw-bar mw-bar-3"></span></span>`;
638                            
639                            if (item.is_al_aire) {
640                                timeSpan.innerHTML = `${barsHtml} AL AIRE`;
641                                timeSpan.style.background = '#e63946';
642                                timeSpan.style.color = '#fff';
643                            } else {
644                                timeSpan.innerHTML = `${clockSvg} ${timeText}`;
645                                timeSpan.style.background = 'rgba(0,0,0,0.3)';
646                                
647                                // Añadir botón Play si el DVR está habilitado
648                                if (info.dvr_enabled && window.dvrFiles && window.dvrFiles.length > 0) {
649                                    // Buscar el archivo MP3 que coincida en tiempo o nombre
650                                    const itemTime = item.inicio ? new Date(item.inicio).getTime() / 1000 : 0;
651                                    let bestMatch = null;
652                                    
653                                    // Buscar coincidencia por cercanía de tiempo (timestamp Unix)
654                                    let closestDiff = Infinity;
655                                    let matchIndex = -1;
656                                    
657                                    for (let i = 0; i < window.dvrFiles.length; i++) {
658                                        const f = window.dvrFiles[i];
659                                        const fileTimeStr = f.replace(".mp3", "").replace(".aac", "");
660                                        const fileTime = parseInt(fileTimeStr, 10);
661                                        if (!isNaN(fileTime)) {
662                                            const diff = Math.abs(fileTime - itemTime);
663                                            if (diff < closestDiff) {
664                                                closestDiff = diff;
665                                                bestMatch = f;
666                                                matchIndex = i;
667                                            }
668                                        }
669                                    }
670                                    
671                                    // Considerar match válido si la diferencia es menor a 150 segundos
672                                    if (bestMatch && closestDiff < 150) {
673                                        const playBtn = document.createElement('button');
674                                        playBtn.className = 'mw-dvr-play-btn';
675                                        playBtn.innerHTML = `<svg viewBox="0 0 24 24" fill="white" style="width:16px;height:16px;"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>`;
676                                        playBtn.style.cssText = 'background: var(--mw-accent, #e63946); border:none; border-radius:50%; width:28px; height:28px; cursor:pointer; margin-left:8px; display:inline-flex; align-items:center; justify-content:center; vertical-align:middle; transition:transform 0.2s; box-shadow: 0 2px 5px rgba(0,0,0,0.3);';
677                                        
678                                        playBtn.onmouseover = () => playBtn.style.transform = 'scale(1.1)';
679                                        playBtn.onmouseout = () => playBtn.style.transform = 'scale(1)';
680                                        
681                                        playBtn.onclick = (e) => {
682                                            e.stopPropagation();
683                                            e.preventDefault();
684                                            console.log("Clic en DVR Btn:", bestMatch);
685                                            if (typeof window.playDvrTrack === 'function') {
686                                                window.playDvrTrack(bestMatch, matchIndex, item);
687                                            } else {
688                                                alert("Error: playDvrTrack no está definido.");
689                                            }
690                                        };
691                                        timeSpan.appendChild(playBtn);
692                                    }
693                                }
694                            }
695                            div.appendChild(timeSpan);
696
697                            historyList.appendChild(div);
698                        });
699                    }
700
701                    // Mostrar estado inicial inmediatamente para que no se vea la pantalla negra (Sincrónico)
702                    updatePlayerFromRaw({ nowplaying: info.nombre || 'Radio en Vivo', coverart: info.defaultimage }, info.defaultimage);
703
704                    // Si DVR está habilitado, cargar la lista de archivos primero
705                    if (info.dvr_enabled) {
706                        fetch(`https://mediaweb.stream:8000/api/dvr/list?id=${settings.radio_id}`)
707                            .then(r => r.json())
708                            .then(files => {
709                                window.dvrFiles = files;
710                                setupSSE();
711                            })
712                            .catch(e => {
713                                console.error("Error cargando lista DVR", e);
714                                setupSSE();
715                            });
716                    } else {
717                        setupSSE();
718                    }
719                    
720                    // Función para reproducir track del pasado
721                    window.playDvrTrack = function(filename, index, item) {
722                        const rawUrl = `https://mediaweb.stream:8000/dvr/${settings.radio_id}/${filename}`;
723                        
724                        window.isDvrMode = true;
725                        window.currentDvrIndex = index;
726                        
727                        audio.src = encodeURI(rawUrl);
728                        audio.load(); // Forzar recarga del source
729                        audio.play().then(() => {
730                            isPlaying = true;
731                            const iconPlayMin = document.getElementById('mw-icon-play-min');
732                            const iconPauseMin = document.getElementById('mw-icon-pause-min');
733                            const iconPlayMax = document.getElementById('mw-icon-play-max');
734                            const iconPauseMax = document.getElementById('mw-icon-pause-max');
735                            if(iconPlayMin) iconPlayMin.style.display = 'none';
736                            if(iconPauseMin) iconPauseMin.style.display = 'block';
737                            if(iconPlayMax) iconPlayMax.style.display = 'none';
738                            if(iconPauseMax) iconPauseMax.style.display = 'block';
739                        }).catch(e => {
740                            console.error("DVR Play error", e);
741                            alert("Error reproduciendo audio del pasado.");
742                        });
743                        
744                        // Forzar UI con los datos de la canción del historial
745                        let forcedSong = item ? item.canciones : "Track en diferido";
746                        let forcedCover = item ? item.portada_api || info.defaultimage : info.defaultimage;
747                        let forcedArtist = "Timeshift";
748                        if (forcedSong.includes(' - ')) {
749                            let parts = forcedSong.split(' - ');
750                            forcedArtist = parts[0].trim();
751                            forcedSong = parts.slice(1).join(' - ').trim();
752                        }
753                        
754                        applyPlayerUI(forcedSong, forcedArtist, forcedCover, info.defaultimage);
755                        
756                        // Reemplazar los Badges de "AL AIRE" por "TIMESHIFT" (y que sirvan para volver)
757                        const liveBadges = document.querySelectorAll('.mw-badge-pill');
758                        liveBadges.forEach(liveBadge => {
759                            liveBadge.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:12px;height:12px;margin-right:4px;vertical-align:middle;"><polyline points="1 4 1 10 7 10"></polyline><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path></svg> <span style="vertical-align:middle;">TIMESHIFT 🔴</span>`;
760                            liveBadge.style.setProperty('background', '#e63946', 'important');
761                            liveBadge.style.cursor = 'pointer';
762                            liveBadge.style.animation = 'mw-pulse 2s infinite';
763                            liveBadge.title = "Haga clic para volver a la transmisión en vivo";
764                            liveBadge.onclick = window.returnToLive;
765                        });
766                        
767                        if (!document.getElementById('mw-dvr-style')) {
768                            const style = document.createElement('style');
769                            style.id = 'mw-dvr-style';
770                            style.textContent = `@keyframes mw-pulse { 0% { box-shadow: 0 0 0 0 rgba(230,57,70,0.7); } 70% { box-shadow: 0 0 0 6px rgba(230,57,70,0); } 100% { box-shadow: 0 0 0 0 rgba(230,57,70,0); } }`;
771                            document.head.appendChild(style);
772                        }
773                    };
774                    
775                    window.playNextDvrTrack = function() {
776                        if (window.currentDvrIndex !== undefined && window.currentDvrIndex + 1 < window.dvrFiles.length) {
777                            const nextIndex = window.currentDvrIndex + 1;
778                            const nextFile = window.dvrFiles[nextIndex];
779                            
780                            let matchedItem = null;
781                            if (window.mwFullHistory) {
782                                const fileTimeStr = nextFile.replace(".mp3", "").replace(".aac", "");
783                                const fileTime = parseInt(fileTimeStr, 10);
784                                if (!isNaN(fileTime)) {
785                                    let closestDiff = Infinity;
786                                    window.mwFullHistory.forEach(item => {
787                                        const itemTime = item.inicio ? new Date(item.inicio).getTime() / 1000 : 0;
788                                        if (itemTime > 0) {
789                                            const diff = Math.abs(fileTime - itemTime);
790                                            if (diff < closestDiff && diff < 150) {
791                                                closestDiff = diff;
792                                                matchedItem = item;
793                                            }
794                                        }
795                                    });
796                                }
797                            }
798                            
799                            window.playDvrTrack(nextFile, nextIndex, matchedItem);
800                        } else {
801                            // Se acabó el historial grabado, volvemos al vivo automáticamente
802                            console.log("Historial terminado. Volviendo al Vivo.");
803                            window.returnToLive();
804                        }
805                    };
806
807                    window.returnToLive = function() {
808                        window.isDvrMode = false;
809                        const liveStreamUrl = currentStreamUrl || (settings.stream_url + 'stream/' + settings.radio_id);
810                        audio.src = liveStreamUrl + (liveStreamUrl.includes('?') ? '&' : '?') + 'nocache=' + new Date().getTime();
811                        audio.load();
812                        audio.play().catch(e => console.error("Error retornando al vivo", e));
813                        
814                        // Restaurar los badges a "AL AIRE"
815                        const liveBadges = document.querySelectorAll('.mw-badge-pill');
816                        liveBadges.forEach(liveBadge => {
817                            liveBadge.innerHTML = `<span class="mw-bars"><span class="mw-bar mw-bar-1"></span><span class="mw-bar mw-bar-2"></span><span class="mw-bar mw-bar-3"></span></span> AL AIRE`;
818                            liveBadge.style.setProperty('background', 'var(--mw-accent, #e63946)', 'important');
819                            liveBadge.style.cursor = 'default';
820                            liveBadge.style.animation = 'none';
821                            liveBadge.onclick = null;
822                            liveBadge.removeAttribute('title');
823                        });
824                        
825                        // Forzar refresco SSE con la última info conocida
826                        if (window.lastSseInfo) {
827                            applyPlayerUI(window.lastSseInfo.sText, window.lastSseInfo.aText, window.lastSseInfo.cover, null);
828                        }
829                    };
830
831                }
832            })
833            .catch(e => {
834                console.error("Error crítico al obtener la configuración de la radio:", e);
835                // Si falla el proxy o la red, intentar forzar la UI al menos
836                const songMax = document.getElementById('mw-song-max');
837                const artistMax = document.getElementById('mw-artist-max');
838                if (songMax) songMax.textContent = 'Señal en Vivo';
839                if (artistMax) artistMax.textContent = 'Conectando...';
840            });
841    }
842
843    // Inicializar el reproductor (Cargar configuración y conectar SSE)
844    updateData();
845
846    // Animación de barras
847    setInterval(() => {
848        const bars = document.querySelectorAll('.mw-bar');
849        bars.forEach(b => {
850            b.style.transform = `scaleY(${Math.floor(Math.random() * 4) + 1})`;
851        });
852    }, 150);
853
854    // Estado inicial del historial según dispositivo
855    if (historyPane && window.matchMedia("(min-width: 769px)").matches) {
856        historyPane.classList.add('open');
857    }
858}
859
860if (document.readyState === 'loading') {
861    document.addEventListener('DOMContentLoaded', initPlayerJS);
862} else {
863    initPlayerJS();
864}
865
866if ('serviceWorker' in navigator) {
867    window.addEventListener('load', () => {
868        navigator.serviceWorker.register(mwPlayerSettings.plugin_url + 'sw.js').catch(err => {
869            console.log('ServiceWorker falló: ', err);
870        });
871    });
872}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.