YouTube Community Exact Date & Time

Display exact "YYYY/MM/DD HH:mm:ss" date on YouTube community posts by fetching source HTML dynamically.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

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

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name           YouTube Community Exact Date & Time
// @name:ja        YouTube コミュニティ投稿日時を詳細表示
// @namespace      https://bsky.app/profile/neon-ai.art
// @homepage       https://github.com/neon-aiart
// @icon           data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⛄</text></svg>
// @version        1.4
// @description    Display exact "YYYY/MM/DD HH:mm:ss" date on YouTube community posts by fetching source HTML dynamically.
// @description:ja YouTubeのコミュニティ投稿に、ソースから取得した正確な日時(秒単位)を表示します。一覧・個別ページ両対応。
// @author         ねおん
// @match          https://*.youtube.com/*
// @grant          none
// @license        PolyForm Noncommercial 1.0.0; https://polyformproject.org/licenses/noncommercial/1.0.0/
// ==/UserScript==

/**
 * ==============================================================================
 * IMPORTANT NOTICE / 重要事項
 * ==============================================================================
 * Copyright (c) 2025-2026 ねおん (Neon)
 * Licensed under the PolyForm Noncommercial License 1.0.0.
 * * [JP] 本スクリプトは個人利用・非営利目的でのみ使用・改変が許可されます。
 * 無断転載、作者名の書き換え、およびクレジットの削除は固く禁じます。
 * 本スクリプトを改変・配布(フォーク)する場合は、必ず元の作者名(ねおん)
 * およびこのクレジット表記を維持してください。
 * * [EN] This script is licensed for personal and non-commercial use only.
 * Unauthorized re-uploading, modification of authorship, or removal of
 * author credits is strictly prohibited. If you fork this project, you MUST
 * retain the original credits and authorship.
 * ==============================================================================
 */

