Mark Watched YouTube Videos

Add an indicator for watched videos on YouTube

目前為 2019-04-14 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name        Mark Watched YouTube Videos
// @namespace   MarkWatchedYouTubeVideos
// @description Add an indicator for watched videos on YouTube
// @version     1.0.16
// @license     AGPL v3
// @author      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
  { background-color: #cec !important }
.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);
    });
  }
})();