您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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()); })();