// ==UserScript==
// @name Mark Watched YouTube Videos
// @namespace MarkWatchedYouTubeVideos
// @description Add an indicator for watched videos on YouTube
// @version 1.0.18
// @license AGPL v3
// @author jcunews
// @website https://gf.qytechs.cn/en/users/85671-jcunews
// @include https://www.youtube.com/*
// @grant GM_getValue
// @grant GM_setValue
// @run-at document-start
// ==/UserScript==
(function() {
//=== config start ===
var maxWatchedVideoAge = 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, res) {
res = -1;
watchedVideos.some(function(v, i) {
if (v.id === vid) {
res = i;
return true;
} else return false;
});
return res;
}
function processVideoItems(selector) {
var items = document.querySelectorAll(selector), i, link;
for (i = items.length-1; i >= 0; i--) {
link = items[i].querySelector("A");
if (link) {
if (watched(getVideoId(link.href)) >= 0) {
items[i].classList.add("watched");
} else items[i].classList.remove("watched");
}
}
}
function processAllVideoItems() {
//home page
processVideoItems(".yt-uix-shelfslider-list>.yt-shelf-grid-item");
//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 doProcessPage() {
//get list of watched videos
watchedVideos = GM_getValue("watchedVideos");
if (!watchedVideos) {
watchedVideos = "[]";
GM_setValue("watchedVideos", watchedVideos);
}
try {
watchedVideos = JSON.parse(watchedVideos);
if (watchedVideos.length && (("object" !== typeof watchedVideos[0]) || !watchedVideos[0].id)) {
watchedVideos = "[]";
GM_setValue("watchedVideos", watchedVideos);
}
} catch(z) {
watchedVideos = "[]";
GM_setValue("watchedVideos", watchedVideos);
}
//remove old watched video history
var i = 0, now = (new Date()).valueOf();
if (maxWatchedVideoAge > 0) {
while (i < watchedVideos.length) {
if (((now - watchedVideos.timestamp) / ageMultiplier) > maxWatchedVideoAge) {
watchedVideos.splice(0, 1);
} else break;
}
}
//check and remember current video
var vid = getVideoId(location.href);
if (vid && (watched(vid) < 0)) {
watchedVideos.push({id: vid, timestamp: now});
GM_setValue("watchedVideos", JSON.stringify(watchedVideos));
}
//=== mark watched videos ===
processAllVideoItems();
}
function processPage() {
setTimeout(doProcessPage, 200);
}
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", function() {
setTimeout(processPage, Math.floor(pageLoadMarkDelay / 2));
});
}
return xhrsend.apply(this, arguments);
};
addEventListener("DOMContentLoaded", function() {
var style = document.createElement("STYLE");
style.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(style);
});
var lastFocusState = document.hasFocus();
addEventListener("blur", function() {
lastFocusState = false;
});
addEventListener("focus", function() {
if (!lastFocusState) processPage();
lastFocusState = true;
});
addEventListener("click", function(ev, vid, i) {
if ((markerMouseButtons.indexOf(ev.button) >= 0) && ev.altKey) {
i = ev.target;
if (i) {
if (i.href) {
vid = getVideoId(i.href);
} else {
i = i.parentNode;
while (i) {
if (i.tagName === "A") {
vid = getVideoId(i.href);
break;
}
i = i.parentNode;
}
}
if (vid) {
i = watched(vid);
if (i >= 0) {
watchedVideos.splice(i, 1);
} else watchedVideos.push({id: vid, timestamp: (new Date()).valueOf()});
GM_setValue("watchedVideos", JSON.stringify(watchedVideos));
processAllVideoItems();
}
}
}
});
if (markerMouseButtons.indexOf(1) >= 0) {
addEventListener("contextmenu", function(ev, vid, i) {
if (ev.altKey) {
i = ev.target;
if (i) {
if (i.href) {
vid = getVideoId(i.href);
} else {
i = i.parentNode;
while (i) {
if (i.tagName === "A") {
vid = getVideoId(i.href);
break;
}
i = i.parentNode;
}
}
if (vid) {
i = watched(vid);
if (i >= 0) {
watchedVideos.splice(i, 1);
} else watchedVideos.push({id: vid, timestamp: (new Date()).valueOf()});
GM_setValue("watchedVideos", JSON.stringify(watchedVideos));
processAllVideoItems();
}
}
}
});
}
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", function() {
setTimeout(processPage, pageLoadMarkDelay);
});
addEventListener("spfprocess", function() {
setTimeout(processPage, contentLoadMarkDelay);
});
}
})();