Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions mods/features/adblock.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { timelyAction, longPressData, MenuServiceItemRenderer, ShelfRenderer, Ti
import { PatchSettings } from '../ui/customYTSettings.js';
import { t } from 'i18next';
import './logServer.js';
import { autoStartCollect, getCachedFullPlaylist, noteInitialPlaylistContents, noteContinuationBatch } from './playlistBatchCollect.js';
import { scheduleCollectAfterNativeSettles, getCachedFullPlaylist, noteInitialPlaylistContents, noteContinuationBatch } from './playlistBatchCollect.js';
import {
appendFileOnlyLog,
detectAndStorePage,
Expand Down Expand Up @@ -680,7 +680,7 @@ function processResponsePayload(payload, detectedPage) {
// never fires, so the slow re-download stays on the critical path.
// Must run BEFORE filterContinuationItems, which reduces an all-watched
// batch to the single kept helper.
noteContinuationBatch(String(window.location?.hash || ''), plc.contents, !!plc?.continuations);
noteContinuationBatch(String(window.location?.hash || ''), plc.contents, plc.continuations);
plc.contents = filterContinuationItems(plc.contents, detectedPage, !!plc?.continuations, 'arrayPayload.playlist.continuation');
}
const arrayTopPlaylistRenderer = payload?.contents?.tvBrowseRenderer?.content?.tvSurfaceContentRenderer?.content?.twoColumnRenderer?.rightColumn?.playlistVideoListRenderer;
Expand Down Expand Up @@ -951,12 +951,13 @@ JSON.parse = function () {
noteInitialPlaylistContents(playlistKey, topPlaylistRenderer.contents);
}
storePlaylistContinuationToken(topPlaylistRenderer.continuations, 'topPlaylist');
// Start collecting the rest of the playlist in the background right
// now, instead of waiting for the user to scroll down once to trigger
// it — see playlistBatchCollect.js's autoStartCollect() doc comment.
// Queue background collection of whatever YouTube does not fetch
// itself — deferred until its own loading goes quiet, so the collector
// resumes from there instead of refetching. See
// scheduleCollectAfterNativeSettles() in playlistBatchCollect.js.
// No-ops when serving from cache: continuations is null, so there is no
// token to collect with.
autoStartCollect(topPlaylistRenderer.continuations);
scheduleCollectAfterNativeSettles(topPlaylistRenderer.continuations, 'page_load');
filterPlaylistRendererContents(topPlaylistRenderer, detectedPage, 'playlist.renderer');
}

Expand Down Expand Up @@ -1005,7 +1006,7 @@ JSON.parse = function () {
// so it collects real items rather than the single kept helper. This is
// the same data the background collector would re-download, only it is
// already here and roughly ten seconds sooner.
noteContinuationBatch(String(window.location?.hash || ''), plc.contents, hasContinuation);
noteContinuationBatch(String(window.location?.hash || ''), plc.contents, plc.continuations);
plc.contents = filterContinuationItems(plc.contents, detectedPage, hasContinuation, 'playlist.continuation');
}

Expand Down
66 changes: 58 additions & 8 deletions mods/features/playlistBatchCollect.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
* no 614KB re-parse.
*
* Two trigger paths feed the same background collector:
* - autoStartCollect(): called by adblock.js right after the initial
* playlist page's own continuation token is parsed, so collection starts
* on page load without needing any scroll at all.
* - scheduleCollectAfterNativeSettles(): called by adblock.js on playlist
* page load and again for each continuation YouTube delivers itself. It
* waits for that native loading to go quiet, then hands autoStartCollect()
* the items already accumulated plus the newest token, so collection
* resumes from where YouTube stopped rather than refetching from batch 2.
* - The XHR send() seed path below: a fallback for when the auto-trigger's
* captured context/url isn't available yet (e.g. very first browse of a
* session) — starts from the first scroll-triggered continuation request.
Expand Down Expand Up @@ -138,6 +140,9 @@ function _clearState() {
// collection on every later one.
window.__ttCollectCancel = false;
window.__ttContinuationAcc = null;
clearTimeout(window.__ttNativeSettleTimer);
window.__ttNativeSettleTimer = null;
window.__ttLatestContinuations = null;
}
window.addEventListener('hashchange', _clearState);
window.addEventListener('popstate', _clearState);
Expand Down Expand Up @@ -206,7 +211,11 @@ function _makeAbort() {
// anti-automation throttling on the endpoint — a real scroll physically
// can't happen that fast. So instead of firing every background fetch back
// to back, wait a human-scroll-like interval before each one.
const BATCH_FETCH_DELAY_MS = 2500;
// Lowered from 2500ms. That figure was chosen while timing was wrongly blamed
// for the {"error":...} rejections; the real cause was missing request headers,
// and no fetch has failed since those were added. It also costs real time now
// that seeding means usually a single fetch — 2.5s of it was pure latency.
const BATCH_FETCH_DELAY_MS = 400;
function _delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
Expand Down Expand Up @@ -485,7 +494,7 @@ function maybeReloadForFullPlaylist(key) {
// so it can hand the raw items straight here. When a response arrives with no
// continuation token the playlist is complete, and the reload can happen
// immediately instead of waiting for a redundant re-download.
export function noteContinuationBatch(key, contents, hasMore) {
export function noteContinuationBatch(key, contents, continuations) {
if (!configRead('enablePlaylistBatchCollect')) return;
if (!Array.isArray(contents) || !contents.length) return;
if (getCachedFullPlaylist(key)) return; // already complete; this is the reloaded pass
Expand All @@ -506,7 +515,11 @@ export function noteContinuationBatch(key, contents, hasMore) {
acc.contents.push(item);
}

if (hasMore) return;
// Each native batch pushes the settle timer out and supersedes the token, so
// the collector resumes from the furthest point YouTube reached.
scheduleCollectAfterNativeSettles(continuations, 'native_batch');

if (continuations) return;

// No continuation token: this was the last batch, so initial + everything
// accumulated is the whole playlist.
Expand All @@ -532,7 +545,43 @@ export function noteContinuationBatch(key, contents, hasMore) {
// or not) — the InnerTube context is client/session info, not tied to any
// one request, so reusing it here is the same assumption _collectAll already
// makes when reusing one captured context across every batch it fetches.
export function autoStartCollect(continuations) {
// Wait for YouTube to stop loading on its own, then resume from where it got
// to — rather than re-downloading everything it already delivered.
//
// Measured on a 68-video all-watched playlist: YouTube fetches the initial 15
// plus three continuations of 15 (60 of 68) in about 0.75s and then stops. All
// three carry hasContinuation:true, so there is never a completion signal, and
// the last 8 items are never fetched unless the user scrolls. Starting the
// collector at page load meant it re-fetched batches 2-5 from scratch — 53
// items, 45 of which were already on screen — taking ~10s, with helper tiles
// visible the whole time.
//
// So: hold off until the native burst goes quiet, then hand the collector the
// items already accumulated plus the newest continuation token. It fetches
// only what is genuinely missing (one batch here instead of four).
const NATIVE_SETTLE_MS = 1200;

export function scheduleCollectAfterNativeSettles(continuations, reason) {
if (!configRead('enablePlaylistBatchCollect')) return;
if (window.__ttPrefetchStarted || window.__ttPrefetchedBatch) return;

const key = playlistKeyFromHash();
if (getCachedFullPlaylist(key)) return; // reloaded pass: already complete

// Newest token wins — each native continuation supersedes the last.
if (_getToken(continuations)) window.__ttLatestContinuations = continuations;

clearTimeout(window.__ttNativeSettleTimer);
window.__ttNativeSettleTimer = setTimeout(() => {
if (playlistKeyFromHash() !== key) return;
const acc = window.__ttContinuationAcc;
const seed = (acc && acc.key === key && Array.isArray(acc.contents)) ? acc.contents : [];
_log('playlist.batch_collect.native_settled', { reason, seeded: seed.length });
autoStartCollect(window.__ttLatestContinuations, seed);
}, NATIVE_SETTLE_MS);
}

function autoStartCollect(continuations, seedContents) {
if (!configRead('enablePlaylistBatchCollect')) return;
if (window.__ttPrefetchStarted || window.__ttPrefetchedBatch) return;

Expand All @@ -548,6 +597,7 @@ export function autoStartCollect(continuations) {
window.__ttCollectCancel = false;
_log('playlist.batch_collect.auto_triggered', {
headerKeys: _lastBrowseHeaders ? Object.keys(_lastBrowseHeaders) : null,
seeded: Array.isArray(seedContents) ? seedContents.length : 0,
});

window.__ttPrefetchStarted = true;
Expand All @@ -560,7 +610,7 @@ export function autoStartCollect(continuations) {
;(async () => {
let collected = null;
try {
collected = await _collectAll(url, { contents: [], continuations }, context, headers);
collected = await _collectAll(url, { contents: Array.isArray(seedContents) ? seedContents : [], continuations }, context, headers);
} catch (err) {
_log('playlist.batch_collect.auto_error', { err: String(err?.message || err) });
}
Expand Down