YouTube Enhancer (Secret Stats)

Integrates "Secret Stats" and "Stream Stats" buttons into the channel page, providing access to detailed analytics and displaying total Shorts views for deeper insights.

当前为 2025-02-28 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube Enhancer (Secret Stats)
  3. // @description Integrates "Secret Stats" and "Stream Stats" buttons into the channel page, providing access to detailed analytics and displaying total Shorts views for deeper insights.
  4. // @icon https://raw.githubusercontent.com/exyezed/youtube-enhancer/refs/heads/main/extras/youtube-enhancer.png
  5. // @version 1.4
  6. // @author exyezed
  7. // @namespace https://github.com/exyezed/youtube-enhancer/
  8. // @supportURL https://github.com/exyezed/youtube-enhancer/issues
  9. // @license MIT
  10. // @match https://www.youtube.com/*
  11. // @grant GM.xmlHttpRequest
  12. // @connect exyezed.vercel.app
  13. // @run-at document-idle
  14. // ==/UserScript==
  15.  
  16. (function() {
  17. 'use strict';
  18.  
  19. const CACHE_DURATION = 60 * 60 * 1000;
  20.  
  21. function getChannelIdentifier() {
  22. const url = window.location.href;
  23. let identifier = '';
  24.  
  25. if (url.includes('/channel/')) {
  26. identifier = url.split('/channel/')[1].split('/')[0];
  27. } else if (url.includes('/@')) {
  28. identifier = url.split('/@')[1].split('/')[0];
  29. }
  30.  
  31. return identifier;
  32. }
  33.  
  34. function getCachedData(identifier) {
  35. try {
  36. const cacheKey = `shorts_stats_${identifier}`;
  37. const cachedData = localStorage.getItem(cacheKey);
  38.  
  39. if (cachedData) {
  40. const { data, timestamp } = JSON.parse(cachedData);
  41. const now = Date.now();
  42.  
  43. if (now - timestamp < CACHE_DURATION) {
  44. return data;
  45. } else {
  46. localStorage.removeItem(cacheKey);
  47. }
  48. }
  49. } catch (error) {
  50. console.error('Error reading cache:', error);
  51. }
  52. return null;
  53. }
  54.  
  55. function setCacheData(identifier, data) {
  56. try {
  57. const cacheKey = `shorts_stats_${identifier}`;
  58. const cacheData = {
  59. data: data,
  60. timestamp: Date.now()
  61. };
  62. localStorage.setItem(cacheKey, JSON.stringify(cacheData));
  63. } catch (error) {
  64. console.error('Error setting cache:', error);
  65. }
  66. }
  67.  
  68. function createStatsButton(text, className, idName, svgPath, statsType) {
  69. const containerDiv = document.createElement('div');
  70. containerDiv.className = `yt-flexible-actions-view-model-wiz__action ${className}-container`;
  71.  
  72. const buttonViewModel = document.createElement('button-view-model');
  73. buttonViewModel.className = `yt-spec-button-view-model ${className}-view-model`;
  74.  
  75. const button = document.createElement('button');
  76. button.className = `yt-spec-button-shape-next yt-spec-button-shape-next--outline yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--enable-backdrop-filter-experiment ${className}-button`;
  77. button.setAttribute('aria-disabled', 'false');
  78. button.setAttribute('aria-label', `View ${text}`);
  79. button.id = idName;
  80. button.style.display = 'flex';
  81. button.style.alignItems = 'center';
  82. button.style.justifyContent = 'center';
  83. button.style.gap = '8px';
  84.  
  85. button.addEventListener('click', () => {
  86. const identifier = getChannelIdentifier();
  87. if (identifier) {
  88. const url = `https://exyezed.vercel.app/stats/${statsType}/${identifier}`;
  89. window.open(url, '_blank');
  90. }
  91. });
  92.  
  93. const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  94. svg.setAttribute("viewBox", "0 0 576 512");
  95. svg.style.width = "20px";
  96. svg.style.height = "20px";
  97. svg.style.fill = "currentColor";
  98. svg.style.flexShrink = "0";
  99. svg.style.display = "flex";
  100. svg.style.alignItems = "center";
  101.  
  102. const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
  103. path.setAttribute("d", svgPath);
  104. svg.appendChild(path);
  105.  
  106. const buttonText = document.createElement('div');
  107. buttonText.className = `yt-spec-button-shape-next__button-text-content ${className}-text`;
  108. buttonText.textContent = text;
  109. buttonText.style.display = 'flex';
  110. buttonText.style.alignItems = 'center';
  111.  
  112. const touchFeedback = document.createElement('yt-touch-feedback-shape');
  113. touchFeedback.style.borderRadius = 'inherit';
  114. touchFeedback.className = `${className}-feedback-shape`;
  115.  
  116. const touchFeedbackDiv = document.createElement('div');
  117. touchFeedbackDiv.className = `yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response ${className}-feedback-response`;
  118. touchFeedbackDiv.setAttribute('aria-hidden', 'true');
  119.  
  120. const strokeDiv = document.createElement('div');
  121. strokeDiv.className = `yt-spec-touch-feedback-shape__stroke ${className}-feedback-stroke`;
  122.  
  123. const fillDiv = document.createElement('div');
  124. fillDiv.className = `yt-spec-touch-feedback-shape__fill ${className}-feedback-fill`;
  125.  
  126. touchFeedbackDiv.appendChild(strokeDiv);
  127. touchFeedbackDiv.appendChild(fillDiv);
  128. touchFeedback.appendChild(touchFeedbackDiv);
  129.  
  130. button.appendChild(svg);
  131. button.appendChild(buttonText);
  132. button.appendChild(touchFeedback);
  133.  
  134. buttonViewModel.appendChild(button);
  135. containerDiv.appendChild(buttonViewModel);
  136.  
  137. return containerDiv;
  138. }
  139.  
  140. function createShortsStats() {
  141. const containerDiv = document.createElement('div');
  142. containerDiv.className = 'yt-flexible-actions-view-model-wiz__action shorts-stats-container';
  143.  
  144. const buttonViewModel = document.createElement('button-view-model');
  145. buttonViewModel.className = 'yt-spec-button-view-model shorts-stats-view-model';
  146.  
  147. const button = document.createElement('div');
  148. button.className = 'yt-spec-button-shape-next yt-spec-button-shape-next--outline yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--enable-backdrop-filter-experiment shorts-stats-button';
  149. button.style.display = 'flex';
  150. button.style.alignItems = 'center';
  151. button.style.justifyContent = 'center';
  152. button.style.gap = '8px';
  153. button.style.cursor = 'default';
  154.  
  155. const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  156. svg.setAttribute("viewBox", "0 0 24 24");
  157. svg.style.width = "20px";
  158. svg.style.height = "20px";
  159. svg.style.fill = "currentColor";
  160. svg.style.flexShrink = "0";
  161.  
  162. const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
  163. path.setAttribute("d", "m18.931 9.99l-1.441-.601l1.717-.913a4.48 4.48 0 0 0 1.874-6.078a4.506 4.506 0 0 0-6.09-1.874L4.792 5.929a4.5 4.5 0 0 0-2.402 4.193a4.52 4.52 0 0 0 2.666 3.904c.036.012 1.442.6 1.442.6l-1.706.901a4.51 4.51 0 0 0-2.369 3.967A4.53 4.53 0 0 0 6.93 24c.725 0 1.437-.174 2.08-.508l10.21-5.406a4.49 4.49 0 0 0 2.39-4.192a4.53 4.53 0 0 0-2.678-3.904Zm-9.334 5.2V8.824l6.007 3.184z");
  164. svg.appendChild(path);
  165.  
  166. const statsText = document.createElement('div');
  167. statsText.className = 'yt-spec-button-shape-next__button-text-content shorts-stats-text';
  168. statsText.textContent = 'Loading...';
  169. statsText.style.display = 'flex';
  170. statsText.style.alignItems = 'center';
  171.  
  172. button.appendChild(svg);
  173. button.appendChild(statsText);
  174.  
  175. buttonViewModel.appendChild(button);
  176. containerDiv.appendChild(buttonViewModel);
  177.  
  178. return containerDiv;
  179. }
  180.  
  181. async function fetchAndUpdateShortsStats(shortsStatsElement) {
  182. const identifier = getChannelIdentifier();
  183. if (!identifier || !shortsStatsElement) return;
  184. const cachedData = getCachedData(identifier);
  185. if (cachedData) {
  186. const statsText = shortsStatsElement.querySelector('.shorts-stats-text');
  187. if (statsText) {
  188. statsText.textContent = cachedData.shorts_views;
  189. }
  190. return;
  191. }
  192. try {
  193. GM.xmlHttpRequest({
  194. method: 'GET',
  195. url: `https://exyezed.vercel.app/api/shorts/${identifier}`,
  196. timeout: 60000, // Increase timeout to 60 seconds
  197. onload: function(response) {
  198. try {
  199. if (response.status !== 200) {
  200. updateStatsError(shortsStatsElement);
  201. return;
  202. }
  203. const data = JSON.parse(response.responseText);
  204. const statsText = shortsStatsElement.querySelector('.shorts-stats-text');
  205. if (statsText) {
  206. statsText.textContent = data.shorts_views;
  207. }
  208. setCacheData(identifier, data);
  209. } catch (error) {
  210. console.error('Error parsing response:', error);
  211. updateStatsError(shortsStatsElement);
  212. }
  213. },
  214. onerror: function(error) {
  215. console.error('Error fetching shorts stats:', error);
  216. updateStatsError(shortsStatsElement);
  217. },
  218. ontimeout: function() {
  219. console.error('Request timed out');
  220. updateStatsError(shortsStatsElement);
  221. }
  222. });
  223. } catch (error) {
  224. console.error('Error in GM.xmlHttpRequest setup:', error);
  225. updateStatsError(shortsStatsElement);
  226. }
  227. }
  228.  
  229. function updateStatsError(shortsStatsElement) {
  230. const statsText = shortsStatsElement.querySelector('.shorts-stats-text');
  231. if (statsText) {
  232. statsText.textContent = 'No Shorts';
  233. }
  234. }
  235.  
  236. function createStatsButtons() {
  237. if (document.querySelector('.secret-stats-container') ||
  238. document.querySelector('.stream-stats-container') ||
  239. document.querySelector('.shorts-stats-container')) {
  240. return;
  241. }
  242.  
  243. const secretStatsSvgPath = "M224 16c-6.7 0-10.8-2.8-15.5-6.1C201.9 5.4 194 0 176 0c-30.5 0-52 43.7-66 89.4C62.7 98.1 32 112.2 32 128c0 14.3 25 27.1 64.6 35.9c-.4 4-.6 8-.6 12.1c0 17 3.3 33.2 9.3 48l-59.9 0C38 224 32 230 32 237.4c0 1.7 .3 3.4 1 5l38.8 96.9C28.2 371.8 0 423.8 0 482.3C0 498.7 13.3 512 29.7 512l388.6 0c16.4 0 29.7-13.3 29.7-29.7c0-58.5-28.2-110.4-71.7-143L415 242.4c.6-1.6 1-3.3 1-5c0-7.4-6-13.4-13.4-13.4l-59.9 0c6-14.8 9.3-31 9.3-48c0-4.1-.2-8.1-.6-12.1C391 155.1 416 142.3 416 128c0-15.8-30.7-29.9-78-38.6C324 43.7 302.5 0 272 0c-18 0-25.9 5.4-32.5 9.9c-4.8 3.3-8.8 6.1-15.5 6.1zm56 208l-12.4 0c-16.5 0-31.1-10.6-36.3-26.2c-2.3-7-12.2-7-14.5 0c-5.2 15.6-19.9 26.2-36.3 26.2L168 224c-22.1 0-40-17.9-40-40l0-14.4c28.2 4.1 61 6.4 96 6.4s67.8-2.3 96-6.4l0 14.4c0 22.1-17.9 40-40 40zm-88 96l16 32L176 480 128 288l64 32zm128-32L272 480 240 352l16-32 64-32z";
  244.  
  245. const streamStatsSvgPath = "M108.2 71c13.8 11.1 16 31.2 5 45C82.4 154.4 64 203 64 256s18.4 101.6 49.1 140c11.1 13.8 8.8 33.9-5 45s-33.9 8.8-45-5C23.7 386.7 0 324.1 0 256S23.7 125.3 63.2 76c11.1-13.8 31.2-16 45-5zm359.7 0c13.8-11.1 33.9-8.8 45 5C552.3 125.3 576 187.9 576 256s-23.7 130.7-63.2 180c-11.1 13.8-31.2 16-45 5s-16-31.2-5-45c30.7-38.4 49.1-87 49.1-140s-18.4-101.6-49.1-140c-11.1-13.8-8.8-33.9 5-45zM232 256a56 56 0 1 1 112 0 56 56 0 1 1 -112 0zm-27.5-74.7c-17.8 19.8-28.5 46-28.5 74.7s10.8 54.8 28.5 74.7c11.8 13.2 10.7 33.4-2.5 45.2s-33.4 10.7-45.2-2.5C129 342.2 112 301.1 112 256s17-86.2 44.8-117.3c11.8-13.2 32-14.3 45.2-2.5s14.3 32 2.545.2zm214.7-42.7C447 169.8 464 210.9 464 256s-17 86.2-44.8 117.3c-11.8 13.2-32 14.3-45.2 2.5s-14.3-32-2.5-45.2c17.8-19.8 28.5-46 28.5-74.7s-10.8-54.8-28.5-74.7c-11.8-13.2-10.7-33.4 2.5-45.2s33.4-10.7 45.2 2.5z";
  246.  
  247. const joinButton = document.querySelector('.yt-flexible-actions-view-model-wiz__action');
  248. if (joinButton) {
  249. const secretStatsButton = createStatsButton('Secret Stats', 'secret-stats', 'secret-stats-button', secretStatsSvgPath, 'secret');
  250. joinButton.parentNode.insertBefore(secretStatsButton, joinButton.nextSibling);
  251.  
  252. const streamStatsButton = createStatsButton('Stream Stats', 'stream-stats', 'stream-stats-button', streamStatsSvgPath, 'stream');
  253. secretStatsButton.parentNode.insertBefore(streamStatsButton, secretStatsButton.nextSibling);
  254.  
  255. const shortsStatsElement = createShortsStats();
  256. streamStatsButton.parentNode.insertBefore(shortsStatsElement, streamStatsButton.nextSibling);
  257.  
  258. fetchAndUpdateShortsStats(shortsStatsElement);
  259. }
  260. }
  261.  
  262. function checkAndAddButtons() {
  263. const joinButton = document.querySelector('.yt-flexible-actions-view-model-wiz__action');
  264. const secretStatsButton = document.querySelector('.secret-stats-container');
  265. const streamStatsButton = document.querySelector('.stream-stats-container');
  266. const shortsStatsElement = document.querySelector('.shorts-stats-container');
  267.  
  268. if (joinButton && (!secretStatsButton || !streamStatsButton || !shortsStatsElement)) {
  269. createStatsButtons();
  270. }
  271. }
  272.  
  273. function clearExpiredCache() {
  274. try {
  275. const now = Date.now();
  276. for (let i = 0; i < localStorage.length; i++) {
  277. const key = localStorage.key(i);
  278. if (key && key.startsWith('shorts_stats_')) {
  279. const cachedData = localStorage.getItem(key);
  280. if (cachedData) {
  281. const { timestamp } = JSON.parse(cachedData);
  282. if (now - timestamp >= CACHE_DURATION) {
  283. localStorage.removeItem(key);
  284. }
  285. }
  286. }
  287. }
  288. } catch (error) {
  289. console.error('Error clearing expired cache:', error);
  290. }
  291. }
  292.  
  293. clearExpiredCache();
  294.  
  295. const observer = new MutationObserver((mutations) => {
  296. checkAndAddButtons();
  297. });
  298.  
  299. observer.observe(document.body, {
  300. childList: true,
  301. subtree: true
  302. });
  303.  
  304. checkAndAddButtons();
  305. console.log('YouTube Enhancer (Secret Stats) is running');
  306. })();