Mark Watched YouTube Videos 1.3.51 (Modified)

Created Diff never expires
3 removals
Lines
Total
Removed
Words
Total
Removed
To continue using this feature, upgrade to
Diffchecker logo
Diffchecker Pro
460 lines
20 additions
Lines
Total
Added
Words
Total
Added
To continue using this feature, upgrade to
Diffchecker logo
Diffchecker Pro
470 lines
// ==UserScript==
// ==UserScript==
// @name Mark Watched YouTube Videos
// @name Mark Watched YouTube Videos
// @namespace MarkWatchedYouTubeVideos
// @namespace MarkWatchedYouTubeVideos
// @version 1.3.51
// @version 1.3.51
// @license AGPL v3
// @license AGPL v3
// @author jcunews
// @author jcunews
// @description Add an indicator for watched videos on YouTube. Use GM menus to display history statistics, backup history, and restore/merge history.
// @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
// @website https://greasyfork.org/en/users/85671-jcunews
// @match *://www.youtube.com/*
// @match *://www.youtube.com/*
// @grant GM_getValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_setValue
// @grant unsafeWindow
// @grant unsafeWindow
// @run-at document-start
// @run-at document-start
// ==/UserScript==
// ==/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.
- 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).
- 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 ===
//=== config start ===
var maxWatchedVideoAge = 5 * 365; //number of days. set to zero to disable (not recommended)
var maxWatchedVideoAge = 0; // 5 * 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 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.:
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 `[0]`, only left button is used, which is ALT+LeftClick.
//if `[1]`, only right button is used, which is ALT+RightClick.
//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.
//if `[0,1]`, any left or right button can be used, which is: ALT+LeftClick or ALT+RightClick.
//=== config end ===
//=== config end ===


var
var
watchedVideos, ageMultiplier = 24 * 60 * 60 * 1000, xu = /\/watch(?:\?|.*?&)v=([^&]+)|\/shorts\/([^\/\?]+)/,
watchedVideos, ageMultiplier = 24 * 60 * 60 * 1000, xu = /\/watch(?:\?|.*?&)v=([^&]+)|\/shorts\/([^\/\?]+)/,
querySelector = Element.prototype.querySelector, querySelectorAll = Element.prototype.querySelectorAll;
querySelector = Element.prototype.querySelector, querySelectorAll = Element.prototype.querySelectorAll;


function getVideoId(url) {
function getVideoId(url) {
var vid = url.match(xu);
var vid = url.match(xu);
if (vid) vid = vid[1] || vid[2];
if (vid) vid = vid[1] || vid[2];
return vid;
return vid;
}
}


function watched(vid) {
function watched(vid) {
return !!watchedVideos.entries[vid];
return !!watchedVideos.entries[vid];
}
}


function processVideoItems(selector) {
function processVideoItems(selector) {
var items = document.querySelectorAll(selector), i, link;
var items = document.querySelectorAll(selector), i, link;
for (i = items.length-1; i >= 0; i--) {
for (i = items.length-1; i >= 0; i--) {
if (link = querySelector.call(items[i], "A")) {
if (link = querySelector.call(items[i], "A")) {
if (watched(getVideoId(link.href))) {
if (watched(getVideoId(link.href))) {
items[i].classList.add("watched");
items[i].classList.add("watched");
} else items[i].classList.remove("watched");
} else items[i].classList.remove("watched");
}
}
}
}
}
}