(function() {
    'use strict';

    const VERSION = '1.4';
    const STORE_KEY = 'youtube-community-exact-date-time';

    const DEBUG = false;

    let intersectionObserver = null;
    let mutationObserver = null;
    const fetchTimers = new Map();   // 高速スクロール時の過剰通信防止タイマー管理
    const completedUrls = new Map(); // 書き換え完了したURLと日時のペアを記憶

    function getFormattedDateTime() {
        const now = new Date();

        const pad = (num) => num.toString().padStart(2, '0'); // 2桁にする関数

        const y = now.getFullYear();
        const m = pad(now.getMonth() + 1);
        const d = pad(now.getDate());
        const h = pad(now.getHours());
        const min = pad(now.getMinutes());
        const s = pad(now.getSeconds());

        return `${y}/${m}/${d} ${h}:${min}:${s}`;
    }

    // 個別の fetch & 書き換えロジック
    async function fetchAndReplace(targetLink, url) {
        if (!targetLink) {
            return;
        }

        // すでに過去に取得済みのURLなら、fetchせずに記憶から一瞬で書き換える
        if (completedUrls.has(url)) {
            // 記憶から戻すときも、ツールチップに元の相対日時をセットしておく
            const data = completedUrls.get(url);
            targetLink.title = data.originalText;
            targetLink.textContent = data.formatted;
            return;
        }

        try {
            const response = await fetch(url);
            const html = await response.text();
            const match = html.match(/"datePublished"\s*:\s*"(.*?)"/);

            if (match && match[1]) {
                const d = new Date(match[1]);
                if (isNaN(d.getTime())) {
                    return;
                }

                const f = (n) => String(n).padStart(2, '0');
                const formatted =
                    `${d.getFullYear()}/${f(d.getMonth() + 1)}/${f(d.getDate())} ` +
                    `${f(d.getHours())}:${f(d.getMinutes())}:${f(d.getSeconds())}`;

                // 書き換える前の元のテキスト(「2 か月前」など)をツールチップ(title)に保存
                const originalText = targetLink.textContent;
                targetLink.title = originalText;

                targetLink.textContent = formatted;

                // 記憶するデータに元のテキストも一緒に含める
                completedUrls.set(url, { formatted, originalText, });

                if (DEBUG) {
                    console.log("✅ 詳細日時書き換え完了:", formatted);
                }
            }
        } catch (e) {
            console.error("日時取得失敗:", e);
        }
    }

    // 画面内に入った要素を処理する(IntersectionObserver のコールバック)
    function handleIntersection(entries) {
        entries.forEach(entry => {
            if (!entry.isIntersecting) {
                // 画面外に出たら fetch 予約をキャンセル(高速スクロール対策)
                if (fetchTimers.has(entry.target)) {
                    clearTimeout(fetchTimers.get(entry.target));
                    fetchTimers.delete(entry.target);
                }
                return;
            }

            // リンクから対象の個別URLを取得
            const targetLink = entry.target;
            const href = targetLink.getAttribute('href');
            if (!href || !href.includes('/post/')) {
                return;
            }
            const fullUrl = window.location.origin + href;

            if (DEBUG) {
                console.groupCollapsed(`[DEBUG] [${getFormattedDateTime()}] handleIntersection`);
                console.log('entry.target:', entry.target);
                console.log('targetLink:', targetLink);
                console.log('window.location.href:', window.location.href);
                console.log('window.location.origin + href:', fullUrl);
                console.groupEnd();
            }

            // 0.3秒画面に留まったら fetch 実行(スクロールで流し見している時は発火しない)
            const timer = setTimeout(() => {
                fetchAndReplace(targetLink, fullUrl);
                intersectionObserver.unobserve(targetLink); // 1回発火したら監視解除
                fetchTimers.delete(targetLink);
            }, 300);

            fetchTimers.set(targetLink, timer);
        });
    }

    // ページ内の「〇ヶ月前」リンクを探して監視対象に登録する
    function scanAndObserve() {
        // 現在のURLが /posts (一覧) または /post/ (個別) の場合のみ実行
        const isTargetPage = window.location.pathname.includes('/posts') || window.location.pathname.includes('/post/');
        if (!isTargetPage) {
            return;
        }

        // タイムスタンプ部分のリンク要素をすべて取得 (#published-time-text 内の a タグ)
        const links = document.querySelectorAll('#published-time-text a[href*="/post/"]');

        links.forEach(link => {
            // テキストにすでに「/」や「:」が含まれていれば、記憶になくても書き換え済みとみなす
            if (link.textContent.includes('/') || link.textContent.includes(':')) {
                return;
            }

            // ページ遷移時は observed フラグを無視して再登録できるようにする
            intersectionObserver.observe(link);
        });
    }

    // 初期化処理
    function init() {
        // 画面遷移のたびに、監視中フラグのリセットと再開を行う
        if (intersectionObserver) {
            intersectionObserver.disconnect();
        }
        if (mutationObserver) {
            mutationObserver.disconnect();
        }

        // 画面遷移時は、画面上の全リンクのobserved属性を綺麗に消し去る
        document.querySelectorAll('#published-time-text a').forEach(el => {
            el.removeAttribute('data-exact-date-observed');
        });

        // 1. 画面表示監視 (IntersectionObserver) の生成
        intersectionObserver = new IntersectionObserver(handleIntersection, {
            root: null,         // ブラウザのビューポートを基準にする
            rootMargin: '50px', // 画面に入る50px手前で少し早めに検知してカクつきを防ぐ
            threshold: 0,
        });

        // 2. DOM変化監視 (MutationObserver) の生成(追加ロード対策)
        mutationObserver = new MutationObserver(() => {
            scanAndObserve();
        });

        mutationObserver.observe(document.body, {
            childList: true,
            subtree: true,
        });

        // 初回スキャンを実行
        scanAndObserve();
    }

    // YouTubeのSPA(画面遷移)イベントに対応
    window.addEventListener('yt-page-data-updated', init);

    // 初回読み込み時
    init();
})();