Mark Watched YouTube Videos

Add an indicator for watched videos on YouTube

当前为 2019-04-14 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Mark Watched YouTube Videos
  3. // @namespace MarkWatchedYouTubeVideos
  4. // @description Add an indicator for watched videos on YouTube
  5. // @version 1.0.16
  6. // @license AGPL v3
  7. // @author jcunews
  8. // @include https://www.youtube.com/*
  9. // @grant GM_getValue
  10. // @grant GM_setValue
  11. // @run-at document-start
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. //=== config start ===
  16. var maxWatchedVideoAge = 365; //number of days. set to zero to disable (not recommended)
  17. var pageLoadMarkDelay = 400; //number of milliseconds to wait before marking video items on page load phase (increase if slow network/browser)
  18. var contentLoadMarkDelay = 600; //number of milliseconds to wait before marking video items on content load phase (increase if slow network/browser)
  19. var markerMouseButtons = [0, 1]; //one or more mouse buttons to use for manual marker toggle. 0=left, 1=right, 2=middle. e.g.:
  20. //if `[0]`, only left button is used, which is ALT+LeftClick.
  21. //if `[1]`, only right button is used, which is ALT+RightClick.
  22. //if `[0,1]`, any left or right button can be used, which is: ALT+LeftClick or ALT+RightClick.
  23. //=== config end ===
  24.  
  25. var watchedVideos, ageMultiplier = 24 * 60 * 60 * 1000;
  26.  
  27. function getVideoId(url) {
  28. var vid = url.match(/\/watch(?:\?|.*?&)v=([^&]+)/);
  29. if (vid) vid = vid[1] || vid[2];
  30. return vid;
  31. }
  32.  
  33. function watched(vid, res) {
  34. res = -1;
  35. watchedVideos.some(function(v, i) {
  36. if (v.id === vid) {
  37. res = i;
  38. return true;
  39. } else return false;
  40. });
  41. return res;
  42. }
  43.  
  44. function processVideoItems(selector) {
  45. var items = document.querySelectorAll(selector), i, link;
  46. for (i = items.length-1; i >= 0; i--) {
  47. link = items[i].querySelector("A");
  48. if (link) {
  49. if (watched(getVideoId(link.href)) >= 0) {
  50. items[i].classList.add("watched");
  51. } else items[i].classList.remove("watched");
  52. }
  53. }
  54. }
  55.  
  56. function processAllVideoItems() {
  57. //home page
  58. processVideoItems(".yt-uix-shelfslider-list>.yt-shelf-grid-item");
  59. //subscriptions page
  60. processVideoItems(".multirow-shelf>.shelf-content>.yt-shelf-grid-item");
  61. //channel/user home page
  62. processVideoItems("#contents>.ytd-item-section-renderer>.ytd-newspaper-renderer,#items>.yt-horizontal-list-renderer"); //old
  63. processVideoItems("#contents>.ytd-channel-featured-content-renderer,#contents>.ytd-shelf-renderer>#grid-container>.ytd-expanded-shelf-contents-renderer"); //new
  64. //channel/user video page
  65. processVideoItems(".yt-uix-slider-list>.featured-content-item,#items>.ytd-grid-renderer");
  66. //channel/user playlist page
  67. processVideoItems(".expanded-shelf>.expanded-shelf-content-list>.expanded-shelf-content-item-wrapper,.ytd-playlist-video-renderer");
  68. //channel/user playlist item page
  69. processVideoItems(".pl-video-list .pl-video-table .pl-video,ytd-playlist-panel-video-renderer");
  70. //channel/user videos page
  71. processVideoItems(".channels-browse-content-grid>.channels-content-item");
  72. //channel/user search page
  73. if (/^\/(?:channel|user)\/.*?\/search/.test(location.pathname)) {
  74. processVideoItems(".ytd-browse #contents>.ytd-item-section-renderer"); //new
  75. }
  76. //search page
  77. processVideoItems("#results>.section-list .item-section>li,#browse-items-primary>.browse-list-item-container"); //old
  78. processVideoItems(".ytd-search #contents>.ytd-item-section-renderer"); //new
  79. //video page sidebar
  80. processVideoItems(".watch-sidebar-body>.video-list>.video-list-item,.playlist-videos-container>.playlist-videos-list>li"); //old
  81. processVideoItems(".ytd-compact-video-renderer"); //new
  82. }
  83.  
  84. function doProcessPage() {
  85. //get list of watched videos
  86. watchedVideos = GM_getValue("watchedVideos");
  87. if (!watchedVideos) {
  88. watchedVideos = "[]";
  89. GM_setValue("watchedVideos", watchedVideos);
  90. }
  91. try {
  92. watchedVideos = JSON.parse(watchedVideos);
  93. if (watchedVideos.length && (("object" !== typeof watchedVideos[0]) || !watchedVideos[0].id)) {
  94. watchedVideos = "[]";
  95. GM_setValue("watchedVideos", watchedVideos);
  96. }
  97. } catch(z) {
  98. watchedVideos = "[]";
  99. GM_setValue("watchedVideos", watchedVideos);
  100. }
  101.  
  102. //remove old watched video history
  103. var i = 0, now = (new Date()).valueOf();
  104. if (maxWatchedVideoAge > 0) {
  105. while (i < watchedVideos.length) {
  106. if (((now - watchedVideos.timestamp) / ageMultiplier) > maxWatchedVideoAge) {
  107. watchedVideos.splice(0, 1);
  108. } else break;
  109. }
  110. }
  111.  
  112. //check and remember current video
  113. var vid = getVideoId(location.href);
  114. if (vid && (watched(vid) < 0)) {
  115. watchedVideos.push({id: vid, timestamp: now});
  116. GM_setValue("watchedVideos", JSON.stringify(watchedVideos));
  117. }
  118.  
  119. //=== mark watched videos ===
  120. processAllVideoItems();
  121. }
  122.  
  123. function processPage() {
  124. setTimeout(doProcessPage, 200);
  125. }
  126.  
  127. var xhropen = XMLHttpRequest.prototype.open, xhrsend = XMLHttpRequest.prototype.send;
  128. XMLHttpRequest.prototype.open = function(method, url) {
  129. this.url_mwyv = url;
  130. return xhropen.apply(this, arguments);
  131. };
  132. XMLHttpRequest.prototype.send = function(method, url) {
  133. if ((/\/\w+_ajax\?|\/results\?search_query/).test(this.url_mwyv) && !this.listened_mwyv) {
  134. this.listened_mwyv = 1;
  135. this.addEventListener("load", function() {
  136. setTimeout(processPage, Math.floor(pageLoadMarkDelay / 2));
  137. });
  138. }
  139. return xhrsend.apply(this, arguments);
  140. };
  141.  
  142. addEventListener("DOMContentLoaded", function() {
  143. var style = document.createElement("STYLE");
  144. style.innerHTML = `
  145. .watched
  146. { background-color: #cec !important }
  147. .playlist-videos-container>.playlist-videos-list>li.watched,
  148. .playlist-videos-container>.playlist-videos-list>li.watched>a,
  149. .playlist-videos-container>.playlist-videos-list>li.watched .yt-ui-ellipsis
  150. { background-color: #030 !important }
  151. `;
  152. document.head.appendChild(style);
  153. });
  154.  
  155. var lastFocusState = document.hasFocus();
  156. addEventListener("blur", function() {
  157. lastFocusState = false;
  158. });
  159. addEventListener("focus", function() {
  160. if (!lastFocusState) processPage();
  161. lastFocusState = true;
  162. });
  163. addEventListener("click", function(ev, vid, i) {
  164. if ((markerMouseButtons.indexOf(ev.button) >= 0) && ev.altKey) {
  165. i = ev.target;
  166. if (i) {
  167. if (i.href) {
  168. vid = getVideoId(i.href);
  169. } else {
  170. i = i.parentNode;
  171. while (i) {
  172. if (i.tagName === "A") {
  173. vid = getVideoId(i.href);
  174. break;
  175. }
  176. i = i.parentNode;
  177. }
  178. }
  179. if (vid) {
  180. i = watched(vid);
  181. if (i >= 0) {
  182. watchedVideos.splice(i, 1);
  183. } else watchedVideos.push({id: vid, timestamp: (new Date()).valueOf()});
  184. GM_setValue("watchedVideos", JSON.stringify(watchedVideos));
  185. processAllVideoItems();
  186. }
  187. }
  188. }
  189. });
  190. if (markerMouseButtons.indexOf(1) >= 0) {
  191. addEventListener("contextmenu", function(ev, vid, i) {
  192. if (ev.altKey) {
  193. i = ev.target;
  194. if (i) {
  195. if (i.href) {
  196. vid = getVideoId(i.href);
  197. } else {
  198. i = i.parentNode;
  199. while (i) {
  200. if (i.tagName === "A") {
  201. vid = getVideoId(i.href);
  202. break;
  203. }
  204. i = i.parentNode;
  205. }
  206. }
  207. if (vid) {
  208. i = watched(vid);
  209. if (i >= 0) {
  210. watchedVideos.splice(i, 1);
  211. } else watchedVideos.push({id: vid, timestamp: (new Date()).valueOf()});
  212. GM_setValue("watchedVideos", JSON.stringify(watchedVideos));
  213. processAllVideoItems();
  214. }
  215. }
  216. }
  217. });
  218. }
  219. if (window["body-container"]) { //old
  220. addEventListener("spfdone", processPage);
  221. processPage();
  222. } else { //new
  223. var t=0;
  224. function pl() {
  225. clearTimeout(t);
  226. t = setTimeout(processPage, 300);
  227. }
  228. (function init(vm) {
  229. if (vm = document.getElementById("visibility-monitor")) {
  230. vm.addEventListener("viewport-load", pl);
  231. } else setTimeout(init, 100);
  232. })();
  233. (function init2(mh) {
  234. if (mh = document.getElementById("masthead")) {
  235. mh.addEventListener("yt-rendererstamper-finished", pl);
  236. } else setTimeout(init2, 100);
  237. })();
  238. addEventListener("load", function() {
  239. setTimeout(processPage, pageLoadMarkDelay);
  240. });
  241. addEventListener("spfprocess", function() {
  242. setTimeout(processPage, contentLoadMarkDelay);
  243. });
  244. }
  245. })();