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
9 changes: 5 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
name: Run Tests

on:
push:
branches:
- main
- develop
pull_request:
branches:
- main
- develop
workflow_dispatch:

concurrency:
group: run-tests-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

env:
# CI is a deterministic release gate. Never let an inherited environment
Expand Down
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Repository instructions

1. All tests must be implemented, fixed, and run in both Chrome and Firefox. Apply fixes made for one browser to the other while respecting each browser's behavior.
2. Always use `gh` first for GitHub interaction.
3. Do not require reviewers for new commits.
4. Run Chrome tests with elevated rights: sandboxed Playwright can cause system crashes. Tests the user runs in their console are not affected.
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,44 @@ All notable changes to YT re:Watch will be documented in this file.
- Added a dedicated IndexedDB cache for YouTube disclosure results, with
long-lived positive and unlabeled results plus bounded retry backoff for
unavailable or malformed responses.
- Added the installed extension version beside the YT re:Watch identity in the
popup and full-page feed, using the extension icon consistently in both.

### Changed

- Subscriptions now retains the selected upload-date or detection-date ordering
across **Reload view** and **Show**. Newly discovered records no longer jump
ahead of that selection, while the default newest-first view continues to use
canonical paginated order.
- Refined full-page view headers so the content title appears when the sidebar
is collapsed and is hidden when the expanded sidebar already supplies the
same label. The remaining description is larger and vertically aligned with
the view icon.
- Reduced redundant YouTube-card work by targeting thumbnail refreshes to the
changed video IDs, coalescing overlay startup, and reserving full focus-driven
rescans for actual overlay-presentation changes.
- Routed background, storage, and AI-label informational traces through the
saved debug setting while keeping warnings and errors visible.

### Fixed

- Fixed popup initialization and live updates so reopening it after a long-lived
YouTube tab does not overlap snapshots, rebuild the same content repeatedly,
or pulse while already-loaded data is being rendered.
- Fixed **Show** after a subscription scan so asynchronously loaded cards settle
in the selected order before the view scrolls to the newly available records.
- Treat removed, private, or otherwise incomplete YouTube AI-label responses as
indeterminate instead of caching them as valid unlabeled results.

### Privacy and reliability

- The feature defaults to Off and clearly explains that it makes paced direct
YouTube lookups for previously unchecked cards near the viewport.
- It reflects only YouTube's disclosure, not a general AI detector; an
unmarked video is not confirmed non-AI.
- Live Chrome and Firefox canaries use a small curated disclosure set and a
bounded TimeLapse Lords channel-feed fallback to detect external fixture
removal without scanning an entire channel page.

## [5.1.0] - 2026-08-14

Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ The full feed page contains:
| View | Purpose |
| --- | --- |
| **Home** | A locally regenerated mix of cached videos using freshness, watch activity, and local feedback |
| **Subscriptions** | Cached uploads from locally followed channels in chronological order |
| **Subscriptions** | Cached uploads from locally followed channels, sortable by upload or detection time |
| **Watch Later** | Locally saved videos, newest first |
| **Shorts** | Known short-form videos and watched Shorts |
| **Playlists** | Local playlists you manage, alongside saved YouTube playlist references that open on YouTube |
Expand All @@ -47,7 +47,9 @@ The full feed page contains:
| **Analytics** | Locally calculated watch statistics and insights |
| **Settings** | Appearance, retention, feed timing, imports, backup/restore, and reset controls |

Home and Subscriptions render stable 50-card pages as you scroll. Checking for new videos is separate from reloading the current local view, so a background scan does not unexpectedly replace or reorder visible cards. When uploads are found, re:Watch offers a deliberate **Show** action.
Home and Subscriptions render stable 50-card pages as you scroll. Subscriptions retains its selected upload-date or detection-date ordering through **Reload view** and **Show**. Checking for new videos is separate from reloading the current local view, so a background scan does not unexpectedly replace or reorder visible cards. When uploads are found, re:Watch offers a deliberate **Show** action.

