YouTube → Supabase Deduplicator & Blocker (with ID logging)

Hide any recommended videos whose IDs already exist in your Supabase table, and log those IDs

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name        YouTube → Supabase Deduplicator & Blocker (with ID logging)
// @description Hide any recommended videos whose IDs already exist in your Supabase table, and log those IDs
// @match       https://www.youtube.com/*
// @run-at      document-end
// @version 0.0.1.20250510181837
// @namespace https://greasyfork.org/users/1435046
// ==/UserScript==
 
(function () {
  'use strict';
 
  // 1. Supabase configuration
  const SUPABASE_URL = 'https://haughsijawbsqwumuryg.supabase.co';
  const SUPABASE_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImhhdWdoc2lqYXdic3F3dW11cnlnIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzM0ODE3MjYsImV4cCI6MjA0OTA1NzcyNn0.stESUMuJEs4CNBWGtxZr1XNp2XpnQeXmKkq3fNaVE-c';
  const TABLE = 'youtube_recommended_videos_table';
  const ID_COLUMN = 'video_id_column';
 
  // 2. Skip search/results or channel pages
  if (location.pathname.startsWith('/results') || location.pathname.startsWith('/@')) {
    return;
  }
 
  // 3. Data structures & selectors
  const existingIds = new Set();
  const ITEM_SELECTOR = 'ytd-rich-item-renderer'; /*, ytd-rich-grid-media, ytd-video-renderer*/
  const TITLE_LINK_SEL = 'a#video-title-link';
 
  // 4. Utility to extract ?v= from a link
  function getVideoId(href) {
    try {
      const url = new URL(href);
      return url.searchParams.get('v');
    } catch {
      return null;
    }
  }
 
  // 5. Hide one item if its ID is in existingIds
  function filterItem(item) {
    const linkEl = item.querySelector(TITLE_LINK_SEL);
    if (!linkEl) return;
 
    const id = getVideoId(linkEl.href);
    if (id && existingIds.has(id)) {
      item.style.display = 'none';
      //item.textContent = 'This video was blocked because it was found in the Supabase database.';
      //item.style.color = 'gray';
 
    }
  }
 
  // 6. Scan all current items
  function scanPage() {
    document.querySelectorAll(ITEM_SELECTOR).forEach(filterItem);
  }
 
  // 7. Fetch existing IDs from Supabase and log them
  async function fetchExistingIds() {
    const url = `${SUPABASE_URL}/rest/v1/${TABLE}?select=${ID_COLUMN}`;
    const res = await fetch(url, {
      headers: {
        'apikey': SUPABASE_KEY,
        'Authorization': `Bearer ${SUPABASE_KEY}`
      }
    });
    if (!res.ok) {
      console.error('Failed to fetch existing IDs:', res.status, res.statusText);
      return;
    }
    const rows = await res.json();
    for (const row of rows) {
      if (row[ID_COLUMN]) {
        existingIds.add(row[ID_COLUMN]);
      }
    }
    // Log the imported IDs
    console.log('Imported video IDs from Supabase:', Array.from(existingIds));
  }
 
  // 8. Initialization
  (async () => {
    await fetchExistingIds();
    scanPage();
 
    // 9. Watch for newly added nodes (infinite scroll, dynamic loading)
    new MutationObserver(mutations => {
      const path = location.pathname;
      if (path.startsWith('/results') || path.startsWith('/@')) return;
      for (const { addedNodes } of mutations) {
        for (const node of addedNodes) {
          if (!(node instanceof HTMLElement)) continue;
          if (node.matches(ITEM_SELECTOR)) {
            filterItem(node);
          } else if (node.querySelectorAll) {
            node.querySelectorAll(ITEM_SELECTOR).forEach(filterItem);
          }
        }
      }
    }).observe(document.body, { childList: true, subtree: true });
 
    // 10. Also re-scan after YouTube navigation
    window.addEventListener('yt-navigate-finish', () => {
      const path = location.pathname;
      if (path.startsWith('/results') || path.startsWith('/@')) return;
      scanPage();
    });
  })();
 
})();