YouTube → Supabase Deduplicator & Blocker (with ID logging)

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

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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();
    });
  })();
 
})();