The compact popup and full-page interface show the installed extension version beside the re:Watch icon, making it easier to identify the active build when reporting a problem.

Watch Later is independent of YouTube's account playlist. Right-click a recognized video or Short and choose **Save to Watch Later (local)**, or use a re:Watch video menu; the full-page Watch Later view provides newest-first Open and Remove actions.

Expand Down
Binary file modified docs/assets/guide/feed-analytics.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/assets/guide/feed-channels.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/assets/guide/feed-history.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/assets/guide/feed-home.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/assets/guide/feed-playlists.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/assets/guide/feed-settings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/assets/guide/feed-shorts.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/assets/guide/feed-subscriptions.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/assets/guide/feed-watch-later.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/assets/guide/popup-continue-watching.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 17 additions & 2 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,25 @@

- Added experimental **AI-labeled video handling** with Off, Badge, Dim, and
Hide modes for videos YouTube itself discloses as made with AI.
- Added a dedicated IndexedDB cache, viewport-driven one-at-a-time lookups,
long cache lifetimes, and bounded retry backoff to keep request volume low.
- Added a dedicated IndexedDB cache, paced viewport-driven lookups with at most
two requests in flight, long cache lifetimes, and bounded retry backoff to
keep request volume low.
- The setting explains that it makes direct YouTube requests when enabled and
is not a general or fully reliable AI detector.
- Fixed Subscriptions so the chosen upload-date or detection-date order survives
**Reload view** and **Show**, without pinning newly discovered videos ahead of
the selected order.
- Fixed popup startup and live-update races that could make already-loaded
content pulse or render repeatedly after a YouTube tab had been open longer.
- Added the installed extension version and consistent re:Watch icon branding
to both the popup and full-page feed.
- Refined full-page headers to avoid duplicate nearby titles when the sidebar is
expanded, while keeping the current view title visible in collapsed mode.
- Reduced redundant overlay and thumbnail work by targeting changed video IDs,
coalescing startup scans, and limiting focus rescans to presentation changes.
- Made background, storage, and AI-label informational output respect debug mode.
- Treats removed or incomplete AI-label responses as indeterminate and uses
bounded live Chrome/Firefox canary fallbacks when an external fixture changes.

## v5.1.0 — 14 August 2026

Expand Down
8 changes: 5 additions & 3 deletions docs/detailed_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,17 @@ Home uses stable 50-card pages. Moving between pages does not duplicate cards or

![The re:Watch Subscriptions tab showing recent cached uploads from locally followed channels.](assets/guide/feed-subscriptions.png)

*Subscriptions lists cached uploads in chronological order.*
*Subscriptions lists cached uploads in the selected upload-date or detection-date order.*

Subscriptions is the complete chronological inventory of regular cached uploads. It also uses stable 50-card pages.
Subscriptions is the complete inventory of regular cached uploads. Choose
upload-date or detection-date ordering; the selection remains active through
**Reload** and **Show**. The view also uses stable 50-card pages.

### Check, Reload, and Show

- **Check** scans eligible followed channels and updates cached feed records. The status beside it reports progress, scheduling, and outcomes.
- **Reload** renders current local storage again without starting a feed request.
- **Show** opens the chronological Subscriptions inventory after a scan or subscription-import handoff.
- **Show** reloads the complete Subscriptions inventory after a scan or subscription-import handoff and retains its selected ordering.

The interface renders cached data before initialization work completes, so an existing feed remains usable during a scan.

Expand Down
6 changes: 4 additions & 2 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,15 @@ Yes. Use the global playlist pause setting or the ignore toggle on a saved playl

### Where do Home and Subscriptions videos come from?

From public RSS uploads cached for channels followed in re:Watch. Home arranges that local inventory; Subscriptions shows it chronologically.
From public RSS uploads cached for channels followed in re:Watch. Home arranges
that local inventory; Subscriptions can order it by upload date or by when
re:Watch detected it.

