YouTube Watch Tracker v1.4

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

// ==UserScript==
// @name         YouTube Watch Tracker v1.4
// @namespace    http://tampermonkey.net/
// @version      1.4
// @description  Tracks YouTube session time, video watch time, daily/monthly stats, with export/import support and toggle UI.
// @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 overlay, toggleBtn, contentBox;
    let hidden = false;

    let lastVideoId = null;

    function getCurrentDate() {
        return new Date().toISOString().slice(0, 10);
    }

    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() {
        const currentDate = getCurrentDate();
        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() {
        const currentDate = getCurrentDate();
        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;
            }
        }

        const box = overlay.querySelector('.ytwt-text');
        if (!box) return;

        box.innerText =
            `📅 Today: ${daily.watched} videos\n🕒 Wasted: ${fmtTime(daily.wasted)}\n⌛ Session: ${fmtTime(sessionTime)}\n\n` +
            `📆 Month: ${monthly.watched} videos\n🕒 Wasted: ${fmtTime(monthly.wasted)}`;
    }

    function createOverlay() {
        overlay = document.createElement('div');
        overlay.style = `
            position: fixed; top: 100px; right: 0;
            background: rgba(0,0,0,0.85); color: #fff;
            padding: 10px; border-radius: 8px 0 0 8px;
            font-size: 13px; font-family: 'Segoe UI';
            font-weight: 600; z-index: 99999;
            white-space: pre; user-select: none;
            box-shadow: 0 0 8px rgba(0,0,0,0.7);
            width: 280px; transition: right 0.3s ease;
        `;

        contentBox = document.createElement('div');
        contentBox.className = 'ytwt-text';
        overlay.appendChild(contentBox);

        const btnContainer = document.createElement('div');
        btnContainer.style = 'margin-top: 6px; display: flex; gap: 6px;';

        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.');
            }
        };

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

        toggleBtn = document.createElement('div');
        toggleBtn.textContent = '←';
        toggleBtn.style = `
            position: fixed; top: 120px; right: 280px;
            width: 20px; height: 40px;
            background: #111; color: #fff;
            display: flex; align-items: center; justify-content: center;
            border-radius: 6px 0 0 6px;
            cursor: pointer; font-size: 18px;
            z-index: 100000; transition: right 0.3s ease, transform 0.3s ease;
        `;
        toggleBtn.onclick = toggleOverlay;
        document.body.appendChild(toggleBtn);

        updateOverlay();
    }

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

    function toggleOverlay() {
        hidden = !hidden;
        if (hidden) {
            overlay.style.right = '-300px';
            toggleBtn.style.right = '0';
            toggleBtn.textContent = '→';
        } else {
            overlay.style.right = '0';
            toggleBtn.style.right = '280px';
            toggleBtn.textContent = '←';
        }
    }

    // Detect URL changes (YouTube SPA navigation)
    function onUrlChange(callback) {
        let lastUrl = location.href;
        new MutationObserver(() => {
            const url = location.href;
            if (url !== lastUrl) {
                lastUrl = url;
                callback(url);
            }
        }).observe(document, { subtree: true, childList: true });
    }

    function observeVideo() {
        let video = null;

        function setupVideo(v) {
            if (videoTimer) clearInterval(videoTimer);
            video = v;
            videoTime = 0;

            ensureDateStats();
            // Increment watched only once per new video id
            const videoId = new URL(location.href).searchParams.get('v');
            if (videoId && videoId !== lastVideoId) {
                lastVideoId = videoId;
                data[getCurrentDate()].watched++;
                save();
                updateOverlay();
            }

            videoTimer = setInterval(() => {
                if (video && video.readyState >= 2 && !video.paused && !video.ended) {
                    ensureDateStats();
                    data[getCurrentDate()].wasted++;
                    videoTime++;
                    if (videoTime % 5 === 0) save();
                    updateOverlay();
                }
            }, 1000);
        }

        // Initially try to get video element
        function trySetup() {
            const v = document.querySelector('video');
            if (v) {
                setupVideo(v);
            }
        }

        // On URL change, reset video timer and re-setup
        onUrlChange(() => {
            trySetup();
        });

        // Observe video element creation/removal for SPA page changes
        new MutationObserver(() => {
            const v = document.querySelector('video');
            if (v && v !== video) {
                setupVideo(v);
            }
        }).observe(document.body, { childList: true, subtree: true });

        trySetup();
    }

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

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

    function initRetry(attempts = 10) {
        if (document.readyState === 'complete') {
            setTimeout(init, 500);
        } else if (attempts > 0) {
            setTimeout(() => initRetry(attempts - 1), 500);
        }
    }

    window.addEventListener('load', () => initRetry());

})();