From f61d09c3223d6f260668cea241bd2d625def1db9 Mon Sep 17 00:00:00 2001 From: KrX3D Date: Sat, 29 Aug 2026 19:58:48 +0200 Subject: [PATCH 1/2] =?UTF-8?q?revert:=20drop=20helper=20row=20collapsing?= =?UTF-8?q?=20=E2=80=94=20ineffective=20and=20unsafe=20on=20recycled=20row?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Confirmed on-device that the previous PR does not work: tile_scan reported rowsCollapsed:1 and rowsCollapsed:2 on real runs, yet the blank slots stayed exactly where they were, the single visible helper still sat in 4th position, and navigating down still had to step through all four. yt-virtual-list positions rows by transform from its own layout math over its data model, so a slot stays reserved whether or not the row element is displayed. Collapsing the row was never going to reclaim the space. It is also unsafe, which I should have caught before shipping it — this repo already removed the same pattern. 6a340fc dropped opacity:0 on helper rows because the virtual list RECYCLES .TXB27d nodes (repointing an existing row at new content instead of creating one), so inline styles ride along to whatever video reuses that node — producing black gaps and navigation hangs. An inline display:none does the same thing, only worse: a real video silently missing instead of a visible gap. Reverted to plain tile removal and left the reasoning in place so it isn't attempted a third time, including why the data model — the only layer that would actually work — is out of reach: 654577e removed clearStaleHelpersFromVListData after confirming every Polymer API (set/splice/notifyPath/render) is undefined on Tizen 5.0. --- mods/features/adblock.js | 51 ++++++++++++++++------------------------ 1 file changed, 20 insertions(+), 31 deletions(-) diff --git a/mods/features/adblock.js b/mods/features/adblock.js index b72667b9..1968772b 100644 --- a/mods/features/adblock.js +++ b/mods/features/adblock.js @@ -268,33 +268,25 @@ function dumpHelperTileStructure(tile, id) { } } -// Collapse a playlist row that no longer holds any tile. +// NOTE: do NOT try to hide or collapse .TXB27d rows here. // -// Removing a helper TILE was never the missing piece — on-device logs show it -// already works (tile_scan reports removed:1/2 via the outerHTML substring -// match). What stays behind is the row element: the virtual list lays out one -// .TXB27d row per data-model entry, and that row keeps its height whether or -// not a tile is inside it. Those empty rows are the blank slots that push real -// content down, and scrolling back over one makes the list re-render its tile -// into it — which is exactly the reported "they appeared again". +// This was attempted (collapse the row left behind after removing a helper +// tile) and confirmed on-device not to work: rowsCollapsed reported 1 and 2 on +// real runs, yet the blank slots stayed exactly where they were and the one +// visible helper still sat in 4th position. yt-virtual-list positions rows by +// transform from its own layout math over its data model, so the slot a row +// occupies is reserved whether or not the row element is displayed. // -// Only ever called on a row we've just emptied, so there is nothing left in it -// to hide by mistake. Inline style + class are lost if the list rebuilds the -// row from its data model; the MutationObserver re-runs the scan on that -// mutation and re-applies. The class is the one isHelperLikePlaylistNode -// already tested for but nothing ever set. -function collapseEmptyHelperRow(row) { - try { - if (!row) return false; - if (row.querySelectorAll('ytlr-tile-renderer, ytlr-grid-tile, ytlr-rich-item-renderer').length > 0) return false; - if (row.classList?.contains('tt-helper-soft-hidden')) return false; - row.classList?.add('tt-helper-soft-hidden'); - row.style?.setProperty('display', 'none', 'important'); - return true; - } catch (_) { return false; } -} - - +// It is also actively unsafe. The virtual list RECYCLES .TXB27d nodes — it +// repoints an existing row at different content rather than creating a new one +// — so any inline style we leave behind rides along to whatever video reuses +// that node. That exact pattern (opacity:0 on helper rows) was removed in +// 6a340fc after it produced black gaps and navigation hangs while scrolling. +// display:none would do the same, only worse: a real video silently missing. +// +// The data model is the only layer that would actually work, and it is out of +// reach: 654577e removed clearStaleHelpersFromVListData after confirming every +// Polymer API (set/splice/notifyPath/render) is undefined on Tizen 5.0. function removeRetiredHelpersFromTiles(reason = 'playlist.helper.tile_scan') { if (window.__ttRemovingHelperTiles) return { scannedTiles: 0, removed: 0, matchedIds: [] }; @@ -303,7 +295,7 @@ function removeRetiredHelpersFromTiles(reason = 'playlist.helper.tile_scan') { const tiles = getPlaylistTileNodes(); if (!tiles.length) return { scannedTiles: 0, removed: 0, matchedIds: [] }; window.__ttRemovingHelperTiles = true; - let matchedTiles = 0, removed = 0, focusRedirected = 0, deferredNoContent = 0, rowsCollapsed = 0; + let matchedTiles = 0, removed = 0, focusRedirected = 0, deferredNoContent = 0; const removedTiles = new Set(), matchedIds = new Set(); try { for (const tile of tiles) { @@ -343,9 +335,6 @@ function removeRetiredHelpersFromTiles(reason = 'playlist.helper.tile_scan') { removedTiles.add(tile); try { tile.remove(); } catch (_) {} removed++; - // The row survives the tile and keeps its height — that leftover - // row is the blank slot. Collapse it now that it's empty. - if (collapseEmptyHelperRow(rowNode)) rowsCollapsed++; } } catch (_) { } break; @@ -357,8 +346,8 @@ function removeRetiredHelpersFromTiles(reason = 'playlist.helper.tile_scan') { } finally { window.__ttRemovingHelperTiles = false; } - appendFileOnlyLog('playlist.helper.tile_scan', { reason, retiredCount: retiredIds.length, scannedTiles: tiles.length, removed, matchedTiles, focusRedirected, deferredNoContent, rowsCollapsed, matchedIds: Array.from(matchedIds) }); - return { scannedTiles: tiles.length, removed, matchedIds: Array.from(matchedIds), matchedTiles, rowsCollapsed }; + appendFileOnlyLog('playlist.helper.tile_scan', { reason, retiredCount: retiredIds.length, scannedTiles: tiles.length, removed, matchedTiles, focusRedirected, deferredNoContent, matchedIds: Array.from(matchedIds) }); + return { scannedTiles: tiles.length, removed, matchedIds: Array.from(matchedIds), matchedTiles }; } function ensurePlaylistHelperObserver() { From 1029077e7c24b769d74410ea0fb241f6027c7194 Mon Sep 17 00:00:00 2001 From: KrX3D Date: Sat, 29 Aug 2026 20:22:11 +0200 Subject: [PATCH 2/2] fix: stand down the keep-one auto-load cascade while batch collect is running MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two mechanisms load the same playlist at once and they are badly out of step. On a 68-video all-watched playlist: 17:53:01.780 keep-one.register BO7rZs_CYQs (initial response) 17:53:01.908 auto_load.trigger attempt 0 17:53:02.058 continuation 15 items 17:53:02.060 keep-one.register yiCntUc1KKk 17:53:02.262 continuation 15 items 17:53:02.265 keep-one.register WKj_hyauRZ8 17:53:03.108 60 items present, all from YouTube's own responses 17:53:04.4 .. 17:53:12.3 background collector, batches 2-5 17:53:12.375 prefetch_ready 53 items, hasMore:false YouTube delivered four batches in ~1.3s, one helper each, while the background collector — paced at 2.5s per fetch to avoid the server rejections fixed earlier — was still on its second and only finished ten seconds later, by which point its 53 merged items had nowhere to go. That is the out-of-sync behaviour reported alongside the helper complaint. IMPORTANT — what is NOT established: whether our schedulePlaylistAutoLoad cascade actually causes those rapid refetches. The competing explanation is simply YouTube refilling an underfull viewport on its own: each batch filters down to a single helper while the screen fits four or five rows, so the list is starved and fetches again regardless of what we do. Our triggers and YouTube's fetches interleave closely enough in this log that neither ordering settles it, and auto_load.verify cannot arbitrate — its counts come from __ttCurrentPlaylistItems, which storePlItems dedupes and which the background collector also feeds. This change is therefore also the experiment that separates the two. When batch collect is active the cascade stands down, so the prefetched batches can land as one merged continuation carrying no further token — meaning hasContinuation is false, the keep-one branch never runs for it, and the single initial helper is retired and removed. If helper count drops to one, our cascade was driving the refetches. If it stays at four, YouTube's viewport refill is, and no DOM-side change will help because each stranded helper lives in the virtual list's data model, unreachable on Tizen 5.0 (654577e). Either way this removes a redundant second loader that finishes ten seconds late. Both cascade call sites log when they stand down instead of doing it silently. Only affects users with enablePlaylistBatchCollect on. --- mods/features/adblock.js | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/mods/features/adblock.js b/mods/features/adblock.js index 1968772b..2c2c3a8d 100644 --- a/mods/features/adblock.js +++ b/mods/features/adblock.js @@ -486,6 +486,31 @@ function clearKeepOneMarkers(items, label = 'continuation') { return cleared; } +// True while playlistBatchCollect.js is fetching (or has already fetched) the +// rest of this playlist on its own. +// +// When it is, the keep-one auto-load cascade below must stay out of the way. +// Timings from a 68-video all-watched playlist show why: our cascade drove +// YouTube through its own continuations at 17:53:01.780, .02.060 and .02.265 — +// three helpers inside half a second, one per response — while the background +// collector was still working and only reached prefetch_ready at 17:53:12.375, +// ten seconds later, with all 53 items nobody needed by then. Every one of +// those helpers is permanently stuck in the virtual list's data model, which is +// unreachable on Tizen 5.0 (654577e), so each is a blank slot for the rest of +// the visit. +// +// Standing down lets the prefetched batches arrive as ONE merged continuation. +// That response carries no further continuation token, so hasContinuation is +// false and the keep-one branch never runs for it — leaving exactly one helper +// (from the initial page load) instead of four, and that one gets retired and +// removed when the merged batch lands. +function isBatchCollectHandlingPlaylist() { + try { + if (!configRead('enablePlaylistBatchCollect')) return false; + return !!(window.__ttPrefetchStarted || window.__ttPrefetchedBatch); + } catch (_) { return false; } +} + function filterContinuationItems(items, pageName, hasContinuation = false, label = 'continuation') { if (pageName === 'playlist' && !hasContinuation) { clearPlaylistHelperVideoIdSet(label); @@ -518,13 +543,18 @@ function filterContinuationItems(items, pageName, hasContinuation = false, label // All items in this batch are watched — auto-fetch the next batch without waiting // for the user to scroll. The continuation token is already stored at this point. // This cascades through all-watched batches automatically until unwatched content appears. - schedulePlaylistAutoLoad(`${label}.keep-one`); + if (isBatchCollectHandlingPlaylist()) { + appendFileOnlyLog(`${label}.keep-one.autoload_skipped`, { reason: 'batch_collect_active' }); + } else { + schedulePlaylistAutoLoad(`${label}.keep-one`); + } return [fallbackItem]; } if (pageName === 'playlist' && !hasContinuation && filteredItems.length === 0) showPlaylistAllHiddenNotice(`${label}.no_continuation_all_hidden`); if (pageName === 'playlist' && hasContinuation && filteredItems.length === 0) { - appendFileOnlyLog(`${label}.empty_batch.autoload`, { pageName, originalCount: Array.isArray(items) ? items.length : 0 }); - schedulePlaylistAutoLoad(`${label}.empty_batch`); + const collecting = isBatchCollectHandlingPlaylist(); + appendFileOnlyLog(`${label}.empty_batch.autoload`, { pageName, originalCount: Array.isArray(items) ? items.length : 0, skipped: collecting }); + if (!collecting) schedulePlaylistAutoLoad(`${label}.empty_batch`); } return filteredItems; }