YouTube Enhancer (Secret Stats)

Integrating "Secret Stats" and "Stream Stats" buttons into the channel page, directing users to detailed analytics pages for insights into the channel.

当前为 2024-10-19 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube Enhancer (Secret Stats)
  3. // @description Integrating "Secret Stats" and "Stream Stats" buttons into the channel page, directing users to detailed analytics pages for insights into the channel.
  4. // @icon https://raw.githubusercontent.com/exyezed/youtube-enhancer/refs/heads/main/extras/youtube-enhancer.png
  5. // @version 1.0
  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 none
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. // Button Creation and Manipulation
  18. function createButton(text, iconName, id) {
  19. const button = document.createElement('button');
  20. button.id = id;
  21. button.className = 'yt-spec-button-shape-next yt-spec-button-shape-next--tonal yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m';
  22. button.style.display = 'inline-flex';
  23. button.style.alignItems = 'center';
  24. button.style.justifyContent = 'center';
  25. button.style.minWidth = 'auto';
  26. button.style.padding = '0 16px';
  27. button.style.height = '36px';
  28. button.style.fontSize = '14px';
  29. button.style.lineHeight = '36px';
  30. button.style.fontWeight = '500';
  31.  
  32. const icon = document.createElement('span');
  33. icon.className = 'material-symbols-outlined';
  34. icon.textContent = iconName;
  35. icon.style.marginRight = '4px';
  36. icon.style.fontSize = '24px';
  37. icon.style.fontWeight = '100';
  38.  
  39. const buttonText = document.createTextNode(text);
  40.  
  41. button.appendChild(icon);
  42. button.appendChild(buttonText);
  43.  
  44. return button;
  45. }
  46.  
  47. function createButtonWrapper(button) {
  48. const buttonWrapper = document.createElement('div');
  49. buttonWrapper.className = 'yt-flexible-actions-view-model-wiz__action';
  50. buttonWrapper.appendChild(button);
  51. return buttonWrapper;
  52. }
  53.  
  54. function addStatsButtons() {
  55. const targetElement = document.querySelector('yt-flexible-actions-view-model');
  56. const lastAction = targetElement ? targetElement.querySelector('div.yt-flexible-actions-view-model-wiz__action:last-child') : null;
  57.  
  58. if (lastAction && !document.querySelector('#YouTubeEnhancerChannelSecretStats')) {
  59. const secretStatsButton = createButton('Secret Stats', 'lock_open', 'YouTubeEnhancerChannelSecretStats');
  60. const streamStatsButton = createButton('Stream Stats', 'cell_tower', 'YouTubeEnhancerStreamStats');
  61.  
  62. secretStatsButton.addEventListener('click', function() {
  63. const channelIdentifier = getChannelIdentifier();
  64. if (channelIdentifier) {
  65. const statsUrl = `https://exyezed.vercel.app/stats/secret/${channelIdentifier}`;
  66. window.open(statsUrl, '_blank');
  67. } else {
  68. alert('Could not determine channel identifier. Please try again on a channel page.');
  69. }
  70. });
  71.  
  72. streamStatsButton.addEventListener('click', function() {
  73. const channelIdentifier = getChannelIdentifier();
  74. if (channelIdentifier) {
  75. const statsUrl = `https://exyezed.vercel.app/stats/stream/${channelIdentifier}`;
  76. window.open(statsUrl, '_blank');
  77. } else {
  78. alert('Could not determine channel identifier. Please try again on a channel page.');
  79. }
  80. });
  81.  
  82. const secretStatsWrapper = createButtonWrapper(secretStatsButton);
  83. const streamStatsWrapper = createButtonWrapper(streamStatsButton);
  84.  
  85. lastAction.insertAdjacentElement('afterend', streamStatsWrapper);
  86. lastAction.insertAdjacentElement('afterend', secretStatsWrapper);
  87. }
  88. }
  89.  
  90. // Channel Identifier Functions
  91. function getChannelIdentifier() {
  92. const channelId = getChannelId();
  93. if (channelId) return channelId;
  94. return getChannelHandle();
  95. }
  96.  
  97. function getChannelId() {
  98. const channelIdMeta = document.querySelector('meta[itemprop="channelId"]');
  99. if (channelIdMeta) {
  100. return channelIdMeta.content;
  101. }
  102.  
  103. const urlParams = new URLSearchParams(window.location.search);
  104. return urlParams.get('channel') || extractChannelIdFromUrl();
  105. }
  106.  
  107. function getChannelHandle() {
  108. const path = window.location.pathname;
  109. const matches = path.match(/\/@([^/]+)/);
  110. return matches ? matches[1] : null;
  111. }
  112.  
  113. function extractChannelIdFromUrl() {
  114. const path = window.location.pathname;
  115. const matches = path.match(/\/(channel|user|c)\/([^/]+)/);
  116. return matches ? matches[2] : null;
  117. }
  118.  
  119. // Utility Functions
  120. function addMaterialIconsStylesheet() {
  121. if (!document.querySelector('#material-icons-stylesheet')) {
  122. const link = document.createElement('link');
  123. link.id = 'material-icons-stylesheet';
  124. link.rel = 'stylesheet';
  125. link.href = 'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,150,0,0&icon_names=cell_tower,lock_open';
  126. document.head.appendChild(link);
  127. }
  128. }
  129.  
  130. function observePageChanges() {
  131. const observer = new MutationObserver((mutations) => {
  132. const targetElement = document.querySelector('yt-flexible-actions-view-model');
  133. if (targetElement && targetElement.querySelector('div.yt-flexible-actions-view-model-wiz__action')) {
  134. addStatsButtons();
  135. observer.disconnect();
  136. }
  137. });
  138.  
  139. observer.observe(document.body, {
  140. childList: true,
  141. subtree: true
  142. });
  143. }
  144.  
  145. // Initialization
  146. function init() {
  147. if (window.location.pathname.includes('/channel/') ||
  148. window.location.pathname.includes('/@') ||
  149. window.location.pathname.includes('/c/') ||
  150. window.location.pathname.includes('/user/')) {
  151. addMaterialIconsStylesheet();
  152. observePageChanges();
  153. }
  154. }
  155.  
  156. // Run on initial page load
  157. init();
  158.  
  159. // Listen for YouTube spa navigation events
  160. window.addEventListener('yt-navigate-finish', init);
  161. })();