PageSourceSearch

https://rqstradio.pages.dev/js/metadata.js?v=71.1-a22

js rqstradio.pages.dev collected 2026-10-03 05:54:40 UTC 6,008 bytes, 170 lines download raw bytes

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.