Compare commits
3 Commits
c3f594d102
...
f2732b36f2
| Author | SHA1 | Date | |
|---|---|---|---|
| f2732b36f2 | |||
| 7c0a202f16 | |||
| cb01a59051 |
@@ -18,6 +18,13 @@
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Show pointer cursor for interactive / clickable elements (override global default) */
|
||||
a, a[href], button, input[type="button"], input[type="submit"],
|
||||
[role="button"], [onclick], .clickable, .icon-btn, .control-btn, label[for],
|
||||
.station-item, [tabindex]:not([tabindex="-1"]) {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
/* Hide Scrollbars */
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
@@ -258,6 +265,22 @@ header {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Make artwork/logo clickable: show pointer cursor */
|
||||
.artwork-placeholder,
|
||||
.artwork-placeholder:hover,
|
||||
.station-logo-img,
|
||||
.station-logo-text {
|
||||
cursor: pointer !important;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* Subtle hover affordance to make clickability clearer */
|
||||
.artwork-placeholder:hover .station-logo-img,
|
||||
.artwork-placeholder:hover .station-logo-text {
|
||||
transform: scale(1.03);
|
||||
transition: transform 160ms ease;
|
||||
}
|
||||
|
||||
.blob {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
|
||||
319
src/main.js
319
src/main.js
@@ -31,6 +31,19 @@ const deviceListEl = document.getElementById('device-list');
|
||||
const logoTextEl = document.querySelector('.station-logo-text');
|
||||
const logoImgEl = document.getElementById('station-logo-img');
|
||||
const artworkPlaceholder = document.querySelector('.artwork-placeholder');
|
||||
// Global error handlers to avoid silent white screen and show errors in UI
|
||||
window.addEventListener('error', (ev) => {
|
||||
try {
|
||||
console.error('Uncaught error', ev.error || ev.message || ev);
|
||||
if (statusTextEl) statusTextEl.textContent = 'Error: ' + (ev.error && ev.error.message ? ev.error.message : ev.message || 'Unknown');
|
||||
} catch (e) { /* ignore */ }
|
||||
});
|
||||
window.addEventListener('unhandledrejection', (ev) => {
|
||||
try {
|
||||
console.error('Unhandled rejection', ev.reason);
|
||||
if (statusTextEl) statusTextEl.textContent = 'Error: ' + (ev.reason && ev.reason.message ? ev.reason.message : String(ev.reason));
|
||||
} catch (e) { /* ignore */ }
|
||||
});
|
||||
// Editor elements
|
||||
const editBtn = document.getElementById('edit-stations-btn');
|
||||
const editorOverlay = document.getElementById('editor-overlay');
|
||||
@@ -46,10 +59,16 @@ const usIndex = document.getElementById('us_index');
|
||||
|
||||
// Init
|
||||
async function init() {
|
||||
restoreSavedVolume();
|
||||
await loadStations();
|
||||
setupEventListeners();
|
||||
updateUI();
|
||||
try {
|
||||
restoreSavedVolume();
|
||||
await loadStations();
|
||||
setupEventListeners();
|
||||
ensureArtworkPointerFallback();
|
||||
updateUI();
|
||||
} catch (e) {
|
||||
console.error('Error during init', e);
|
||||
if (statusTextEl) statusTextEl.textContent = 'Init error: ' + (e && e.message ? e.message : String(e));
|
||||
}
|
||||
}
|
||||
|
||||
// Volume persistence
|
||||
@@ -153,59 +172,236 @@ async function loadStations() {
|
||||
statusTextEl.textContent = 'Error loading stations';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- Current Song Polling ---
|
||||
const currentSongPollers = new Map(); // stationId -> intervalId
|
||||
const currentSongPollers = new Map(); // stationId -> { intervalId, timeoutId }
|
||||
|
||||
// Attempt to discover the radio.svet24 asset query token used by lastsongs URLs.
|
||||
// This fetches a stable asset JS (as used on the site) and extracts the value
|
||||
// between `?` and `")` using a JS-friendly regex. It caches the token on the
|
||||
// `station._assetToken` property so pollers can append it to lastsongs requests.
|
||||
async function getAssetTokenForStation(station) {
|
||||
try {
|
||||
if (!station) return null;
|
||||
if (station._assetToken) return station._assetToken;
|
||||
|
||||
// Primary known asset location used on the site
|
||||
const assetUrl = 'https://radio.svet24.si/assets/index-IMUbJO1D.js';
|
||||
|
||||
let body = null;
|
||||
try {
|
||||
// Use backend proxy to avoid CORS
|
||||
body = await invoke('fetch_url', { url: assetUrl });
|
||||
} catch (e) {
|
||||
// If fetching the known asset fails, try the station's homepage as a fallback
|
||||
try {
|
||||
const home = station.raw && (station.raw.www || station.raw.homepage || station.raw.url);
|
||||
if (home) body = await invoke('fetch_url', { url: String(home) });
|
||||
} catch (e2) {
|
||||
// swallow fallback errors
|
||||
}
|
||||
}
|
||||
|
||||
if (!body) return null;
|
||||
|
||||
// Extract the token: grep -oP '\?\K[^"]+(?="\))' equivalence in JS
|
||||
// We'll capture the group after the question mark and before the '"))' sequence.
|
||||
const m = body.match(/\?([^"\)]+)(?="\))/);
|
||||
if (m && m[1]) {
|
||||
const token = m[1].replace(/^\?/, '');
|
||||
station._assetToken = token;
|
||||
console.debug('getAssetTokenForStation: found token for', station.id || station.name, token);
|
||||
return token;
|
||||
}
|
||||
|
||||
console.debug('getAssetTokenForStation: token not found in fetched asset for', station.id || station.name);
|
||||
return null;
|
||||
} catch (e) {
|
||||
console.debug('getAssetTokenForStation failed', e && e.message ? e.message : e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function stopCurrentSongPollers() {
|
||||
for (const id of currentSongPollers.values()) {
|
||||
clearInterval(id);
|
||||
for (const entry of currentSongPollers.values()) {
|
||||
try { if (entry && entry.intervalId) clearInterval(entry.intervalId); } catch (e) {}
|
||||
try { if (entry && entry.timeoutId) clearTimeout(entry.timeoutId); } catch (e) {}
|
||||
}
|
||||
currentSongPollers.clear();
|
||||
}
|
||||
|
||||
function startCurrentSongPollers() {
|
||||
// Clear existing
|
||||
// Clear existing (we only run poller for the currently selected station)
|
||||
stopCurrentSongPollers();
|
||||
|
||||
stations.forEach((s, idx) => {
|
||||
const url = s.raw && s.raw.currentSong;
|
||||
if (url && typeof url === 'string' && url.length > 0) {
|
||||
// fetch immediately and then every 10s
|
||||
fetchAndStoreCurrentSong(s, idx, url);
|
||||
const iid = setInterval(() => fetchAndStoreCurrentSong(s, idx, url), 10000);
|
||||
currentSongPollers.set(s.id || idx, iid);
|
||||
}
|
||||
});
|
||||
const idx = currentIndex;
|
||||
const s = stations[idx];
|
||||
if (!s) return;
|
||||
|
||||
// Prefer explicit `currentSong` endpoint, fall back to `lastSongs` endpoint
|
||||
let url = s.raw && (s.raw.currentSong || s.raw.lastSongs);
|
||||
|
||||
if (url && typeof url === 'string' && url.length > 0) {
|
||||
const baseUrl = String(url);
|
||||
|
||||
// Create a wrapper that computes the effective fetch URL each time so that
|
||||
// any token discovered asynchronously on the station object is applied
|
||||
// without restarting the poller.
|
||||
const doFetch = () => {
|
||||
try {
|
||||
let fetchUrl = baseUrl;
|
||||
if (/lastsongsxml/i.test(fetchUrl)) {
|
||||
const token = s._assetToken || null;
|
||||
if (token) {
|
||||
fetchUrl += (fetchUrl.includes('?') ? '&' : '?') + token;
|
||||
} else {
|
||||
// temporary cache-busting param while token discovery runs
|
||||
fetchUrl += (fetchUrl.includes('?') ? '&' : '?') + Date.now();
|
||||
// start background retrieval of token for future requests
|
||||
getAssetTokenForStation(s).catch(() => {});
|
||||
}
|
||||
}
|
||||
fetchAndStoreCurrentSong(s, idx, fetchUrl);
|
||||
} catch (e) {
|
||||
console.debug('doFetch wrapper error for station', s.id || idx, e);
|
||||
}
|
||||
};
|
||||
|
||||
// fetch immediately and then every 10s
|
||||
doFetch();
|
||||
const iid = setInterval(doFetch, 10000);
|
||||
// save baseUrl for potential restart and track interval/timeout
|
||||
s._lastSongsBaseUrl = baseUrl;
|
||||
currentSongPollers.set(s.id || idx, { intervalId: iid, timeoutId: null });
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAndStoreCurrentSong(station, idx, url) {
|
||||
try {
|
||||
let data = null;
|
||||
try {
|
||||
const resp = await fetch(url, { cache: 'no-store' });
|
||||
const ct = resp.headers.get('content-type') || '';
|
||||
if (ct.includes('application/json')) {
|
||||
data = await resp.json();
|
||||
// If the URL is remote (http/https), use the backend `fetch_url` to bypass CORS.
|
||||
let rawBody = null;
|
||||
if (/^https?:\/\//i.test(url)) {
|
||||
rawBody = await invoke('fetch_url', { url });
|
||||
} else {
|
||||
const txt = await resp.text();
|
||||
try { data = JSON.parse(txt); } catch (e) { data = null; }
|
||||
const resp = await fetch(url, { cache: 'no-store' });
|
||||
rawBody = await resp.text();
|
||||
}
|
||||
} catch (fetchErr) {
|
||||
// Possibly blocked by CORS — fall back to backend fetch via Tauri invoke
|
||||
|
||||
// Debug: show fetched body length (avoid huge dumps)
|
||||
console.debug('fetchAndStoreCurrentSong fetched', url, 'len=', rawBody ? String(rawBody.length) : 'null');
|
||||
|
||||
// Try to parse JSON. Some endpoints double-encode JSON as a string (i.e.
|
||||
// the response is a JSON string containing escaped JSON). Handle both
|
||||
// cases: direct object, or string that needs a second parse.
|
||||
try {
|
||||
const body = await invoke('fetch_url', { url });
|
||||
try { data = JSON.parse(body); } catch (e) { data = null; }
|
||||
} catch (invokeErr) {
|
||||
console.debug('Both fetch and backend fetch failed for', url, fetchErr, invokeErr);
|
||||
if (!rawBody) {
|
||||
data = null;
|
||||
} else {
|
||||
const first = JSON.parse(rawBody);
|
||||
if (typeof first === 'string') {
|
||||
// e.g. "{\"title\":...}"
|
||||
try {
|
||||
data = JSON.parse(first);
|
||||
} catch (e2) {
|
||||
// If second parse fails, attempt to unescape common backslashes and reparse
|
||||
const unescaped = first.replace(/\\"/g, '"').replace(/\\n/g, '').replace(/\\/g, '');
|
||||
try {
|
||||
data = JSON.parse(unescaped);
|
||||
} catch (e3) {
|
||||
console.debug('Failed second-stage parse for', url, e3);
|
||||
data = null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
data = first;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug('Failed to parse JSON from', url, e);
|
||||
data = null;
|
||||
}
|
||||
|
||||
// Normalise different shapes to a currentSong object if possible
|
||||
let now = null;
|
||||
if (data) {
|
||||
if (data.currentSong && (data.currentSong.artist || data.currentSong.title)) {
|
||||
now = { artist: data.currentSong.artist || '', title: data.currentSong.title || '' };
|
||||
} else if (Array.isArray(data.lastSongs) && data.lastSongs.length > 0) {
|
||||
const first = data.lastSongs[0];
|
||||
if (first && (first.artist || first.title)) now = { artist: first.artist || '', title: first.title || '' };
|
||||
} else if (data.artist || data.title) {
|
||||
now = { artist: data.artist || '', title: data.title || '' };
|
||||
} else if (data.title && !data.artist) {
|
||||
// Some stations use `title` only
|
||||
now = { artist: '', title: data.title };
|
||||
}
|
||||
}
|
||||
|
||||
if (data && (data.artist || data.title)) {
|
||||
station.currentSongInfo = { artist: data.artist || '', title: data.title || '' };
|
||||
if (now) {
|
||||
station.currentSongInfo = now;
|
||||
// update UI if this is the currently loaded station
|
||||
if (idx === currentIndex) updateNowPlayingUI();
|
||||
|
||||
// If we have timing info (from the provider), schedule a single-shot
|
||||
// refresh to align with the next song instead of polling continuously.
|
||||
try {
|
||||
const key = station.id || idx;
|
||||
// prefer the provider currentSong object if available
|
||||
const providerCS = data && data.currentSong ? data.currentSong : null;
|
||||
const startStr = providerCS && providerCS.playTimeStartSec ? providerCS.playTimeStartSec : (providerCS && providerCS.playTimeStart ? providerCS.playTimeStart : null);
|
||||
const lengthStr = providerCS && providerCS.playTimeLengthSec ? providerCS.playTimeLengthSec : (providerCS && providerCS.playTimeLength ? providerCS.playTimeLength : null);
|
||||
if (startStr && lengthStr) {
|
||||
// parse start time `HH:MM:SS` (or `H:MM:SS`) and length `M:SS` or `MM:SS`
|
||||
const nowDate = new Date();
|
||||
const parts = startStr.split(':').map(p=>Number(p));
|
||||
if (parts.length >= 2) {
|
||||
const startDate = new Date(nowDate.getFullYear(), nowDate.getMonth(), nowDate.getDate(), parts[0], parts[1], parts[2]||0, 0);
|
||||
// adjust if start appears to be on previous day (large positive diff)
|
||||
const deltaStart = startDate.getTime() - nowDate.getTime();
|
||||
if (deltaStart > 12*3600*1000) startDate.setDate(startDate.getDate() - 1);
|
||||
if (deltaStart < -12*3600*1000) startDate.setDate(startDate.getDate() + 1);
|
||||
|
||||
const lenParts = lengthStr.split(':').map(p=>Number(p));
|
||||
let lenSec = 0;
|
||||
if (lenParts.length === 3) lenSec = lenParts[0]*3600 + lenParts[1]*60 + lenParts[2];
|
||||
else if (lenParts.length === 2) lenSec = lenParts[0]*60 + lenParts[1];
|
||||
else lenSec = Number(lenParts[0]) || 0;
|
||||
|
||||
const endMs = startDate.getTime() + (lenSec * 1000);
|
||||
const msUntilEnd = endMs - nowDate.getTime();
|
||||
if (msUntilEnd > 1000) {
|
||||
// Clear existing interval and timeouts for this station
|
||||
const entry = currentSongPollers.get(key);
|
||||
if (entry && entry.intervalId) {
|
||||
try { clearInterval(entry.intervalId); } catch (e) {}
|
||||
}
|
||||
if (entry && entry.timeoutId) {
|
||||
try { clearTimeout(entry.timeoutId); } catch (e) {}
|
||||
}
|
||||
|
||||
// Schedule a one-shot fetch at song end. After firing, restart pollers
|
||||
const timeoutId = setTimeout(async () => {
|
||||
try {
|
||||
// re-fetch using the same effective URL (may include token)
|
||||
await fetchAndStoreCurrentSong(station, idx, url);
|
||||
} catch (e) {
|
||||
console.debug('scheduled fetch failed for', key, e);
|
||||
} finally {
|
||||
// If still on this station, restart the regular poller
|
||||
if (currentIndex === idx) startCurrentSongPollers();
|
||||
}
|
||||
}, msUntilEnd + 250);
|
||||
|
||||
currentSongPollers.set(key, { intervalId: null, timeoutId });
|
||||
console.debug('Scheduled next fetch for', key, 'in ms=', msUntilEnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug('Failed scheduling next-song fetch', e);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore fetch errors silently (network/CORS) but keep console for debugging
|
||||
@@ -218,9 +414,10 @@ function updateNowPlayingUI() {
|
||||
if (!station) return;
|
||||
|
||||
if (nowPlayingEl && nowArtistEl && nowTitleEl) {
|
||||
if (station.currentSongInfo && station.currentSongInfo.artist && station.currentSongInfo.title) {
|
||||
nowArtistEl.textContent = station.currentSongInfo.artist;
|
||||
nowTitleEl.textContent = station.currentSongInfo.title;
|
||||
// Show now-playing if we have either an artist or a title (some stations only provide title)
|
||||
if (station.currentSongInfo && (station.currentSongInfo.artist || station.currentSongInfo.title)) {
|
||||
nowArtistEl.textContent = station.currentSongInfo.artist || '';
|
||||
nowTitleEl.textContent = station.currentSongInfo.title || '';
|
||||
nowPlayingEl.classList.remove('hidden');
|
||||
} else {
|
||||
nowArtistEl.textContent = '';
|
||||
@@ -416,6 +613,45 @@ function setupEventListeners() {
|
||||
// Hotkeys?
|
||||
}
|
||||
|
||||
// If CSS doesn't produce a pointer, this helper forces a pointer when the
|
||||
// mouse is inside the artwork placeholder's bounding rect. This handles
|
||||
// cases where an invisible overlay or ancestor blocks pointer styling.
|
||||
function ensureArtworkPointerFallback() {
|
||||
try {
|
||||
const ap = artworkPlaceholder;
|
||||
if (!ap) return;
|
||||
|
||||
// Quick inline style fallback (helps when CSS is overridden)
|
||||
try { ap.style.cursor = 'pointer'; } catch (e) {}
|
||||
try { if (logoImgEl) logoImgEl.style.cursor = 'pointer'; } catch (e) {}
|
||||
try { if (logoTextEl) logoTextEl.style.cursor = 'pointer'; } catch (e) {}
|
||||
|
||||
let active = false;
|
||||
const onMove = (ev) => {
|
||||
try {
|
||||
const r = ap.getBoundingClientRect();
|
||||
const x = ev.clientX, y = ev.clientY;
|
||||
const inside = x >= r.left && x <= r.right && y >= r.top && y <= r.bottom;
|
||||
if (inside && !active) {
|
||||
document.body.style.cursor = 'pointer';
|
||||
active = true;
|
||||
} else if (!inside && active) {
|
||||
document.body.style.cursor = '';
|
||||
active = false;
|
||||
}
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
window.addEventListener('mousemove', onMove);
|
||||
// remove on unload
|
||||
window.addEventListener('beforeunload', () => {
|
||||
try { window.removeEventListener('mousemove', onMove); } catch (e) {}
|
||||
});
|
||||
} catch (e) {
|
||||
console.debug('ensureArtworkPointerFallback failed', e);
|
||||
}
|
||||
}
|
||||
|
||||
function loadStation(index) {
|
||||
if (index < 0 || index >= stations.length) return;
|
||||
const station = stations[index];
|
||||
@@ -444,17 +680,18 @@ function loadStation(index) {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Fallback to single-letter/logo text
|
||||
// Fallback: show the full station name when no logo is provided
|
||||
logoImgEl.src = '';
|
||||
logoImgEl.classList.add('hidden');
|
||||
const numberMatch = station.name.match(/\d+/);
|
||||
if (numberMatch) {
|
||||
logoTextEl.textContent = numberMatch[0];
|
||||
} else {
|
||||
logoTextEl.textContent = station.name.charAt(0).toUpperCase();
|
||||
try {
|
||||
logoTextEl.textContent = (station.name || '').trim();
|
||||
} catch (e) {
|
||||
logoTextEl.textContent = '';
|
||||
}
|
||||
logoTextEl.classList.remove('hidden');
|
||||
}
|
||||
// When loading a station, ensure only this station's poller runs
|
||||
try { startCurrentSongPollers(); } catch (e) { console.debug('startCurrentSongPollers failed in loadStation', e); }
|
||||
}
|
||||
|
||||
// Check if an image URL is reachable and valid
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
"poster": "",
|
||||
"lastSongs": "http://data.radio.si/api/lastsongsxml/radio1/json",
|
||||
"epg": "http://spored.radio.si/api/now/radio1",
|
||||
"currentSong": "https://radio1.si/?handler=CurrentSong",
|
||||
"defaultText": "www.radio1.si",
|
||||
"www": "https://www.radio1.si",
|
||||
"mountPoints": [
|
||||
@@ -201,7 +200,6 @@
|
||||
"liveAudio": "http://live.radio.si/Radio80",
|
||||
"liveVideo": null,
|
||||
"poster": null,
|
||||
"currentSong": "https://radio80.si/?handler=CurrentSong",
|
||||
"lastSongs": "http://data.radio.si/api/lastsongsxml/radio80/json",
|
||||
"epg": "http://spored.radio.si/api/now/radio80",
|
||||
"defaultText": "www.radio80.si",
|
||||
@@ -1340,29 +1338,5 @@
|
||||
"dabPass": null,
|
||||
"dabDefaultImg": "http://media.radio.si/logo/dns/hit/320x240.png",
|
||||
"small": false
|
||||
},
|
||||
{
|
||||
"id": "RadioCity",
|
||||
"title": "Radio City",
|
||||
"slogan": "",
|
||||
"logo": "https://radiocity.si/data/uploads/2020/06/2025.02.10-City-Logo-5-small.png",
|
||||
"liveAudio": "https://stream1.radiocity.si/CityMp3128.mp3",
|
||||
"liveVideo": null,
|
||||
"poster": "",
|
||||
"lastSongs": "",
|
||||
"epg": "",
|
||||
"defaultText": "",
|
||||
"www": "https://radiocity.si/",
|
||||
"mountPoints": [
|
||||
"CityMp3128"
|
||||
],
|
||||
"social": [],
|
||||
"enabled": true,
|
||||
"radioApiIO": "",
|
||||
"rpUid": "",
|
||||
"dabUser": null,
|
||||
"dabPass": null,
|
||||
"dabDefaultImg": null,
|
||||
"small": false
|
||||
}
|
||||
]
|
||||
@@ -394,6 +394,22 @@ body {
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* Make artwork/logo clickable: show pointer cursor */
|
||||
.artwork-placeholder,
|
||||
.artwork-placeholder:hover,
|
||||
.station-logo-img,
|
||||
.station-logo-text {
|
||||
cursor: pointer !important;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* Subtle hover affordance to make clickability clearer */
|
||||
.artwork-placeholder:hover .station-logo-img,
|
||||
.artwork-placeholder:hover .station-logo-text {
|
||||
transform: scale(1.03);
|
||||
transition: transform 160ms ease;
|
||||
}
|
||||
|
||||
/* Track Info */
|
||||
.track-info {
|
||||
text-align: center;
|
||||
|
||||
11
tools/check_counts.js
Normal file
11
tools/check_counts.js
Normal file
@@ -0,0 +1,11 @@
|
||||
const fs = require('fs');
|
||||
const path = 'd:/Sites/Work/RadioCast/src/main.js';
|
||||
const s = fs.readFileSync(path,'utf8');
|
||||
const counts = {'(':0,')':0,'{':0,'}','[':0,']':0,'`':0,'"':0,"'":0};
|
||||
for (const ch of s) { if (counts.hasOwnProperty(ch)) counts[ch]++; }
|
||||
console.log('counts:', counts);
|
||||
// Also print last 50 characters and line count
|
||||
console.log('length:', s.length);
|
||||
const lines = s.split(/\r?\n/);
|
||||
console.log('lines:', lines.length);
|
||||
for (let i = Math.max(0, lines.length-20); i < lines.length; i++) console.log((i+1)+': '+lines[i]);
|
||||
24
tools/find_unclosed.cjs
Normal file
24
tools/find_unclosed.cjs
Normal file
@@ -0,0 +1,24 @@
|
||||
const fs = require('fs');
|
||||
const path = 'd:/Sites/Work/RadioCast/src/main.js';
|
||||
const s = fs.readFileSync(path,'utf8');
|
||||
const lines = s.split(/\r?\n/);
|
||||
let balance = 0;
|
||||
let maxBalance = 0;
|
||||
let maxLine = -1;
|
||||
for (let i=0;i<lines.length;i++){
|
||||
const line = lines[i];
|
||||
for (const ch of line){
|
||||
if (ch==='{' ) balance++;
|
||||
else if (ch==='}') balance--;
|
||||
}
|
||||
if (balance>maxBalance){ maxBalance = balance; maxLine = i+1; }
|
||||
}
|
||||
console.log('final balance:', balance, 'maxBalance:', maxBalance, 'maxLine:', maxLine);
|
||||
console.log('last 40 lines:');
|
||||
for (let i=Math.max(0, lines.length-40); i<lines.length;i++) console.log((i+1)+': '+lines[i]);
|
||||
|
||||
// Print context around maxLine
|
||||
if (maxLine>0){
|
||||
console.log('\nContext around max imbalance at line', maxLine);
|
||||
for (let i=Math.max(1, maxLine-5); i<=Math.min(lines.length, maxLine+5); i++) console.log(i+': '+lines[i-1]);
|
||||
}
|
||||
24
tools/find_unclosed.js
Normal file
24
tools/find_unclosed.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const fs = require('fs');
|
||||
const path = 'd:/Sites/Work/RadioCast/src/main.js';
|
||||
const s = fs.readFileSync(path,'utf8');
|
||||
const lines = s.split(/\r?\n/);
|
||||
let balance = 0;
|
||||
let maxBalance = 0;
|
||||
let maxLine = -1;
|
||||
for (let i=0;i<lines.length;i++){
|
||||
const line = lines[i];
|
||||
for (const ch of line){
|
||||
if (ch==='{' ) balance++;
|
||||
else if (ch==='}') balance--;
|
||||
}
|
||||
if (balance>maxBalance){ maxBalance = balance; maxLine = i+1; }
|
||||
}
|
||||
console.log('final balance:', balance, 'maxBalance:', maxBalance, 'maxLine:', maxLine);
|
||||
console.log('last 40 lines:');
|
||||
for (let i=Math.max(0, lines.length-40); i<lines.length;i++) console.log((i+1)+': '+lines[i]);
|
||||
|
||||
// Print context around maxLine
|
||||
if (maxLine>0){
|
||||
console.log('\nContext around max imbalance at line', maxLine);
|
||||
for (let i=Math.max(1, maxLine-5); i<=Math.min(lines.length, maxLine+5); i++) console.log(i+': '+lines[i-1]);
|
||||
}
|
||||
24
tools/find_unmatched.cjs
Normal file
24
tools/find_unmatched.cjs
Normal file
@@ -0,0 +1,24 @@
|
||||
const fs=require('fs');
|
||||
const path='d:/Sites/Work/RadioCast/src/main.js';
|
||||
const s=fs.readFileSync(path,'utf8');
|
||||
const lines=s.split(/\r?\n/);
|
||||
const stack=[];
|
||||
for(let i=0;i<lines.length;i++){
|
||||
const line=lines[i];
|
||||
for(let j=0;j<line.length;j++){
|
||||
const ch=line[j];
|
||||
if(ch==='{' ) stack.push({line:i+1,col:j+1});
|
||||
else if(ch==='}'){
|
||||
if(stack.length>0) stack.pop(); else console.log('Unmatched closing } at',i+1,j+1);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('Unmatched openings left:', stack.length);
|
||||
if(stack.length>0) console.log('First unmatched opening at', stack[0]);
|
||||
if(stack.length>0) console.log('Last unmatched opening at', stack[stack.length-1]);
|
||||
if(stack.length>0){
|
||||
const sidx=Math.max(1, stack[stack.length-1].line-5);
|
||||
const eidx=Math.min(lines.length, stack[stack.length-1].line+5);
|
||||
console.log('\nContext:');
|
||||
for(let i=sidx;i<=eidx;i++) console.log(i+': '+lines[i-1]);
|
||||
}
|
||||
Reference in New Issue
Block a user