您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
Add an indicator for watched videos on YouTube. Use GM menus to display history statistics, backup history, and restore history.
当前为
// ==UserScript== // @name Mark Watched YouTube Videos // @namespace MarkWatchedYouTubeVideos // @version 1.1.24 // @license AGPL v3 // @author jcunews // @description Add an indicator for watched videos on YouTube. Use GM menus to display history statistics, backup history, and restore history. // @website http://greasyfork.icu/en/users/85671-jcunews // @match *://www.youtube.com/* // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_setValue // @run-at document-start // ==/UserScript== /* Use ALT+LeftClick or ALT+RightClick on a video list item to manually toggle the watched marker. The mouse button is defined in the script and can be changed. */ (() => { //=== config start === var maxWatchedVideoAge = 5 * 365; //number of days. set to zero to disable (not recommended) var pageLoadMarkDelay = 400; //number of milliseconds to wait before marking video items on page load phase (increase if slow network/browser) var contentLoadMarkDelay = 600; //number of milliseconds to wait before marking video items on content load phase (increase if slow network/browser) var markerMouseButtons = [0, 1]; //one or more mouse buttons to use for manual marker toggle. 0=left, 1=right, 2=middle. e.g.: //if `[0]`, only left button is used, which is ALT+LeftClick. //if `[1]`, only right button is used, which is ALT+RightClick. //if `[0,1]`, any left or right button can be used, which is: ALT+LeftClick or ALT+RightClick. //=== config end === var watchedVideos, ageMultiplier = 24 * 60 * 60 * 1000; function getVideoId(url) { var vid = url.match(/\/watch(?:\?|.*?&)v=([^&]+)/); if (vid) vid = vid[1] || vid[2]; return vid; } function watched(vid) { return !!watchedVideos.entries[vid]; } function processVideoItems(selector) { var items = document.querySelectorAll(selector), i, link; for (i = items.length-1; i >= 0; i--) { if (link = items[i].querySelector("A")) { if (watched(getVideoId(link.href))) { items[i].classList.add("watched"); } else items[i].classList.remove("watched"); } } } function processAllVideoItems() { //home page processVideoItems(".yt-uix-shelfslider-list>.yt-shelf-grid-item"); processVideoItems("#contents.ytd-rich-grid-renderer>ytd-rich-item-renderer,#contents.ytd-rich-shelf-renderer ytd-rich-item-renderer.ytd-rich-shelf-renderer"); //subscriptions page processVideoItems(".multirow-shelf>.shelf-content>.yt-shelf-grid-item"); //channel/user home page processVideoItems("#contents>.ytd-item-section-renderer>.ytd-newspaper-renderer,#items>.yt-horizontal-list-renderer"); //old processVideoItems("#contents>.ytd-channel-featured-content-renderer,#contents>.ytd-shelf-renderer>#grid-container>.ytd-expanded-shelf-contents-renderer"); //new //channel/user video page processVideoItems(".yt-uix-slider-list>.featured-content-item,#items>.ytd-grid-renderer"); //channel/user playlist page processVideoItems(".expanded-shelf>.expanded-shelf-content-list>.expanded-shelf-content-item-wrapper,.ytd-playlist-video-renderer"); //channel/user playlist item page processVideoItems(".pl-video-list .pl-video-table .pl-video,ytd-playlist-panel-video-renderer"); //channel/user videos page processVideoItems(".channels-browse-content-grid>.channels-content-item"); //channel/user search page if (/^\/(?:channel|user)\/.*?\/search/.test(location.pathname)) { processVideoItems(".ytd-browse #contents>.ytd-item-section-renderer"); //new } //search page processVideoItems("#results>.section-list .item-section>li,#browse-items-primary>.browse-list-item-container"); //old processVideoItems(".ytd-search #contents>.ytd-item-section-renderer"); //new //video page sidebar processVideoItems(".watch-sidebar-body>.video-list>.video-list-item,.playlist-videos-container>.playlist-videos-list>li"); //old processVideoItems(".ytd-compact-video-renderer"); //new } function addHistory(vid, time, noSave) { watchedVideos.entries[vid] = time; watchedVideos.index.push(vid); if (!noSave) GM_setValue("watchedVideos", JSON.stringify(watchedVideos)); } function delHistory(index, noSave) { delete watchedVideos.entries[watchedVideos.index[index]]; watchedVideos.index.splice(index, 1); if (!noSave) GM_setValue("watchedVideos", JSON.stringify(watchedVideos)); } function parseData(s, a) { try { s = JSON.parse(s); //convert to new format if old format. //old: [{id:<strVID>, timestamp:<numDate>}, ...] //new: {entries:{<stdVID>:<numDate>, ...}, index:[<strVID>, ...]} if (Array.isArray(s)) { a = s; s = {entries: {}, index: []}; a.forEach(o => { s.entries[o.id] = o.timestamp; s.index.push(o.id); }); } else if (("object" !== typeof s) || ("object" !== typeof s.entries) || !Array.isArray(s.index)) return null; return s; } catch(z) { return null; } } function getHistory(a, b) { a = GM_getValue("watchedVideos") || '{"entries": {}, "index": []}'; if (b = parseData(a)) { watchedVideos = b; } else a = JSON.stringify(watchedVideos = {entries: {}, index: []}); GM_setValue("watchedVideos", a); } function doProcessPage() { //get list of watched videos getHistory(); //remove old watched video history var now = (new Date()).valueOf(), changed, vid; if (maxWatchedVideoAge > 0) { while (watchedVideos.index.length) { if (((now - watchedVideos.entries[watchedVideos.index[0]]) / ageMultiplier) > maxWatchedVideoAge) { delHistory(0, false); changed = true; } else break; } if (changed) GM_setValue("watchedVideos", JSON.stringify(watchedVideos)); } //check and remember current video if ((vid = getVideoId(location.href)) && !watched(vid)) addHistory(vid, now); //mark watched videos processAllVideoItems(); } function processPage() { setTimeout(doProcessPage, 200); } function toggleMarker(ele, i) { if (ele) { if (ele.href) { i = getVideoId(ele.href); } else { ele = ele.parentNode; while (ele) { if (ele.tagName === "A") { i = getVideoId(ele.href); break; } ele = ele.parentNode; } } if (i) { if ((ele = watchedVideos.index.indexOf(i)) >= 0) { delHistory(ele); } else addHistory(i, (new Date()).valueOf()); processAllVideoItems(); } } } var xhropen = XMLHttpRequest.prototype.open, xhrsend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function(method, url) { this.url_mwyv = url; return xhropen.apply(this, arguments); }; XMLHttpRequest.prototype.send = function(method, url) { if ((/\/\w+_ajax\?|\/results\?search_query/).test(this.url_mwyv) && !this.listened_mwyv) { this.listened_mwyv = 1; this.addEventListener("load", () => { setTimeout(processPage, Math.floor(pageLoadMarkDelay / 2)); }); } return xhrsend.apply(this, arguments); }; addEventListener("DOMContentLoaded", sty => { sty = document.createElement("STYLE"); sty.innerHTML = ` .watched, .watched .yt-ui-ellipsis { background-color: #cec !important } html[dark] .watched, html[dark] .watched .yt-ui-ellipsis, .playlist-videos-container>.playlist-videos-list>li.watched, .playlist-videos-container>.playlist-videos-list>li.watched>a, .playlist-videos-container>.playlist-videos-list>li.watched .yt-ui-ellipsis { background-color: #030 !important }`; document.head.appendChild(sty); }); var lastFocusState = document.hasFocus(); addEventListener("blur", () => { lastFocusState = false; }); addEventListener("focus", () => { if (!lastFocusState) processPage(); lastFocusState = true; }); addEventListener("click", (ev) => { if ((markerMouseButtons.indexOf(ev.button) >= 0) && ev.altKey) toggleMarker(ev.target); }); if (markerMouseButtons.indexOf(1) >= 0) { addEventListener("contextmenu", (ev) => { if (ev.altKey) toggleMarker(ev.target); }); } if (window["body-container"]) { //old addEventListener("spfdone", processPage); processPage(); } else { //new var t = 0; function pl() { clearTimeout(t); t = setTimeout(processPage, 300); } (function init(vm) { if (vm = document.getElementById("visibility-monitor")) { vm.addEventListener("viewport-load", pl); } else setTimeout(init, 100); })(); (function init2(mh) { if (mh = document.getElementById("masthead")) { mh.addEventListener("yt-rendererstamper-finished", pl); } else setTimeout(init2, 100); })(); addEventListener("load", () => { setTimeout(processPage, pageLoadMarkDelay); }); addEventListener("spfprocess", () => { setTimeout(processPage, contentLoadMarkDelay); }); } GM_registerMenuCommand("Display History Statistics", () => { function sum(r, v) { return r + v; } function avg(arr) { return Math.round(arr.reduce(sum, 0) / arr.length); } var pd, pm, py, ld = [], lm = [], ly = []; getHistory(); Object.keys(watchedVideos.entries).forEach((k, t) => { t = new Date(watchedVideos.entries[k]); if (!pd || (pd !== t.getDate())) { ld.push(1); pd = t.getDate(); } else ld[ld.length - 1]++; if (!pm || (pm !== (t.getMonth() + 1))) { lm.push(1); pm = t.getMonth() + 1; } else lm[lm.length - 1]++; if (!py || (py !== t.getFullYear())) { ly.push(1); py = t.getFullYear(); } else ly[ly.length - 1]++; }); alert(`\ Number of entries: ${watchedVideos.index.length} Oldest entry: ${(new Date(watchedVideos.entries[watchedVideos.index[0]])).toLocaleString()} Newest entry: ${(new Date(watchedVideos.entries[watchedVideos.index[watchedVideos.index.length - 1]])).toLocaleString()} Average viewed videos per day: ${avg(ld)} Average viewed videos per month: ${avg(lm)} Average viewed videos per year: ${avg(ly)}\ `); }); GM_registerMenuCommand("Backup History Data", (a, b) => { document.body.appendChild(a = document.createElement("A")).href = URL.createObjectURL(new Blob([JSON.stringify(watchedVideos)], {type: "application/json"})); a.download = `MarkWatchedYouTubeVideos_${(new Date()).toISOString()}.json`; a.click(); a.remove(); URL.revokeObjectURL(a.href); }); GM_registerMenuCommand("Restore History Data", (a, b) => { (a = document.createElement("DIV")).id = "mwyvrh_ujs"; a.innerHTML = `<style> #mwyvrh_ujs{ display:flex;position:fixed;z-index:99999;left:0;top:0;right:0;bottom:0;margin:0;border:none;padding:0;background:rgb(0,0,0,0.5); color:#000;font-family:sans-serif;font-size:12pt;line-height:12pt;font-weight:normal;cursor:pointer; } #mwyvrhb_ujs{ margin:auto;border:.3rem solid #007;border-radius:.3rem;padding:.5rem;background-color:#fff;cursor:auto; } #mwyvrht_ujs{margin-bottom:1rem;font-size:14pt;line-height:14pt;font-weight:bold} #mwyvrhi_ujs{display:block;margin:1rem auto .5rem auto;width:12rem;overflow:hidden} </style> <div id="mwyvrhb_ujs"> <div id="mwyvrht_ujs">Mark Watched YouTube Videos</div> Please select a file to restore history data from. <input id="mwyvrhi_ujs" type="file" multiple /> </div>`; a.onclick = e => { (e.target === a) && a.remove(); }; (b = a.querySelector("#mwyvrhi_ujs")).oninput = r => { r = new FileReader(); r.onload = o => { if (o = parseData(r = r.result)) { if (confirm(`Selected history data file contains ${o.index.length} entries.\n\nRestore from this data?`)) { watchedVideos = o; GM_setValue("watchedVideos", r); a.remove(); } } else alert("Invalid history data file."); }; r.readAsText(b.files[0]); }; document.documentElement.appendChild(a); b.click(); }); })();