Mark Watched YouTube Videos

Add an indicator for watched videos on YouTube

As of 15.02.2018. See ბოლო ვერსია.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name        Mark Watched YouTube Videos
// @namespace   MarkWatchedYouTubeVideos
// @description Add an indicator for watched videos on YouTube
// @version     1.0.8
// @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");
    //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"); //old
    processVideoItems("#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"); //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);
    })();
  }
})();