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.