Mark Watched YouTube Videos

Add an indicator for watched videos on YouTube

当前为 2018-02-15 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        Mark Watched YouTube Videos
// @namespace   MarkWatchedYouTubeVideos
// @description Add an indicator for watched videos on YouTube
// @version     1.0.7
// @license     AGPL v3
// @author      jcunews
// @include     https://www.youtube.com/*
// @grant       GM_getValue
// @grant       GM_setValue
// ==/UserScript==

(function() {
  
  //=== config start ===
  var maxWatchedVideoAge = 60; //number of days. set to zero to disable (not recommended)
  //=== 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"); //old
    processVideoItems("#items>.yt-horizontal-list-renderer"); //old
    processVideoItems("#contents>.ytd-channel-featured-content-renderer"); //new
    processVideoItems("#contents>.ytd-shelf-renderer>#grid-container>.ytd-expanded-shelf-contents-renderer"); //new
    //channel/user video page
    processVideoItems(".yt-uix-slider-list>.featured-content-item");
    processVideoItems("#items>.ytd-grid-renderer");
    //channel/user playlist page
    processVideoItems(".expanded-shelf>.expanded-shelf-content-list>.expanded-shelf-content-item-wrapper");
    //channel/user playlist item page
    processVideoItems(".pl-video-list .pl-video-table .pl-video");
    //channel/user videos page
    processVideoItems(".channels-browse-content-grid>.channels-content-item");
    //search page
    processVideoItems("#results>.section-list .item-section>li"); //old
    processVideoItems("#browse-items-primary>.browse-list-item-container"); //old
    processVideoItems(".ytd-browse #contents>.ytd-item-section-renderer"); //new
    processVideoItems(".ytd-search #contents>.ytd-item-section-renderer"); //new
    //video page sidebar
    processVideoItems(".watch-sidebar-body>.video-list>.video-list-item"); //old
    processVideoItems(".playlist-videos-container>.playlist-videos-list>li"); //old
    processVideoItems("#items>.ytd-watch-next-secondary-results-renderer .ytd-compact-video-renderer"); //new
  }

  function processPage() {
    //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();
  }

  var style = document.createElement("STYLE");
  style.innerHTML = '\
/* subscription page, channel/user home page feeds */\
.watched .yt-lockup-content, .watched .yt-lockup-content *,\
/* channel/user home page videos, channel/user videos page */\
.watched .channels-content-item,\
/* video page */\
.watched,\
.watched .content-wrapper,\
.watched>a\
    { background-color: #cec }\
.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 (!ev.button && 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"]) {
    addEventListener("spfdone", processPage); //old
    processPage();
  } else { //new
    var t=0;
    (function init(vm) {
      if (vm = document.querySelector("#visibility-monitor")) {
        vm.addEventListener("viewport-load", function() {
          clearTimeout(t);
          t = setTimeout(processPage, 300);
        });
      } else setTimeout(init, 100);
    })();
  }
})();