### What is the difference between Check, Reload, and Show?

- **Check** scans eligible channel feeds and updates local records.
- **Reload** redraws from local storage without starting a scan.
- **Show** opens the full chronological Subscriptions inventory.
- **Show** reloads the full Subscriptions inventory while retaining its selected upload-date or detection-date order.

### Does feed search query YouTube?

Expand Down
4 changes: 2 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ YT re:Watch keeps a private, account-independent YouTube history in your browser
| --- | --- |
| History | Keeps watched videos, progress, duration, and resume points locally |
| Home | Builds a mixed local feed from followed channels |
| Subscriptions | Shows a chronological inventory of cached subscription videos |
| Subscriptions | Shows cached subscription videos ordered by upload or detection time |
| Shorts | Separates watched Shorts from ordinary history |
| Watch Later | Keeps videos explicitly saved for later, independently of a YouTube account |
| Channels | Manages local follows and reviewable ignored-channel records |
Expand Down Expand Up @@ -44,7 +44,7 @@ The feed renders cached local records first.

- **Check** scans eligible followed channels and updates cached feed records.
- **Reload** rebuilds the visible view from local storage without starting a network scan.
- **Show** opens the complete chronological subscription inventory after a scan or import handoff.
- **Show** reloads the complete subscription inventory after a scan or import handoff while preserving the selected upload-date or detection-date order.

Public YouTube RSS feeds provide new uploads. re:Watch does not use OAuth, mutate the user's YouTube subscriptions, or perform remote search.

Expand Down
8 changes: 6 additions & 2 deletions docs/technical.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,16 @@ The interface distinguishes network work from presentation:

- Check asks the scheduler to scan eligible channels.
- Reload reconstructs projections from local storage.
- Show changes to the chronological inventory.
- Show reloads the complete subscription inventory without replacing its selected ordering.
- Opening Home regenerates local ranking without initiating a Home-owned request.

## Views and pagination

`feed-view-data.js` creates canonical projections. Home ranks the ready inventory with freshness, affinity, impression, and channel-diversity signals. Subscriptions sorts regular feed records newest first.
`feed-view-data.js` creates canonical projections. Home ranks the ready inventory
with freshness, affinity, impression, and channel-diversity signals.
Subscriptions defaults to canonical newest-first pagination and can apply a
persisted upload-date or detection-date ordering without pinning newly
discovered records outside that selection.

Home and Subscriptions use stable 50-record keyset pages. Page traversal must neither duplicate records nor allow a concurrent ordering change to corrupt the current boundary.

