From ad30f75d5dd6ce8faab8a2f1ccb039e3093dcc65 Mon Sep 17 00:00:00 2001 From: KrX3D Date: Sat, 29 Aug 2026 21:14:41 +0200 Subject: [PATCH] =?UTF-8?q?revert:=20restore=20the=20per-batch=20playlist?= =?UTF-8?q?=20helper=20=E2=80=94=20reusing=20one=20stalls=20loading?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit My last change was wrong and made things worse. Capping the playlist at a single helper and returning [] for later all-watched batches stops the playlist loading entirely: keep-one.register BO7rZs_CYQs keep-one.reused active:["BO7rZs_CYQs"] continuation.detected x1 and nothing further, ever A 68-video playlist got two batches in and stalled showing only the helper. Confirmed deliberately: video 68 was set to 20% watched against an 80% hide threshold so it had to render, and it never appeared, because batch 5 was never fetched. The reasoning that led there was sound as far as it went — YouTube does refill an underfull viewport on its own, which is why disabling our schedulePlaylistAutoLoad cascade changed nothing. What I missed is that its refill loop also needs each continuation response to append at least one item. Hand it an empty batch and it treats the fetch as satisfied and stops asking, so the chain dies at the last non-empty response. The per-batch helper is not redundant: it is the single item that keeps the list starved enough to request more while still receiving something each time. Reverted to one helper per batch, with that failure mode recorded at the call site so it isn't attempted again. The blank slots stay, and remain unfixable from the DOM side while the virtual list's data model is unreachable on Tizen 5.0 (654577e). Keeps the cascade guard from #677, which a prior run verified is safe on its own: with autoload_skipped x3 YouTube still pulled the whole playlist unaided in 780ms. --- mods/features/adblock.js | 68 ++++++++++++---------------------------- 1 file changed, 20 insertions(+), 48 deletions(-) diff --git a/mods/features/adblock.js b/mods/features/adblock.js index f0df86bc..4d3ec378 100644 --- a/mods/features/adblock.js +++ b/mods/features/adblock.js @@ -529,48 +529,35 @@ function filterContinuationItems(items, pageName, hasContinuation = false, label clearKeepOneMarkers(items, label); let filteredItems = hideVideo(items, pageName); filteredItems = filterShortsFromItems(filteredItems, pageName); + // Every all-watched batch keeps its OWN helper. Do not try to reuse a single + // helper across batches and return [] for the rest — that was tried and it + // stops the playlist loading dead: + // + // keep-one.register BO7rZs_CYQs + // keep-one.reused active:["BO7rZs_CYQs"] + // continuation.detected x1 <- and nothing further, ever + // + // A 68-video playlist got two batches in and stalled showing only the helper. + // Confirmed deliberately: video 68 was set to 20% watched against an 80% hide + // threshold so it had to render, and it never appeared, because batch 5 was + // never fetched. + // + // YouTube's refill loop needs each continuation response to append at least + // one item. Hand it an empty batch and it treats the fetch as satisfied and + // stops asking, so the chain dies at the last non-empty response. The + // per-batch helper is what keeps that loop alive — the list stays starved + // enough to request more while still receiving something each time. The + // stranded blank slots are the price, and the data model that would let us + // clean them up is unreachable on Tizen 5.0 (654577e). if (pageName === 'playlist' && hasContinuation && filteredItems.length === 0 && Array.isArray(items) && items.length > 0) { const reverseItems = [...items].reverse(); const fallbackItem = reverseItems.find(item => item?.tileRenderer?.header?.tileHeaderRenderer?.thumbnail?.thumbnails?.length) || reverseItems.find(item => item?.tileRenderer) || items[items.length - 1]; - // Cap the playlist at ONE helper for the whole visit. - // - // A helper exists so the list is never left empty, because an empty list - // is what makes YouTube TV reload the page. It does NOT need to be a fresh - // one per batch: an all-watched 68-video playlist was measured keeping one - // helper per response — four blank slots, each permanently stranded in the - // virtual list's data model, which is unreachable on Tizen 5.0 (654577e). - // - // Keeping the existing helper instead is safe because the list stays - // non-empty either way, and it is sufficient because YouTube drives the - // remaining batches itself. That was confirmed by disabling our own - // schedulePlaylistAutoLoad cascade entirely (autoload_skipped x3) and - // watching YouTube still pull the whole playlist in 780ms unaided - // — 18:44:01.022, .239, .455 — purely because filtering leaves the - // viewport starved. So dropping the extra helpers costs no loading. - // Scoped to this playlist. The helper sets live on window and are only - // wiped when a !hasContinuation response arrives, so navigating away - // mid-load leaves them populated — reusing one of those on the NEXT - // playlist would mean never keeping a helper there, leaving its list - // empty, which is the exact condition that makes YouTube TV reload the - // page. Tie the reuse to the hash the helper was created under. - const playlistKey = String(window.location?.hash || ''); - const helperKeyMatches = window.__ttHelperPlaylistKey === playlistKey; - const existingHelper = helperKeyMatches && - (getPlaylistHelperVideoIdSet().size > 0 || getRetiredPlaylistHelperVideoIdSet().size > 0); - if (existingHelper) { - appendFileOnlyLog(`${label}.keep-one.reused`, { - active: Array.from(getPlaylistHelperVideoIdSet()), - retired: getRetiredPlaylistHelperVideoIdSet().size, - }); - return []; - } if (fallbackItem && typeof fallbackItem === 'object') { fallbackItem.__ttKeepOneForContinuation = true; fallbackItem.__ttKeepOneForContinuationLabel = `${label}.visible`; fallbackItem.__ttKeepOneForContinuationParseSeq = Number(window.__ttParseSeq || 0); - window.__ttHelperPlaylistKey = playlistKey; registerPlaylistHelperVideoId(getItemVideoId(fallbackItem), `${label}.keep-one`); } // All items in this batch are watched — auto-fetch the next batch without waiting @@ -594,21 +581,6 @@ function filterContinuationItems(items, pageName, hasContinuation = false, label function filterPlaylistRendererContents(playlistRenderer, pageName, label = 'playlist.renderer') { if (!playlistRenderer || !Array.isArray(playlistRenderer.contents)) return; - if (pageName === 'playlist') { - // A page-level playlist response means a freshly rendered list, so no - // helper from an earlier visit can still be on screen. Reset the one-per- - // playlist cap here: the sets live on window and are otherwise only wiped - // by a !hasContinuation response, so leaving a playlist mid-load would - // strand them — and a stranded set would make the cap skip creating this - // playlist's own helper, leaving the list empty, which is what makes - // YouTube TV reload the page. Also covers the page "restart" 654577e - // documented, where stale retired ids caused the observer to strip - // freshly rendered tiles. - getPlaylistHelperVideoIdSet().clear(); - getRetiredPlaylistHelperVideoIdSet().clear(); - window.__ttHelperPlaylistKey = null; - updateAllHelperHideStyles(); - } const hasContinuation = !!playlistRenderer?.continuations; const before = playlistRenderer.contents.length; playlistRenderer.contents = filterContinuationItems(playlistRenderer.contents, pageName, hasContinuation, label);