videoTracing

count how many times a video recommended to you in Youtube

  1. // ==UserScript==
  2. // @name videoTracing
  3. // @namespace http://tampermonkey.net/
  4. // @version 2025-04-10.2
  5. // @description count how many times a video recommended to you in Youtube
  6. // @author gn_gf
  7. // @match https://www.youtube.com/
  8. // @license MIT
  9. // ==/UserScript==
  10.  
  11. (function () {
  12. 'use strict';
  13. const DEBUG = true;
  14. const CONTENT_ID = 'contents';
  15. const videoIdSet = new Set();
  16. const videoStateObject = function (id, lastEditTime, times) {
  17. this.id = id;
  18. this.et = lastEditTime;
  19. this.c = times;
  20. };
  21. const SCRIPT_NAME = GM_info.script.name;
  22. const debegLog = (content) => {
  23. if (DEBUG) {
  24. console.log(`${SCRIPT_NAME}:[DEBUG]:${content}`);
  25. }
  26. };
  27.  
  28. const getVideoId = (path) => path.split('?')[1].split('&')[0].split('=')[1];
  29.  
  30. const getAllVideoWhenInit = () => {
  31. let nodeListOf = document.querySelectorAll('#video-title-link');
  32. if (nodeListOf.length === 0) {
  33. return [];
  34. }
  35. return Array.from(nodeListOf);
  36. };
  37.  
  38. const getVideoState = () => {
  39. let videoState = localStorage.getItem('videoState');
  40. if (videoState === null) {
  41. return [];
  42. }
  43. return JSON.parse(videoState);
  44. };
  45.  
  46. const updateVideoState = (videoId, videoState) => {
  47. let videoStateItem = videoState.find((item) => item.id === videoId);
  48. if (videoStateItem === undefined) {
  49. videoStateItem = new videoStateObject(videoId, new Date().getTime(), 1);
  50. videoState.push(videoStateItem);
  51. } else {
  52. if (!videoIdSet.has(videoId)) {
  53. videoStateItem.c += 1;
  54. videoStateItem.et = new Date().getTime();
  55. }
  56. }
  57. videoIdSet.add(videoId);
  58. return videoStateItem;
  59. };
  60.  
  61. const drawDot = (videoElement, videoState) => {
  62. let parent = videoElement.parentNode.parentNode.parentNode.parentNode;
  63. if (!parent.id || parent.id !== 'dismissible') {
  64. debegLog(`FROM DrawDow:${videoState.id}:parent id not match:${parent}`);
  65. return;
  66. }
  67. if (parent.firstChild.id && parent.firstChild.id === 'mydot') {
  68. parent.firstChild.innerText = videoState.c;
  69. return;
  70. }
  71. let element = document.createElement('span');
  72. element.id = 'mydot';
  73. element.style.position = 'absolute';
  74. element.style.backgroundColor = '#f00a';
  75. element.style.color = 'white';
  76. element.style.borderRadius = '20px';
  77. element.style.display = 'inline-block';
  78. element.style.fontSize = '3em';
  79. element.style.zIndex = '100';
  80. element.style.margin = '10px';
  81. element.style.padding = '10px';
  82. element.style.textAlign = 'center';
  83. element.innerText = videoState.c;
  84. parent.insertBefore(element, parent.firstChild);
  85. };
  86.  
  87. const saveVideoState = (videoState) => {
  88. localStorage.setItem('videoState', JSON.stringify(videoState));
  89. };
  90. const VIDEO_STATE = getVideoState();
  91.  
  92. //add a MutationObserver to get new videoIds when youtube page add new video when scroll
  93. const videoContentObserve = new MutationObserver((mutations) => {
  94. mutations.forEach((mutation) => {
  95. if (mutation.type === 'childList' && mutation.addedNodes) {
  96. mutation.addedNodes.forEach((node) => {
  97. if (node.querySelectorAll) {
  98. const newVideos = node.querySelectorAll('#video-title-link');
  99. newVideos.forEach(video => {
  100. const videoId = getVideoId(video.getAttribute('href'));
  101. let state = updateVideoState(videoId, VIDEO_STATE);
  102. drawDot(video, state);
  103. });
  104. }
  105. });
  106. }
  107. });
  108. if (VIDEO_STATE.length >= 1000) {
  109. // remove old 100 items sort by lastEditTime
  110. VIDEO_STATE.sort((a, b) => a.lastEditTime - b.lastEditTime).splice(0, VIDEO_STATE.length - 1000 + 200);
  111. }
  112. saveVideoState(VIDEO_STATE);
  113. });
  114.  
  115. let bodyObserve = new MutationObserver((mutations) => {
  116. mutations.forEach((mutation) => {
  117. if (mutation.type === 'childList' && mutation.addedNodes) {
  118. mutation.addedNodes.forEach((node) => {
  119. node.id && debegLog(node.id);
  120. if (node.id && node.id === CONTENT_ID) {
  121. videoContentObserve.observe(node, {subtree: true, childList: true});
  122. bodyObserve.disconnect();
  123. return;
  124. }
  125. });
  126. }
  127. });
  128. });
  129.  
  130. //run script when all page loaded
  131. window.addEventListener('load', () => {
  132. //start the MutationObserver only for video container
  133. const videoContainer = document.querySelector('#contents');
  134. if (!videoContainer) {
  135. bodyObserve.observe(document.body, {childList: true, subtree: true});
  136. } else {
  137. videoContentObserve.observe(videoContainer, {childList: true, subtree: true});
  138. bodyObserve = null;
  139. }
  140.  
  141. let initVideos = getAllVideoWhenInit();
  142. initVideos.forEach((videoElement) => {
  143. let id = getVideoId(videoElement.getAttribute('href'));
  144. let state = updateVideoState(id, VIDEO_STATE);
  145. drawDot(videoElement, state);
  146. });
  147. saveVideoState(VIDEO_STATE);
  148. });
  149. })();