Expand Down
Binary file modified helpers/screenshots/chrome_analytics.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified helpers/screenshots/chrome_channels.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified helpers/screenshots/chrome_history.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified helpers/screenshots/chrome_home.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified helpers/screenshots/chrome_subscriptions.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified helpers/screenshots/firefox_analytics.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified helpers/screenshots/firefox_channels.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified helpers/screenshots/firefox_history.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified helpers/screenshots/firefox_home.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified helpers/screenshots/firefox_subscriptions.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions scripts/build-chrome-unpacked.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,13 @@ cp "$ROOT"/src/*.js "$ROOT"/src/*.html "$ROOT/build/e2e/chrome/"
cp "$ROOT/src/icon"*.png "$ROOT/build/e2e/chrome/"
cp "$ROOT/src/manifest.chrome.json" "$ROOT/build/e2e/chrome/manifest.json"

# Enable production-owned diagnostic counters only in the generated E2E build.
node - "$ROOT/build/e2e/chrome/background.js" "$ROOT/build/e2e/chrome/content.js" <<'NODE'
const fs = require('node:fs');
const marker = 'globalThis.__YTVHT_TEST__ = globalThis.__YTVHT_TEST__ || {};\n';
for (const file of process.argv.slice(2)) {
fs.writeFileSync(file, marker + fs.readFileSync(file, 'utf8'));
}
NODE

echo "Unpacked Chrome E2E extension: $ROOT/build/e2e/chrome"
9 changes: 9 additions & 0 deletions scripts/build-firefox-unpacked.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ cp "$ROOT"/src/*.js "$ROOT"/src/*.html "$ROOT/build/e2e/firefox/"
cp "$ROOT/src/icon"*.png "$ROOT/build/e2e/firefox/"
cp "$ROOT/src/manifest.firefox.json" "$ROOT/build/e2e/firefox/manifest.json"

# Enable production-owned diagnostic counters only in the generated E2E build.
node - "$ROOT/build/e2e/firefox/background.js" "$ROOT/build/e2e/firefox/content.js" <<'NODE'
const fs = require('node:fs');
const marker = 'globalThis.__YTVHT_TEST__ = globalThis.__YTVHT_TEST__ || {};\n';
for (const file of process.argv.slice(2)) {
fs.writeFileSync(file, marker + fs.readFileSync(file, 'utf8'));
}
NODE

node - "$ROOT/build/e2e/firefox/manifest.json" <<'NODE'
const fs = require('node:fs');

Expand Down
38 changes: 36 additions & 2 deletions src/background.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,37 @@
console.log('YouTube Video History Tracker background script running.');
let backgroundDebugEnabled = false;

function debugLog(...args) {
if (backgroundDebugEnabled) console.log(...args);
}

function updateBackgroundDebugSetting(settings) {
backgroundDebugEnabled = settings?.debug === true;
}

function recordTestMessage(message) {
const metrics = globalThis.__YTVHT_TEST__?.backgroundMetrics;
if (!metrics) return;
metrics.messageTypes = metrics.messageTypes || {};
metrics.storageMethods = metrics.storageMethods || {};
metrics.messageTypes[message.type] = (metrics.messageTypes[message.type] || 0) + 1;
if (message.type === 'ytStorageCall' && message.method) {
metrics.storageMethods[message.method] = (metrics.storageMethods[message.method] || 0) + 1;
}
}

// Keep informational service-worker traces behind the user-facing debug
// setting. Warnings and errors remain unconditional below.
globalThis.ytvhtDebugLog = debugLog;
chrome.storage.local.get(['settings'], (result) => {
if (chrome.runtime.lastError) return;
updateBackgroundDebugSetting(result?.settings);
debugLog('YouTube Video History Tracker background script running.');
});
chrome.storage.onChanged.addListener((changes, area) => {
if (area === 'local' && changes.settings) {
updateBackgroundDebugSetting(changes.settings.newValue);
}
});

// Load shared storage modules when this file runs as a Chrome MV3 service worker.
if (typeof importScripts === 'function') {
Expand Down Expand Up @@ -40,7 +73,8 @@ const stateManager = {

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
(async () => {
console.log('Background script received message:', message.type, 'from sender:', sender.tab ? 'content' : 'popup');
recordTestMessage(message);
debugLog('Background script received message:', message.type, 'from sender:', sender.tab ? 'content' : 'popup');

if (message.type === 'getAnnouncements') {
if (!isLocalSubscriptionSender(sender) || !Number.isInteger(sender.tab?.id)) {
Expand Down
5 changes: 3 additions & 2 deletions src/content-ai-labels-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@

const controller = new AbortController();
controllers.set(requestId, controller);
const debug = event.data.debug === true;
try {
console.info('[YTVHT AI] Page-world lookup started', { videoId: event.data.payload?.videoId });
if (debug) console.info('[YTVHT AI] Page-world lookup started', { videoId: event.data.payload?.videoId });
const response = await fetch('/youtubei/v1/next?prettyPrint=false&alt=json', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
Expand All @@ -31,7 +32,7 @@
body: JSON.stringify(event.data.payload)
});
const responseData = response.ok ? await response.json() : null;
console.info('[YTVHT AI] Page-world lookup finished', {
if (debug) console.info('[YTVHT AI] Page-world lookup finished', {
videoId: event.data.payload?.videoId,
httpStatus: response.status
});
Expand Down
37 changes: 27 additions & 10 deletions src/content-ai-labels.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
let activeLookups = 0;
let timer = null;
let nextRequestAt = 0;
let debug = false;
let pageBridgeReady = false;
let pageBridgePromise = null;
let nextRequestId = 0;
Expand All @@ -42,11 +43,7 @@
const queuedIds = new Set();

function diagnostic(message, data) {
// This is intentionally visible while the experimental option is
// enabled. It gives users a way to distinguish a real YouTube
// response from an unchanged card when the external contract
// changes.
console.info('[YTVHT AI]', message, data || '');
log(`[YTVHT AI] ${message}`, data);
}

function videoIdFor(card) {
Expand Down Expand Up @@ -100,15 +97,19 @@
function parse(response) {
const contents = response?.contents?.twoColumnWatchNextResults?.results?.results?.contents;
if (!Array.isArray(contents)) return 'unknown';
return contents.some((item) => item?.videoPrimaryInfoRenderer?.badges?.some((badge) => {
const primaryInfo = contents.find((item) => item?.videoPrimaryInfoRenderer)?.videoPrimaryInfoRenderer;
// Removed/private videos can return HTTP 200 with an item section
// but no video metadata. Do not cache that as a valid no-label result.
if (!primaryInfo) return 'unknown';
return primaryInfo.badges?.some((badge) => {
const renderer = badge?.metadataBadgeRenderer;
// `label` is the stable signal observed in the response. The
// accessibility text is a defensive fallback for a renderer
// variation, rather than a title/thumbnail heuristic.
return renderer?.label === 'AI' || /made with ai/i.test(
renderer?.accessibilityData?.label || ''
);
})) ? 'ai' : 'unlabeled';
}) ? 'ai' : 'unlabeled';
}

function ensurePageBridge() {
Expand Down Expand Up @@ -156,7 +157,7 @@
};
window.addEventListener('message', onMessage);
signal?.addEventListener('abort', onAbort, { once: true });
window.postMessage({ channel: PAGE_CHANNEL, type: 'lookup', requestId, token, payload }, location.origin);
window.postMessage({ channel: PAGE_CHANNEL, type: 'lookup', requestId, token, payload, debug }, location.origin);
});
}

Expand Down Expand Up @@ -257,8 +258,20 @@
}

function start(settings) {
const nextMode = MODES.has(settings?.aiLabeledVideoHandling)
? settings.aiLabeledVideoHandling
: 'off';
const nextDebug = settings?.debug === true;
// Focus/settings refresh and general content initialization can
// converge on startup. Keep an in-flight scan instead of aborting
// it and caching the aborted request as an unknown result.
if (!stopped && nextMode === mode) {
debug = nextDebug;
return;
}
stop();
mode = MODES.has(settings?.aiLabeledVideoHandling) ? settings.aiLabeledVideoHandling : 'off';
debug = nextDebug;
mode = nextMode;
if (mode === 'off') return;
diagnostic('Enabled', { mode });
stopped = false;
Expand Down Expand Up @@ -302,11 +315,15 @@
const nextMode = MODES.has(settings?.aiLabeledVideoHandling)
? settings.aiLabeledVideoHandling
: 'off';
const nextDebug = settings?.debug === true;
// A focus refresh reads the same setting most of the time. Do
// not tear down visual state and make already-known AI cards
// flash back to normal merely because the YouTube tab regained
// focus.
if (!stopped && nextMode === mode) return;
if (!stopped && nextMode === mode) {
debug = nextDebug;
return;
}
start(settings);
}

Expand Down
Loading