Resolve the full review backlog (issues #1-#13)
Some checks failed
Build TV app / build (push) Failing after 2s
Some checks failed
Build TV app / build (push) Failing after 2s
Control server security and robustness: - Socket read timeout, handshake deadline and a client cap so an idle or hostile connection can no longer pin threads forever (#1) - Per-address rate limiting that counts every failed hello, Origin checking on the upgrade, and a separate revocable session token for code-authenticated clients so the guessable path no longer yields the QR credential (#2) - Credentials excluded from cloud backup and device transfer, constant time comparisons, and a pairing reset in the settings (#3) - Playlist fetches restricted to http(s), capped at 24 MB and bounded by an overall transfer deadline (#4) - Port conflicts and MediaSession id collisions no longer crash the app; the remote degrades to unavailable with a plain-language note (#11) Player: - Seeking no longer collapses to position 0 when the duration is unknown (#5) - Pause acts on playWhenReady, so pausing during a stall works and playback cannot resume in the background after leaving the app (#6) - Playback failures stay on screen with a retry action instead of silently dropping back to the list (#7) - Reconnects are spaced 1s/3s/8s and re-entering the channel just closed waits out a short grace period, which is what the provider needs to release the previous session (#12) Channel list and remote: - Leaving playback returns to the channel the viewer came from (#13) - The remote only rebuilds its list when the data changed, never overwrites a focused input and carries indices instead of scanning (#8) - Pairing retry reconnects properly, resets its backoff and validates the code before spending an attempt (#9) - M3U parsing keeps commas in names, strips a BOM and rejects payloads that are not playlists, covered by unit tests under tests/ (#10) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -320,6 +320,7 @@
|
||||
ws.onclose = () => {
|
||||
state.connected = false; state.authorized = false;
|
||||
renderConnection();
|
||||
state.ws = null;
|
||||
setTimeout(connect, state.retryDelay);
|
||||
state.retryDelay = Math.min(state.retryDelay * 1.6, 10000);
|
||||
};
|
||||
@@ -340,7 +341,7 @@
|
||||
if (msg.token) localStorage.setItem('castarr_token', msg.token);
|
||||
$('tv-name').textContent = msg.device || 'TV';
|
||||
if (msg.status) { state.status = msg.status; }
|
||||
if (msg.channels) { state.channels = msg.channels; }
|
||||
if (msg.channels) { state.channels = msg.channels.map((c, i) => { c._idx = i; return c; }); }
|
||||
if (msg.extras) { state.extras = msg.extras; }
|
||||
state.playlistUrl = msg.playlistUrl || '';
|
||||
$('pair-error').textContent = '';
|
||||
@@ -358,10 +359,10 @@
|
||||
case 'status':
|
||||
state.status = msg;
|
||||
renderStatus();
|
||||
renderChannels();
|
||||
renderChannelsIfChanged();
|
||||
break;
|
||||
case 'channels':
|
||||
state.channels = msg.channels || [];
|
||||
state.channels = (msg.channels || []).map((c, i) => { c._idx = i; return c; });
|
||||
state.playlistUrl = msg.playlistUrl || '';
|
||||
if (msg.extras) { state.extras = msg.extras; }
|
||||
renderChannels();
|
||||
@@ -446,6 +447,7 @@
|
||||
$('setup').classList.add('open');
|
||||
return;
|
||||
}
|
||||
list.innerHTML = '';
|
||||
const favSet = new Set(state.extras.favorites || []);
|
||||
const favOn = state.extras.favoritesSupported && state.favOnly;
|
||||
const shown = favOn ? filtered.filter((c) => favSet.has(c.backendId)) : filtered;
|
||||
@@ -462,9 +464,18 @@
|
||||
});
|
||||
list.appendChild(bar);
|
||||
}
|
||||
if (!shown.length) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'empty';
|
||||
empty.textContent = favOn
|
||||
? 'Keine Favoriten — Stern auf einem Sender antippen.'
|
||||
: 'Keine Sender gefunden.';
|
||||
list.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
const frag = document.createDocumentFragment();
|
||||
shown.slice(0, 500).forEach((c) => {
|
||||
const idx = state.channels.indexOf(c);
|
||||
const idx = c._idx;
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'chan' + (c.url === state.status.url || c.name === state.status.channel ? ' current' : '');
|
||||
const num = document.createElement('span');
|
||||
@@ -517,12 +528,27 @@
|
||||
btn.addEventListener('click', () => playChannel(c));
|
||||
frag.appendChild(btn);
|
||||
});
|
||||
list.innerHTML = '';
|
||||
list.appendChild(frag);
|
||||
if (state.playlistUrl) $('playlist-input').value = state.playlistUrl;
|
||||
// Never overwrite an input the user is typing in.
|
||||
const pl = $('playlist-input');
|
||||
if (state.playlistUrl && document.activeElement !== pl) pl.value = state.playlistUrl;
|
||||
}
|
||||
|
||||
function renderAll() { renderStatus(); renderChannels(); }
|
||||
// Rebuilding the list on every 2s status push swallowed taps that landed
|
||||
// between touchstart and touchend. Only rebuild when something changed.
|
||||
let listSignature = '';
|
||||
function renderChannelsIfChanged() {
|
||||
const sig = [
|
||||
state.channels.length, state.searchTerm, state.favOnly,
|
||||
(state.extras.favorites || []).join(','),
|
||||
state.status.channel || '',
|
||||
].join('|');
|
||||
if (sig === listSignature) return;
|
||||
listSignature = sig;
|
||||
renderChannels();
|
||||
}
|
||||
|
||||
function renderAll() { renderStatus(); listSignature = ''; renderChannels(); }
|
||||
|
||||
// --- actions ---
|
||||
function playChannel(c) {
|
||||
@@ -583,10 +609,17 @@
|
||||
|
||||
$('pair-btn').addEventListener('click', () => {
|
||||
const code = $('code-input').value.trim();
|
||||
if (code.length !== 4) { $('pair-error').textContent = 'Bitte 4 Ziffern eingeben.'; return; }
|
||||
if (!/^[0-9]{4}$/.test(code)) {
|
||||
$('pair-error').textContent = 'Bitte 4 Ziffern eingeben.';
|
||||
return;
|
||||
}
|
||||
// A fresh attempt must not inherit the backoff from earlier failures.
|
||||
state.retryDelay = 1000;
|
||||
$('pair-error').textContent = 'Verbinde…';
|
||||
manualCode = code;
|
||||
$('pair-error').textContent = '';
|
||||
if (state.ws) state.ws.close(); else connect();
|
||||
// A closed socket ignores close(), so the retry has to reconnect.
|
||||
if (state.ws && state.ws.readyState <= 1) state.ws.close(); else connect();
|
||||
});
|
||||
|
||||
let toastTimer = null;
|
||||
|
||||
Reference in New Issue
Block a user