1(function () { 2 const cfg = () => window.RQST_CONFIG; 3 const lastValid = { 1: null, 2: null }; 4 const decoder = new TextDecoder('utf-8'); 5 6 function cleanMojibake(str) { 7 if (!str) return ''; 8 9 return String(str) 10 .replace(/ââ¬â¢/g, 'â') 11 .replace(/ââ¬Ë/g, 'â') 12 .replace(/ââ¬Å/g, 'â') 13 .replace(/ââ¬ï¿½/g, 'â') 14 .replace(/ââ¬â/g, 'â') 15 .replace(/ââ¬â/g, 'â') 16 .replace(/ââ¬Â¦/g, 'â¦') 17 .replace(/à /g, ' ') 18 .replace(/Ã/g, '') 19 .replace(/á/g, 'á') 20 .replace(/é/g, 'é') 21 .replace(/ÃÂ/g, 'Ã') 22 .replace(/ó/g, 'ó') 23 .replace(/ú/g, 'ú') 24 .replace(/ñ/g, 'ñ') 25 .replace(/ÃÂ/g, 'Ã') 26 .replace(/Ãâ°/g, 'Ã') 27 .replace(/ÃÂ/g, 'Ã') 28 .replace(/Ãâ/g, 'Ã') 29 .replace(/ÃÅ¡/g, 'Ã') 30 .replace(/Ãâ/g, 'Ã') 31 .replace(/[\uFFFD]/g, '') 32 .replace(/\s+/g, ' ') 33 .trim(); 34 } 35 36 function parseTrackString(rawText, signalId) { 37 const text = cleanMojibake(rawText); 38 const parts = text.split('|').map(s => cleanMojibake(s)); 39 const [artistTitle = '', album = '', program = ''] = parts; 40 const [artistRaw, titleRaw] = artistTitle.includes(' - ') 41 ? artistTitle.split(' - ', 2) 42 : ['', artistTitle]; 43 44 return { 45 artist: cleanMojibake(artistRaw || 'Desconocido'), 46 title: cleanMojibake(titleRaw || 'Sin tÃtulo'), 47 album: cleanMojibake(album || ''), 48 program: cleanMojibake(program || cfg().signals[signalId]?.defaultProgram || ''), 49 fetchedAt: new Date().toISOString() 50 }; 51 } 52 53 function parseMetadata(text, signalId) { 54 // Compatibilidad con respuestas antiguas tipo .xsl especÃfico: 55 // algo, Artist - Title | Album | Program 56 const info = text.split(',')[1]?.trim() || text; 57 return parseTrackString(info, signalId); 58 } 59 60 function rowsToMap(table) { 61 const map = {}; 62 Array.from(table.querySelectorAll('tr')).forEach(row => { 63 const cells = Array.from(row.querySelectorAll('td, th')).map(cell => cleanMojibake(cell.textContent)); 64 if (cells.length >= 2) map[cells[0].toLowerCase()] = cells.slice(1).join(' '); 65 }); 66 return map; 67 } 68 69 function parseIcecastStatus(htmlText, signalId) { 70 const signal = cfg().signals[signalId]; 71 const targetMount = signal.mount; 72 73 try { 74 const doc = new DOMParser().parseFromString(htmlText, 'text/html'); 75 const tables = Array.from(doc.querySelectorAll('table')); 76 77 for (const table of tables) { 78 const map = rowsToMap(table); 79 const mount = map.mount || ''; 80 if (cleanMojibake(mount) !== targetMount) continue; 81 82 const playing = map['en reproducción'] || map['currently playing'] || map['stream title'] || map.title || ''; 83 if (playing) return parseTrackString(playing, signalId); 84 } 85 } catch (err) { 86 console.warn('DOMParser Icecast parse failed, trying text fallback:', err); 87 } 88 89 const plain = cleanMojibake(htmlText.replace(/<[^>]+>/g, '\n')); 90 const mountIndex = plain.indexOf(targetMount); 91 if (mountIndex === -1) return null; 92 93 const segment = plain.slice(mountIndex, mountIndex + 1400); 94 const match = segment.match(/En reproducción\s+(.+?)\s+(Oyentes|Bitrate|Formato|Enlaces)/i) 95 || segment.match(/Currently playing\s+(.+?)\s+(Listeners|Bitrate|Format|Links)/i); 96 97 if (!match || !match[1]) return null; 98 return parseTrackString(match[1], signalId); 99 } 100 101 function updateDom(signalId, meta, degraded) { 102 const program = meta.program || cfg().signals[signalId].defaultProgram || 'No information'; 103 104 const programEl = document.getElementById(`program-${signalId}`); 105 if (programEl) programEl.textContent = program; 106 107 window.RQST_UI.setNowPlaying(signalId, { 108 program, 109 artist: meta.artist, 110 title: meta.title, 111 album: meta.album || 'No information' 112 }, degraded); 113 } 114 115 function updateCover(signalId, meta) { 116 // La búsqueda admite álbum o tema ausentes, pero exige un artista válido. 117 // Los valores vacÃos/literales inválidos terminan en la carátula del programa. 118 if (window.RQST_COVERS.isInvalidValue(meta?.artist) 119 || String(meta?.artist || '').trim().toUpperCase() === 'RQST') { 120 window.RQST_COVERS.useProgramCover(signalId); 121 return; 122 } 123 124 window.RQST_COVERS.fetchExternal(signalId, meta.artist, meta.title, { 125 album: meta.album, 126 delayMs: cfg().coverUpdateDelayMs || 0 127 }); 128 } 129 130 async function update(signalId) { 131 const signal = cfg().signals[signalId]; 132 const statusUrl = signal.metadata || cfg().metadataStatusUrl; 133 134 try { 135 const result = await window.RQST_NET.fetchArrayBufferThroughCandidates(statusUrl, { 136 timeoutMs: cfg().network?.timeoutMs || 7000, 137 cache: 'no-store' 138 }); 139 const text = decoder.decode(result.buffer).trim(); 140 const meta = signal.mount 141 ? parseIcecastStatus(text, signalId) 142 : parseMetadata(text, signalId); 143 144 if (!meta) throw new Error(`Mount not found or metadata empty: ${signal.mount}`); 145
146 lastValid[signalId] = meta; 147 updateDom(signalId, meta, false); 148 updateCover(signalId, meta); 149 window.RQST_UI.setMetadataState(signalId, 'ok'); 150 return meta; 151 } catch (err) { 152 console.warn(`Metadata signal ${signalId} unavailable:`, err?.details || err); 153 window.RQST_UI.setMetadataState(signalId, 'degraded'); 154 155 // Ante cualquier falla de metadata, la identidad editorial del programa 156 // tiene prioridad. No se conserva una carátula externa anterior. 157 window.RQST_COVERS.useProgramCover(signalId); 158 159 if (lastValid[signalId]) { 160 updateDom(signalId, lastValid[signalId], true); 161 return lastValid[signalId]; 162 } 163 164 window.RQST_UI.setRoller(signalId, 'Metadata unavailable. The audio stream remains independent.'); 165 return null; 166 } 167 } 168 169 window.RQST_METADATA = { update, parseMetadata, parseIcecastStatus, cleanMojibake }; 170})();
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.