YouTube Enhancer (Real-Time Subscriber Count)

Adds an overlay to YouTube channel banners showing real-time subscriber count.

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

  1. // ==UserScript==
  2. // @name YouTube Enhancer (Real-Time Subscriber Count)
  3. // @description Adds an overlay to YouTube channel banners showing real-time subscriber count.
  4. // @icon https://raw.githubusercontent.com/exyezed/youtube-enhancer/refs/heads/main/extras/youtube-enhancer.png
  5. // @version 1.2
  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. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. // Constants
  18. const OPTIONS = ['subscribers', 'views', 'videos'];
  19. const FONT_LINK = "https://fonts.googleapis.com/css2?family=Rubik:wght@400;700&display=swap";
  20. const API_BASE_URL = 'https://exyezed.vercel.app/api/channel/';
  21. const STATS_API_URL = 'https://api.livecounts.io/youtube-live-subscriber-counter/stats/';
  22. const DEFAULT_UPDATE_INTERVAL = 2000;
  23. const DEFAULT_OVERLAY_OPACITY = 0.75;
  24.  
  25. // Global variables
  26. let overlay = null;
  27. let isUpdating = false;
  28. let intervalId = null;
  29. let currentChannelName = null;
  30. let updateInterval = parseInt(localStorage.getItem('youtubeEnhancerInterval')) || DEFAULT_UPDATE_INTERVAL;
  31. let overlayOpacity = parseFloat(localStorage.getItem('youtubeEnhancerOpacity')) || DEFAULT_OVERLAY_OPACITY;
  32.  
  33. const lastSuccessfulStats = new Map();
  34. const previousStats = new Map();
  35.  
  36. // Initialization
  37. function init() {
  38. loadFonts();
  39. initializeLocalStorage();
  40. addStyles();
  41. observePageChanges();
  42. addNavigationListener();
  43. }
  44.  
  45. function loadFonts() {
  46. const fontLink = document.createElement("link");
  47. fontLink.rel = "stylesheet";
  48. fontLink.href = FONT_LINK;
  49. document.head.appendChild(fontLink);
  50. }
  51.  
  52. function initializeLocalStorage() {
  53. OPTIONS.forEach(option => {
  54. if (localStorage.getItem(`show-${option}`) === null) {
  55. localStorage.setItem(`show-${option}`, 'true');
  56. }
  57. });
  58. }
  59.  
  60. function addStyles() {
  61. const style = document.createElement('style');
  62. style.textContent = `
  63. .settings-button {
  64. position: absolute;
  65. top: 12px;
  66. right: 12px;
  67. width: 16px;
  68. height: 16px;
  69. cursor: pointer;
  70. z-index: 2;
  71. transition: transform 0.3s ease;
  72. }
  73. .settings-button:hover {
  74. transform: rotate(45deg);
  75. }
  76. .settings-menu {
  77. position: absolute;
  78. top: 35px;
  79. right: 12px;
  80. background: rgba(0, 0, 0, 0.95);
  81. padding: 10px;
  82. border-radius: 6px;
  83. z-index: 2;
  84. display: none;
  85. }
  86. .settings-menu.show {
  87. display: flex;
  88. }
  89. .interval-slider, .opacity-slider {
  90. width: 160px;
  91. margin: 5px 0;
  92. height: 4px;
  93. }
  94. .interval-value, .opacity-value {
  95. color: white;
  96. font-size: 12px;
  97. margin-top: 3px;
  98. margin-bottom: 8px;
  99. }
  100. .setting-group {
  101. margin-bottom: 10px;
  102. }
  103. .setting-group:last-child {
  104. margin-bottom: 0;
  105. }
  106. @keyframes spin {
  107. from { transform: rotate(0deg); }
  108. to { transform: rotate(360deg); }
  109. }
  110. `;
  111. document.head.appendChild(style);
  112. }
  113.  
  114. // UI Components
  115. function createSettingsButton() {
  116. const button = document.createElement('div');
  117. button.className = 'settings-button';
  118. const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  119. svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
  120. svg.setAttribute("viewBox", "0 0 512 512");
  121. const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
  122. path.setAttribute("fill", "white");
  123. path.setAttribute("d", "M495.9 166.6c3.2 8.7 .5 18.4-6.4 24.6l-43.3 39.4c1.1 8.3 1.7 16.8 1.7 25.4s-.6 17.1-1.7 25.4l43.3 39.4c6.9 6.2 9.6 15.9 6.4 24.6c-4.4 11.9-9.7 23.3-15.8 34.3l-4.7 8.1c-6.6 11-14 21.4-22.1 31.2c-5.9 7.2-15.7 9.6-24.5 6.8l-55.7-17.7c-13.4 10.3-28.2 18.9-44 25.4l-12.5 57.1c-2 9.1-9 16.3-18.2 17.8c-13.8 2.3-28 3.5-42.5 3.5s-28.7-1.2-42.5-3.5c-9.2-1.5-16.2-8.7-18.2-17.8l-12.5-57.1c-15.8-6.5-30.6-15.1-44-25.4L83.1 425.9c-8.8 2.8-18.6 .3-24.5-6.8c-8.1-9.8-15.5-20.2-22.1-31.2l-4.7-8.1c-6.1-11-11.4-22.4-15.8-34.3c-3.2-8.7-.5-18.4 6.4-24.6l43.3-39.4C64.6 273.1 64 264.6 64 256s.6-17.1 1.7-25.4L22.4 191.2c-6.9-6.2-9.6-15.9-6.4-24.6c4.4-11.9 9.7-23.3 15.8-34.3l4.7-8.1c6.6-11 14-21.4 22.1-31.2c5.9-7.2 15.7-9.6 24.5-6.8l55.7 17.7c13.4-10.3 28.2-18.9 44-25.4l12.5-57.1c2-9.1 9-16.3 18.2-17.8C227.3 1.2 241.5 0 256 0s28.7 1.2 42.5 3.5c9.2 1.5 16.2 8.7 18.2 17.8l12.5 57.1c15.8 6.5 30.6 15.1 44 25.4l55.7-17.7c8.8-2.8 18.6-.3 24.5 6.8c8.1 9.8 15.5 20.2 22.1 31.2l4.7 8.1c6.1 11 11.4 22.4 15.8 34.3zM256 336a80 80 0 1 0 0-160 80 80 0 1 0 0 160z");
  124. svg.appendChild(path);
  125. button.appendChild(svg);
  126. return button;
  127. }
  128.  
  129. function createSettingsMenu() {
  130. const menu = document.createElement('div');
  131. menu.className = 'settings-menu';
  132. menu.style.gap = '15px';
  133. menu.style.width = '360px';
  134. const displaySection = createDisplaySection();
  135. const controlsSection = createControlsSection();
  136. menu.appendChild(displaySection);
  137. menu.appendChild(controlsSection);
  138. return menu;
  139. }
  140.  
  141. function createDisplaySection() {
  142. const displaySection = document.createElement('div');
  143. displaySection.style.flex = '1';
  144. const displayLabel = document.createElement('label');
  145. displayLabel.textContent = 'Display Options';
  146. displayLabel.style.marginBottom = '10px';
  147. displayLabel.style.display = 'block';
  148. displayLabel.style.fontSize = '16px';
  149. displayLabel.style.fontWeight = 'bold';
  150. displaySection.appendChild(displayLabel);
  151. OPTIONS.forEach(option => {
  152. const checkboxContainer = document.createElement('div');
  153. checkboxContainer.style.display = 'flex';
  154. checkboxContainer.style.alignItems = 'center';
  155. checkboxContainer.style.marginTop = '5px';
  156. const checkbox = document.createElement('input');
  157. checkbox.type = 'checkbox';
  158. checkbox.id = `show-${option}`;
  159. checkbox.checked = localStorage.getItem(`show-${option}`) !== 'false';
  160. checkbox.style.marginRight = '8px';
  161. checkbox.style.cursor = 'pointer';
  162. const checkboxLabel = document.createElement('label');
  163. checkboxLabel.htmlFor = `show-${option}`;
  164. checkboxLabel.textContent = option.charAt(0).toUpperCase() + option.slice(1);
  165. checkboxLabel.style.cursor = 'pointer';
  166. checkboxLabel.style.color = 'white';
  167. checkboxLabel.style.fontSize = '14px';
  168. checkbox.addEventListener('change', () => {
  169. localStorage.setItem(`show-${option}`, checkbox.checked);
  170. updateDisplayState();
  171. });
  172. checkboxContainer.appendChild(checkbox);
  173. checkboxContainer.appendChild(checkboxLabel);
  174. displaySection.appendChild(checkboxContainer);
  175. });
  176.  
  177. return displaySection;
  178. }
  179.  
  180. function createControlsSection() {
  181. const controlsSection = document.createElement('div');
  182. controlsSection.style.flex = '1';
  183. const intervalLabel = document.createElement('label');
  184. intervalLabel.textContent = 'Update Interval';
  185. intervalLabel.style.display = 'block';
  186. intervalLabel.style.marginBottom = '5px';
  187. intervalLabel.style.fontSize = '16px';
  188. intervalLabel.style.fontWeight = 'bold';
  189. const intervalSlider = document.createElement('input');
  190. intervalSlider.type = 'range';
  191. intervalSlider.min = '2';
  192. intervalSlider.max = '10';
  193. intervalSlider.value = updateInterval / 1000;
  194. intervalSlider.step = '1';
  195. intervalSlider.className = 'interval-slider';
  196. const intervalValue = document.createElement('div');
  197. intervalValue.className = 'interval-value';
  198. intervalValue.textContent = `${intervalSlider.value}s`;
  199. intervalValue.style.marginBottom = '15px';
  200. intervalValue.style.fontSize = '14px';
  201. intervalSlider.addEventListener('input', (e) => {
  202. const newInterval = parseInt(e.target.value) * 1000;
  203. intervalValue.textContent = `${e.target.value}s`;
  204. updateInterval = newInterval;
  205. localStorage.setItem('youtubeEnhancerInterval', newInterval);
  206. if (intervalId) {
  207. clearInterval(intervalId);
  208. intervalId = setInterval(() => {
  209. updateOverlayContent(overlay, currentChannelName);
  210. }, newInterval);
  211. }
  212. });
  213. const opacityLabel = document.createElement('label');
  214. opacityLabel.textContent = 'Background Opacity';
  215. opacityLabel.style.display = 'block';
  216. opacityLabel.style.marginBottom = '5px';
  217. opacityLabel.style.fontSize = '16px';
  218. opacityLabel.style.fontWeight = 'bold';
  219. const opacitySlider = document.createElement('input');
  220. opacitySlider.type = 'range';
  221. opacitySlider.min = '50';
  222. opacitySlider.max = '90';
  223. opacitySlider.value = overlayOpacity * 100;
  224. opacitySlider.step = '5';
  225. opacitySlider.className = 'opacity-slider';
  226. const opacityValue = document.createElement('div');
  227. opacityValue.className = 'opacity-value';
  228. opacityValue.textContent = `${opacitySlider.value}%`;
  229. opacityValue.style.fontSize = '14px';
  230. opacitySlider.addEventListener('input', (e) => {
  231. const newOpacity = parseInt(e.target.value) / 100;
  232. opacityValue.textContent = `${e.target.value}%`;
  233. overlayOpacity = newOpacity;
  234. localStorage.setItem('youtubeEnhancerOpacity', newOpacity);
  235. if (overlay) {
  236. overlay.style.backgroundColor = `rgba(0, 0, 0, ${newOpacity})`;
  237. }
  238. });
  239. controlsSection.appendChild(intervalLabel);
  240. controlsSection.appendChild(intervalSlider);
  241. controlsSection.appendChild(intervalValue);
  242. controlsSection.appendChild(opacityLabel);
  243. controlsSection.appendChild(opacitySlider);
  244. controlsSection.appendChild(opacityValue);
  245.  
  246. return controlsSection;
  247. }
  248.  
  249. function createSpinner() {
  250. const spinnerContainer = document.createElement('div');
  251. spinnerContainer.style.position = 'absolute';
  252. spinnerContainer.style.top = '0';
  253. spinnerContainer.style.left = '0';
  254. spinnerContainer.style.width = '100%';
  255. spinnerContainer.style.height = '100%';
  256. spinnerContainer.style.display = 'flex';
  257. spinnerContainer.style.justifyContent = 'center';
  258. spinnerContainer.style.alignItems = 'center';
  259. spinnerContainer.classList.add('spinner-container');
  260.  
  261. const spinner = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  262. spinner.setAttribute("viewBox", "0 0 512 512");
  263. spinner.setAttribute("width", "64");
  264. spinner.setAttribute("height", "64");
  265. spinner.classList.add('loading-spinner');
  266. spinner.style.animation = "spin 1s linear infinite";
  267.  
  268. const secondaryPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
  269. secondaryPath.setAttribute("d", "M0 256C0 114.9 114.1 .5 255.1 0C237.9 .5 224 14.6 224 32c0 17.7 14.3 32 32 32C150 64 64 150 64 256s86 192 192 192c69.7 0 130.7-37.1 164.5-92.6c-3 6.6-3.3 14.8-1 22.2c1.2 3.7 3 7.2 5.4 10.3c1.2 1.5 2.6 3 4.1 4.3c.8 .7 1.6 1.3 2.4 1.9c.4 .3 .8 .6 1.3 .9s.9 .6 1.3 .8c5 2.9 10.6 4.3 16 4.3c11 0 21.8-5.7 27.7-16c-44.3 76.5-127 128-221.7 128C114.6 512 0 397.4 0 256z");
  270. secondaryPath.style.opacity = "0.4";
  271. secondaryPath.style.fill = "white";
  272.  
  273. const primaryPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
  274. primaryPath.setAttribute("d",
  275. "M224 32c0-17.7 14.3-32 32-32C397.4 0 512 114.6 512 256c0 46.6-12.5 90.4-34.3 128c-8.8 15.3-28.4 20.5-43.7 11.7s-20.5-28.4-11.7-43.7c16.3-28.2 25.7-61 25.7-96c0-106-86-192-192-192c-17.7 0-32-14.3-32-32z");
  276. primaryPath.style.fill = "white";
  277.  
  278. spinner.appendChild(secondaryPath);
  279. spinner.appendChild(primaryPath);
  280. spinnerContainer.appendChild(spinner);
  281. return spinnerContainer;
  282. }
  283.  
  284. function createSVGIcon(path) {
  285. const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  286. svg.setAttribute("viewBox", "0 0 640 512");
  287. svg.setAttribute("width", "2rem");
  288. svg.setAttribute("height", "2rem");
  289. svg.style.marginRight = "0.5rem";
  290. svg.style.display = "none";
  291.  
  292. const svgPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
  293. svgPath.setAttribute("d", path);
  294. svgPath.setAttribute("fill", "white");
  295.  
  296. svg.appendChild(svgPath);
  297. return svg;
  298. }
  299.  
  300. function createStatContainer(className, iconPath) {
  301. const container = document.createElement('div');
  302. Object.assign(container.style, {
  303. display: 'flex',
  304. flexDirection: 'column',
  305. alignItems: 'center',
  306. justifyContent: 'center',
  307. visibility: 'hidden',
  308. width: '33%',
  309. height: '100%',
  310. padding: '0 1rem'
  311. });
  312.  
  313. const numberContainer = document.createElement('div');
  314. Object.assign(numberContainer.style, {
  315. display: 'flex',
  316. flexDirection: 'column',
  317. alignItems: 'center',
  318. justifyContent: 'center'
  319. });
  320.  
  321. const differenceElement = document.createElement('div');
  322. differenceElement.classList.add(`${className}-difference`);
  323. Object.assign(differenceElement.style, {
  324. fontSize: '2.5rem',
  325. height: '2.5rem',
  326. marginBottom: '1rem'
  327. });
  328.  
  329. const digitContainer = createNumberContainer();
  330. digitContainer.classList.add(`${className}-number`);
  331. Object.assign(digitContainer.style, {
  332. fontSize: '4rem',
  333. fontWeight: 'bold',
  334. lineHeight: '1',
  335. height: '4rem',
  336. fontFamily: 'inherit',
  337. letterSpacing: '0.025em'
  338. });
  339.  
  340. numberContainer.appendChild(differenceElement);
  341. numberContainer.appendChild(digitContainer);
  342.  
  343. const labelContainer = document.createElement('div');
  344. Object.assign(labelContainer.style, {
  345. display: 'flex',
  346. alignItems: 'center',
  347. marginTop: '0.5rem'
  348. });
  349.  
  350. const icon = createSVGIcon(iconPath);
  351. Object.assign(icon.style, {
  352. width: '2rem',
  353. height: '2rem',
  354. marginRight: '0.75rem'
  355. });
  356.  
  357. const labelElement = document.createElement('div');
  358. labelElement.classList.add(`${className}-label`);
  359. labelElement.style.fontSize = '2rem';
  360.  
  361. labelContainer.appendChild(icon);
  362. labelContainer.appendChild(labelElement);
  363.  
  364. container.appendChild(numberContainer);
  365. container.appendChild(labelContainer);
  366.  
  367. return container;
  368. }
  369.  
  370. function createOverlay(bannerElement) {
  371. clearExistingOverlay();
  372.  
  373. if (!bannerElement) return null;
  374.  
  375. const overlay = document.createElement('div');
  376. overlay.classList.add('channel-banner-overlay');
  377. Object.assign(overlay.style, {
  378. position: 'absolute',
  379. top: '0',
  380. left: '0',
  381. width: '100%',
  382. height: '100%',
  383. backgroundColor: `rgba(0, 0, 0, ${overlayOpacity})`,
  384. borderRadius: '15px',
  385. zIndex: '1',
  386. display: 'flex',
  387. justifyContent: 'space-around',
  388. alignItems: 'center',
  389. color: 'white',
  390. fontFamily: 'Rubik, sans-serif'
  391. });
  392.  
  393. const settingsButton = createSettingsButton();
  394. const settingsMenu = createSettingsMenu();
  395. overlay.appendChild(settingsButton);
  396. overlay.appendChild(settingsMenu);
  397.  
  398. settingsButton.addEventListener('click', (e) => {
  399. e.stopPropagation();
  400. settingsMenu.classList.toggle('show');
  401. });
  402.  
  403. document.addEventListener('click', (e) => {
  404. if (!settingsMenu.contains(e.target) && !settingsButton.contains(e.target)) {
  405. settingsMenu.classList.remove('show');
  406. }
  407. });
  408.  
  409. const spinner = createSpinner();
  410. overlay.appendChild(spinner);
  411.  
  412. const subscribersElement = createStatContainer('subscribers', "M144 160c-44.2 0-80-35.8-80-80S99.8 0 144 0s80 35.8 80 80s-35.8 80-80 80zm368 0c-44.2 0-80-35.8-80-80s35.8-80 80-80s80 35.8 80 80s-35.8 80-80 80zM0 298.7C0 239.8 47.8 192 106.7 192h42.7c15.9 0 31 3.5 44.6 9.7c-1.3 7.2-1.9 14.7-1.9 22.3c0 38.2 16.8 72.5 43.3 96c-.2 0-.4 0-.7 0H21.3C9.6 320 0 310.4 0 298.7zM405.3 320c-.2 0-.4 0-.7 0c26.6-23.5 43.3-57.8 43.3-96c0-7.6-.7-15-1.9-22.3c13.6-6.3 28.7-9.7 44.6-9.7h42.7C592.2 192 640 239.8 640 298.7c0 11.8-9.6 21.3-21.3 21.3H405.3zM416 224c0 53-43 96-96 96s-96-43-96-96s43-96 96-96s96 43 96 96zM128 485.3C128 411.7 187.7 352 261.3 352H378.7C452.3 352 512 411.7 512 485.3c0 14.7-11.9 26.7-26.7 26.7H154.7c-14.7 0-26.7-11.9-26.7-26.7z");
  413. const viewsElement = createStatContainer('views', "M288 32c-80.8 0-145.5 36.8-192.6 80.6C48.6 156 17.3 208 2.5 243.7c-3.3 7.9-3.3 16.7 0 24.6C17.3 304 48.6 356 95.4 399.4C142.5 443.2 207.2 480 288 480s145.5-36.8 192.6-80.6c46.8-43.5 78.1-95.4 93-131.1c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C433.5 68.8 368.8 32 288 32zM144 256a144 144 0 1 1 288 0 144 144 0 1 1 -288 0zm144-64c0 35.3-28.7 64-64 64c-7.1 0-13.9-1.2-20.3-3.3c-5.5-1.8-11.9 1.6-11.7 7.4c.3 6.9 1.3 13.8 3.2 20.7c13.7 51.2 66.4 81.6 117.6 67.9s81.6-66.4 67.9-117.6c-11.1-41.5-47.8-69.4-88.6-71.1c-5.8-.2-9.2 6.1-7.4 11.7c2.1 6.4 3.3 13.2 3.3 20.3z");
  414. const videosElement = createStatContainer('videos', "M0 128C0 92.7 28.7 64 64 64H320c35.3 0 64 28.7 64 64V384c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V128zM559.1 99.8c10.4 5.6 16.9 16.4 16.9 28.2V384c0 11.8-6.5 22.6-16.9 28.2s-23 5-32.9-1.6l-96-64L416 337.1V320 192 174.9l14.2-9.5 96-64c9.8-6.5 22.4-7.2 32.9-1.6z");
  415.  
  416. overlay.appendChild(subscribersElement);
  417. overlay.appendChild(viewsElement);
  418. overlay.appendChild(videosElement);
  419. bannerElement.appendChild(overlay);
  420. updateDisplayState();
  421. return overlay;
  422. }
  423.  
  424. // Helper Functions
  425. function fetchWithGM(url, headers = {}) {
  426. return new Promise((resolve, reject) => {
  427. GM_xmlhttpRequest({
  428. method: "GET",
  429. url: url,
  430. headers: headers,
  431. onload: function(response) {
  432. if (response.status === 200) {
  433. resolve(JSON.parse(response.responseText));
  434. } else {
  435. reject(new Error(`Failed to fetch: ${response.status}`));
  436. }
  437. },
  438. onerror: function(error) {
  439. reject(error);
  440. },
  441. });
  442. });
  443. }
  444.  
  445. async function fetchChannelId(channelName) {
  446. try {
  447. const response = await fetchWithGM(`${API_BASE_URL}${channelName}`);
  448. if (!response || !response.channel_id) {
  449. throw new Error('Invalid channel ID response');
  450. }
  451. return response.channel_id;
  452. } catch (error) {
  453. console.error('Error fetching channel ID:', error);
  454. const metaTag = document.querySelector('meta[itemprop="channelId"]');
  455. if (metaTag && metaTag.content) {
  456. return metaTag.content;
  457. }
  458. const urlMatch = window.location.href.match(/channel\/(UC[\w-]+)/);
  459. if (urlMatch && urlMatch[1]) {
  460. return urlMatch[1];
  461. }
  462. throw new Error('Could not determine channel ID');
  463. }
  464. }
  465. async function fetchChannelStats(channelId) {
  466. try {
  467. let retries = 3;
  468. let lastError;
  469. while (retries > 0) {
  470. try {
  471. const stats = await fetchWithGM(
  472. `${STATS_API_URL}${channelId}`,
  473. {
  474. origin: "https://livecounts.io",
  475. referer: "https://livecounts.io/",
  476. }
  477. );
  478. if (!stats || typeof stats.followerCount === 'undefined') {
  479. throw new Error('Invalid stats response');
  480. }
  481. lastSuccessfulStats.set(channelId, stats);
  482. return stats;
  483. } catch (e) {
  484. lastError = e;
  485. retries--;
  486. if (retries > 0) {
  487. await new Promise(resolve => setTimeout(resolve, 1000));
  488. }
  489. }
  490. }
  491. if (lastSuccessfulStats.has(channelId)) {
  492. return lastSuccessfulStats.get(channelId);
  493. }
  494. const fallbackStats = {
  495. followerCount: 0,
  496. bottomOdos: [0, 0],
  497. error: true
  498. };
  499. const subCountElem = document.querySelector('#subscriber-count');
  500. if (subCountElem) {
  501. const subText = subCountElem.textContent;
  502. const subMatch = subText.match(/[\d,]+/);
  503. if (subMatch) {
  504. fallbackStats.followerCount = parseInt(subMatch[0].replace(/,/g, ''));
  505. }
  506. }
  507. return fallbackStats;
  508. } catch (error) {
  509. console.error('Error fetching channel stats:', error);
  510. throw error;
  511. }
  512. }
  513.  
  514. function clearExistingOverlay() {
  515. const existingOverlay = document.querySelector('.channel-banner-overlay');
  516. if (existingOverlay) {
  517. existingOverlay.remove();
  518. }
  519. if (intervalId) {
  520. clearInterval(intervalId);
  521. intervalId = null;
  522. }
  523. lastSuccessfulStats.clear();
  524. previousStats.clear();
  525. isUpdating = false;
  526. overlay = null;
  527. }
  528.  
  529. function createDigitElement() {
  530. const digit = document.createElement('span');
  531. Object.assign(digit.style, {
  532. display: 'inline-block',
  533. width: '0.6em',
  534. textAlign: 'center',
  535. marginRight: '0.025em',
  536. marginLeft: '0.025em'
  537. });
  538. return digit;
  539. }
  540.  
  541. function createCommaElement() {
  542. const comma = document.createElement('span');
  543. comma.textContent = ',';
  544. Object.assign(comma.style, {
  545. display: 'inline-block',
  546. width: '0.3em',
  547. textAlign: 'center'
  548. });
  549. return comma;
  550. }
  551.  
  552. function createNumberContainer() {
  553. const container = document.createElement('div');
  554. Object.assign(container.style, {
  555. display: 'flex',
  556. justifyContent: 'center',
  557. alignItems: 'center',
  558. letterSpacing: '0.025em'
  559. });
  560. return container;
  561. }
  562.  
  563. function updateDigits(container, newValue) {
  564. const newValueStr = newValue.toString();
  565. const digits = [];
  566. for (let i = newValueStr.length - 1; i >= 0; i -= 3) {
  567. const start = Math.max(0, i - 2);
  568. digits.unshift(newValueStr.slice(start, i + 1));
  569. }
  570. while (container.firstChild) {
  571. container.removeChild(container.firstChild);
  572. }
  573. let digitIndex = 0;
  574. for (let i = 0; i < digits.length; i++) {
  575. const group = digits[i];
  576. for (let j = 0; j < group.length; j++) {
  577. const digitElement = createDigitElement();
  578. digitElement.textContent = group[j];
  579. container.appendChild(digitElement);
  580. digitIndex++;
  581. }
  582. if (i < digits.length - 1) {
  583. container.appendChild(createCommaElement());
  584. }
  585. }
  586. let elementIndex = 0;
  587. for (let i = 0; i < digits.length; i++) {
  588. const group = digits[i];
  589. for (let j = 0; j < group.length; j++) {
  590. const digitElement = container.children[elementIndex];
  591. const newDigit = parseInt(group[j]);
  592. const currentDigit = parseInt(digitElement.textContent || '0');
  593. if (currentDigit !== newDigit) {
  594. animateDigit(digitElement, currentDigit, newDigit);
  595. }
  596. elementIndex++;
  597. }
  598. if (i < digits.length - 1) {
  599. elementIndex++;
  600. }
  601. }
  602. }
  603.  
  604. function animateDigit(element, start, end) {
  605. const duration = 1000;
  606. const startTime = performance.now();
  607.  
  608. function update(currentTime) {
  609. const elapsed = currentTime - startTime;
  610. const progress = Math.min(elapsed / duration, 1);
  611. const easeOutQuart = 1 - Math.pow(1 - progress, 4);
  612. const current = Math.round(start + (end - start) * easeOutQuart);
  613. element.textContent = current;
  614.  
  615. if (progress < 1) {
  616. requestAnimationFrame(update);
  617. }
  618. }
  619.  
  620. requestAnimationFrame(update);
  621. }
  622.  
  623. function showContent(overlay) {
  624. const spinnerContainer = overlay.querySelector('.spinner-container');
  625. if (spinnerContainer) {
  626. spinnerContainer.remove();
  627. }
  628.  
  629. const containers = overlay.querySelectorAll('div[style*="visibility: hidden"]');
  630. containers.forEach(container => {
  631. container.style.visibility = 'visible';
  632. });
  633.  
  634. const icons = overlay.querySelectorAll('svg[style*="display: none"]');
  635. icons.forEach(icon => {
  636. icon.style.display = 'block';
  637. });
  638. }
  639.  
  640. function updateDifferenceElement(element, currentValue, previousValue) {
  641. if (!previousValue) return;
  642. const difference = currentValue - previousValue;
  643. if (difference === 0) {
  644. element.textContent = '';
  645. return;
  646. }
  647. const sign = difference > 0 ? '+' : '';
  648. element.textContent = `${sign}${difference.toLocaleString()}`;
  649. element.style.color = difference > 0 ? '#1ed760' : '#f3727f';
  650. setTimeout(() => {
  651. element.textContent = '';
  652. }, 1000);
  653. }
  654.  
  655. function updateDisplayState() {
  656. const overlay = document.querySelector('.channel-banner-overlay');
  657. if (!overlay) return;
  658. const statContainers = overlay.querySelectorAll('div[style*="width"]');
  659. if (!statContainers.length) return;
  660. let visibleCount = 0;
  661. const visibleContainers = [];
  662. statContainers.forEach(container => {
  663. const numberContainer = container.querySelector('[class$="-number"]');
  664. if (!numberContainer) return;
  665. const type = numberContainer.className.replace('-number', '');
  666. const isVisible = localStorage.getItem(`show-${type}`) !== 'false';
  667. if (isVisible) {
  668. container.style.display = 'flex';
  669. visibleCount++;
  670. visibleContainers.push(container);
  671. } else {
  672. container.style.display = 'none';
  673. }
  674. });
  675. visibleContainers.forEach(container => {
  676. container.style.width = '';
  677. container.style.margin = '';
  678. switch (visibleCount) {
  679. case 1:
  680. container.style.width = '100%';
  681. break;
  682. case 2:
  683. container.style.width = '50%';
  684. break;
  685. case 3:
  686. container.style.width = '33.33%';
  687. break;
  688. default:
  689. container.style.display = 'none';
  690. }
  691. });
  692. overlay.style.display = 'flex';
  693. }
  694.  
  695. async function updateOverlayContent(overlay, channelName) {
  696. if (isUpdating || channelName !== currentChannelName) return;
  697. isUpdating = true;
  698. try {
  699. const channelId = await fetchChannelId(channelName);
  700. const stats = await fetchChannelStats(channelId);
  701. if (channelName !== currentChannelName) {
  702. isUpdating = false;
  703. return;
  704. }
  705. if (stats.error) {
  706. const containers = overlay.querySelectorAll('[class$="-number"]');
  707. containers.forEach(container => {
  708. if (container.classList.contains('subscribers-number') && stats.followerCount > 0) {
  709. updateDigits(container, stats.followerCount);
  710. } else {
  711. container.textContent = '---';
  712. }
  713. });
  714. return;
  715. }
  716.  
  717. const updateElement = (className, value, label) => {
  718. const numberContainer = overlay.querySelector(`.${className}-number`);
  719. const differenceElement = overlay.querySelector(`.${className}-difference`);
  720. const labelElement = overlay.querySelector(`.${className}-label`);
  721. if (numberContainer) {
  722. updateDigits(numberContainer, value);
  723. }
  724. if (differenceElement && previousStats.has(channelId)) {
  725. const previousValue = className === 'subscribers' ?
  726. previousStats.get(channelId).followerCount :
  727. previousStats.get(channelId).bottomOdos[className === 'views' ? 0 : 1];
  728. updateDifferenceElement(differenceElement, value, previousValue);
  729. }
  730. if (labelElement) {
  731. labelElement.textContent = label;
  732. }
  733. };
  734. updateElement('subscribers', stats.followerCount, 'Subscribers');
  735. updateElement('views', stats.bottomOdos[0], 'Views');
  736. updateElement('videos', stats.bottomOdos[1], 'Videos');
  737. if (!previousStats.has(channelId)) {
  738. showContent(overlay);
  739. }
  740. previousStats.set(channelId, stats);
  741. } catch (error) {
  742. console.error(`Error updating overlay: ${error.message}`);
  743. const containers = overlay.querySelectorAll('[class$="-number"]');
  744. containers.forEach(container => {
  745. container.textContent = '---';
  746. });
  747. } finally {
  748. isUpdating = false;
  749. }
  750. }
  751.  
  752. function addOverlay(bannerElement) {
  753. const channelName = window.location.pathname.split("/")[1].replace("@", "");
  754.  
  755. if (channelName === currentChannelName && overlay) {
  756. return;
  757. }
  758.  
  759. currentChannelName = channelName;
  760. overlay = createOverlay(bannerElement);
  761.  
  762. if (overlay) {
  763. if (intervalId) {
  764. clearInterval(intervalId);
  765. }
  766.  
  767. intervalId = setInterval(() => {
  768. updateOverlayContent(overlay, channelName);
  769. }, updateInterval);
  770.  
  771. updateOverlayContent(overlay, channelName);
  772. }
  773. }
  774.  
  775. function isChannelPage() {
  776. return window.location.pathname.startsWith("/@") ||
  777. window.location.pathname.startsWith("/channel/") ||
  778. window.location.pathname.startsWith("/c/");
  779. }
  780.  
  781. function observePageChanges() {
  782. const observer = new MutationObserver((mutations) => {
  783. for (const mutation of mutations) {
  784. if (mutation.type === 'childList') {
  785. const bannerElement = document.getElementById('page-header-banner-sizer');
  786. if (bannerElement && isChannelPage()) {
  787. addOverlay(bannerElement);
  788. break;
  789. }
  790. }
  791. }
  792. });
  793.  
  794. observer.observe(document.body, {
  795. childList: true,
  796. subtree: true
  797. });
  798. }
  799.  
  800. function addNavigationListener() {
  801. window.addEventListener("yt-navigate-finish", () => {
  802. if (!isChannelPage()) {
  803. clearExistingOverlay();
  804. currentChannelName = null;
  805. } else {
  806. const bannerElement = document.getElementById('page-header-banner-sizer');
  807. if (bannerElement) {
  808. addOverlay(bannerElement);
  809. }
  810. }
  811. });
  812. }
  813.  
  814. // Initialize the script
  815. init();
  816. console.log('YouTube Enhancer (Real-Time Subscriber Count) is running');
  817. })();