YouTube → Coolhole Queue Buttons

CH queues to Coolhole. CP focuses the hole and clicks Work (Earn CP). All major YouTube domains.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

ستحتاج إلى تثبيت إضافة مثل Stylus لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتتمكن من تثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

(لدي بالفعل مثبت أنماط للمستخدم، دعني أقم بتثبيته!)

// ==UserScript==
// @name         YouTube → Coolhole Queue Buttons
// @namespace    coolhole-queue-buttons
// @version      2.6.8
// @description  CH queues to Coolhole. CP focuses the hole and clicks Work (Earn CP). All major YouTube domains.
// @author       soapylerd
// @match        *://*.youtube.com/*
// @match        *://youtube.com/*
// @match        *://m.youtube.com/*
// @match        *://www.youtube.com/*
// @match        *://*.youtube.co.uk/*
// @match        *://youtube.co.uk/*
// @match        *://*.youtube.ca/*
// @match        *://youtube.ca/*
// @match        *://*.youtube.de/*
// @match        *://youtube.de/*
// @match        *://*.youtube.fr/*
// @match        *://youtube.fr/*
// @match        *://*.youtube.ie/*
// @match        *://youtube.ie/*
// @match        *://*.youtube.nl/*
// @match        *://youtube.nl/*
// @match        *://*.youtube.es/*
// @match        *://youtube.es/*
// @match        *://*.youtube.it/*
// @match        *://youtube.it/*
// @match        *://*.youtube.pl/*
// @match        *://youtube.pl/*
// @match        *://*.youtube.com.au/*
// @match        *://youtube.com.au/*
// @match        *://*.youtube.co.jp/*
// @match        *://youtube.co.jp/*
// @match        *://*.youtube.in/*
// @match        *://youtube.in/*
// @match        *://*.youtube.com.br/*
// @match        *://youtube.com.br/*
// @match        *://*.youtube.ru/*
// @match        *://youtube.ru/*
// @match        *://*.youtube.com.mx/*
// @match        *://youtube.com.mx/*
// @match        *://*.youtube.se/*
// @match        *://youtube.se/*
// @match        *://*.youtube.no/*
// @match        *://youtube.no/*
// @match        *://*.youtube.fi/*
// @match        *://youtube.fi/*
// @match        *://*.youtube.dk/*
// @match        *://youtube.dk/*
// @match        *://*.youtube.be/*
// @match        *://youtube.be/*
// @match        *://*.youtube.at/*
// @match        *://youtube.at/*
// @match        *://*.youtube.ch/*
// @match        *://youtube.ch/*
// @match        *://*.youtube.pt/*
// @match        *://youtube.pt/*
// @match        *://*.youtube.com.ar/*
// @match        *://youtube.com.ar/*
// @match        *://*.youtube.co.za/*
// @match        *://youtube.co.za/*
// @match        *://*.youtube.com.tr/*
// @match        *://youtube.com.tr/*
// @match        *://*.youtube.com.sg/*
// @match        *://youtube.com.sg/*
// @match        *://*.youtube.ph/*
// @match        *://youtube.ph/*
// @match        *://*.youtube.co.id/*
// @match        *://youtube.co.id/*
// @match        *://*.youtube.ae/*
// @match        *://youtube.ae/*
// @match        *://*.youtube.co.il/*
// @match        *://youtube.co.il/*
// @match        https://coolhole.org/*
// @match        https://new.coolhole.org/*
// @noframes
// @connect      www.youtube.com
// @connect      youtube.com
// @run-at       document-idle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addValueChangeListener
// @grant        GM_openInTab
// @grant        GM.setValue
// @grant        GM.getValue
// @grant        GM.openInTab
// @license      MIT
// ==/UserScript==

