YouTube Watched Subscription Hider

Remove (using Hide) watched videos from YouTube subscription page.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         YouTube Watched Subscription Hider
// @namespace    http://tampermonkey.net/
// @version      0.5
// @description  Remove (using Hide) watched videos from YouTube subscription page.
// @author       Surf Archer
// @icon         https://www.youtube.com/favicon.ico
// @require      https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js
// @match        https://www.youtube.com/*
// @match        http://www.youtube.com/*
// @match        https://youtube.com/*
// @match        http://youtube.com/*
// @grant        none
// ==/UserScript==
//
// VERSION HISTORY
//	x0.1	13-Apr-2021	Private internal version.
//	v0.2	01-Jan-2020	First public version.
//	v0.3	20-Jan-2023	Update hide endpoint name after YouTube changed it.
//	v0.4	27-Jan-2023	Update API call after YouTube changed it.
//	v0.5	20-Jun-2023	Update after YouTube changed it's page design and layout.

'use strict';

logMsg("Initialising Watched Subscription Hider...");

const DEBUG = false;
const INITIAL_DELAY_MS = 1500;
const RUN_EVERY_MS = 333;
const PERCENT_COMPLETE_HIDE = 90;
const DEBUG_HEARTBEAT_EVERY = (2 * 60);



// Setup code.
injectJS();



function injectJS() {
  logMsg("Injecting Javascript...");

  var script = document.createElement("script");
  script.type = "application/javascript";

  var textContent = ("(" + injectScript + ")();");

  textContent = textContent.replace("const DEBUG = false;", "const DEBUG = "+DEBUG+";");
  textContent = textContent.replace("const INITIAL_DELAY_MS = 0;", "const INITIAL_DELAY_MS = "+INITIAL_DELAY_MS+";");
  textContent = textContent.replace("const RUN_EVERY_MS = 0;", "const RUN_EVERY_MS = "+RUN_EVERY_MS+";");
  textContent = textContent.replace("const PERCENT_COMPLETE_HIDE = 0;", "const PERCENT_COMPLETE_HIDE = "+PERCENT_COMPLETE_HIDE+";");
  textContent = textContent.replace("const DEBUG_HEARTBEAT_EVERY = 0;", "const DEBUG_HEARTBEAT_EVERY = "+DEBUG_HEARTBEAT_EVERY+";");

  script.textContent = textContent;
  document.body.appendChild(script);

  logMsg("Javascript injected!");
}

