Greasy Fork

来自缓存

Greasy Fork is available in English.

YouTube 完整观看次数与上传日期(改写版

显示完整查看次数和上传日期 (无时间 - 仅在视频页面).

// ==UserScript==
// @name         YouTube Full View Count and Upload Date (Rewrite)
// @name:es      Recuento total de visitas y fecha de subida en YouTube (reescritura)
// @name:fr      Nombre total de vues et date de mise en ligne sur YouTube (réécriture)
// @name:de      YouTube Vollständige Aufrufanzahl und Upload-Datum (Neufassung)
// @name:it      Numero totale di visualizzazioni e data di caricamento su YouTube (riscrittura)
// @name:pt      Contagem total de visualizações e data de carregamento no YouTube (reescrita)
// @name:ru      YouTube: полное количество просмотров и дата загрузки (переписано)
// @name:zh-CN   YouTube 完整观看次数与上传日期(改写版
// @name:ja      YouTube 総視聴回数とアップロード日 (改訂版)
// @name:ko      YouTube 전체 조회수 및 업로드 날짜 (재작성)
// @name:nl      YouTube Volledig aantal weergaven en uploaddatum (herschreven)
// @namespace    http://greasyfork.icu/users/1514993-wewe
// @version      1.0
// @description  Displays full view count and upload date (Without time - On video pages only).
// @description:es Muestra el conteo completo de vistas y la fecha de carga (Sin hora - Solo en páginas de video).
// @description:fr Affiche le nombre complet de vues et la date de téléchargement (Sans heure - Sur les pages vidéo seulement).
// @description:de Zeigt die vollständige Aufrufanzahl und das Upload-Datum an (Ohne Uhrzeit - Nur auf Videoseiten).
// @description:it Visualizza il conteggio completo delle visualizzazioni e la data di caricamento (Senza orario - Solo sulle pagine video).
// @description:pt Exibe a contagem completa de visualizações e a data de upload (Sem hora - Apenas em páginas de vídeo).
// @description:ru Отображает полный счёт просмотров и дату загрузки (Без времени - Только на страницах видео).
// @description:zh-CN 显示完整查看次数和上传日期 (无时间 - 仅在视频页面).
// @description:ja 完全視聴回数とアップロード日を表示 (時間なし - ビデオページのみ).
// @description:ko 전체 조회수 및 업로드 날짜 표시 (시간 없음 - 비디오 페이지에서만).
// @description:nl Toont de volledige weergavetelling en upload-datum (Zonder tijd - Alleen op videopagina's).
// @author       ^wewe
// @match        http*://*.youtube.com/watch?v=*
// @run-at       document-start
// @grant        none
// @license      CC-BY-SA-4.0; https://creativecommons.org/licenses/by-sa/4.0/legalcode
// @copyright    2025 ^wewe
// ==/UserScript==
 
/*
THE SOFTWARE PROVIDES NO WARRANTY AND IS PROVIDED "AS IS." THE AUTHOR IS NOT LIABLE OR RESPONSIBLE FOR ANY DAMAGES ARISING FROM ITS USE.
*/
 
/*
Creative Commons Attribution-ShareAlike 4.0 International License
 
Copyright (c) 2025 by ^wewe
 
You are free to:
- Share: copy and redistribute the material in any medium or format
- Adapt: remix, transform, and build upon the material
 
Under the following terms:
- Attribution: You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.
- ShareAlike: If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original.
- No additional restrictions: You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits.
 
No warranties are given. The license does not grant trademark rights or permissions for publicity/privacy rights. The licensor is not liable for damages. Private use is explicitly permitted. When distributing, the source code (this userscript) must be included.
 
See the full license text at: https://creativecommons.org/licenses/by-sa/4.0/legalcode
*/

(function () {
  'use strict';

  // --- selectors to try ---
  const INFO_SELECTORS = [
    '#info-container #info',
    'ytd-video-primary-info-renderer #info-contents',
    'ytd-video-primary-info-renderer #info',
    '#info-contents',
    'ytd-video-primary-info-renderer'
  ];
  const TOOLTIP_SELECTORS = [
    'ytd-watch-info-text tp-yt-paper-tooltip',
    'ytd-video-primary-info-renderer tp-yt-paper-tooltip',
    'tp-yt-paper-tooltip'
  ];

  // state
  let lastInfoText = '';
  let lastInfoEl = null;
  let localObserversAttached = false;
  let infoObserver = null;
  let tooltipObserver = null;
  let containerObserver = null;
  let rootObserver = null;

  // language/label state
  let siteLang = (document.documentElement && document.documentElement.lang) || navigator.language || 'en';
  let localizedViews = null; // { unit: 'visualizaciones', order: 'after' } or null

  // throttle with RAF
  let rafScheduled = false;
  function scheduleProcess() {
    if (!rafScheduled) {
      rafScheduled = true;
      requestAnimationFrame(() => {
        rafScheduled = false;
        ensureContainerObserver();
        process();
      });
    }
  }

  // helpers
  function queryFirst(selectors, root = document) {
    for (const s of selectors) {
      const found = root.querySelector(s);
      if (found) return found;
    }
    return null;
  }

  // Extract unit and order from a sample text like "1,234,567 views" or "visualizaciones 1.234.567"
  function extractUnitDetails(sample) {
    if (!sample || !sample.trim()) return null;
    const s = sample.trim();

    // try "number then unit" => after
    const after = s.match(/^[\s\u00A0]*([\d\.,\u202F\u2009\u200B\+\-]+)\s*(.+)$/);
    if (after && after[2]) {
      const unit = after[2].trim();
      if (unit) return { unit, order: 'after' };
    }

    // try "unit then number" => before
    const before = s.match(/^(.+?)\s*([\d\.,\u202F\u2009\u200B\+\-]+)[\s\u00A0]*$/);
    if (before && before[1]) {
      const unit = before[1].trim();
      if (unit) return { unit, order: 'before' };
    }

    // fallback: try to remove digits/separators and keep remainder
    const fallbackUnit = s.replace(/[\d\.,\s\u00A0\u202F\u2009\+\-]+/g, '').trim();
    if (fallbackUnit) return { unit: fallbackUnit, order: 'after' };

    return null;
  }

  // find localized views label from tooltip or info element
  function getLocalizedViewsLabel(infoEl, tooltipText) {
    // 1) from tooltip part if present (tooltipText expected like "1.234.567 visualizaciones")
    if (tooltipText) {
      const p = tooltipText.split('•').map(x => x.trim())[0];
      const det = extractUnitDetails(p);
      if (det) return det;
    }
    // 2) from infoEl count area
    try {
      const countText = (infoEl && (infoEl.querySelector('#count')?.textContent || infoEl.textContent)) || '';
      const candidate = countText.split('•')[0] || countText;
      const det = extractUnitDetails(candidate);
      if (det) return det;
    } catch (e) { /* ignore */ }

    return null;
  }

  // format functions use siteLang (explicit)
  function formatViewCount(view, locale) {
    const digits = String(view).replace(/[^\d]/g, '');
    const n = Number(digits);
    if (isNaN(n)) return String(view || '');
    try { return new Intl.NumberFormat(locale).format(n); } catch (e) { return String(n); }
  }

  function formatDateString(s, locale) {
    if (!s) return '';
    const d = new Date(s);
    if (!isNaN(d.getTime())) {
      try {
        return d.toLocaleDateString(locale, { year: 'numeric', month: 'short', day: 'numeric' });
      } catch (e) {
        return d.toLocaleDateString();
      }
    }
    return s;
  }

  // read JSON metadata (returns object with viewCount (string/number) and uploadDate (string) or null)
  function readFromJson() {
    try {
      let resp = window.ytInitialPlayerResponse;
      if (!resp && window.ytplayer && window.ytplayer.config && window.ytplayer.config.args && window.ytplayer.config.args.player_response) {
        try { resp = JSON.parse(window.ytplayer.config.args.player_response); } catch (e) { resp = null; }
      }
      if (resp) {
        const view = resp.videoDetails && (resp.videoDetails.viewCount || resp.videoDetails.view_count || resp.videoDetails.viewcount);
        const micro = resp.microformat && (resp.microformat.playerMicroformatRenderer || resp.microformat);
        const upload = micro && (micro.publishDate || micro.uploadDate || micro.publishTime || micro.uploadedAt || micro.datePublished);
        if (view || upload) return { viewCount: view, uploadDate: upload };
      }
    } catch (e) { /* ignore */ }

    // try JSON-LD
    try {
      const scripts = Array.from(document.querySelectorAll('script[type="application/ld+json"]'));
      for (const s of scripts) {
        try {
          const j = JSON.parse(s.textContent);
          const arr = Array.isArray(j) ? j : [j];
          for (const obj of arr) {
            if (!obj) continue;
            if (obj['@type'] === 'VideoObject' || obj.uploadDate || obj.interactionCount) {
              const view = obj.interactionCount || obj.interactioncount || obj.viewCount;
              const upload = obj.uploadDate || obj.datePublished || obj.uploadedAt;
              if (view || upload) return { viewCount: view, uploadDate: upload };
            }
          }
        } catch (e) { /* ignore parse errors */ }
      }
    } catch (e) { /* ignore */ }

    // try meta tags
    try {
      const metaView = document.querySelector('meta[itemprop="interactionCount"], meta[name="interactionCount"], meta[property="interactionCount"]');
      if (metaView && metaView.content) {
        const view = metaView.content;
        const metaDate = document.querySelector('meta[itemprop="datePublished"], meta[itemprop="uploadDate"], meta[property="og:video:release_date"]');
        const upload = metaDate && metaDate.content;
        return { viewCount: view, uploadDate: upload };
      }
    } catch (e) { /* ignore */ }

    return null;
  }

  // write view+date text into infoEl safely
  function applyPartsToInfo(infoEl, viewText, dateText) {
    if (!infoEl || (!viewText && !dateText)) return false;

    const vt = (viewText || '').trim();
    const dt = (dateText || '').trim();

    // common patterns
    const spans = infoEl.querySelectorAll('span');
    if (spans && spans.length >= 3) {
      const left = spans[0].textContent.trim();
      const right = spans[2].textContent.trim();
      if (left === vt && right === dt) { lastInfoText = infoEl.textContent.trim(); return false; }
      spans[0].textContent = vt;
      spans[2].textContent = dt;
      lastInfoText = infoEl.textContent.trim();
      return true;
    }

    // '#count' and '#info-strings'
    const countEl = document.querySelector('#count') || infoEl.querySelector('#count');
    const infoStrings = document.querySelector('#info-strings') || infoEl.querySelector('#info-strings');
    if (countEl && infoStrings) {
      const countFS = countEl.querySelector('yt-formatted-string') || countEl;
      const infoFS = infoStrings.querySelector('yt-formatted-string') || infoStrings;
      if (countFS && infoFS) {
        if (countFS.textContent.trim() !== vt || infoFS.textContent.trim() !== dt) {
          countFS.textContent = vt;
          infoFS.textContent = dt;
          lastInfoText = infoEl.textContent.trim();
          return true;
        }
        return false;
      }
    }

    // yt-formatted-string fallback
    const formatted = infoEl.querySelectorAll('yt-formatted-string');
    if (formatted && formatted.length >= 2) {
      if (formatted[0].textContent.trim() === vt && formatted[1].textContent.trim() === dt) { lastInfoText = infoEl.textContent.trim(); return false; }
      formatted[0].textContent = vt;
      formatted[1].textContent = dt;
      lastInfoText = infoEl.textContent.trim();
      return true;
    }

    // last resort: join with bullet
    try {
      const combined = (vt && dt) ? `${vt} • ${dt}` : (vt || dt || '');
      infoEl.textContent = combined;
      lastInfoText = infoEl.textContent.trim();
      return true;
    } catch (e) { /* ignore */ }

    return false;
  }

  // main processing
  function process() {
    // update siteLang from html lang if changed
    siteLang = (document.documentElement && document.documentElement.lang) || siteLang;

    const infoEl = queryFirst(INFO_SELECTORS);
    if (!infoEl) {
      lastInfoEl = null;
      return;
    }

    // if replaced, detach old observers and reset lastInfoText so we always reapply
    if (infoEl !== lastInfoEl) {
      detachLocalObservers();
      lastInfoEl = infoEl;
      lastInfoText = '';
    }

    // read tooltip text if present
    const tooltipEl = queryFirst(TOOLTIP_SELECTORS) || infoEl.querySelector('tp-yt-paper-tooltip');
    const tooltipText = tooltipEl ? (tooltipEl.textContent || tooltipEl.getAttribute('title') || '') : '';

    // try tooltip first (localized)
    let viewText = null, dateText = null;

    if (tooltipText && tooltipText.trim()) {
      const parts = tooltipText.split('•').map(p => p.trim()).filter(Boolean);
      if (parts.length >= 2) {
        viewText = parts[0];
        dateText = parts[1];
        // capture localized unit + order for fallback use later
        const det = extractUnitDetails(parts[0]);
        if (det) localizedViews = det;
      }
    }

    // if tooltip not usable, fallback to JSON/metadata and build localized strings
    if (!viewText || !dateText) {
      const j = readFromJson();
      if (j) {
        // ensure we have localizedViews; try to capture from page if not set
        if (!localizedViews) {
          localizedViews = getLocalizedViewsLabel(infoEl, tooltipText);
        }

        if (j.viewCount) {
          const formatted = formatViewCount(j.viewCount, siteLang);
          if (localizedViews && localizedViews.unit) {
            viewText = (localizedViews.order === 'before') ? `${localizedViews.unit} ${formatted}` : `${formatted} ${localizedViews.unit}`;
          } else {
            // last-resort English label (should be rare; we tried to capture UI label already)
            viewText = `${formatted} views`;
          }
        }

        if (j.uploadDate) {
          dateText = formatDateString(j.uploadDate, siteLang);
        }
      }
    }

    if (!viewText && !dateText) return;

    applyPartsToInfo(infoEl, viewText, dateText);

    // attach local observers for the small nodes
    if (!localObserversAttached) {
      attachLocalObservers(infoEl, tooltipEl);
    }
  }

  // attach observers
  function attachLocalObservers(infoEl, tooltipEl) {
    detachLocalObservers();
    try {
      infoObserver = new MutationObserver(scheduleProcess);
      infoObserver.observe(infoEl, { childList: true, subtree: true, characterData: true });

      if (tooltipEl) {
        tooltipObserver = new MutationObserver(scheduleProcess);
        tooltipObserver.observe(tooltipEl, { childList: true, subtree: true, characterData: true });
      }

      localObserversAttached = true;
    } catch (e) {
      localObserversAttached = false;
    }
  }

  function detachLocalObservers() {
    if (infoObserver) { try { infoObserver.disconnect(); } catch (e) {} infoObserver = null; }
    if (tooltipObserver) { try { tooltipObserver.disconnect(); } catch (e) {} tooltipObserver = null; }
    localObserversAttached = false;
  }

  // ensure a small container observer watches for replacements (e.g. More/Show less)
  function ensureContainerObserver() {
    if (containerObserver) return;
    const container = document.querySelector('ytd-video-primary-info-renderer') || document.querySelector('#primary') || document.documentElement;
    if (container) {
      try {
        containerObserver = new MutationObserver(scheduleProcess);
        containerObserver.observe(container, { childList: true, subtree: true });
        // we can create a rootObserver placeholder disconnected; it's not needed once containerObserver exists
        if (rootObserver) try { rootObserver.disconnect(); } catch (e) {}
      } catch (e) { /* ignore */ }
    }
  }

  // monitor html[lang] changes (if user changes UI language without full reload)
  const htmlAttrObserver = new MutationObserver(muts => {
    for (const m of muts) {
      if (m.attributeName === 'lang') {
        siteLang = (document.documentElement && document.documentElement.lang) || navigator.language || siteLang;
        localizedViews = null; // recapture in next pass
        scheduleProcess();
      }
    }
  });
  try { htmlAttrObserver.observe(document.documentElement || document.body, { attributes: true }); } catch (e) { /* ignore */ }

  // initial start
  function start() {
    // global observer until we find container
    try {
      rootObserver = new MutationObserver(scheduleProcess);
      rootObserver.observe(document.documentElement || document.body, { childList: true, subtree: true });
    } catch (e) { /* ignore */ }

    // react to YouTube SPA events
    window.addEventListener('yt-navigate-finish', () => { lastInfoText = ''; lastInfoEl = null; localizedViews = null; scheduleProcess(); }, { passive: true });
    window.addEventListener('yt-page-data-updated', () => { lastInfoText = ''; lastInfoEl = null; localizedViews = null; scheduleProcess(); }, { passive: true });

    scheduleProcess();
  }

  start();

})();