(() => {
  'use strict';

  const gm = {
    setValue(key, value) {
      try {
        if (typeof GM_setValue === 'function') return GM_setValue(key, value);
        if (typeof GM?.setValue === 'function') return GM.setValue(key, value);
      } catch (e) {
        console.warn('[CoolholeQueue] setValue failed', e);
      }
    },
    getValue(key, fallback) {
      try {
        if (typeof GM_getValue === 'function') return GM_getValue(key, fallback);
        if (typeof GM?.getValue === 'function') return fallback;
      } catch (e) {
        console.warn('[CoolholeQueue] getValue failed', e);
      }
      return fallback;
    },
    onValueChange(key, callback) {
      try {
        if (typeof GM_addValueChangeListener === 'function') {
          return GM_addValueChangeListener(key, callback);
        }
      } catch (e) {
        console.warn('[CoolholeQueue] value change listener unavailable', e);
      }
      return null;
    },
    openInTab(url, options = { active: true, insert: true, setParent: true }) {
      try {
        if (typeof GM_openInTab === 'function') return GM_openInTab(url, options);
        if (typeof GM?.openInTab === 'function') return GM.openInTab(url, options);
      } catch (e) {
        console.warn('[CoolholeQueue] openInTab failed, using window.open', e);
      }
      try {
        window.open(url, '_blank', 'noopener,noreferrer');
      } catch {
        try {
          location.assign(url);
        } catch (e2) {
          console.error('[CoolholeQueue] could not open URL', e2);
        }
      }
    },
  };

  const HOSTS = Object.freeze(['coolhole.org', 'new.coolhole.org']);
  const PRIMARY_HOST = 'coolhole.org';
  const queueKey = 'cq_queue';
  const workKey = 'cq_work';
  const aliveKey = (h) => `cq_alive_${h}`;
  const ALIVE_MAX_MS = 15_000;
  const BAD_TITLES = /^(shared\s*link|youtube|video|null|undefined|\s*)$/i;
  const CARD_SELECTORS = [
    'ytd-rich-item-renderer',
    'ytd-rich-grid-media',
    'ytd-grid-video-renderer',
    'ytd-video-renderer',
    'ytd-compact-video-renderer',
    'ytd-playlist-video-renderer',
    'ytd-reel-item-renderer',
    'yt-lockup-view-model',
    'ytd-lockup-view-model',
    'ytm-rich-item-renderer',
    'ytm-video-with-context-renderer',
    'ytm-compact-video-renderer',
  ].join(',');

  const host = location.hostname;
  const isYouTube = /(?:^|\.)youtube\./i.test(host);
  const isCoolhole = HOSTS.includes(host);

  try {
    if (isYouTube) initYouTubeSide();
    else if (isCoolhole) initCoolholeSide();
  } catch (e) {
    console.error('[CoolholeQueue] fatal init error', e);
  }

  function injectStyles(css) {
    try {
      const el = document.createElement('style');
      el.textContent = css;
      (document.head ?? document.documentElement).append(el);
    } catch (e) {
      console.error('[CoolholeQueue] injectStyles failed', e);
    }
  }

  function cleanTitle(value) {
    if (value == null) return null;
    const text = String(value).replace(/\s+/g, ' ').trim();
    if (!text || BAD_TITLES.test(text)) return null;
    return text.replace(/\s*-\s*YouTube\s*$/i, '').trim() || null;
  }

  function anyHostAlive() {
    return HOSTS.some((h) => {
      const ts = gm.getValue(aliveKey(h), 0);
      return typeof ts === 'number' && Date.now() - ts < ALIVE_MAX_MS;
    });
  }

  // ======================================================================
  // YOUTUBE
  // ======================================================================
  function initYouTubeSide() {
    injectStyles(`
      .cq-pill {
        display: none;
        position: fixed;
        top: 0;
        left: 0;
        z-index: 2147483000;
        align-items: stretch;
        border-radius: 999px;
        overflow: hidden;
        box-shadow: 0 2px 8px rgba(0,0,0,.4), inset 0 0 0 1px rgba(255,255,255,.08);
        font-family: Roboto, Arial, sans-serif;
        line-height: 1;
        user-select: none;
        white-space: nowrap;
        pointer-events: auto;
      }
      .cq-pill .cq-seg {
        cursor: pointer;
        border: 0;
        margin: 0;
        padding: 5px 9px;
        font-size: 10px;
        font-weight: 700;
        letter-spacing: 0.06em;
        color: #fff;
        transition: background .15s ease, filter .15s ease, transform .1s ease;
        min-width: 2.4em;
        text-align: center;
      }
      .cq-pill .cq-seg:active { transform: scale(0.97); }
      .cq-pill .cq-seg.cq-sent { filter: brightness(1.25); }
      .cq-pill .cq-seg-ch {
        background: #4a4a4a;
        border-right: 1px solid rgba(0,0,0,.25);
      }
      .cq-pill .cq-seg-cp {
        background: #606060;
      }
      .cq-pill .cq-seg-ch:hover {
        background: linear-gradient(180deg, #e53935, #b71c1c);
      }
      .cq-pill .cq-seg-cp:hover {
        background: linear-gradient(180deg, #1184e8, #0a5fad);
      }
      #cq-diag {
        position: fixed;
        bottom: 16px;
        left: 16px;
        max-width: 360px;
        padding: 10px 12px;
        border-radius: 8px;
        background: #1e1e1e;
        color: #f5f5f5;
        font: 12px/1.4 system-ui, sans-serif;
        z-index: 2147483646;
        box-shadow: 0 4px 16px rgba(0,0,0,.45);
        border: 1px solid #c62828;
      }
      #cq-diag strong { color: #ef5350; }
      #cq-diag button {
        margin: 8px 6px 0 0;
        cursor: pointer;
        border: 0;
        border-radius: 4px;
        padding: 4px 10px;
        font-size: 11px;
        font-weight: 600;
      }
      #cq-diag .cq-diag-dismiss { background: #424242; color: #fff; }
      #cq-diag .cq-diag-retry { background: #1184e8; color: #fff; }
    `);

    let scanScheduled = false;
    let scanCount = 0;
    let diagShown = false;

    const scheduleScan = () => {
      if (scanScheduled) return;
      scanScheduled = true;
      setTimeout(() => {
        scanScheduled = false;
        scanCards();
        injectWatchPage();
        maybeShowDiagnostics();
      }, 250);
    };

    document.addEventListener('yt-navigate-finish', () => {
      diagShown = false;
      hidePillNow();
      setTimeout(() => {
        scanCards();
        injectWatchPage();
        maybeShowDiagnostics();
      }, 400);
    });

    setTimeout(() => {
      scanCards();
      injectWatchPage();
      maybeShowDiagnostics();
    }, 600);
    setTimeout(maybeShowDiagnostics, 4000);
    setTimeout(maybeShowDiagnostics, 8000);

    try {
      new MutationObserver(scheduleScan).observe(document.documentElement, {
        childList: true,
        subtree: true,
      });
    } catch (e) {
      console.error('[CoolholeQueue] MutationObserver failed', e);
      setInterval(() => {
        scanCards();
        injectWatchPage();
      }, 2000);
    }

    let lastHref = location.href;
    setInterval(() => {
      if (location.href !== lastHref) {
        lastHref = location.href;
        diagShown = false;
        hidePillNow();
        scheduleScan();
      }
      scanCards();
      injectWatchPage();
    }, 2000);

    function videoIdFromHref(href) {
      try {
        const url = new URL(href, location.href);
        if (url.hostname.includes('youtu.be')) {
          return url.pathname.slice(1).split('/')[0] || null;
        }
        if (url.pathname === '/watch') return url.searchParams.get('v');
        const shorts = url.pathname.match(/^\/shorts\/([^/?]+)/);
        if (shorts) return shorts[1];
        const embed = url.pathname.match(/^\/embed\/([^/?]+)/);
        if (embed) return embed[1];
      } catch {
        /* ignore */
      }
      return null;
    }

    function currentWatchVideoId() {
      if (location.pathname === '/watch') {
        return new URLSearchParams(location.search).get('v');
      }
      return location.pathname.match(/^\/shorts\/([^/?]+)/)?.[1] ?? null;
    }

    function videoIdFromCard(card) {
      const link =
        card.querySelector('a#thumbnail[href]') ??
        card.querySelector('a#video-title-link[href]') ??
        card.querySelector('a#video-title[href]') ??
        card.querySelector('a[href*="/watch"]') ??
        card.querySelector('a[href*="/shorts/"]');
      return link?.href ? videoIdFromHref(link.href) : null;
    }

    function titleFromCard(card) {
      const candidates = [];
      const titleEl =
        card.querySelector('#video-title') ??
        card.querySelector('a#video-title-link') ??
        card.querySelector('[id="video-title"]') ??
        card.querySelector('yt-formatted-string#video-title') ??
        card.querySelector('h3 a') ??
        card.querySelector('a[title]');
      if (titleEl) {
        candidates.push(titleEl.getAttribute('title'), titleEl.textContent);
      }
      const thumb = card.querySelector('a#thumbnail, a[href*="/watch"], a[href*="/shorts/"]');
      const aria = thumb?.getAttribute('aria-label');
      if (aria) candidates.push(aria.replace(/\s+by\s+.+$/i, '').trim());
      for (const candidate of candidates) {
        const cleaned = cleanTitle(candidate);
        if (cleaned) return cleaned;
      }
      return null;
    }

    function titleFromWatchPage() {
      const el =
        document.querySelector('h1.ytd-watch-metadata yt-formatted-string') ??
        document.querySelector('ytd-watch-metadata h1 yt-formatted-string') ??
        document.querySelector('h1 yt-formatted-string') ??
        document.querySelector('#title h1') ??
        document.querySelector('yt-shorts-video-title-view-model h2') ??
        document.querySelector('h2.ytd-reel-player-header-renderer');
      if (el?.textContent) return cleanTitle(el.textContent);
      return cleanTitle(document.title?.replace(/\s*-\s*YouTube\s*$/i, ''));
    }

    function findMenu(card) {
      return (
        card.querySelector('ytd-menu-renderer') ??
        card.querySelector('#menu') ??
        card.querySelector('button[aria-label="More actions"]') ??
        card.querySelector('button[aria-label="Action menu"]') ??
        card.querySelector('button[aria-label*="More"]')
      );
    }

    function findWatchMenu() {
      return (
        document.querySelector('#actions ytd-menu-renderer') ??
        document.querySelector('#actions-inner ytd-menu-renderer') ??
        document.querySelector('ytd-watch-metadata ytd-menu-renderer') ??
        document.querySelector('#menu-container ytd-menu-renderer') ??
        document.querySelector('ytd-menu-renderer.ytd-watch-metadata') ??
        document.querySelector('#actions button[aria-label="More actions"]') ??
        document.querySelector('#actions button[aria-label*="More"]') ??
        document.querySelector('ytd-reel-player-overlay-renderer ytd-menu-renderer') ??
        document.querySelector('#actions.ytd-reel-player-overlay-renderer ytd-menu-renderer') ??
        document.querySelector('ytd-shorts ytd-menu-renderer')
      );
    }

    function flash(btn) {
      btn?.classList.add('cq-sent');
      setTimeout(() => btn?.classList.remove('cq-sent'), 900);
    }

    function sendQueue(videoId, title, btn) {
      if (!videoId) {
        console.warn('[CoolholeQueue] missing videoId');
        return;
      }
      const payload = {
        videoId: String(videoId),
        title: title ?? null,
        n: Date.now(),
      };
      try {
        if (anyHostAlive()) {
          gm.setValue(queueKey, payload);
          flash(btn);
          console.log(
            `%c[CoolholeQueue]%c → queue: ${title ?? videoId}`,
            'background:#1184e8;color:#fff;font-weight:600;padding:1px 6px;border-radius:3px;',
            'color:inherit;'
          );
          return;
        }
        const hash =
          `cq_add=${encodeURIComponent(videoId)}` +
          (title ? `&cq_title=${encodeURIComponent(title)}` : '') +
          `&n=${payload.n}`;
        gm.openInTab(`https://${PRIMARY_HOST}/#${hash}`, { active: true, insert: true, setParent: true });
        gm.setValue(queueKey, payload);
        flash(btn);
      } catch (e) {
        console.error('[CoolholeQueue] send failed', e);
        alert('CoolholeQueue error: could not send video. See the browser console for details.');
      }
    }

    function sendWork(btn) {
      try {
        const payload = { n: Date.now() };
        if (anyHostAlive()) {
          gm.setValue(workKey, payload);
          flash(btn);
          console.log(
            `%c[CoolholeQueue]%c → Work`,
            'background:#1184e8;color:#fff;font-weight:600;padding:1px 6px;border-radius:3px;',
            'color:inherit;'
          );
          return;
        }
        gm.openInTab(`https://${PRIMARY_HOST}/#cq_work=1&n=${payload.n}`, {
          active: true,
          insert: true,
          setParent: true,
        });
        gm.setValue(workKey, payload);
        flash(btn);
      } catch (e) {
        console.error('[CoolholeQueue] work signal failed', e);
        alert('CoolholeQueue error: could not signal Work. See the browser console.');
      }
    }

    const currentCtx = { id: null, title: null };
    let hideTimer = null;

    function clearHideTimer() {
      if (hideTimer) {
        clearTimeout(hideTimer);
        hideTimer = null;
      }
    }

    function scheduleHide() {
      clearHideTimer();
      hideTimer = setTimeout(() => {
        sharedPill.style.display = 'none';
      }, 250);
    }

    function hidePillNow() {
      clearHideTimer();
      sharedPill.style.display = 'none';
    }

    function buildSharedPill() {
      const pill = document.createElement('div');
      pill.className = 'cq-pill';
      pill.setAttribute('role', 'group');
      pill.title = 'Coolhole';

      const ch = document.createElement('button');
      ch.type = 'button';
      ch.className = 'cq-seg cq-seg-ch';
      ch.textContent = 'CH';
      ch.title = 'Queue to Coolhole';
      ch.addEventListener('click', (event) => {
        event.preventDefault();
        event.stopPropagation();
        event.stopImmediatePropagation();
        if (!currentCtx.id) {
          alert('Could not detect a video ID for this item.');
          return;
        }
        sendQueue(currentCtx.id, currentCtx.title, ch);
      });

      const cp = document.createElement('button');
      cp.type = 'button';
      cp.className = 'cq-seg cq-seg-cp';
      cp.textContent = 'CP';
      cp.title = 'Focus Coolhole and Work (Earn CP)';
      cp.addEventListener('click', (event) => {
        event.preventDefault();
        event.stopPropagation();
        event.stopImmediatePropagation();
        sendWork(cp);
      });

      pill.append(ch, cp);
      pill.addEventListener('mouseenter', clearHideTimer);
      pill.addEventListener('mouseleave', scheduleHide);

      const mount = () => {
        (document.body ?? document.documentElement).append(pill);
      };
      if (document.body) mount();
      else document.addEventListener('DOMContentLoaded', mount, { once: true });
      return pill;
    }

    const sharedPill = buildSharedPill();

    function positionPill(anchorEl, preferAbove) {
      try {
        const rect = anchorEl.getBoundingClientRect();
        const pillRect = sharedPill.getBoundingClientRect();
        const margin = 6;
        const spaceAbove = rect.top;
        const spaceBelow = window.innerHeight - rect.bottom;
        const placeAbove = preferAbove
          ? spaceAbove >= pillRect.height + margin || spaceAbove >= spaceBelow
          : spaceBelow < pillRect.height + margin && spaceAbove > spaceBelow;
        const top = placeAbove ? rect.top - pillRect.height - margin : rect.bottom + margin;
        let left = rect.right - pillRect.width;
        if (left < 4) left = 4;
        if (left + pillRect.width > window.innerWidth - 4) {
          left = window.innerWidth - pillRect.width - 4;
        }
        sharedPill.style.top = `${Math.max(4, top)}px`;
        sharedPill.style.left = `${left}px`;
      } catch (e) {
        console.warn('[CoolholeQueue] positionPill failed', e);
      }
    }

    function showPillFor(anchorEl, ctx, preferAbove) {
      if (!ctx || !ctx.id || !anchorEl) return;
      currentCtx.id = ctx.id;
      currentCtx.title = ctx.title;
      clearHideTimer();
      sharedPill.style.visibility = 'hidden';
      sharedPill.style.display = 'inline-flex';
      positionPill(anchorEl, preferAbove);
      sharedPill.style.visibility = 'visible';
    }

    window.addEventListener('scroll', () => hidePillNow(), true);
    window.addEventListener('resize', () => hidePillNow());

    function attachHoverTrigger(triggerEl, getContext, preferAbove, positionEl) {
      if (!triggerEl || triggerEl.dataset.cqHooked === '1') return;
      triggerEl.dataset.cqHooked = '1';
      const anchor = positionEl || triggerEl;
      triggerEl.addEventListener('mouseenter', () => {
        let ctx = null;
        try {
          ctx = getContext();
        } catch (e) {
          console.warn('[CoolholeQueue] getContext failed', e);
        }
        if (ctx && ctx.id) showPillFor(anchor, ctx, preferAbove);
      });
      triggerEl.addEventListener('mouseleave', scheduleHide);
    }

    function injectUnderMenu(card) {
      if (card.dataset.cqHooked === '1') return;
      const menu = findMenu(card);
      if (!menu || !videoIdFromCard(card)) return;
      attachHoverTrigger(
        card,
        () => ({ id: videoIdFromCard(card), title: titleFromCard(card) }),
        false,
        menu
      );
    }

    function injectWatchPage() {
      const id = currentWatchVideoId();
      if (!id) return;
      const menu = findWatchMenu();
      if (!menu) return;
      attachHoverTrigger(
        menu,
        () => ({ id: currentWatchVideoId(), title: titleFromWatchPage() }),
        true
      );
    }

    function scanCards() {
      scanCount += 1;
      let touched = 0;
      for (const card of document.querySelectorAll(CARD_SELECTORS)) {
        try {
          injectUnderMenu(card);
          if (card.dataset.cqHooked === '1') touched += 1;
        } catch (e) {
          console.warn('[CoolholeQueue] card inject error', e);
        }
      }
      const total = document.querySelectorAll('[data-cq-hooked="1"]').length;
      if (scanCount <= 3 || scanCount % 10 === 0) {
        console.log(`[CoolholeQueue] scan #${scanCount} — hover triggers: ${total} (cards: ${touched})`);
      }
    }

    function shouldExpectPills() {
      const path = location.pathname ?? '';
      if (
        path === '/' ||
        path.startsWith('/feed') ||
        path.startsWith('/results') ||
        path.startsWith('/watch') ||
        path.startsWith('/shorts') ||
        path.startsWith('/channel') ||
        path.startsWith('/@') ||
        path.startsWith('/playlist')
      ) {
        return true;
      }
      return document.querySelector(CARD_SELECTORS) != null;
    }

    function collectDiagReasons() {
      const reasons = [];
      const cards = document.querySelectorAll(CARD_SELECTORS);
      const menus = document.querySelectorAll(
        'ytd-menu-renderer, #menu, button[aria-label="More actions"], button[aria-label*="More"]'
      );
      const hooks = document.querySelectorAll('[data-cq-hooked="1"]');
      if (!document.body) reasons.push('Page body not ready yet.');
      if (cards.length === 0) reasons.push('No video cards found.');
      else reasons.push(`Found ${cards.length} video card(s).`);
      if (menus.length === 0) reasons.push('No ⋮ menus found.');
      else reasons.push(`Found ${menus.length} menu control(s).`);
      if (hooks.length === 0) reasons.push('Zero hover triggers attached.');
      else reasons.push(`${hooks.length} hover trigger(s) attached.`);
      if (typeof GM_setValue !== 'function' && typeof GM?.setValue !== 'function') {
        reasons.push('Userscript storage APIs missing.');
      }
      return reasons;
    }

    function maybeShowDiagnostics() {
      if (diagShown || !shouldExpectPills()) return;
      if (document.querySelectorAll('[data-cq-hooked="1"]').length > 0) return;
      if (scanCount < 2) return;
      diagShown = true;
      const reasons = collectDiagReasons();
      console.warn('[CoolholeQueue] Diagnostics:', reasons);
      document.getElementById('cq-diag')?.remove();
      const box = document.createElement('div');
      box.id = 'cq-diag';
      box.innerHTML =
        '<strong>CoolholeQueue:</strong> buttons did not appear.<br>' +
        reasons.map((r) => `• ${r}`).join('<br>') +
        '<br><button type="button" class="cq-diag-retry">Retry scan</button>' +
        '<button type="button" class="cq-diag-dismiss">Dismiss</button>';
      (document.body ?? document.documentElement).append(box);
      box.querySelector('.cq-diag-dismiss')?.addEventListener('click', () => box.remove());
      box.querySelector('.cq-diag-retry')?.addEventListener('click', () => {
        box.remove();
        diagShown = false;
        scanCount = 0;
        scanCards();
        injectWatchPage();
        setTimeout(maybeShowDiagnostics, 1500);
      });
    }
  }

  // ======================================================================
  // COOLHOLE
  // ======================================================================
  function initCoolholeSide() {
    const SUCCESS_LINES = [
      'Queued: {title}',
      'Dropped "{title}" at the end of the line',
      'Into the hole: {title}',
      'Locked and loaded: {title}',
      'Added to the pile: {title}',
      'Queue fed. "{title}" is in.',
      'Yoink — "{title}" is yours now',
      'Slid "{title}" onto the runway',
      'Fresh meat for the playlist: {title}',
      'The hole accepts "{title}"',
      'Stashed "{title}" for later',
      'One more for the road: {title}',
      'Sealed the deal: {title}',
      'Coolhole ate "{title}"',
      'Parked "{title}" at the back',
    ];
    const FAIL_LINES = [
      'Could not auto-queue — open the Add panel and try again.',
      'Queue miss. Open Library / Add and retry.',
      'The hole spat it back out. Try the Add panel.',
      'Auto-queue failed. Manual Add is your friend.',
    ];
    const MAX_QUEUE_MSG =
      'Max items already in the hole — wait for your video to finish, then try again.';

    const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
    const formatLine = (template, title) =>
      template.replaceAll('{title}', title || 'Unknown video');

    function getRecentErrorText() {
      const chunks = [];
      const selectors = [
        '#messagebuffer .server-whisper',
        '#messagebuffer .server-msg',
        '#messagebuffer .action',
        '#messagebuffer .chat-msg-server',
        '.alert-danger',
        '.alert-warning',
        '#qfail',
        '#queuefail',
        '.queue-error',
      ];
      for (const sel of selectors) {
        try {
          document.querySelectorAll(sel).forEach((el) => {
            const t = el.textContent?.trim();
            if (t) chunks.push(t);
          });
        } catch {
          /* ignore */
        }
      }
      try {
        const buf = document.querySelector('#messagebuffer');
        if (buf) {
          for (const n of Array.from(buf.querySelectorAll('div, span, p, li')).slice(-15)) {
            const t = n.textContent?.trim();
            if (t) chunks.push(t);
          }
        }
      } catch {
        /* ignore */
      }
      return chunks.join('\n');
    }

    function isMaxQueueError(text) {
      if (!text) return false;
      if (/already have \d+\s+items?\s+queued/i.test(text)) return true;
      if (/wait for one to play/i.test(text)) return true;
      if (/limit\s*\d+/i.test(text) && /queued/i.test(text)) return true;
      if (/queue(?:d)?\s*(?:is\s*)?(?:full|limit)/i.test(text)) return true;
      if (/too many (videos?|items?)/i.test(text)) return true;
      return false;
    }

    function fancyLog(label, ok, gold, maxed) {
      const style = ok
        ? gold
          ? 'background:linear-gradient(90deg,#f9a825,#ffd54f,#f9a825);color:#1a1200;font-weight:700;padding:2px 8px;border-radius:4px;'
          : 'background:#1184e8;color:#fff;font-weight:600;padding:2px 8px;border-radius:4px;'
        : maxed
          ? 'background:#ef6c00;color:#fff;font-weight:600;padding:2px 8px;border-radius:4px;'
          : 'background:#c62828;color:#ffebee;font-weight:600;padding:2px 8px;border-radius:4px;';
      const tag = ok ? (gold ? '★ GOLD QUEUE' : 'QUEUE') : maxed ? 'MAX QUEUE' : 'FAIL';
      console.log(`%c[CoolholeQueue] ${tag}%c ${label ?? ''}`, style, 'color:inherit;font-weight:500;');
    }

    injectStyles(`
      #cq-toast {
        position: fixed;
        bottom: 20px;
        right: 20px;
        padding: 10px 14px;
        border-radius: 6px;
        color: #fff;
        font: 600 13px/1.35 system-ui, sans-serif;
        z-index: 999999;
        opacity: 0;
        transform: translateY(10px) scale(0.98);
        transition: opacity .25s ease, transform .25s ease;
        pointer-events: none;
        max-width: 380px;
        box-shadow: 0 2px 8px rgba(0,0,0,.22);
      }
      #cq-toast.show { opacity: 1; transform: translateY(0) scale(1); }
      #cq-toast.cq-fail { animation: cq-shake .45s ease; }
      #cq-toast.cq-max {
        background: linear-gradient(180deg, #fb8c00, #ef6c00) !important;
      }
      #cq-toast.cq-gold {
        color: #1a1200;
        background: linear-gradient(135deg, #f9a825, #ffd54f 40%, #ffb300 70%, #f9a825);
        background-size: 200% 200%;
        animation: cq-gold-shine 1.6s ease-in-out infinite;
        box-shadow: 0 2px 10px rgba(249,168,37,.35);
      }
      @keyframes cq-gold-shine {
        0% { background-position: 0% 50%; }
        50% { background-position: 100% 50%; }
        100% { background-position: 0% 50%; }
      }
      @keyframes cq-shake {
        0%, 100% { transform: translateY(0) translateX(0); }
        20% { transform: translateY(0) translateX(-6px); }
        40% { transform: translateY(0) translateX(6px); }
        60% { transform: translateY(0) translateX(-4px); }
        80% { transform: translateY(0) translateX(4px); }
      }
      @media (prefers-reduced-motion: reduce) {
        #cq-toast, #cq-toast.cq-fail, #cq-toast.cq-gold {
          animation: none !important;
          transition: opacity .15s ease !important;
        }
      }
    `);

    function showToast(msg, ok, gold, maxed) {
      let toast = document.getElementById('cq-toast');
      if (!toast) {
        toast = document.createElement('div');
        toast.id = 'cq-toast';
        (document.body ?? document.documentElement).append(toast);
      }
      toast.textContent = msg;
      toast.classList.remove('show', 'cq-fail', 'cq-gold', 'cq-max');
      if (maxed) {
        toast.style.background = '';
        toast.classList.add('cq-max');
      } else if (!ok) {
        toast.style.background = 'linear-gradient(180deg, #e53935, #b71c1c)';
        toast.classList.add('cq-fail');
      } else if (gold) {
        toast.style.background = '';
        toast.classList.add('cq-gold');
      } else {
        toast.style.background = 'linear-gradient(180deg, #1184e8, #0a5fad)';
      }
      void toast.offsetWidth;
      toast.classList.add('show');
      clearTimeout(toast._cqTimer);
      toast._cqTimer = setTimeout(() => {
        toast.classList.remove('show', 'cq-fail', 'cq-gold', 'cq-max');
      }, maxed ? 5200 : gold ? 4200 : 3600);
    }

    const beat = () => gm.setValue(aliveKey(host), Date.now());
    beat();
    setInterval(beat, 5000);
    window.addEventListener('beforeunload', () => gm.setValue(aliveKey(host), 0));

    function getWorkButton() {
      return (
        document.querySelector('#job-actions button.job-action-work') ||
        document.querySelector('button.job-action-work') ||
        document.querySelector('button[title="Earn CP"]')
      );
    }

    async function doWorkAction() {
      try {
        window.focus();
      } catch {
        /* ignore */
      }

      let btn = getWorkButton();
      if (!btn) {
        for (let i = 0; i < 20 && !btn; i++) {
          await new Promise((r) => setTimeout(r, 250));
          btn = getWorkButton();
        }
      }

      if (!btn) {
        showToast('Work button not found — are you logged in with a job?', false, false, false);
        console.warn('[CoolholeQueue] Work button not in DOM');
        return;
      }

      if (btn.disabled || btn.classList.contains('work-cooling')) {
        const label = (btn.textContent || '').replace(/\s+/g, ' ').trim() || 'cooling down';
        showToast(`Work is on cooldown (${label})`, false, false, true);
        return;
      }

      btn.click();
      showToast('Work — earning CP', true, false, false);
      console.log(
        '%c[CoolholeQueue]%c Work clicked',
        'background:#1184e8;color:#fff;font-weight:700;padding:2px 8px;border-radius:4px;',
        'color:inherit;'
      );
    }

    gm.onValueChange(workKey, (_name, _old, next) => {
      if (!next?.n) return;
      doWorkAction();
    });

    gm.onValueChange(queueKey, (_name, _old, next) => {
      if (!next?.videoId) return;
      queueVideo(String(next.videoId), cleanTitle(next.title));
    });

    window.addEventListener('hashchange', processHash);
    window.addEventListener('load', () => setTimeout(processHash, 600));
    if (document.readyState !== 'loading') setTimeout(processHash, 600);

    function parseHashRequest() {
      if (/(?:^|[&#])cq_work=1(?:&|$)/.test(location.hash)) {
        return { work: true };
      }
      const add = location.hash.match(/cq_add=([^&]+)/);
      if (!add) return null;
      const titleMatch = location.hash.match(/cq_title=([^&]+)/);
      return {
        videoId: decodeURIComponent(add[1]),
        title: titleMatch ? cleanTitle(decodeURIComponent(titleMatch[1])) : null,
      };
    }

    function clearHash() {
      try {
        history.replaceState(null, '', `${location.pathname}${location.search}`);
      } catch {
        location.hash = '';
      }
    }

    async function fetchTitle(videoId) {
      try {
        const oembed = new URL('https://www.youtube.com/oembed');
        oembed.searchParams.set('url', `https://www.youtube.com/watch?v=${videoId}`);
        oembed.searchParams.set('format', 'json');
        const res = await fetch(oembed);
        if (!res.ok) return null;
        const data = await res.json();
        return cleanTitle(data?.title);
      } catch {
        return null;
      }
    }

    function waitFor(selector, timeoutMs = 12_000) {
      return new Promise((resolve, reject) => {
        const existing = document.querySelector(selector);
        if (existing) {
          resolve(existing);
          return;
        }
        const observer = new MutationObserver(() => {
          const found = document.querySelector(selector);
          if (found) {
            observer.disconnect();
            clearTimeout(timer);
            resolve(found);
          }
        });
        observer.observe(document.documentElement, { childList: true, subtree: true });
        const timer = setTimeout(() => {
          observer.disconnect();
          reject(new Error(`timed out waiting for ${selector}`));
        }, timeoutMs);
      });
    }

    function getSelectors() {
      if (document.querySelector('#queue-url') || host === 'new.coolhole.org') {
        return { mediaUrl: '#queue-url', queueEnd: '#btn-queue', expandBtn: null, expandTarget: null };
      }
      return {
        mediaUrl: '#mediaurl',
        queueEnd: '#queue_end',
        expandBtn: '#showmediaurl',
        expandTarget: '#addfromurl',
      };
    }

    async function ensurePanelOpen(sel) {
      if (!sel.expandTarget) return;
      const panel = document.querySelector(sel.expandTarget);
      if (panel?.classList.contains('in') || panel?.classList.contains('show')) return;
      if (sel.expandBtn) {
        document.querySelector(sel.expandBtn)?.click();
        await new Promise((r) => setTimeout(r, 250));
      }
      if (panel) {
        panel.classList.add('in', 'show');
        panel.style.display = 'block';
        panel.style.height = 'auto';
      }
    }

    function setNativeInputValue(input, value) {
      const descriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
      if (descriptor?.set) descriptor.set.call(input, value);
      else input.value = value;
      input.dispatchEvent(new Event('input', { bubbles: true }));
      input.dispatchEvent(new Event('change', { bubbles: true }));
    }

    let lastQueued = { id: null, at: 0 };

    async function queueVideo(videoId, title) {
      if (!videoId) return;
      const now = Date.now();
      if (lastQueued.id === videoId && now - lastQueued.at < 2000) return;
      lastQueued = { id: videoId, at: now };

      const displayTitle = cleanTitle(title) ?? (await fetchTitle(videoId)) ?? videoId;
      const sel = getSelectors();

      try {
        await ensurePanelOpen(sel);
        const input = await waitFor(sel.mediaUrl);
        const queueEndBtn = await waitFor(sel.queueEnd);

        if (queueEndBtn.disabled) {
          await new Promise((r) => setTimeout(r, 250));
          if (isMaxQueueError(getRecentErrorText())) {
            showToast(MAX_QUEUE_MSG, false, false, true);
            fancyLog(displayTitle, false, false, true);
            return;
          }
          throw new Error('Queue button is disabled');
        }

        const before = getRecentErrorText();
        setNativeInputValue(input, `https://www.youtube.com/watch?v=${videoId}`);
        await new Promise((r) => setTimeout(r, 80));
        queueEndBtn.click();
        await new Promise((r) => setTimeout(r, 600));

        const after = getRecentErrorText();
        const fresh = after.length > before.length ? after.slice(before.length) : after;

        if (isMaxQueueError(fresh) || isMaxQueueError(after)) {
          showToast(MAX_QUEUE_MSG, false, false, true);
          fancyLog(displayTitle, false, false, true);
          return;
        }

        const gold = Math.random() < 0.12;
        showToast(formatLine(pick(SUCCESS_LINES), displayTitle), true, gold, false);
        fancyLog(displayTitle, true, gold, false);
      } catch (err) {
        console.error('[CoolholeQueue] failed:', err);
        const errText = String(err?.message || '') + '\n' + getRecentErrorText();
        if (isMaxQueueError(errText)) {
          showToast(MAX_QUEUE_MSG, false, false, true);
          fancyLog(displayTitle, false, false, true);
        } else {
          showToast(pick(FAIL_LINES), false, false, false);
          fancyLog(displayTitle, false, false, false);
        }
      }
    }

    function processHash() {
      const req = parseHashRequest();
      if (!req) return;
      clearHash();
      if (req.work) {
        doWorkAction();
        return;
      }
      if (req.videoId) queueVideo(req.videoId, req.title);
    }
  }
})();