videoTracing

count how many times a video recommended to you in Youtube

// ==UserScript==
// @name         videoTracing
// @namespace    http://tampermonkey.net/
// @version      2025-04-10.2
// @description  count how many times a video recommended to you in Youtube
// @author       gn_gf
// @match        https://www.youtube.com/
// @license MIT
// ==/UserScript==

(function () {
  'use strict';
  const DEBUG = true;
  const CONTENT_ID = 'contents';
  const videoIdSet = new Set();
  const videoStateObject = function (id, lastEditTime, times) {
    this.id = id;
    this.et = lastEditTime;
    this.c = times;
  };
  const SCRIPT_NAME = GM_info.script.name;
  const debegLog = (content) => {
    if (DEBUG) {
      console.log(`${SCRIPT_NAME}:[DEBUG]:${content}`);
    }
  };

  const getVideoId = (path) => path.split('?')[1].split('&')[0].split('=')[1];

  const getAllVideoWhenInit = () => {
    let nodeListOf = document.querySelectorAll('#video-title-link');
    if (nodeListOf.length === 0) {
      return [];
    }
    return Array.from(nodeListOf);
  };

  const getVideoState = () => {
    let videoState = localStorage.getItem('videoState');
    if (videoState === null) {
      return [];
    }
    return JSON.parse(videoState);
  };

  const updateVideoState = (videoId, videoState) => {
    let videoStateItem = videoState.find((item) => item.id === videoId);
    if (videoStateItem === undefined) {
      videoStateItem = new videoStateObject(videoId, new Date().getTime(), 1);
      videoState.push(videoStateItem);
    } else {
      if (!videoIdSet.has(videoId)) {
        videoStateItem.c += 1;
        videoStateItem.et = new Date().getTime();
      }
    }
    videoIdSet.add(videoId);
    return videoStateItem;
  };

  const drawDot = (videoElement, videoState) => {
    let parent = videoElement.parentNode.parentNode.parentNode.parentNode;
    if (!parent.id || parent.id !== 'dismissible') {
      debegLog(`FROM DrawDow:${videoState.id}:parent id not match:${parent}`);
      return;
    }
    if (parent.firstChild.id && parent.firstChild.id === 'mydot') {
      parent.firstChild.innerText = videoState.c;
      return;
    }
    let element = document.createElement('span');
    element.id = 'mydot';
    element.style.position = 'absolute';
    element.style.backgroundColor = '#f00a';
    element.style.color = 'white';
    element.style.borderRadius = '20px';
    element.style.display = 'inline-block';
    element.style.fontSize = '3em';
    element.style.zIndex = '100';
    element.style.margin = '10px';
    element.style.padding = '10px';
    element.style.textAlign = 'center';
    element.innerText = videoState.c;
    parent.insertBefore(element, parent.firstChild);
  };

  const saveVideoState = (videoState) => {
    localStorage.setItem('videoState', JSON.stringify(videoState));
  };
  const VIDEO_STATE = getVideoState();

  //add a MutationObserver to get new videoIds when youtube page add new video when scroll
  const videoContentObserve = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      if (mutation.type === 'childList' && mutation.addedNodes) {
        mutation.addedNodes.forEach((node) => {
          if (node.querySelectorAll) {
            const newVideos = node.querySelectorAll('#video-title-link');
            newVideos.forEach(video => {
              const videoId = getVideoId(video.getAttribute('href'));
              let state = updateVideoState(videoId, VIDEO_STATE);
              drawDot(video, state);
            });
          }
        });
      }
    });
    if (VIDEO_STATE.length >= 1000) {
      // remove old 100 items sort by lastEditTime
      VIDEO_STATE.sort((a, b) => a.lastEditTime - b.lastEditTime).splice(0, VIDEO_STATE.length - 1000 + 200);
    }
    saveVideoState(VIDEO_STATE);
  });

  let bodyObserve = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      if (mutation.type === 'childList' && mutation.addedNodes) {
        mutation.addedNodes.forEach((node) => {
          node.id && debegLog(node.id);
          if (node.id && node.id === CONTENT_ID) {
            videoContentObserve.observe(node, {subtree: true, childList: true});
            bodyObserve.disconnect();
            return;
          }
        });
      }
    });
  });

  //run script when all page loaded
  window.addEventListener('load', () => {
    //start the MutationObserver only for video container
    const videoContainer = document.querySelector('#contents');
    if (!videoContainer) {
      bodyObserve.observe(document.body, {childList: true, subtree: true});
    } else {
      videoContentObserve.observe(videoContainer, {childList: true, subtree: true});
      bodyObserve = null;
    }

    let initVideos = getAllVideoWhenInit();
    initVideos.forEach((videoElement) => {
      let id = getVideoId(videoElement.getAttribute('href'));
      let state = updateVideoState(id, VIDEO_STATE);
      drawDot(videoElement, state);
    });
    saveVideoState(VIDEO_STATE);
  });
})();