// ==UserScript== // @name YouTube History Backspace Remover // @namespace http://tampermonkey.net/ // @version 2.0 // @description Hover over a video in YouTube watch history and press Backspace to remove it // @author You // @match https://www.youtube.com/feed/history // @match https://www.youtube.com/feed/history/* // @grant none // ==/UserScript== (function() { 'use strict'; // videoId → feedbackToken, populated from ytInitialData let tokenMap = {}; let hoveredLink = null; let isProcessing = false; // --- Token extraction from ytInitialData --- function extractTokens(data) { const tokens = {}; (function walk(obj) { if (!obj || typeof obj !== 'object') return; if (Array.isArray(obj)) { obj.forEach(walk); return; } if (obj.listItemViewModel) { const lv = obj.listItemViewModel; const title = lv.title?.content || ''; if (title.toLowerCase().includes('remove from watch history')) { const ep = lv.rendererContext?.commandContext?.onTap?.innertubeCommand?.feedbackEndpoint; if (ep?.feedbackToken && ep?.contentId) { tokens[ep.contentId] = ep.feedbackToken; } } } for (const v of Object.values(obj)) walk(v); })(data); return tokens; } function refreshTokens() { if (!window.ytInitialData) return; const found = extractTokens(window.ytInitialData); const n = Object.keys(found).length; if (n > 0) { tokenMap = Object.assign(tokenMap, found); console.log(`[YT History Remover] ${n} tokens loaded (total: ${Object.keys(tokenMap).length})`); } } // --- SAPISIDHASH for Authorization header --- async function buildAuthHeader() { const sapisid = document.cookie.match(/(?:^|;\s*)SAPISID=([^;]+)/)?.[1]; if (!sapisid) return null; const ts = Math.floor(Date.now() / 1000); const buf = await crypto.subtle.digest('SHA-1', new TextEncoder().encode(`${ts} ${sapisid} https://www.youtube.com`)); const hex = [...new Uint8Array(buf)].map(b => b.toString(16).padStart(2, '0')).join(''); return `SAPISIDHASH ${ts}_${hex}`; } // --- API call to remove from history --- async function callFeedbackApi(feedbackToken) { const ctx = window.ytcfg?.data_?.INNERTUBE_CONTEXT; if (!ctx) { console.log('[YT History Remover] No INNERTUBE_CONTEXT'); return false; } const auth = await buildAuthHeader(); const headers = { 'Content-Type': 'application/json', 'X-YouTube-Client-Name': '1', 'X-YouTube-Client-Version': window.ytcfg?.data_?.INNERTUBE_CLIENT_VERSION || '', 'X-Origin': 'https://www.youtube.com', }; if (auth) headers['Authorization'] = auth; const res = await fetch('/youtubei/v1/feedback?prettyPrint=false', { method: 'POST', headers, credentials: 'include', body: JSON.stringify({ context: ctx, feedbackTokens: [feedbackToken] }), }); if (!res.ok) { console.log(`[YT History Remover] HTTP ${res.status}`); return false; } const json = await res.json(); return json.feedbackResponses?.[0]?.isProcessed === true; } // --- DOM helpers --- function getVideoId(href) { try { const url = new URL(href); if (url.pathname.startsWith('/shorts/')) return url.pathname.split('/')[2] || null; return url.searchParams.get('v'); } catch { return null; } } function findVideoLink(el) { while (el && el !== document.body) { if (el.tagName === 'A' && el.href && (el.href.includes('/watch?') || el.href.includes('/shorts/'))) return el; el = el.parentElement; } return null; } function hideInDOM(videoId) { for (const sel of [`a[href*="v=${videoId}"]`, `a[href*="/shorts/${videoId}"]`]) { const link = document.querySelector(sel); if (!link) continue; const row = link.closest('yt-lockup-view-model') || link.closest('ytm-shorts-lockup-view-model') || link.closest('ytd-item-section-renderer'); if (row) { row.style.display = 'none'; return; } } } // --- Main remove action --- async function tryRemove() { if (isProcessing || !hoveredLink) return; isProcessing = true; try { const videoId = getVideoId(hoveredLink.href); if (!videoId) return; const token = tokenMap[videoId]; if (!token) { console.log(`[YT History Remover] No token for ${videoId}. Tokens available: ${Object.keys(tokenMap).length}`); return; } const ok = await callFeedbackApi(token); if (ok) { hideInDOM(videoId); delete tokenMap[videoId]; hoveredLink = null; console.log(`[YT History Remover] Removed ${videoId}`); } else { console.log(`[YT History Remover] API returned not-processed for ${videoId}`); } } catch (e) { console.error('[YT History Remover]', e); } finally { isProcessing = false; } } // --- Event listeners --- document.addEventListener('mouseover', e => { if (isProcessing) return; const link = findVideoLink(e.target); if (link) hoveredLink = link; }); document.addEventListener('keydown', e => { if (e.key !== 'Backspace' || !hoveredLink || isProcessing) return; const ae = document.activeElement; if (ae && (ae.tagName === 'INPUT' || ae.tagName === 'TEXTAREA' || ae.isContentEditable)) return; e.preventDefault(); tryRemove(); }); document.addEventListener('mouseout', e => { if (isProcessing || !hoveredLink) return; const rt = e.relatedTarget; if (!rt || !hoveredLink.contains(rt)) hoveredLink = null; }); // Re-extract when YouTube finishes a SPA navigation document.addEventListener('yt-navigate-finish', () => setTimeout(refreshTokens, 500)); // Polling: re-read ytInitialData (updated by YouTube on SPA nav) and clean stale refs setInterval(() => { refreshTokens(); if (hoveredLink && !document.contains(hoveredLink)) hoveredLink = null; }, 2000); // Initial extraction refreshTokens(); console.log(`[YT History Remover] Ready. Tokens loaded: ${Object.keys(tokenMap).length}`); })();