YouTube Watch Tracker v1.2

Tracks YouTube session time, video watch time, daily/monthly stats, with export/import support.

目前為 2025-06-27 提交的版本,檢視 最新版本

// ==UserScript==
// @name         YouTube Watch Tracker v1.2
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Tracks YouTube session time, video watch time, daily/monthly stats, with export/import support.
// @author       Void
// @match        *://*.youtube.com/*
// @grant        none
// @license      CC-BY-ND-4.0
// ==/UserScript==

(function () {
    'use strict';

    const STORAGE_KEY = 'yt_watch_tracker_data';
    let data = {};
    let sessionTime = 0;
    let videoTime = 0;
    let videoTimer = null;
    let sessionTimer = null;
    let currentDate = new Date().toISOString().slice(0, 10);
    let overlay;

    function load() {
        try {
            const stored = localStorage.getItem(STORAGE_KEY);
            if (stored) data = JSON.parse(stored);
        } catch {
            data = {};
        }
    }

    function save() {
        localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
    }

    function ensureDateStats() {
        if (!data[currentDate]) {
            data[currentDate] = { watched: 0, wasted: 0, session: 0 };
        }
    }

    function fmtTime(t) {
        const h = Math.floor(t / 3600);
        const m = Math.floor((t % 3600) / 60);
        const s = t % 60;
        return `${h}h ${String(m).padStart(2, '0')}m ${String(s).padStart(2, '0')}s`;
    }

    function updateOverlay() {
        ensureDateStats();
        const daily = data[currentDate];
        const month = currentDate.slice(0, 7);
        let monthly = { watched: 0, wasted: 0, session: 0 };

        for (const [date, stats] of Object.entries(data)) {
            if (date.startsWith(month)) {
                monthly.watched += stats.watched;
                monthly.wasted += stats.wasted;
                monthly.session += stats.session;
            }
        }

        overlay.querySelector('.ytwt-text').textContent =
            `📅 Today: ${daily.watched} videos | 🕒 Wasted: ${fmtTime(daily.wasted)} | ⌛ Session: ${fmtTime(sessionTime)}\n` +
            `📆 This Month: ${monthly.watched} vids | 🕒 Wasted: ${fmtTime(monthly.wasted)}`;
    }

    function createOverlay() {
        overlay = document.createElement('div');
        overlay.style = `
            position: fixed; bottom: 10px; left: 10px; background: rgba(0,0,0,0.85);
            color: #fff; padding: 8px; border-radius: 8px; font-size: 13px;
            font-family: 'Segoe UI'; font-weight: 600; z-index: 99999; white-space: pre;
            pointer-events: auto; box-shadow: 0 0 8px rgba(0,0,0,0.7); user-select: none;
        `;
        const text = document.createElement('div');
        text.className = 'ytwt-text';
        overlay.appendChild(text);

        const exportBtn = document.createElement('button');
        exportBtn.textContent = 'Export';
        exportBtn.style = btnStyle();
        exportBtn.onclick = () => {
            navigator.clipboard.writeText(JSON.stringify(data, null, 2));
            alert('Data copied to clipboard.');
        };

        const importBtn = document.createElement('button');
        importBtn.textContent = 'Import';
        importBtn.style = btnStyle();
        importBtn.onclick = () => {
            const json = prompt('Paste data to import:');
            try {
                const parsed = JSON.parse(json);
                if (typeof parsed === 'object') {
                    data = parsed;
                    save();
                    updateOverlay();
                    alert('Import successful.');
                } else throw 0;
            } catch {
                alert('Invalid JSON.');
            }
        };

        overlay.appendChild(exportBtn);
        overlay.appendChild(importBtn);
        document.body.appendChild(overlay);
    }

    function btnStyle() {
        return `
            margin-left: 6px; background: #333; color: #eee; border: 1px solid #555;
            border-radius: 4px; padding: 2px 8px; font-size: 12px; cursor: pointer;
        `;
    }

    function observeVideo() {
        new MutationObserver(() => {
            const video = document.querySelector('video');
            if (!video || video === window.__lastYTVideo) return;
            window.__lastYTVideo = video;

            ensureDateStats();
            data[currentDate].watched++;
            save();
            updateOverlay();

            clearInterval(videoTimer);
            videoTimer = setInterval(() => {
                if (video.readyState >= 2 && !video.paused && !video.ended) {
                    data[currentDate].wasted++;
                    videoTime++;
                    if (videoTime % 5 === 0) save();
                    updateOverlay();
                }
            }, 1000);
        }).observe(document.body, { childList: true, subtree: true });
    }

    function startSessionTimer() {
        sessionTimer = setInterval(() => {
            sessionTime++;
            ensureDateStats();
            data[currentDate].session++;
            if (sessionTime % 10 === 0) save();
            updateOverlay();
        }, 1000);
    }

    function init() {
        load();
        ensureDateStats();
        createOverlay();
        observeVideo();
        startSessionTimer();
    }

    window.addEventListener('load', () => setTimeout(init, 500));
})();