2026-07-26-claude-fix-queue-history-tracking.md 7.2 KB

Session Report: Fix history tracking for YouTube queue videos

User's original requests (verbatim)

Request 1:

currently, the extension has no problems when updating the "history" data when viewing a youtube video. after it has started playing, I look in the history tab of the popup window, and I see the video, and the fact that I started watching it at the current time. however, if the video is part of a "queue" in youtube, it doesn't seem to register in the history. why might that be? take a look at content_scripts/content.js first, then popup/popup.js after that.

Request 2 (clarifying questions before implementation):

to be clear, will your solution

  • not break any existing functionality?
  • still only work on the "standard" youtube watch page? (ie: it should not record the in-thumnail video player on the front page that youtube now has)
  • work whether it is the first video of a "queue" or any subsequent video in the queue?

Request 3 (go-ahead, with additional requirements):

yes, go ahead and make the change

as part of the change, ensure that only the true video id - which I think is denoted by the v= parameter in the url - is recorded as part of playlist entries or history entries. namely, other parameters in the url (like list=, index=, and pp=) should be removed. that way, when the extension navigates to the urls, youtube won't think that it's part of one of their playlists or queues.

in the case of videos in the queue (where the page is not fully reloaded), ensure that the history entry reflects the up-to-date video id and title, etc.

Diagnosis

content_scripts/content.js ran document.querySelector("video") once, at content-script injection time, and attached play/playing/pause/ended listeners directly to that one <video> element reference.

YouTube's watch page is a single-page app. When a video finishes and YouTube auto-advances to the next item in a native queue, it does a client-side (pushState) transition rather than a full page load/reload — the underlying <video> element gets replaced. Firefox doesn't re-inject content scripts for that kind of transition, so the old listeners (bound to a now-detached element) never fire for the new video, and no message ever reaches background.js's updateHistory(). popup/popup.js was not implicated — it just passively renders whatever is in storage.local.history.

This is distinct from the extension's own playlist "advance" logic (background.js's advance()), which uses browser.tabs.update() to do a real navigation and does trigger content-script reinjection — which is why the extension's own playlists worked fine while YouTube's native queue did not.

Changes made

  1. content_scripts/content.js — Replaced the one-time vid.addEventListener(...) block with document-level listeners in the capture phase for play/playing/pause/ended. Capture-phase listeners on document receive events from any element, including ones created after initial script injection, so they survive YouTube's SPA queue transitions regardless of whether it's the first video or a later one in the queue. Each handler filters to evt.target.tagName === "VIDEO" && evt.target.closest("#movie_player"), scoping recording to the standard watch-page player only (excludes the homepage/search-results hover-preview thumbnail player and other non-#movie_player video elements, which live in a different DOM container).

  2. shared/playlist-utils.js — Added cleanVideoUrl(url), which extracts the v param via the existing extractVideoId() and rebuilds a bare https://www.youtube.com/watch?v=<id> URL, dropping list, index, pp, and any other params. Applied it inside addVideoToPlaylist() so both entry points that create playlist videos — addCurrentPageToPlaylist() in the popup and addLinkToPlaylist() (context menu) in the background script — always store clean URLs, regardless of whether the current tab/link happened to be mid-queue.

  3. content_scripts/content.js (payload) — msgPlayEvt() now sends PlaylistUtils.cleanVideoUrl(window.location.href) instead of the raw window.location.href, so history entries (background.js's updateHistory(), keyed off the v param) are written with the clean URL too. Since the URL and title are now read fresh at the moment each event actually fires (no longer tied to a stale captured element/reference), a queue's second, third, etc. video each produces its own history entry with its own up-to-date video ID and title.

  4. manifest.json — Added shared/playlist-utils.js to the content_scripts.js array (loaded before content_scripts/content.js) so the content script can call PlaylistUtils.cleanVideoUrl() directly instead of duplicating that logic.

  5. CLAUDE.md — Updated the one-line description of shared/playlist-utils.js to note it's now also used by the content script, and mentions URL normalization among its responsibilities.

Files touched

  • content_scripts/content.js
  • shared/playlist-utils.js
  • manifest.json
  • CLAUDE.md

Follow-up request (verbatim)

while I test it, go ahead and commit the changes.

after that, answer me this: something similar seems to be happening when watching youtube shorts. even previous to this change, the history correctly recorded that a short was viewed, but it merges all of them into one history entry, with the title of the latest short as the title in the history (constantly getting overwritten). it's video id is null, which makes sense, since those ones have a url like https://www.youtube.com/shorts/<video-id> (no v= url param). that might explain why they're getting clumped together. could you add a check for when watching shorts, to parse the video url accordingly, so that they get treated normally in the history?

Diagnosis (Shorts)

Confirmed: Shorts URLs (https://www.youtube.com/shorts/<id>) carry the video ID in the path, not a v query param. Two places assumed a v param specifically:

  1. shared/playlist-utils.js's extractVideoId() only ever checked searchParams.get("v").
  2. background.js's updateHistory() didn't even call the shared helper — it did its own inline new URL(message.url).searchParams.get("v"), bypassing PlaylistUtils entirely.

Both returned null for every Shorts URL, so every Shorts view was written to history[null], continuously overwriting the same entry (hence the merged entry with only the latest short's title).

Changes made (Shorts)

  1. shared/playlist-utils.jsextractVideoId() now falls back to matching /shorts/<id> out of the pathname when there's no v param. cleanVideoUrl() preserves the /shorts/<id> form (rather than rewriting to /watch?v=<id>) while still stripping any extra query params (e.g. ?feature=share).
  2. background.jsupdateHistory() now calls PlaylistUtils.extractVideoId(message.url) instead of its own inline parsing, so it benefits from the Shorts handling (and any future URL-format fixes) automatically.

Verified extractVideoId/cleanVideoUrl against sample watch and Shorts URLs (with and without extra query params) in a standalone Node check — correct video ID and clean URL produced in all cases.

Files touched (Shorts follow-up)

  • shared/playlist-utils.js
  • background.js