function injectScript() {
  logMsg("Initialising subscriptionWatchedHide...");

  // This following consts get modified to "carry into" from the outer process during injection.
  const DEBUG = false;
  const INITIAL_DELAY_MS = 0;
  const RUN_EVERY_MS = 0;
  const PERCENT_COMPLETE_HIDE = 0;
  const DEBUG_HEARTBEAT_EVERY = 0;

  var currentBeat=0;

  if(!window.CryptoJS) {
    addScriptToPage("//cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js");
  }

  setTimeout(function(){setInterval(function(){hideWatched();}, RUN_EVERY_MS);}, INITIAL_DELAY_MS);
  //setTimeout(function(){hideWatched();}, INITIAL_DELAY_MS);

	function hideWatched() {
		if(DEBUG && ++currentBeat >= DEBUG_HEARTBEAT_EVERY) {
			logMsg("HEARTBEAT");
			currentBeat=0;
		}

		if(window.location.pathname.toLowerCase() == "/feed/subscriptions") {
			var foundVideo=false;
			//var grid=document.querySelector("ytd-section-list-renderer").querySelector("ytd-grid-renderer");
			//var section=grid.querySelector("ytd-item-section-renderer");

			//var subs=pageManager.querySelector('.style-scope.ytd-browse.grid.grid-4-columns[page-subtype="subscriptions"] #primary #contents');
			//var section=pageManager.querySelector('.grid[page-subtype="subscriptions"] #primary #contents');

			var pageManager=document.getElementById('page-manager');
			logDebug(pageManager);
			var sections=pageManager.querySelector('.ytd-browse.grid[page-subtype="subscriptions"] #primary #contents');
			logDebug(sections);
			//var section=sections.querySelector('ytd-item-section-renderer');
      var section=sections.querySelector('ytd-rich-grid-row');
			logDebug(section);

			while(section != null && !foundVideo) {
        //var video=section.querySelector('ytd-grid-video-renderer');
				var video=section.querySelector('ytd-rich-grid-media');
				if(video == null) {
					// Handle the full-width format (i.e., non-grid) video.
					video=section.querySelector("ytd-video-renderer");
				}
				while(video != null && !foundVideo) {
					if(video.data.hasOwnProperty("thumbnailOverlays")) {
						var to=video.data.thumbnailOverlays;
						if(to.length > 1) {
							if(to[0].hasOwnProperty("thumbnailOverlayResumePlaybackRenderer")) {
								if(to[0].thumbnailOverlayResumePlaybackRenderer.percentDurationWatched >= PERCENT_COMPLETE_HIDE) {
									foundVideo=true;
									hideVideo(video);
								}
							}
						}
					}
					if(video.isDismissed) {
						video.remove();
					}
					if(!foundVideo) {
						video=video.nextSibling;
					}
				}
				if(!foundVideo) {
					section=section.nextSibling;
				}
			}
		}
	}

	function hideVideo(video) {
		var ret=false;
		var vId=video.data.videoId;
		var vTitle=video.data.title.runs[0].text;
		logMsg("Hiding video. (ID: "+vId+"  Title: "+vTitle+")");
		if(doDismissal(video)) {
			ret=true;
		} else {
			logMsg("  Dismissal failed!");
		}
		video.remove();
		return ret;
	}

  // UTILITY FUNCTIONS.
	function getEndpoint(elem, endpointName) {
		var ret=null;
		var menuItems=elem.data.menu.menuRenderer.items;

		for (var i = 0; i < menuItems.length && ret === null; i++) {
      var se=menuItems[i].menuServiceItemRenderer.serviceEndpoint;
			if(endpointName in se) {
				ret=menuItems[i].menuServiceItemRenderer.serviceEndpoint;
			}
		}
		return ret;
	}

  function doDismissal(video) {
		var ret=false;
		//var ep=getEndpoint(video, "dismissalEndpoint");	// v0.2
		var ep=getEndpoint(video, "feedbackEndpoint");		// v0.3 20-Jan-2023 Youtube changed the endpoint name.

		// Build the body part.
		//var b={"context":{}, "items":[]};	// v0.2
		var b={"context":{}, "feedbackTokens":[]};		// v0.4 27-Jan-2023 Remove "items" since Youtube changed the API format.
		
		b.context=window.ytcfg.get("INNERTUBE_CONTEXT");
		b.context.client.screenWidthPoints=window.innerWidth;
		b.context.client.screenHeightPoints=window.innerHeight;
		b.context.client.screenPixelDensity=Math.round(window.devicePixelRatio || 1);
		b.context.client.screenDensityFloat=window.devicePixelRatio || 1;
		b.context.client.utcOffsetMinutes=-Math.floor((new Date).getTimezoneOffset());
		b.context.client.userInterfaceTheme="USER_INTERFACE_THEME_LIGHT";
		b.context.request.internalExperimentFlags=[];
		b.context.request.consistencyTokenJars=[];
		b.context.user={};
		b.context.clientScreenNonce=window.ytcfg.get("client-screen-nonce");

		//b.items[0] = ep.dismissalEndpoint.dismissal;	// v0.2
		//b.items[0] = ep.feedbackEndpoint.dismissal;			// v0.3 20-Jan-2023 Youtube changed the endpoint name.
    // v0.4 27-Jan-2023 Remove "items" since Youtube changed the API format.
		
		b.feedbackTokens[0] = ep.feedbackEndpoint.feedbackToken;	// v0.4 27-Jan-2023 Add "feedbackEndpoint" since Youtube changed the API format.

		// Add in the parts specific to the srcRow.
		b.context.clickTracking={"clickTrackingParams" : ep.clickTrackingParams};

		var s=JSON.stringify(b);
		
		// Now build the request.
		var r={"credentials": "include", "headers":{}, "referrer": "", "body": "", "method": "POST", "mode": "cors"};
		if(!("user-agent" in r.headers) && !("User-Agent" in r.headers)) {
			r.headers['User-Agent']=navigator.userAgent;
		}
		r.headers.Accept="*/*";
		r.headers['Accept-Language']=(navigator.language || navigator.userLanguage);
		r.headers['Content-Type']="application/json";
		r.headers.Authorization=sapisidHash();
		if(!("x-goog-authuser" in r.headers) && !("X-Goog-Authuser" in r.headers) && !("X-Goog-AuthUser" in r.headers)) {
			r.headers['X-Goog-AuthUser']=window.ytcfg.get("SESSION_INDEX");
		}
		r.headers['X-Origin']=window.location.origin;

		r.referrer=window.location.href;
		r.body=s;

		// Dispatch the fetch with the right key and wait for it to finish.
		var key=window.ytcfg.get("INNERTUBE_API_KEY");

		var url=window.location.origin+ep.commandMetadata.webCommandMetadata.apiUrl;
		var promise=fetch(url+"?key="+key, r);

		promise.then(value => {
			logDebug(promise);
			ret=true;
		});

		return ret;
	}


  // GENERIC UTILITY FUNCTIONS
  function addScriptToPage(s) {
    var script = document.createElement("script");
    script.setAttribute("src", s);
    document.body.appendChild(script);
  }

  function logDebug(msg, force=false) {
    if(DEBUG || force) {
      if(typeof msg === 'string') {
        console.debug("[yt-sub-watched-hide] "+msg);
      } else {
        console.debug("[yt-sub-watched-hide] Logging variable/object below...");
        console.debug(msg);
      }
    }
  }

  function logMsg(msg) {
    console.log("[yt-sub-watched-hide] "+msg);
  }

  function sapisidHash() {
    var ret="";

    // First get the cookie value.
    var cookies=decodeURIComponent(document.cookie).split(';');
    const SC1="SAPISIDHASH=";
    const SC2="__Secure-3PAPISID=";
    var cval="";
    for(var i=0; i < cookies.length && cval == ""; i++) {
      var c=cookies[i].trim();
      if(c.indexOf(SC1) == 0) {
        cval=c.substring(SC1.length, c.length);
      } else if(c.indexOf(SC2) == 0) {
        cval=c.substring(SC2.length, c.length);
      }
    }

    // Now generate the hash.
    if(cval != "") {
      var timeSecs = Math.floor(new Date().getTime()/1000);
      var s=timeSecs+" "+cval+" https://www.youtube.com"
      var h=CryptoJS.SHA1(s);
      s=h.toString();
      ret="SAPISIDHASH "+timeSecs+"_"+s;
    }
    return ret;
  }

  logMsg("Initialisation of YouTube Watched Subscription Hider finished...");
}

function logMsg(msg) {
  console.log("[yt-sub-watched-hide] "+msg);
}