CH queues to Coolhole. CP focuses the hole and clicks Work (Earn CP). All major YouTube domains.
// ==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);
}
}
})();