返回首頁 

Mark Watched YouTube Videos

Add an indicator for watched videos on YouTube. Use GM menus to display history statistics, backup history, and restore/merge history.


Install this script?
// ==UserScript==// @name        Mark Watched YouTube Videos// @namespace   MarkWatchedYouTubeVideos// @version     1.4.63// @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/merge history.// @website     https://greasyfork.org/en/users/85671-jcunews// @match       *://www.youtube.com/*// @grant       GM_getValue// @grant       GM_registerMenuCommand// @grant       GM_setValue// @grant       unsafeWindow// @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/merging 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   = 10 * 365; //number of days. set to zero to disable (not recommended)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 ===varwatchedVideos, ageMultiplier = 24 * 60 * 60 * 1000, xu = /(?:\/watch(?:\?|.*?&)v=|\/embed\/)([^\/\?&]+)|\/shorts\/([^\/\?]+)/,querySelector = Element.prototype.querySelector, querySelectorAll = Element.prototype.querySelectorAll;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 = querySelector.call(items[i], "A")) {if (watched(getVideoId(link.href))) {items[i].classList.add("watched");} else items[i].classList.remove("watched");}}}function processAllVideoItems() {//home pageprocessVideoItems(`.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,#contents.ytd-rich-grid-renderer>ytd-rich-grid-row ytd-rich-grid-media`);//subscriptions pageprocessVideoItems(`.multirow-shelf>.shelf-content>.yt-shelf-grid-item`);//history:watch pageprocessVideoItems(`ytd-section-list-renderer[page-subtype="history"] .ytd-item-section-renderer>ytd-video-renderer`);//channel/user home pageprocessVideoItems(`#contents>.ytd-item-section-renderer>.ytd-newspaper-renderer,#items>.yt-horizontal-list-renderer`); //oldprocessVideoItems(`#contents>.ytd-channel-featured-content-renderer,#contents>.ytd-shelf-renderer>#grid-container>.ytd-expanded-shelf-contents-renderer`); //new//channel/user video pageprocessVideoItems(`.yt-uix-slider-list>.featured-content-item,.channels-browse-content-grid>.channels-content-item,#items>.ytd-grid-renderer`);//channel/user shorts pageprocessVideoItems(`ytd-rich-item-renderer ytd-rich-grid-slim-media`);//channel/user playlist pageprocessVideoItems(`.expanded-shelf>.expanded-shelf-content-list>.expanded-shelf-content-item-wrapper,.ytd-playlist-video-renderer`);//channel/user playlist item pageprocessVideoItems(`.pl-video-list .pl-video-table .pl-video,ytd-playlist-panel-video-renderer`);//channel/user search pageif (/^\/(?:(?:c|channel|user)\/)?.*?\/search/.test(location.pathname)) {processVideoItems(`.ytd-browse #contents>.ytd-item-section-renderer`); //new}//search pageprocessVideoItems(`#r###lts>.section-list .item-section>li,#browse-items-primary>.browse-list-item-container`); //oldprocessVideoItems(`.ytd-search #contents>ytd-video-renderer,.ytd-search #contents>ytd-playlist-renderer,.ytd-search #items>ytd-video-renderer`); //new//video pageprocessVideoItems(`.watch-sidebar-body>.video-list>.video-list-item,.playlist-videos-container>.playlist-videos-list>li`); //oldprocessVideoItems(`.ytd-compact-video-renderer,.ytd-compact-radio-renderer`); //new}function addHistory(vid, time, noSave, i) {if (!watchedVideos.entries[vid]) {watchedVideos.index.push(vid);} else {i = watchedVideos.index.indexOf(vid);if (i >= 0) watchedVideos.index.push(watchedVideos.index.splice(i, 1)[0])}watchedVideos.entries[vid] = time;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));}var dc, ut;function parseData(s, a, i, j, z) {try {dc = false;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;//reconstruct index if brokenif (s.index.length !== (a = Object.keys(s.entries)).length) {s.index = a.map(k => [k, s.entries[k]]).sort((x, y) => x[1] - y[1]).map(v => v[0]);dc = true;}return s;} catch(z) {return null;}}function parseYouTubeData(s, a) {try {s = JSON.parse(s);//convert to native format if YouTube format.//old: [{titleUrl:<strUrl>, time:<strIsoDate>}, ...] (excludes irrelevant properties)//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] || m[2]] = t;s.index.push(m[1] || m[2]);}});s.index.reverse();return s;} else return null;} catch(a) {return null;}}function mergeData(o, a) {o.index.forEach(i => {if (watchedVideos.entries[i]) {if (watchedVideos.entries[i] < o.entries[i]) watchedVideos.entries[i] = o.entries[i];} else watchedVideos.entries[i] = o.entries[i];});a = Object.keys(watchedVideos.entries);watchedVideos.index = a.map(k => [k, watchedVideos.entries[k]]).sort((x, y) => x[1] - y[1]).map(v => v[0]);}function getHistory(a, b) {a = GM_getValue("watchedVideos");if (a === undefined) {a = '{"entries": {}, "index": []}';} else if ("object" === typeof a) a = JSON.stringify(a);if (b = parseData(a)) {watchedVideos = b;if (dc) b = JSON.stringify(b);} else b = JSON.stringify(watchedVideos = {entries: {}, index: []});GM_setValue("watchedVideos", b);}function doProcessPage() {//get list of watched videosgetHistory();//remove old watched video historyvar 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 videoif ((vid = getVideoId(location.href)) && !watched(vid)) addHistory(vid, now);//mark watched videosprocessAllVideoItems();}function processPage() {setTimeout(doProcessPage, Math.floor(contentLoadMarkDelay / 2));}function delayedProcessPage() {setTimeout(doProcessPage, contentLoadMarkDelay);}function toggleMarker(ele, i) {if (ele) {if (!ele.href && (i = ele.closest('a'))) ele = i;if (ele.href) {i = getVideoId(ele.href);} else {while (ele) {while (ele && (!ele.__data || !ele.__data.data || !ele.__data.data.videoId)) ele = ele.__dataHost || ele.parentNode;if (ele) {i = ele.__data.data.videoId;break}}}if (i) {if ((ele = watchedVideos.index.indexOf(i)) >= 0) {delHistory(ele);} else addHistory(i, (new Date()).valueOf());processAllVideoItems();}}}var rxListUrl = /\/\w+_ajax\?|\/r###lts\?search_query|\/v1\/(browse|next|search)\?/;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 (rxListUrl.test(this.url_mwyv) && !this.listened_mwyv) {this.listened_mwyv = 1;this.addEventListener("load", delayedProcessPage);}return xhrsend.apply(this, arguments);};var fetch_ = unsafeWindow.fetch;unsafeWindow.fetch = function(opt) {let url = opt.url || opt;if (rxListUrl.test(opt.url || opt)) {return fetch_.apply(this, arguments).finally(delayedProcessPage);} else return fetch_.apply(this, arguments);};var nac = unsafeWindow.Node.prototype.appendChild;unsafeWindow.Node.prototype.appendChild = function(e) {var z;if ((this.tagName === "BODY") && (e?.tagName === "IFRAME")) {var r = nac.apply(this, arguments);try {if (/^about:blank\b/.test(e.contentWindow.location.href)) e.contentWindow.fetch = fetch} catch(z) {}return r} else return nac.apply(this, arguments)}var to = {createHTML: s => s}, tp = window.trustedTypes?.createPolicy ? trustedTypes.createPolicy("", to) : to, html = s => tp.createHTML(s);addEventListener("DOMContentLoaded", sty => {sty = document.createElement("STYLE");sty.innerHTML = html(`.watched:not(ytd-thumbnail):not(.details):not(.metadata), .watched .yt-ui-ellipsis{ outline: .2em solid #aca; border-radius: 1em; background-color: #cec !important }html[dark] .watched:not(ytd-thumbnail):not(.details):not(.metadata), 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{ outline: .2em solid #040; border-radius: 1em; background-color: #030 !important }`);document.head.appendChild(sty);var nde = Node.prototype.dispatchEvent;Node.prototype.dispatchEvent = function(ev) {if (ev.type === "yt-service-request-completed") {clearTimeout(ut);ut = setTimeout(doProcessPage, contentLoadMarkDelay / 2)}return nde.apply(this, arguments)};});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) {ev.stopImmediatePropagation();ev.stopPropagation();ev.preventDefault();toggleMarker(ev.target);}}, true);if (markerMouseButtons.indexOf(1) >= 0) {addEventListener("contextmenu", (ev) => {if (ev.altKey) toggleMarker(ev.target);});}if (window["body-container"]) { //oldaddEventListener("spfdone", processPage);processPage();} else { //newvar 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", delayedProcessPage);addEventListener("spfprocess", delayedProcessPage);}GM_registerMenuCommand("Display History Statistics", () => {function sum(r, v) {return r + v;}function avg(arr, cnt) {arr = Object.values(arr);cnt = cnt || arr?.length;return arr?.length ? Math.round(arr.reduce(sum, 0) / cnt) : "(n/a)";}var t0 = Infinity, t1 = -Infinity, d0 = Infinity, d1 = -Infinity, ld = {}, e0, e1, o0, o1, sp, ad, am, ay;getHistory();Object.keys(watchedVideos.entries).forEach((k, t, a) => {t = new Date(watchedVideos.entries[k]);a = t.getTime();if (a < t0) t0 = a;if (a > t1) t1 = a;a = Math.floor(a / 86400000);if (a < d0) d0 = a;if (a > d1) d1 = a;ld[a] = (ld[a] || 0) + 1;});d1 -= d0 - 1;if (watchedVideos.index.length) {e0 = (o0 = new Date(t0)).toLocaleString();e1 = (o1 = new Date(t1)).toLocaleString();t1 = o1.getFullYear() - o0.getFullYear();if ((t0 = o1.getMonth() - o0.getMonth()) < 0) {t0 += 12;t1--}if ((d0 = o1.getDate() - o0.getDate()) < 0) {d0 += 30;if (--t0 < 0) {t0 += 12;t1 --}}sp = `${t1} years ${t0} months ${d0} days (${d1} days total)`;ad = avg(ld, d1);am = avg(ld, d1 / 30);ay = avg(ld, d1 / 365);} else e0 = e1 = sp = ad = am = ay = "(n/a)";alert(`\Number of entries: ${watchedVideos.index.length}Oldest entry: ${e0}Newest entry: ${e1}Time span: ${sp}Average viewed videos per day: ${ad}Average viewed videos per month: ${am}Average viewed videos per year: ${ay}History data size: ${JSON.stringify(watchedVideos).length} bytes\`);});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(o) {if (confirm(`Selected history data file contains ${o.index.length} entries.\n\nRestore from this data?`)) {if (mwyvrhm_ujs.checked) {mergeData(o);} else watchedVideos = o;GM_setValue("watchedVideos", JSON.stringify(watchedVideos));a.remove();doProcessPage();}}if (window.mwyvrh_ujs) return;(a = document.createElement("DIV")).id = "mwyvrh_ujs";a.innerHTML = html(`<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 .5em;background-color:#fff;cursor:auto;}#mwyvrht_ujs{margin-bottom:1rem;font-size:14pt;line-height:14pt;font-weight:bold}#mwyvrhmc_ujs{margin:.5em 0 1em 0;text-align:center}#mwyvrhi_ujs{display:block;margin:1rem auto .5rem auto;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.<div id="mwyvrhmc_ujs"><label><input id="mwyvrhm_ujs" type="checkbox" checked /> Merge history data instead of replace.</label></div><input id="mwyvrhi_ujs" type="file" multiple /></div>`);a.onclick = e => {(e.target === a) && a.remove();};(b = querySelector.call(a, "#mwyvrhi_ujs")).onchange = r => {r = new FileReader();r.onload = (o, t) => {if (o = parseData(r = r.r###lt)) { //parse as native formatif (o.index.length) {askRestore(o);} else alert("File doesn't contain any history entry.");} else if (o = parseYouTubeData(r)) { //parse as YouTube formatif (o.index.length) {askRestore(o);} else alert("File doesn't contain any history entry.");} else { //parse as URL listo = {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] || s[2]] = t;o.index.push(s[1] || s[2]);}});if (o.index.length) {askRestore(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();});})();