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.

სკრიპტის ინსტალაცია

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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;
  }
})();