YouTube Video Age and Category Filter

Filters old YouTube videos and hides videos in certain categories with a modern blur overlay.

Εγκατάσταση αυτού του κώδικαΒοήθεια
Κώδικας προτεινόμενος από τον δημιιουργό

Μπορεί, επίσης, να σας αρέσει ο κώδικας YouTube Volume and Time Mouse Controlled.

Εγκατάσταση αυτού του κώδικα
// ==UserScript==
// @name         YouTube Video Age and Category Filter
// @namespace    PoKeRGT
// @version      1.27
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @description  Filters old YouTube videos and hides videos in certain categories with a modern blur overlay.
// @author       PoKeRGT
// @match        https://www.youtube.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @connect      www.youtube.com
// @run-at       document-start
// @homepageURL  https://github.com/PoKeRGT/userscripts
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  // --- Objeto de configuración por defecto ---
  const defaultConfig = {
    'maxVideoAge': 15,
    'categoriesToHide': ['Music', 'Sports'],
    'notSeenBorderColor': '#00FF00',
    'seenBorderColor': '#FF0000',
    'debug': false,
    'partiallySeenBorderColor': '#8A2BE2',
    'iconUrlByAge': 'https://upload.wikimedia.org/wikipedia/commons/e/e1/Calendar_%2889059%29_-_The_Noun_Project.svg',
    'iconUrlByCategory': 'https://upload.wikimedia.org/wikipedia/commons/4/4b/Discrete_category.svg',
    'overlayBlurAmount': 8
  };

  // --- Bloque de inicialización granular ---
  for (const [key, defaultValue] of Object.entries(defaultConfig)) {
    if (typeof GM_getValue(key) === 'undefined') {
      GM_setValue(key, defaultValue);
    }
  }

  // --- Carga de la configuración ---
  const MAX_VIDEO_AGE = GM_getValue('maxVideoAge');
  const CATEGORIES_TO_HIDE = GM_getValue('categoriesToHide');
  const NOT_SEEN_BORDER_COLOR = GM_getValue('notSeenBorderColor');
  const SEEN_BORDER_COLOR = GM_getValue('seenBorderColor');
  const DEBUG = GM_getValue('debug');
  const PARTIALLY_SEEN_BORDER_COLOR = GM_getValue('partiallySeenBorderColor');
  const ICON_URL_BY_AGE = GM_getValue('iconUrlByAge');
  const ICON_URL_BY_CATEGORY = GM_getValue('iconUrlByCategory');
  const OVERLAY_BLUR_AMOUNT = GM_getValue('overlayBlurAmount');

  function logDebug(...args) { if (DEBUG) console.log('[YT Filter DEBUG]', ...args); }

  logDebug('Script loaded. Initializing...');

  const processedVideos = new WeakSet();

  const observer = new MutationObserver((mutations) => {
    for (const mutation of mutations) {
      for (const node of mutation.addedNodes) {
        if (node.nodeType === 1) {
          const item = node.matches('ytd-rich-item-renderer') ? node : node.querySelector('ytd-rich-item-renderer');
          if (item) handleVideoItem(item);
        }
      }
    }
  });

  observer.observe(document.documentElement, { childList: true, subtree: true });
  logDebug('MutationObserver is now watching for new video items.');

  function handleVideoItem(videoItem) {
    if (processedVideos.has(videoItem)) { return; }
    processedVideos.add(videoItem);

    const thumbnailElement = videoItem.querySelector('yt-thumbnail-view-model');
    if (!thumbnailElement) { return; }

    const progressBarContainer = videoItem.querySelector('yt-thumbnail-overlay-progress-bar-view-model');
    if (progressBarContainer) {
      const progressBar = progressBarContainer.querySelector('.ytThumbnailOverlayProgressBarHostWatchedProgressBarSegment');
      const progressWidth = progressBar ? parseFloat(progressBar.style.width) : 0;
      if (progressWidth >= 95) changeElementStyle(thumbnailElement, 'seen');
      else if (progressWidth > 0) changeElementStyle(thumbnailElement, 'partially_seen');
      return;
    }

    const videoLinkElement = videoItem.querySelector('a.yt-lockup-metadata-view-model__title');
    if (videoLinkElement && videoLinkElement.href) {
      const videoUrl = new URL(videoLinkElement.href, document.baseURI).href;
      const videoTitle = videoLinkElement.textContent.trim() || videoLinkElement.getAttribute('aria-label') || 'Untitled Video';
      fetchVideoDetails(videoUrl, videoTitle, thumbnailElement);
    }
  }

  /**
   * Crea un overlay con efecto blur y una etiqueta informativa.
   * @param {HTMLElement} thumbnailEl El elemento de la miniatura.
   * @param {object} reason Objeto con los detalles del ocultamiento.
   */
  function createBlurOverlay(thumbnailEl, reason) {
    if (thumbnailEl.querySelector('.filter-blur-overlay')) return;

    const overlayContainer = document.createElement('div');
    overlayContainer.className = 'filter-blur-overlay';
    Object.assign(overlayContainer.style, {
      position: 'absolute', top: '0', left: '0', width: '100%', height: '100%',
      zIndex: '10', borderRadius: '12px', overflow: 'hidden', cursor: 'pointer'
    });

    const blurEffect = document.createElement('div');
    Object.assign(blurEffect.style, {
      width: '100%', height: '100%',
      backdropFilter: `blur(${OVERLAY_BLUR_AMOUNT}px) grayscale(0.3)`,
      backgroundColor: 'rgba(0, 0, 0, 0.1)'
    });

    const badge = document.createElement('div');
    Object.assign(badge.style, {
      position: 'absolute', top: '8px', left: '8px',
      display: 'flex', alignItems: 'center',
      padding: '4px 8px', backgroundColor: 'rgba(20, 20, 20, 0.8)',
      borderRadius: '8px', color: 'white', fontFamily: 'Roboto, Arial, sans-serif',
      fontSize: '12px', fontWeight: '500'
    });

    const icon = document.createElement('img');
    icon.src = reason.type === 'age' ? ICON_URL_BY_AGE : ICON_URL_BY_CATEGORY;
    Object.assign(icon.style, {
      width: '16px', height: '16px', marginRight: '6px',
      filter: 'invert(1)'
    });

    const text = document.createElement('span');
    let labelText = reason.value.toUpperCase();
    // --- MODIFICADO: Añade 'days' al texto si el detalle es por antigüedad ---
    if (reason.details) {
      labelText += ` (${reason.details} days)`;
    }
    text.textContent = labelText;

    badge.appendChild(icon);
    badge.appendChild(text);
    overlayContainer.appendChild(blurEffect);
    overlayContainer.appendChild(badge);

    overlayContainer.onclick = (e) => {
      e.preventDefault();
      e.stopPropagation();
      const videoItem = thumbnailEl.closest('ytd-rich-item-renderer');
      const videoLink = videoItem.querySelector('a.yt-lockup-metadata-view-model__title');
      if (videoLink) window.location.href = videoLink.href;
    };

    thumbnailEl.style.position = 'relative';
    thumbnailEl.appendChild(overlayContainer);
  }

  function changeElementStyle(element, prop, details = '') {
    if (!element) return;
    logDebug(`Applying style "${prop}" to element:`, element);
    element.style.overflow = 'hidden';
    element.style.borderRadius = '12px';

    switch (prop) {
      case 'hidden_by_age':
        createBlurOverlay(element, { type: 'age', value: 'OLD', details: details });
        break;
      case 'hidden_by_category':
        createBlurOverlay(element, { type: 'category', value: details });
        break;
      case 'not_seen':
        element.style.border = `4px solid ${NOT_SEEN_BORDER_COLOR}`;
        element.style.boxSizing = 'border-box';
        break;
      case 'seen':
        element.style.border = `4px solid ${SEEN_BORDER_COLOR}`;
        element.style.boxSizing = 'border-box';
        break;
      case 'partially_seen':
        element.style.border = `4px solid ${PARTIALLY_SEEN_BORDER_COLOR}`;
        element.style.boxSizing = 'border-box';
        break;
      default:
        logDebug(`Unknown style property: "${prop}"`);
        break;
    }
  }

  function fetchVideoDetails(videoUrl, videoTitle, elementToChange) {
    GM_xmlhttpRequest({
      method: 'GET', url: videoUrl,
      onload: (response) => {
        if (response.status >= 400) return;

        const metaTags = response.responseText.match(/<meta [^>]*>/g) || [];
        let isHidden = false;

        const category = findMetaTagContent(metaTags, 'itemprop="genre"');
        if (category && CATEGORIES_TO_HIDE.includes(category)) {
          isHidden = true;
          logDebug(`Hiding "${videoTitle}" (Category: ${category})`);
          changeElementStyle(elementToChange, 'hidden_by_category', category);
        }

        if (!isHidden) {
          const uploadDateStr = findMetaTagContent(metaTags, 'itemprop="uploadDate"');
          if (uploadDateStr) {
            const uploadDate = new Date(uploadDateStr);
            const today = new Date();
            const diffInDays = Math.ceil((today - uploadDate) / (1000 * 60 * 60 * 24));
            if (diffInDays > MAX_VIDEO_AGE) {
              logDebug(`Hiding "${videoTitle}" (Age: ${diffInDays} days)`);
              // --- MODIFICADO: Se pasa diffInDays en lugar de la fecha completa ---
              changeElementStyle(elementToChange, 'hidden_by_age', diffInDays);
            } else {
              changeElementStyle(elementToChange, 'not_seen');
            }
          } else {
            changeElementStyle(elementToChange, 'not_seen');
          }
        }
      },
      onerror: (error) => console.error(`Network error on "${videoTitle}":`, error)
    });
  }

  function findMetaTagContent(metaTags, property) {
    const tag = metaTags.find(t => t.includes(property));
    if (tag) {
      const contentMatch = tag.match(/content="([^"]+)"/);
      return contentMatch ? contentMatch[1] : null;
    }
    return null;
  }
})();
长期地址
遇到问题?请前往 GitHub 提 Issues。