function processAllVideoItems() {
function processAllVideoItems() {
//home page
//home page
processVideoItems(`.yt-uix-shelfslider-list>.yt-shelf-grid-item`);
processVideoItems(`.yt-uix-shelfslider-list>.yt-shelf-grid-item`);
processVideoItems(`
processVideoItems(`
#contents.ytd-rich-grid-renderer>ytd-rich-item-renderer,
#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-shelf-renderer ytd-rich-item-renderer.ytd-rich-shelf-renderer,
#contents.ytd-rich-grid-renderer>ytd-rich-grid-row ytd-rich-grid-media`);
#contents.ytd-rich-grid-renderer>ytd-rich-grid-row ytd-rich-grid-media`);
//subscriptions page
//subscriptions page
processVideoItems(`.multirow-shelf>.shelf-content>.yt-shelf-grid-item`);
processVideoItems(`.multirow-shelf>.shelf-content>.yt-shelf-grid-item`);
//history:watch page
//history:watch page
processVideoItems(`ytd-section-list-renderer[page-subtype="history"] .ytd-item-section-renderer>ytd-video-renderer`);
processVideoItems(`ytd-section-list-renderer[page-subtype="history"] .ytd-item-section-renderer>ytd-video-renderer`);
//channel/user home page
//channel/user home page
processVideoItems(`
processVideoItems(`
#contents>.ytd-item-section-renderer>.ytd-newspaper-renderer,
#contents>.ytd-item-section-renderer>.ytd-newspaper-renderer,
#items>.yt-horizontal-list-renderer`); //old
#items>.yt-horizontal-list-renderer`); //old
processVideoItems(`
processVideoItems(`
#contents>.ytd-channel-featured-content-renderer,
#contents>.ytd-channel-featured-content-renderer,
#contents>.ytd-shelf-renderer>#grid-container>.ytd-expanded-shelf-contents-renderer`); //new
#contents>.ytd-shelf-renderer>#grid-container>.ytd-expanded-shelf-contents-renderer`); //new
//channel/user video page
//channel/user video page
processVideoItems(`
processVideoItems(`
.yt-uix-slider-list>.featured-content-item,
.yt-uix-slider-list>.featured-content-item,
.channels-browse-content-grid>.channels-content-item,
.channels-browse-content-grid>.channels-content-item,
#items>.ytd-grid-renderer`);
#items>.ytd-grid-renderer`);
//channel/user playlist page
//channel/user playlist page
processVideoItems(`
processVideoItems(`
.expanded-shelf>.expanded-shelf-content-list>.expanded-shelf-content-item-wrapper,
.expanded-shelf>.expanded-shelf-content-list>.expanded-shelf-content-item-wrapper,
.ytd-playlist-video-renderer`);
.ytd-playlist-video-renderer`);
//channel/user playlist item page
//channel/user playlist item page
processVideoItems(`
processVideoItems(`
.pl-video-list .pl-video-table .pl-video,
.pl-video-list .pl-video-table .pl-video,
ytd-playlist-panel-video-renderer`);
ytd-playlist-panel-video-renderer`);
//channel/user search page
//channel/user search page
if (/^\/(?:c|channel|user)\/.*?\/search/.test(location.pathname)) {
if (/^\/(?:c|channel|user)\/.*?\/search/.test(location.pathname)) {
processVideoItems(`.ytd-browse #contents>.ytd-item-section-renderer`); //new
processVideoItems(`.ytd-browse #contents>.ytd-item-section-renderer`); //new
}
}
//search page
//search page
processVideoItems(`
processVideoItems(`
#results>.section-list .item-section>li,
#results>.section-list .item-section>li,
#browse-items-primary>.browse-list-item-container`); //old
#browse-items-primary>.browse-list-item-container`); //old
processVideoItems(`
processVideoItems(`
.ytd-search #contents>ytd-video-renderer,
.ytd-search #contents>ytd-video-renderer,
.ytd-search #contents>ytd-playlist-renderer,
.ytd-search #contents>ytd-playlist-renderer,
.ytd-search #items>ytd-video-renderer`); //new
.ytd-search #items>ytd-video-renderer`); //new
//video page
//video page
processVideoItems(`
processVideoItems(`
.watch-sidebar-body>.video-list>.video-list-item,
.watch-sidebar-body>.video-list>.video-list-item,
.playlist-videos-container>.playlist-videos-list>li`); //old
.playlist-videos-container>.playlist-videos-list>li`); //old
processVideoItems(`
processVideoItems(`
.ytd-compact-video-renderer,
.ytd-compact-video-renderer,
.ytd-compact-radio-renderer`); //new
.ytd-compact-radio-renderer`); //new
}
}


function addHistory(vid, time, noSave, i) {
function addHistory(vid, time, noSave, i) {
if (!watchedVideos.entries[vid]) {
if (!watchedVideos.entries[vid]) {
watchedVideos.index.push(vid);
watchedVideos.index.push(vid);
} else {
} else {
i = watchedVideos.index.indexOf(vid);
i = watchedVideos.index.indexOf(vid);
if (i >= 0) watchedVideos.index.push(watchedVideos.index.splice(i, 1)[0])
if (i >= 0) watchedVideos.index.push(watchedVideos.index.splice(i, 1)[0])
}
}
watchedVideos.entries[vid] = time;
watchedVideos.entries[vid] = time;
if (!noSave) GM_setValue("watchedVideos", JSON.stringify(watchedVideos));
if (!noSave) GM_setValue("watchedVideos", JSON.stringify(watchedVideos));
}
}


function delHistory(index, noSave) {
function delHistory(index, noSave) {
delete watchedVideos.entries[watchedVideos.index[index]];
delete watchedVideos.entries[watchedVideos.index[index]];
watchedVideos.index.splice(index, 1);
watchedVideos.index.splice(index, 1);
if (!noSave) GM_setValue("watchedVideos", JSON.stringify(watchedVideos));
if (!noSave) GM_setValue("watchedVideos", JSON.stringify(watchedVideos));
}
}


var dc, ut;
var dc, ut;
function parseData(s, a, i, j, z) {
function parseData(s, a, i, j, z) {
try {
try {
dc = false;
dc = false;
s = JSON.parse(s);
s = JSON.parse(s);
//convert to new format if old format.
//convert to new format if old format.
//old: [{id:<strVID>, timestamp:<numDate>}, ...]
//old: [{id:<strVID>, timestamp:<numDate>}, ...]
//new: {entries:{<stdVID>:<numDate>, ...}, index:[<strVID>, ...]}
//new: {entries:{<stdVID>:<numDate>, ...}, index:[<strVID>, ...]}
if (Array.isArray(s) && (!s.length || (("object" === typeof s[0]) && s[0].id && s[0].timestamp))) {
if (Array.isArray(s) && (!s.length || (("object" === typeof s[0]) && s[0].id && s[0].timestamp))) {
a = s;
a = s;
s = {entries: {}, index: []};
s = {entries: {}, index: []};
a.forEach(o => {
a.forEach(o => {
s.entries[o.id] = o.timestamp;
s.entries[o.id] = o.timestamp;
s.index.push(o.id);
s.index.push(o.id);
});
});
} else if (("object" !== typeof s) || ("object" !== typeof s.entries) || !Array.isArray(s.index)) return null;
} else if (("object" !== typeof s) || ("object" !== typeof s.entries) || !Array.isArray(s.index)) return null;
//reconstruct index if broken
//reconstruct index if broken
if (s.index.length !== (a = Object.keys(s.entries)).length) {
if (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]);
s.index = a.map(k => [k, s.entries[k]]).sort((x, y) => x[1] - y[1]).map(v => v[0]);
dc = true;
dc = true;
}
}
return s;
return s;
} catch(z) {
} catch(z) {
return null;
return null;
}
}
}
}


function parseYouTubeData(s, a) {
function parseYouTubeData(s, a) {
try {
try {
s = JSON.parse(s);
s = JSON.parse(s);
//convert to native format if YouTube format.
//convert to native format if YouTube format.
//old: [{titleUrl:<strUrl>, time:<strIsoDate>}, ...] (excludes irrelevant properties)
//old: [{titleUrl:<strUrl>, time:<strIsoDate>}, ...] (excludes irrelevant properties)
//new: {entries:{<stdVID>:<numDate>, ...}, index:[<strVID>, ...]}
//new: {entries:{<stdVID>:<numDate>, ...}, index:[<strVID>, ...]}
if (Array.isArray(s) && (!s.length || (("object" === typeof s[0]) && s[0].titleUrl && s[0].time))) {
if (Array.isArray(s) && (!s.length || (("object" === typeof s[0]) && s[0].titleUrl && s[0].time))) {
a = s;
a = s;
s = {entries: {}, index: []};
s = {entries: {}, index: []};
a.forEach((o, m, t) => {
a.forEach((o, m, t) => {
if (o.titleUrl && (m = o.titleUrl.match(xu))) {
if (o.titleUrl && (m = o.titleUrl.match(xu))) {
if (isNaN(t = (new Date(o.time)).getTime())) t = (new Date()).getTime();
if (isNaN(t = (new Date(o.time)).getTime())) t = (new Date()).getTime();
s.entries[m[1] || m[2]] = t;
s.entries[m[1] || m[2]] = t;
s.index.push(m[1] || m[2]);
s.index.push(m[1] || m[2]);
}
}
});
});
s.index.reverse();
s.index.reverse();
return s;
return s;
} else return null;
} else return null;
} catch(a) {
} catch(a) {
return null;
return null;
}
}
}
}


function mergeData(o, a) {
function mergeData(o, a) {
o.index.forEach(i => {
o.index.forEach(i => {
if (watchedVideos.entries[i]) {
if (watchedVideos.entries[i]) {
if (watchedVideos.entries[i] < o.entries[i]) watchedVideos.entries[i] = o.entries[i];
if (watchedVideos.entries[i] < o.entries[i]) watchedVideos.entries[i] = o.entries[i];
} else watchedVideos.entries[i] = o.entries[i];
} else watchedVideos.entries[i] = o.entries[i];
});
});
a = Object.keys(watchedVideos.entries);
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]);
watchedVideos.index = a.map(k => [k, watchedVideos.entries[k]]).sort((x, y) => x[1] - y[1]).map(v => v[0]);
}
}


function getHistory(a, b) {
function getHistory(a, b) {
a = GM_getValue("watchedVideos");
a = GM_getValue("watchedVideos");
if (a === undefined) {
if (a === undefined) {
a = '{"entries": {}, "index": []}';
a = '{"entries": {}, "index": []}';
} else if ("object" === typeof a) a = JSON.stringify(a);
} else if ("object" === typeof a) a = JSON.stringify(a);
if (b = parseData(a)) {
if (b = parseData(a)) {
watchedVideos = b;
watchedVideos = b;
if (dc) b = JSON.stringify(b);
if (dc) b = JSON.stringify(b);
} else b = JSON.stringify(watchedVideos = {entries: {}, index: []});
} else b = JSON.stringify(watchedVideos = {entries: {}, index: []});
GM_setValue("watchedVideos", b);
GM_setValue("watchedVideos", b);
}
}


function doProcessPage() {
function doProcessPage() {
//get list of watched videos
//get list of watched videos
getHistory();
getHistory();


//remove old watched video history
//remove old watched video history
var now = (new Date()).valueOf(), changed, vid;
var now = (new Date()).valueOf(), changed, vid;
if (maxWatchedVideoAge > 0) {
if (maxWatchedVideoAge > 0) {
while (watchedVideos.index.length) {
while (watchedVideos.index.length) {
if (((now - watchedVideos.entries[watchedVideos.index[0]]) / ageMultiplier) > maxWatchedVideoAge) {
if (((now - watchedVideos.entries[watchedVideos.index[0]]) / ageMultiplier) > maxWatchedVideoAge) {
delHistory(0, false);
delHistory(0, false);
changed = true;
changed = true;
} else break;
} else break;
}
}
if (changed) GM_setValue("watchedVideos", JSON.stringify(watchedVideos));
if (changed) GM_setValue("watchedVideos", JSON.stringify(watchedVideos));
}
}


//check and remember current video
//check and remember current video
if ((vid = getVideoId(location.href)) && !watched(vid)) addHistory(vid, now);
if ((vid = getVideoId(location.href)) && !watched(vid)) addHistory(vid, now);


//mark watched videos
//mark watched videos
processAllVideoItems();
processAllVideoItems();
}
}


function processPage() {
function processPage() {
setTimeout(doProcessPage, Math.floor(contentLoadMarkDelay / 2));
setTimeout(doProcessPage, Math.floor(contentLoadMarkDelay / 2));
}
}


function delayedProcessPage() {
function delayedProcessPage() {
setTimeout(doProcessPage, contentLoadMarkDelay);
setTimeout(doProcessPage, contentLoadMarkDelay);
}
}


function toggleMarker(ele, i) {
function toggleMarker(ele, i) {
if (ele) {
if (ele) {
if (ele.href) {
if (ele.href) {
i = getVideoId(ele.href);
i = getVideoId(ele.href);
} else {
} else {
while (ele) {
while (ele) {
while (ele && (!ele.__data || !ele.__data.data || !ele.__data.data.videoId)) ele = ele.__dataHost || ele.parentNode;
while (ele && (!ele.__data || !ele.__data.data || !ele.__data.data.videoId)) ele = ele.__dataHost || ele.parentNode;
if (ele) {
if (ele) {
i = ele.__data.data.videoId;
i = ele.__data.data.videoId;
break
break
}
}
}
}
}
}
if (i) {
if (i) {
if ((ele = watchedVideos.index.indexOf(i)) >= 0) {
if ((ele = watchedVideos.index.indexOf(i)) >= 0) {
delHistory(ele);
delHistory(ele);
} else addHistory(i, (new Date()).valueOf());
} else addHistory(i, (new Date()).valueOf());
processAllVideoItems();
processAllVideoItems();
}
}
}
}
}
}


var rxListUrl = /\/\w+_ajax\?|\/results\?search_query|\/v1\/(browse|next|search)\?/;
var rxListUrl = /\/\w+_ajax\?|\/results\?search_query|\/v1\/(browse|next|search)\?/;
var xhropen = XMLHttpRequest.prototype.open, xhrsend = XMLHttpRequest.prototype.send;
var xhropen = XMLHttpRequest.prototype.open, xhrsend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function(method, url) {
XMLHttpRequest.prototype.open = function(method, url) {
this.url_mwyv = url;
this.url_mwyv = url;
return xhropen.apply(this, arguments);
return xhropen.apply(this, arguments);
};
};
XMLHttpRequest.prototype.send = function(method, url) {
XMLHttpRequest.prototype.send = function(method, url) {
if (rxListUrl.test(this.url_mwyv) && !this.listened_mwyv) {
if (rxListUrl.test(this.url_mwyv) && !this.listened_mwyv) {
this.listened_mwyv = 1;
this.listened_mwyv = 1;
this.addEventListener("load", delayedProcessPage);
this.addEventListener("load", delayedProcessPage);
}
}
return xhrsend.apply(this, arguments);
return xhrsend.apply(this, arguments);
};
};


var fetch_ = unsafeWindow.fetch;
var fetch_ = unsafeWindow.fetch;
unsafeWindow.fetch = function(opt) {
unsafeWindow.fetch = function(opt) {
let url = opt.url || opt;
let url = opt.url || opt;
if (rxListUrl.test(opt.url || opt)) {
if (rxListUrl.test(opt.url || opt)) {
return fetch_.apply(this, arguments).finally(delayedProcessPage);
return fetch_.apply(this, arguments).finally(delayedProcessPage);
} else return fetch_.apply(this, arguments);
} else return fetch_.apply(this, arguments);
};
};


addEventListener("DOMContentLoaded", sty => {
addEventListener("DOMContentLoaded", sty => {
sty = document.createElement("STYLE");
sty = document.createElement("STYLE");
sty.innerHTML = `
sty.innerHTML = `
.watched:not(ytd-thumbnail):not(.details):not(.metadata), .watched .yt-ui-ellipsis
.watched:not(ytd-thumbnail):not(.details):not(.metadata), .watched .yt-ui-ellipsis {
{ outline: .2em solid #aca; background-color: #cec !important }
/* outline: .2em solid #aca; */
/* background-color: #cec !important */
opacity: 0.25;
-webkit-filter: grayscale(1);
filter: grayscale(1);
}
html[dark] .watched, html[dark] .watched .yt-ui-ellipsis,
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,
.playlist-videos-container>.playlist-videos-list>li.watched>a,
.playlist-videos-container>.playlist-videos-list>li.watched>a,
.playlist-videos-container>.playlist-videos-list>li.watched .yt-ui-ellipsis
.playlist-videos-container>.playlist-videos-list>li.watched .yt-ui-ellipsis {
{ outline: .2em solid #030; background-color: #030 !important }`;
/* outline: .2em solid #030; */
/* background-color: #030 !important */
opacity: 0.25;
-webkit-filter: grayscale(1);
filter: grayscale(1);
}`;
document.head.appendChild(sty);
document.head.appendChild(sty);
var nde = Node.prototype.dispatchEvent;
var nde = Node.prototype.dispatchEvent;
Node.prototype.dispatchEvent = function(ev) {
Node.prototype.dispatchEvent = function(ev) {
if (ev.type === "yt-service-request-completed") {
if (ev.type === "yt-service-request-completed") {
clearTimeout(ut);
clearTimeout(ut);
ut = setTimeout(doProcessPage, contentLoadMarkDelay / 2)
ut = setTimeout(doProcessPage, contentLoadMarkDelay / 2)
}
}
return nde.apply(this, arguments)
return nde.apply(this, arguments)
};
};
});
});


var lastFocusState = document.hasFocus();
var lastFocusState = document.hasFocus();
addEventListener("blur", () => {
addEventListener("blur", () => {
lastFocusState = false;
lastFocusState = false;
});
});
addEventListener("focus", () => {
addEventListener("focus", () => {
if (!lastFocusState) processPage();
if (!lastFocusState) processPage();
lastFocusState = true;
lastFocusState = true;
});
});
addEventListener("click", (ev) => {
addEventListener("click", (ev) => {
if ((markerMouseButtons.indexOf(ev.button) >= 0) && ev.altKey) {
if ((markerMouseButtons.indexOf(ev.button) >= 0) && ev.altKey) {
ev.stopImmediatePropagation();
ev.stopImmediatePropagation();
ev.stopPropagation();
ev.stopPropagation();
ev.preventDefault();
ev.preventDefault();
toggleMarker(ev.target);
toggleMarker(ev.target);
}
}
}, true);
}, true);


if (markerMouseButtons.indexOf(1) >= 0) {
if (markerMouseButtons.indexOf(1) >= 0) {
addEventListener("contextmenu", (ev) => {
addEventListener("contextmenu", (ev) => {
if (ev.altKey) toggleMarker(ev.target);
if (ev.altKey) toggleMarker(ev.target);
});
});
}
}
if (window["body-container"]) { //old
if (window["body-container"]) { //old
addEventListener("spfdone", processPage);
addEventListener("spfdone", processPage);
processPage();
processPage();
} else { //new
} else { //new
var t = 0;
var t = 0;
function pl() {
function pl() {
clearTimeout(t);
clearTimeout(t);
t = setTimeout(processPage, 300);
t = setTimeout(processPage, 300);
}
}
(function init(vm) {
(function init(vm) {
if (vm = document.getElementById("visibility-monitor")) {
if (vm = document.getElementById("visibility-monitor")) {
vm.addEventListener("viewport-load", pl);
vm.addEventListener("viewport-load", pl);
} else setTimeout(init, 100);
} else setTimeout(init, 100);
})();
})();
(function init2(mh) {
(function init2(mh) {
if (mh = document.getElementById("masthead")) {
if (mh = document.getElementById("masthead")) {
mh.addEventListener("yt-rendererstamper-finished", pl);
mh.addEventListener("yt-rendererstamper-finished", pl);
} else setTimeout(init2, 100);
} else setTimeout(init2, 100);
})();
})();
addEventListener("load", delayedProcessPage);
addEventListener("load", delayedProcessPage);
addEventListener("spfprocess", delayedProcessPage);
addEventListener("spfprocess", delayedProcessPage);
}
}


GM_registerMenuCommand("Display History Statistics", () => {
GM_registerMenuCommand("Display History Statistics", () => {
function sum(r, v) {
function sum(r, v) {
return r + v;
return r + v;
}
}
function avg(arr) {
function avg(arr) {
return arr && arr.length ? Math.round(arr.reduce(sum, 0) / arr.length) : "(n/a)";
return arr && arr.length ? Math.round(arr.reduce(sum, 0) / arr.length) : "(n/a)";
}
}
var pd, pm, py, ld = [], lm = [], ly = [];
var pd, pm, py, ld = [], lm = [], ly = [];
getHistory();
getHistory();
Object.keys(watchedVideos.entries).forEach((k, t) => {
Object.keys(watchedVideos.entries).forEach((k, t) => {
t = new Date(watchedVideos.entries[k]);
t = new Date(watchedVideos.entries[k]);
if (!pd || (pd !== t.getDate())) {
if (!pd || (pd !== t.getDate())) {
ld.push(1);
ld.push(1);
pd = t.getDate();
pd = t.getDate();
} else ld[ld.length - 1]++;
} else ld[ld.length - 1]++;
if (!pm || (pm !== (t.getMonth() + 1))) {
if (!pm || (pm !== (t.getMonth() + 1))) {
lm.push(1);
lm.push(1);
pm = t.getMonth() + 1;
pm = t.getMonth() + 1;
} else lm[lm.length - 1]++;
} else lm[lm.length - 1]++;
if (!py || (py !== t.getFullYear())) {
if (!py || (py !== t.getFullYear())) {
ly.push(1);
ly.push(1);
py = t.getFullYear();
py = t.getFullYear();
} else ly[ly.length - 1]++;
} else ly[ly.length - 1]++;
});
});
if (watchedVideos.index.length) {
if (watchedVideos.index.length) {
pd = (new Date(watchedVideos.entries[watchedVideos.index[0]])).toLocaleString();
pd = (new Date(watchedVideos.entries[watchedVideos.index[0]])).toLocaleString();
pm = (new Date(watchedVideos.entries[watchedVideos.index[watchedVideos.index.length - 1]])).toLocaleString();
pm = (new Date(watchedVideos.entries[watchedVideos.index[watchedVideos.index.length - 1]])).toLocaleString();
} else {
} else {
pd = "(n/a)";
pd = "(n/a)";
pm = "(n/a)";
pm = "(n/a)";
}
}
alert(`\
alert(`\
Number of entries: ${watchedVideos.index.length}
Number of entries: ${watchedVideos.index.length}
Oldest entry: ${pd}
Oldest entry: ${pd}
Newest entry: ${pm}
Newest entry: ${pm}


Average viewed videos per day: ${avg(ld)}
Average viewed videos per day: ${avg(ld)}
Average viewed videos per month: ${avg(lm)}
Average viewed videos per month: ${avg(lm)}
Average viewed videos per year: ${avg(ly)}\
Average viewed videos per year: ${avg(ly)}\
`);
`);
});
});


GM_registerMenuCommand("Backup History Data", (a, b) => {
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"}));
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.download = `MarkWatchedYouTubeVideos_${(new Date()).toISOString()}.json`;
a.click();
a.click();
a.remove();
a.remove();
URL.revokeObjectURL(a.href);
URL.revokeObjectURL(a.href);
});
});


GM_registerMenuCommand("Restore History Data", (a, b) => {
GM_registerMenuCommand("Restore History Data", (a, b) => {
function askRestore(o) {
function askRestore(o) {
if (confirm(`Selected history data file contains ${o.index.length} entries.\n\nRestore from this data?`)) {
if (confirm(`Selected history data file contains ${o.index.length} entries.\n\nRestore from this data?`)) {
if (mwyvrhm_ujs.checked) {
if (mwyvrhm_ujs.checked) {
mergeData(o);
mergeData(o);
} else watchedVideos = o;
} else watchedVideos = o;
GM_setValue("watchedVideos", JSON.stringify(watchedVideos));
GM_setValue("watchedVideos", JSON.stringify(watchedVideos));
a.remove();
a.remove();
doProcessPage();
doProcessPage();
}
}
}
}
if (window.mwyvrh_ujs) return;
if (window.mwyvrh_ujs) return;
(a = document.createElement("DIV")).id = "mwyvrh_ujs";
(a = document.createElement("DIV")).id = "mwyvrh_ujs";
a.innerHTML = `<style>
a.innerHTML = `<style>
#mwyvrh_ujs{
#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);
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;
color:#000;font-family:sans-serif;font-size:12pt;line-height:12pt;font-weight:normal;cursor:pointer;
}
}
#mwyvrhb_ujs{
#mwyvrhb_ujs{
margin:auto;border:.3rem solid #007;border-radius:.3rem;padding:.5rem .5em;background-color:#fff;cursor:auto;
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}
#mwyvrht_ujs{margin-bottom:1rem;font-size:14pt;line-height:14pt;font-weight:bold}
#mwyvrhmc_ujs{margin:.5em 0 1em 0;text-align:center}
#mwyvrhmc_ujs{margin:.5em 0 1em 0;text-align:center}
#mwyvrhi_ujs{display:block;margin:1rem auto .5rem auto;overflow:hidden}
#mwyvrhi_ujs{display:block;margin:1rem auto .5rem auto;overflow:hidden}
</style>
</style>
<div id="mwyvrhb_ujs">
<div id="mwyvrhb_ujs">
<div id="mwyvrht_ujs">Mark Watched YouTube Videos</div>
<div id="mwyvrht_ujs">Mark Watched YouTube Videos</div>
Please select a file to restore history data from.
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>
<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 />
<input id="mwyvrhi_ujs" type="file" multiple />
</div>`;
</div>`;
a.onclick = e => {
a.onclick = e => {
(e.target === a) && a.remove();
(e.target === a) && a.remove();
};
};
(b = querySelector.call(a, "#mwyvrhi_ujs")).onchange = r => {
(b = querySelector.call(a, "#mwyvrhi_ujs")).onchange = r => {
r = new FileReader();
r = new FileReader();
r.onload = (o, t) => {
r.onload = (o, t) => {
if (o = parseData(r = r.result)) { //parse as native format
if (o = parseData(r = r.result)) { //parse as native format
if (o.index.length) {
if (o.index.length) {
askRestore(o);
askRestore(o);
} else alert("File doesn't contain any history entry.");
} else alert("File doesn't contain any history entry.");
} else if (o = parseYouTubeData(r)) { //parse as YouTube format
} else if (o = parseYouTubeData(r)) { //parse as YouTube format
if (o.index.length) {
if (o.index.length) {
askRestore(o);
askRestore(o);
} else alert("File doesn't contain any history entry.");
} else alert("File doesn't contain any history entry.");
} else { //parse as URL list
} else { //parse as URL list
o = {entries: {}, index: []};
o = {entries: {}, index: []};
t = (new Date()).getTime();
t = (new Date()).getTime();
r = r.replace(/\r/g, "").split("\n");
r = r.replace(/\r/g, "").split("\n");
while (r.length && !r[0].trim()) r.shift();
while (r.length && !r[0].trim()) r.shift();
if (r.length && xu.test(r[0])) {
if (r.length && xu.test(r[0])) {
r.forEach(s => {
r.forEach(s => {
if (s = s.match(xu)) {
if (s = s.match(xu)) {
o.entries[s[1] || s[2]] = t;
o.entries[s[1] || s[2]] = t;
o.index.push(s[1] || s[2]);
o.index.push(s[1] || s[2]);
}
}
});
});
if (o.index.length) {
if (o.index.length) {
askRestore(o);
askRestore(o);
} else alert("File doesn't contain any history entry.");
} else alert("File doesn't contain any history entry.");
} else alert("Invalid history data file.");
} else alert("Invalid history data file.");
}
}
};
};
r.readAsText(b.files[0]);
r.readAsText(b.files[0]);
};
};
document.documentElement.appendChild(a);
document.documentElement.appendChild(a);
b.click();
b.click();
});
});
})();
})();