您需要先安装一个扩展,例如 篡改猴、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.2.29 // @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. - For restoring history, source file can also be a YouTube's history data JSON (downloadable from https://support.google.com/accounts/answer/3024190?hl=en). Or a list of YouTube video URLs (using current time as timestamps). */ (() => { //=== 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, xu = /\/watch(?:\?|.*?&)v=([^&]+)/; function getVideoId(url) { var vid = url.match(xu); 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"); //history:watch page processVideoItems('ytd-section-list-renderer[page-subtype="history"] .ytd-item-section-renderer>ytd-video-renderer'); //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) && (!s.length || (("object" === typeof s[0]) && s[0].id && s[0].timestamp))) { 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(a) { return null; } } function parseYouTubeData(s, a) { try { s = JSON.parse(s); //convert to native format if YouTube format. //old: [{id:<strVID>, timestamp:<numDate>}, ...] //new: {entries:{<stdVID>:<numDate>, ...}, index:[<strVID>, ...]} if (Array.isArray(s) && (!s.length || (("object" === typeof s[0]) && s[0].titleUrl && s[0].time))) { a = s; s = {entries: {}, index: []}; a.forEach((o, m, t) => { if (o.titleUrl && (m = o.titleUrl.match(xu))) { if (isNaN(t = (new Date(o.time)).getTime())) t = (new Date()).getTime(); s.entries[m[1]] = t; s.index.push(m[1]); } }); s.index.reverse(); return s; } else return null; } catch(a) { 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 arr && arr.length ? Math.round(arr.reduce(sum, 0) / arr.length) : "(n/a)"; } 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]++; }); if (watchedVideos.index.length) { pd = (new Date(watchedVideos.entries[watchedVideos.index[0]])).toLocaleString(); pm = (new Date(watchedVideos.entries[watchedVideos.index[watchedVideos.index.length - 1]])).toLocaleString(); } else { pd = "(n/a)"; pm = "(n/a)"; } alert(`\ Number of entries: ${watchedVideos.index.length} Oldest entry: ${pd} Newest entry: ${pm} 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) => { function askRestore(r, o) { if (confirm(`Selected history data file contains ${o.index.length} entries.\n\nRestore from this data?`)) { watchedVideos = o; GM_setValue("watchedVideos", r); a.remove(); doProcessPage(); } } (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")).onchange = r => { r = new FileReader(); r.onload = (o, t) => { if (o = parseData(r = r.result)) { //parse as native format if (o.index.length) { askRestore(r, o); } else alert("File doesn't contain any history entry."); } else if (o = parseYouTubeData(r)) { //parse as YouTube format if (o.index.length) { askRestore(JSON.stringify(o), o); } else alert("File doesn't contain any history entry."); } else { //parse as URL list o = {entries: {}, index: []}; t = (new Date()).getTime(); r = r.replace(/\r/g, "").split("\n"); while (r.length && !r[0].trim()) r.shift(); if (r.length && xu.test(r[0])) { r.forEach(s => { if (s = s.match(xu)) { o.entries[s[1]] = t; o.index.push(s[1]); } }); if (o.index.length) { askRestore(JSON.stringify(o), o); } else alert("File doesn't contain any history entry."); } else alert("Invalid history data file."); } }; r.readAsText(b.files[0]); }; document.documentElement.appendChild(a); b.click(); }); })();