Features and behaviour
Reference for what YouTube Queue does and how it works. For the phased task history see implementation-plan.md.
What it is
A Chrome extension that gives YouTube a persistent, cross-tab video queue. You collect videos from anywhere in the browser; one designated tab works through them, advancing automatically as each video ends.
It drives the real YouTube player rather than an embedded one, so watch history, Premium, age-restricted videos and embed-disabled videos all keep working.
Filtering the queue
Click the funnel icon in the side panel or the queue on YouTube to open Filter by title or channel and find videos as you type. The filter is hidden by default; closing it clears the query and restores the full list. Matching ignores letter case and surrounding spaces. Clear filter restores the full list, and the match count reports when no videos match.
Play matching videos directly. In the side panel, drag matching entries to reorder them or right-click for Move to next, Move to top, and Move to bottom. Moves use positions in the full queue, including hidden entries. Filtering only changes the visible list; playback, Play next, and queue-wide actions still use the full queue. Each view keeps its filter while open, including when the queue updates. In the in-page queue, filtering temporarily disables drag reordering.
Adding to the queue
| Method | Where it works | Notes |
|---|---|---|
| Hover a thumbnail, click + | Any YouTube page — home, search, sidebar, channel, Shorts shelf | A single floating button follows the pointer. Turns green briefly on success. |
| Right-click a video link → Add to YouTube Queue | Any site on the web, not just YouTube | Most robust path: no DOM scraping, only URL parsing. |
| Right-click a watch page → Add this video to YouTube Queue | YouTube video pages | For when there is no link to target. |
| Alt+Shift+Q | The active YouTube video tab | Rebindable at chrome://extensions/shortcuts. |
Recognised URL shapes: /watch?v=, youtu.be/, /shorts/, /live/, /embed/, /v/.
Title, channel and thumbnail are fetched from YouTube’s public oEmbed endpoint — no API key, no quota, no OAuth. Total video length is optional and enabled by default. When enabled, it is fetched asynchronously from the watch page after the queue entry is stored, so adding a video is not delayed. Videos with embedding disabled return 401/404 from oEmbed, in which case the entry falls back to the video id plus a constructed thumbnail. Playback is unaffected.
Adding a video already present in the queue is ignored, regardless of its status. The thumbnail button shows an inline warning; context-menu and keyboard-shortcut adds use a browser notification. The side panel reports the duplicate inline.
New videos use the placement selected in Options: Next inserts immediately after the current queue item, while Last position appends after all existing entries. Last position is the default, including for existing installs and invalid stored values. If Next is selected without a current queue item, the video is appended as a safe fallback. Placement affects list order only; shuffle still chooses pending items at play time.
The queue
Entries carry a status: pending → playing → played, or failed.
Side panel (click the toolbar icon; each entry also shows its total length):
| Control | Behaviour |
|---|---|
| Add current | Add the active tab’s YouTube video without opening a context menu. |
| Drag an entry | Reorder |
| ▶ on an entry | Play it now — also acts as “play from here”; an interrupted video stays pending |
| ✕ on an entry | Remove it |
| Right-click an entry → Move to next | Move it immediately after the current entry without starting playback; hidden when no valid current entry exists or the selected entry is current |
| Right-click an entry → Move to top | Move it to the first position without changing status or playback |
| Right-click an entry → Move to bottom | Move it to the last position without changing status or playback |
| Right-click an entry → Copy video link | Show its canonical watch URL in selectable text for manual copying |
| Right-click a played entry → Mark as pending | Mark it pending again so it can be played again |
| ⇌ | Shuffle on/off |
| ↻ | Cycle repeat: off → all → one |
| Play next | Advance manually |
| Tidy | Drop played and failed entries |
| Clear | Empty the queue (confirms first) |
Entries are badged NOW while playing and NEXT for whatever will play next. No NEXT badge appears while shuffling, because the choice is only made at play time.
Move actions preserve status, timestamps, failure details, metadata, and the current queue entry. Already-correct positions and stale selections produce a no-op notice. Copying a link does not request clipboard permission or change queue state.
The toolbar icon carries a badge with the pending count.
In-page panel: the same queue also renders in the watch page’s right-hand column, above the related videos, where YouTube’s own playlist box sits. It can be turned off in Options, follows YouTube’s light/dark theme, collapses with its state remembered across sessions, and stays mounted as YouTube changes the page layout. It remains visible when empty and explains how to add videos.
The panel shows each entry’s status and duration when available. Its controls include Play next, shuffle, repeat, Tidy, filtering, and a queue-actions menu for Clear queue, Settings, and Analytics. Clear asks for confirmation. Each entry can be played or removed; its action menu can move it next, to the top, or to the bottom, mark a played entry pending, or show its video link for manual copying. Drag entries to reorder them when no filter is active. Queue actions show in-page feedback when they succeed or fail.
Side-panel themes
Choose System, Light, Dark, or Midnight in Options. A custom theme can define hex colors and optional CSS in a JSON object, validated using the published theme schema. Selecting a preset disables custom CSS.
Settings navigation
The Options page groups Playback, Queue, Appearance, and Analytics settings. Use its search field to find settings by keyword; category links jump to a section, and direct links can open Analytics or tag settings.
Playback
The player tab
Exactly one tab is the player tab; every other YouTube tab is input-only. This removes all cross-tab races over who advances the queue.
When you press play, the extension picks a tab in this order:
- The existing player tab, if still open
- The YouTube tab you are currently looking at
- Any other open YouTube tab
- A new background tab
If the player tab is closed mid-queue, ownership is dropped and the next play re-adopts or creates one.
Auto-advance
The content script watches the native <video> element. timeupdate records the furthest playback position and reports once the played threshold is reached, which marks the item played without advancing the queue. The native ended event advances to the next item, and its report is idempotent.
The threshold is configurable from the extension’s Options page and defaults to 100%, so new installs do not mark videos played early. Once the assigned current entry becomes played, the player tab shows Skip to next; clicking it uses the same shuffle, repeat, exhaustion, and navigation path as the side panel. Queue advancement still waits for the video to end unless the user clicks one of the manual controls.
Chapter skipping
Options can enable automatic skipping for labeled chapters in the assigned player tab. Each enabled rule matches a case-insensitive substring of the chapter title and can optionally require a case-insensitive substring of the video channel’s display name. Rules are combined with OR semantics; the two fields in one rule are combined with AND semantics.
Chapter discovery prefers visible player chapter markers and falls back to serialized chapter data on the page. If YouTube does not expose valid chapter boundaries, playback continues normally. A matching chapter is sought forward to its next boundary with a small guard, or to the video end for the final chapter. Skipping does not change queue status, ordering, shuffle, repeat, or played-threshold behavior. Each actual skip shows a brief, non-interactive in-page toast containing the chapter title; repeated nearby skips replace the current toast rather than stacking notifications.
These rules target labeled chapters only; they do not intentionally bypass standalone YouTube pre-roll, mid-roll, or post-roll ads. While YouTube exposes ad-showing or ad-interrupting on the player, chapter evaluation pauses. Other ad states are outside the extension’s detection boundary.
Navigation between videos
The extension first asks the player tab to navigate through YouTube’s own SPA router, then verifies the URL actually changed. If the router declines, it falls back to a full page load. The verdict is cached for the browser session so the verification timeout is not paid on every advance.
Watch the console for soft navigation succeeded or soft navigation ignored by the router.
Not hijacking manual browsing
The content script engages only when both are true: this tab is the player tab, and the video on screen is the one the queue expects. Navigate the player tab somewhere else by hand and the watcher disengages.
Reports from a page the queue has already moved past are rejected, so a slow tab reporting late cannot skip an extra video.
Shuffle and repeat
| Mode | Behaviour |
|---|---|
| Shuffle off, repeat off | Next pending entry in list order |
| Shuffle on | A random pending entry, chosen at play time |
| Repeat all | When nothing is pending, played entries are recycled. Failed entries are not, so recycling always terminates. |
| Repeat one | Replays the current entry — except after a playback failure, which would otherwise reload a broken video forever |
Resilience
| Situation | Handling |
|---|---|
| Deleted, private or region-blocked video | Marked failed with a reason, then skipped. Detected via the media error event, YouTube’s error screen, and a 20 s watchdog for media that never loads. |
| Pre-roll or mid-roll ad | Ads reuse the same <video> element, so an ad ending fires ended too. Ignored while the player carries ad-showing/ad-interrupting. Insurance for when an ad blocker fails, not a primary path. |
| “Video paused. Continue watching?” | Auto-confirmed, but only in the player tab, only after 5 s of user idleness, and only when the video is genuinely paused. yt-confirm-dialog-renderer is a generic confirm dialog, so unconditional clicking could confirm destructive actions. |
| Shorts | #shorts-player is recognised. Shorts set loop and may never fire ended; the loop boundary is treated as the end, while timeupdate only tracks the played threshold. |
| Extension reloaded during development | Orphaned content scripts detect the invalidated context and shut themselves down instead of spamming errors. |
| YouTube SPA navigation | Undocumented yt-navigate-finish / yt-page-data-updated events, backed by a 1 s href poll in case they get renamed. |
Architecture
Side panel ─┐ ┌─ chrome.storage.local (queue)
├─ messages ─→ Service worker ─┤
Content ────┘ (writes) └─ chrome.storage.session (player tab id)
│
└─ reads storage directly + onChanged (no round trip)
Every queue write goes through the service worker. chrome.storage has no transactions, so concurrent queue read-modify-write from multiple contexts would silently lose updates. The standalone Options page writes settings directly because each update replaces the complete settings object. Reads go direct and stay fast.
MV3 service workers are ephemeral, so no state is ever held in memory — every mutation re-reads from storage first.
Storage
| Key | Area | Contents |
|---|---|---|
queue | local | Entries, current entry, shuffle, repeat |
settings | local | Playback and queue settings from Options, including the played threshold |
sidepanelTheme | local | Selected preset or custom side-panel theme |
panelCollapsed | local | In-page panel collapse state |
playerTabId | session | Which tab is the player tab |
softNavSupported | session | Whether the SPA router accepts synthetic clicks |
Storage sits behind a four-method StorageAdapter seam (src/shared/storage/adapter.js), so moving to chrome.storage.sync or IndexedDB touches one file.
Build
There is none. Plain ES modules, loaded unpacked. Type checking is editor-only via JSDoc and jsconfig.json, using a hand-written types/chrome.d.ts in place of @types/chrome.
MV3 content scripts cannot be ES modules, so src/content/loader.js is a classic-script stub that dynamic-imports the real module tree.
Security notes
- All dynamic text is rendered with
textContent; video titles are attacker-controlled and this is an extension page withchrome.*privileges. - Thumbnail URLs are validated as
http:/https:before reaching animg.src. - Elements are built with
createElement— YouTube enforcesrequire-trusted-types-for 'script', soinnerHTMLwould throw anyway.
Selector rot
Every YouTube selector lives in src/content/selectors.js. YouTube A/B tests its front end continuously, so that file is the first place to look when something stops working.
The thumbnail hover button deliberately uses no YouTube selectors. It recognises a thumbnail by three properties that survive redesigns: the href parses to a video id, the anchor wraps an <img>, and the element is at least 120×60.
Known limitations
- The played threshold is evaluated from the native video’s duration; unusual player behavior can make the
timeupdatesignal less precise thanended. - Metadata for embed-disabled videos degrades to the video id.
- Video length lookup is enabled by default and can be disabled in Options. If enabled but watch-page metadata is unavailable, the side panel shows
Length unavailable; when disabled, the duration control is omitted entirely. - The queue does not store watched time or resume positions. Optional local analytics stores playback intervals separately and never syncs them.
- Queue is local to one browser profile; no cross-device sync yet.
- The queue does not interleave with YouTube’s own playlists.
- No Firefox build; Firefox MV3 differs and has no
sidePanel.
Local watch analytics
Enable Local watch analytics in Options, then open the Analytics chart icon beside the side-panel filter or the link in Options. Analytics settings are at the bottom of Options; dashboard links jump directly to the tracking or tag settings. Collection is off by default, independent of queue ownership, and local to this browser profile. Incognito recording has a separate opt-in, also off by default; Chrome must additionally allow the extension in incognito.
The dashboard includes Today, 7 days, 30 days, all history, and custom dates; daily time spent; comparison with the previous equal period; channel playback and share changes; video-length distribution; format and background playback totals; sessions; and searchable history. Time spent merges overlapping tabs, whereas channel, format, and content totals can overlap. Completion uses watched media ranges rather than the furthest seek position. History shows whole-visit completion, content consumed, and replayed content, alongside time spent within the selected dates. Replaying content from an earlier retained visit also counts as replaying.
Configure up to 20 value tags in Options, using text, emojis, or symbols. The initial tags are Unrated, Worthwhile, Leisure, and Low-value. Rename, add, or remove tags; stable IDs preserve assignments when names change. The fallback tag can be renamed but cannot be removed. Removing another tag clears its assignments without deleting viewing history. Videos using the fallback option inherit their channel tag, if any; otherwise they use the fallback tag. Tags are personal judgments, never automatic classifications of titles. Custom tag names appear in totals, selectors, and CSV; JSON includes tag definitions and stable assignments. A viewing session ends after ten minutes without observed playback. Shorts sessions use the same gap rule.
History, consent, labels, and tracking-change records are saved at the extension origin in IndexedDB. The worker owns writes; cumulative checkpoints and epochs protect against retries and resurrection after deletion. The dashboard computes totals from retained visits, so deleting data removes its contribution without leaving stale rollups. Dashboard refresh currently scans history in 250-record batches; very large profiles may need a future indexed-query implementation.
Options offers unlimited retention, 90 days, or one year. Cleanup runs when analytics is used, including during collection; entire playback visits crossing the cutoff are removed. Deleting a video, channel, selected dates, or everything also removes labels with no remaining history. Date deletion removes entire visits overlapping the selection. Turning tracking off preserves history. These actions do not change the queue.
JSON exports contain the complete retained history, watched ranges, labels, consent, collection changes, and timezone context. History, daily, and channel CSV exports follow the selected dates and escape spreadsheet formulas in untrusted titles. Pause tracking before exporting for an unchanged snapshot. Restore/import is not yet available. With channel images off, the dashboard makes no external requests. Optional channel images are off by default and can be enabled in Options or the dashboard. They load asynchronously for the top ten channels from official YouTube channel Open Graph metadata and the yt3.googleusercontent.com / yt3.ggpht.com image CDNs. Lookups omit credentials, do not block collection or opt-out, and validate paths and CDN URLs. Avatar URLs are cached for six hours in session storage (up to 50 entries; failed lookups retry after five minutes); image bytes use normal browser HTTP caching. Images and cached URLs are excluded from analytics history and exports. Initials remain if an image cannot load. Turning images off or deleting history clears the temporary URL cache. There are no remote fonts/scripts or analytics telemetry. Approximate storage usage and protection from automatic eviction are available under Your data. Browser storage protection is not a backup; uninstalling or losing the browser profile can remove history.
Your data reports current exportable analytics data as its UTF-8 JSON size, separately from the browser’s approximate origin footprint. Where available, the browser estimate is broken down into IndexedDB and service-worker storage, using KiB/MiB/GiB units. Current data size excludes internal ownership metadata, temporary avatar caches, database recovery logs, and cached worker code; it is not an on-disk measurement. The JSON byte count is calculated one record at a time from the loaded history, settings, and tag assignments, without creating a second history-sized string. Browser quota is a storage limit, not reserved disk space.
IndexedDB’s recovery logs can contain earlier checkpoint versions, so a small history can occupy substantially more disk space than its serialized records. Chrome rolls logs over and compacts obsolete versions automatically as its storage engine operates; cleanup has no fixed timer and an idle database need not shrink just because time passes. The extension cannot force compaction and does not maintain its own archive of old checkpoints. Existing configuration reads use read-only transactions; initialization and migrations remain atomic writes. See LevelDB storage and compaction.
Playback observations exclude detected ads, seeks, pauses, buffering, and large clock discontinuities. Background throttling, sleep, abrupt closes, unrecognized ad states, and YouTube DOM changes can reduce accuracy. Foreground/background visibility does not establish attention. Collection starts after opt-in and does not cover earlier viewing, other profiles, phones, TVs, or external embeds. Live YouTube selectors still require manual verification after site changes.