YouTube Enhancer (Real-Time Subscriber Count)

Display Real-Time Subscriber Count.

  1. // ==UserScript==
  2. // @name YouTube Enhancer (Real-Time Subscriber Count)
  3. // @description Display Real-Time Subscriber Count.
  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. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. const OPTIONS = ['subscribers', 'views', 'videos'];
  18. const FONT_LINK = "https://fonts.googleapis.com/css2?family=Rubik:wght@400;700&display=swap";
  19. const STATS_API_URL = 'https://api.livecounts.io/youtube-live-subscriber-counter/stats/';
  20. const DEFAULT_UPDATE_INTERVAL = 2000;
  21. const DEFAULT_OVERLAY_OPACITY = 0.75;
  22.  
  23. let overlay = null;
  24. let isUpdating = false;
  25. let intervalId = null;
  26. let currentChannelName = null;
  27. let updateInterval = parseInt(localStorage.getItem('youtubeEnhancerInterval')) || DEFAULT_UPDATE_INTERVAL;
  28. let overlayOpacity = parseFloat(localStorage.getItem('youtubeEnhancerOpacity')) || DEFAULT_OVERLAY_OPACITY;
  29.  
  30. const lastSuccessfulStats = new Map();
  31. const previousStats = new Map();
  32. let previousUrl = location.href;
  33. let isChecking = false;
  34.  
  35. async function fetchChannel(url) {
  36. if (isChecking) return null;
  37. isChecking = true;
  38. try {
  39. const response = await fetch(url, {
  40. credentials: 'same-origin'
  41. });
  42. if (!response.ok) return null;
  43. const html = await response.text();
  44. const match = html.match(/var ytInitialData = (.+?);<\/script>/);
  45. return match && match[1] ? JSON.parse(match[1]) : null;
  46. } catch (error) {
  47. return null;
  48. } finally {
  49. isChecking = false;
  50. }
  51. }
  52.  
  53. async function getChannelInfo(url) {
  54. const data = await fetchChannel(url);
  55. if (!data) return null;
  56. try {
  57. const channelName = data?.metadata?.channelMetadataRenderer?.title || "Unknown";
  58. const channelId = data?.metadata?.channelMetadataRenderer?.externalId || null;
  59. return {channelName, channelId};
  60. } catch (e) {
  61. return null;
  62. }
  63. }
  64.  
  65. function isChannelPageUrl(url) {
  66. return url.includes('youtube.com/') &&
  67. (url.includes('/channel/') || url.includes('/@')) &&
  68. !url.includes('/video/') &&
  69. !url.includes('/watch');
  70. }
  71.  
  72. function checkUrlChange() {
  73. const currentUrl = location.href;
  74. if (currentUrl !== previousUrl) {
  75. previousUrl = currentUrl;
  76. if (isChannelPageUrl(currentUrl)) {
  77. setTimeout(() => getChannelInfo(currentUrl), 500);
  78. }
  79. }
  80. }
  81.  
  82. history.pushState = (function(f) {
  83. return function() {
  84. f.apply(this, arguments);
  85. checkUrlChange();
  86. };
  87. })(history.pushState);
  88.  
  89. history.replaceState = (function(f) {
  90. return function() {
  91. f.apply(this, arguments);
  92. checkUrlChange();
  93. };
  94. })(history.replaceState);
  95.  
  96. window.addEventListener('popstate', checkUrlChange);
  97. setInterval(checkUrlChange, 1000);
  98.  
  99. function init() {
  100. loadFonts();
  101. initializeLocalStorage();
  102. addStyles();
  103. observePageChanges();
  104. addNavigationListener();
  105. if (isChannelPageUrl(location.href)) {
  106. getChannelInfo(location.href);
  107. }
  108. }
  109.  
  110. function loadFonts() {
  111. const fontLink = document.createElement("link");
  112. fontLink.rel = "stylesheet";
  113. fontLink.href = FONT_LINK;
  114. document.head.appendChild(fontLink);
  115. }
  116.  
  117. function initializeLocalStorage() {
  118. OPTIONS.forEach(option => {
  119. if (localStorage.getItem(`show-${option}`) === null) {
  120. localStorage.setItem(`show-${option}`, 'true');
  121. }
  122. });
  123. }
  124.  
  125. function addStyles() {
  126. const style = document.createElement('style');
  127. style.textContent = `
  128. .settings-button {
  129. position: absolute;
  130. top: 12px;
  131. right: 12px;
  132. width: 16px;
  133. height: 16px;
  134. cursor: pointer;
  135. z-index: 2;
  136. transition: transform 0.3s ease;
  137. }
  138. .settings-button:hover {
  139. transform: rotate(45deg);
  140. }
  141. .settings-menu {
  142. position: absolute;
  143. top: 35px;
  144. right: 12px;
  145. background: rgba(0, 0, 0, 0.95);
  146. padding: 10px;
  147. border-radius: 6px;
  148. z-index: 2;
  149. display: none;
  150. }
  151. .settings-menu.show {
  152. display: flex;
  153. }
  154. .interval-slider, .opacity-slider {
  155. width: 160px;
  156. margin: 5px 0;
  157. height: 4px;
  158. }
  159. .interval-value, .opacity-value {
  160. color: white;
  161. font-size: 12px;
  162. margin-top: 3px;
  163. margin-bottom: 8px;
  164. }
  165. .setting-group {
  166. margin-bottom: 10px;
  167. }
  168. .setting-group:last-child {
  169. margin-bottom: 0;
  170. }
  171. @keyframes spin {
  172. from { transform: rotate(0deg); }
  173. to { transform: rotate(360deg); }
  174. }
  175. `;
  176. document.head.appendChild(style);
  177. }
  178.  
  179. function createSettingsButton() {
  180. const button = document.createElement('div');
  181. button.className = 'settings-button';
  182. const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  183. svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
  184. svg.setAttribute("viewBox", "0 0 512 512");
  185. const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
  186. path.setAttribute("fill", "white");
  187. 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");
  188. svg.appendChild(path);
  189. button.appendChild(svg);
  190. return button;
  191. }
  192.  
  193. function createSettingsMenu() {
  194. const menu = document.createElement('div');
  195. menu.className = 'settings-menu';
  196. menu.style.gap = '15px';
  197. menu.style.width = '360px';
  198. const displaySection = createDisplaySection();
  199. const controlsSection = createControlsSection();
  200. menu.appendChild(displaySection);
  201. menu.appendChild(controlsSection);
  202. return menu;
  203. }
  204.  
  205. function createDisplaySection() {
  206. const displaySection = document.createElement('div');
  207. displaySection.style.flex = '1';
  208. const displayLabel = document.createElement('label');
  209. displayLabel.textContent = 'Display Options';
  210. displayLabel.style.marginBottom = '10px';
  211. displayLabel.style.display = 'block';
  212. displayLabel.style.fontSize = '16px';
  213. displayLabel.style.fontWeight = 'bold';
  214. displaySection.appendChild(displayLabel);
  215. OPTIONS.forEach(option => {
  216. const checkboxContainer = document.createElement('div');
  217. checkboxContainer.style.display = 'flex';
  218. checkboxContainer.style.alignItems = 'center';
  219. checkboxContainer.style.marginTop = '5px';
  220. const checkbox = document.createElement('input');
  221. checkbox.type = 'checkbox';
  222. checkbox.id = `show-${option}`;
  223. checkbox.checked = localStorage.getItem(`show-${option}`) !== 'false';
  224. checkbox.style.marginRight = '8px';
  225. checkbox.style.cursor = 'pointer';
  226. const checkboxLabel = document.createElement('label');
  227. checkboxLabel.htmlFor = `show-${option}`;
  228. checkboxLabel.textContent = option.charAt(0).toUpperCase() + option.slice(1);
  229. checkboxLabel.style.cursor = 'pointer';
  230. checkboxLabel.style.color = 'white';
  231. checkboxLabel.style.fontSize = '14px';
  232. checkbox.addEventListener('change', () => {
  233. localStorage.setItem(`show-${option}`, checkbox.checked);
  234. updateDisplayState();
  235. });
  236. checkboxContainer.appendChild(checkbox);
  237. checkboxContainer.appendChild(checkboxLabel);
  238. displaySection.appendChild(checkboxContainer);
  239. });
  240.  
  241. return displaySection;
  242. }
  243.  
  244. function createControlsSection() {
  245. const controlsSection = document.createElement('div');
  246. controlsSection.style.flex = '1';
  247. const intervalLabel = document.createElement('label');
  248. intervalLabel.textContent = 'Update Interval';
  249. intervalLabel.style.display = 'block';
  250. intervalLabel.style.marginBottom = '5px';
  251. intervalLabel.style.fontSize = '16px';
  252. intervalLabel.style.fontWeight = 'bold';
  253. const intervalSlider = document.createElement('input');
  254. intervalSlider.type = 'range';
  255. intervalSlider.min = '2';
  256. intervalSlider.max = '10';
  257. intervalSlider.value = updateInterval / 1000;
  258. intervalSlider.step = '1';
  259. intervalSlider.className = 'interval-slider';
  260. const intervalValue = document.createElement('div');
  261. intervalValue.className = 'interval-value';
  262. intervalValue.textContent = `${intervalSlider.value}s`;
  263. intervalValue.style.marginBottom = '15px';
  264. intervalValue.style.fontSize = '14px';
  265. intervalSlider.addEventListener('input', (e) => {
  266. const newInterval = parseInt(e.target.value) * 1000;
  267. intervalValue.textContent = `${e.target.value}s`;
  268. updateInterval = newInterval;
  269. localStorage.setItem('youtubeEnhancerInterval', newInterval);
  270. if (intervalId) {
  271. clearInterval(intervalId);
  272. intervalId = setInterval(() => {
  273. updateOverlayContent(overlay, currentChannelName);
  274. }, newInterval);
  275. }
  276. });
  277. const opacityLabel = document.createElement('label');
  278. opacityLabel.textContent = 'Background Opacity';
  279. opacityLabel.style.display = 'block';
  280. opacityLabel.style.marginBottom = '5px';
  281. opacityLabel.style.fontSize = '16px';
  282. opacityLabel.style.fontWeight = 'bold';
  283. const opacitySlider = document.createElement('input');
  284. opacitySlider.type = 'range';
  285. opacitySlider.min = '50';
  286. opacitySlider.max = '90';
  287. opacitySlider.value = overlayOpacity * 100;
  288. opacitySlider.step = '5';
  289. opacitySlider.className = 'opacity-slider';
  290. const opacityValue = document.createElement('div');
  291. opacityValue.className = 'opacity-value';
  292. opacityValue.textContent = `${opacitySlider.value}%`;
  293. opacityValue.style.fontSize = '14px';
  294. opacitySlider.addEventListener('input', (e) => {
  295. const newOpacity = parseInt(e.target.value) / 100;
  296. opacityValue.textContent = `${e.target.value}%`;
  297. overlayOpacity = newOpacity;
  298. localStorage.setItem('youtubeEnhancerOpacity', newOpacity);
  299. if (overlay) {
  300. overlay.style.backgroundColor = `rgba(0, 0, 0, ${newOpacity})`;
  301. }
  302. });
  303. controlsSection.appendChild(intervalLabel);
  304. controlsSection.appendChild(intervalSlider);
  305. controlsSection.appendChild(intervalValue);
  306. controlsSection.appendChild(opacityLabel);
  307. controlsSection.appendChild(opacitySlider);
  308. controlsSection.appendChild(opacityValue);
  309.  
  310. return controlsSection;
  311. }
  312.  
  313. function createSpinner() {
  314. const spinnerContainer = document.createElement('div');
  315. spinnerContainer.style.position = 'absolute';
  316. spinnerContainer.style.top = '0';
  317. spinnerContainer.style.left = '0';
  318. spinnerContainer.style.width = '100%';
  319. spinnerContainer.style.height = '100%';
  320. spinnerContainer.style.display = 'flex';
  321. spinnerContainer.style.justifyContent = 'center';
  322. spinnerContainer.style.alignItems = 'center';
  323. spinnerContainer.classList.add('spinner-container');
  324.  
  325. const spinner = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  326. spinner.setAttribute("viewBox", "0 0 512 512");
  327. spinner.setAttribute("width", "64");
  328. spinner.setAttribute("height", "64");
  329. spinner.classList.add('loading-spinner');
  330. spinner.style.animation = "spin 1s linear infinite";
  331.  
  332. const secondaryPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
  333. 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");
  334. secondaryPath.style.opacity = "0.4";
  335. secondaryPath.style.fill = "white";
  336.  
  337. const primaryPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
  338. primaryPath.setAttribute("d",
  339. "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");
  340. primaryPath.style.fill = "white";
  341.  
  342. spinner.appendChild(secondaryPath);
  343. spinner.appendChild(primaryPath);
  344. spinnerContainer.appendChild(spinner);
  345. return spinnerContainer;
  346. }
  347.  
  348. function createSVGIcon(path) {
  349. const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  350. svg.setAttribute("viewBox", "0 0 640 512");
  351. svg.setAttribute("width", "2rem");
  352. svg.setAttribute("height", "2rem");
  353. svg.style.marginRight = "0.5rem";
  354. svg.style.display = "none";
  355.  
  356. const svgPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
  357. svgPath.setAttribute("d", path);
  358. svgPath.setAttribute("fill", "white");
  359.  
  360. svg.appendChild(svgPath);
  361. return svg;
  362. }
  363.  
  364. function createStatContainer(className, iconPath) {
  365. const container = document.createElement('div');
  366. Object.assign(container.style, {
  367. display: 'flex',
  368. flexDirection: 'column',
  369. alignItems: 'center',
  370. justifyContent: 'center',
  371. visibility: 'hidden',
  372. width: '33%',
  373. height: '100%',
  374. padding: '0 1rem'
  375. });
  376.  
  377. const numberContainer = document.createElement('div');
  378. Object.assign(numberContainer.style, {
  379. display: 'flex',
  380. flexDirection: 'column',
  381. alignItems: 'center',
  382. justifyContent: 'center'
  383. });
  384.  
  385. const differenceElement = document.createElement('div');
  386. differenceElement.classList.add(`${className}-difference`);
  387. Object.assign(differenceElement.style, {
  388. fontSize: '2.5rem',
  389. height: '2.5rem',
  390. marginBottom: '1rem'
  391. });
  392.  
  393. const digitContainer = createNumberContainer();
  394. digitContainer.classList.add(`${className}-number`);
  395. Object.assign(digitContainer.style, {
  396. fontSize: '4rem',
  397. fontWeight: 'bold',
  398. lineHeight: '1',
  399. height: '4rem',
  400. fontFamily: 'inherit',
  401. letterSpacing: '0.025em'
  402. });
  403.  
  404. numberContainer.appendChild(differenceElement);
  405. numberContainer.appendChild(digitContainer);
  406.  
  407. const labelContainer = document.createElement('div');
  408. Object.assign(labelContainer.style, {
  409. display: 'flex',
  410. alignItems: 'center',
  411. marginTop: '0.5rem'
  412. });
  413.  
  414. const icon = createSVGIcon(iconPath);
  415. Object.assign(icon.style, {
  416. width: '2rem',
  417. height: '2rem',
  418. marginRight: '0.75rem'
  419. });
  420.  
  421. const labelElement = document.createElement('div');
  422. labelElement.classList.add(`${className}-label`);
  423. labelElement.style.fontSize = '2rem';
  424.  
  425. labelContainer.appendChild(icon);
  426. labelContainer.appendChild(labelElement);
  427.  
  428. container.appendChild(numberContainer);
  429. container.appendChild(labelContainer);
  430.  
  431. return container;
  432. }
  433.  
  434. function createOverlay(bannerElement) {
  435. clearExistingOverlay();
  436.  
  437. if (!bannerElement) return null;
  438.  
  439. const overlay = document.createElement('div');
  440. overlay.classList.add('channel-banner-overlay');
  441. Object.assign(overlay.style, {
  442. position: 'absolute',
  443. top: '0',
  444. left: '0',
  445. width: '100%',
  446. height: '100%',
  447. backgroundColor: `rgba(0, 0, 0, ${overlayOpacity})`,
  448. borderRadius: '15px',
  449. zIndex: '1',
  450. display: 'flex',
  451. justifyContent: 'space-around',
  452. alignItems: 'center',
  453. color: 'white',
  454. fontFamily: 'Rubik, sans-serif'
  455. });
  456.  
  457. const settingsButton = createSettingsButton();
  458. const settingsMenu = createSettingsMenu();
  459. overlay.appendChild(settingsButton);
  460. overlay.appendChild(settingsMenu);
  461.  
  462. settingsButton.addEventListener('click', (e) => {
  463. e.stopPropagation();
  464. settingsMenu.classList.toggle('show');
  465. });
  466.  
  467. document.addEventListener('click', (e) => {
  468. if (!settingsMenu.contains(e.target) && !settingsButton.contains(e.target)) {
  469. settingsMenu.classList.remove('show');
  470. }
  471. });
  472.  
  473. const spinner = createSpinner();
  474. overlay.appendChild(spinner);
  475.  
  476. 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");
  477. 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");
  478. 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");
  479.  
  480. overlay.appendChild(subscribersElement);
  481. overlay.appendChild(viewsElement);
  482. overlay.appendChild(videosElement);
  483. bannerElement.appendChild(overlay);
  484. updateDisplayState();
  485. return overlay;
  486. }
  487.  
  488. function fetchWithGM(url, headers = {}) {
  489. return new Promise((resolve, reject) => {
  490. GM_xmlhttpRequest({
  491. method: "GET",
  492. url: url,
  493. headers: headers,
  494. onload: function(response) {
  495. if (response.status === 200) {
  496. resolve(JSON.parse(response.responseText));
  497. } else {
  498. reject(new Error(`Failed to fetch: ${response.status}`));
  499. }
  500. },
  501. onerror: function(error) {
  502. reject(error);
  503. },
  504. });
  505. });
  506. }
  507.  
  508. async function fetchChannelId(channelName) {
  509. try {
  510. const channelInfo = await getChannelInfo(window.location.href);
  511. if (channelInfo && channelInfo.channelId) {
  512. return channelInfo.channelId;
  513. }
  514. const metaTag = document.querySelector('meta[itemprop="channelId"]');
  515. if (metaTag && metaTag.content) {
  516. return metaTag.content;
  517. }
  518. const urlMatch = window.location.href.match(/channel\/(UC[\w-]+)/);
  519. if (urlMatch && urlMatch[1]) {
  520. return urlMatch[1];
  521. }
  522. throw new Error('Could not determine channel ID');
  523. } catch (error) {
  524. const metaTag = document.querySelector('meta[itemprop="channelId"]');
  525. if (metaTag && metaTag.content) {
  526. return metaTag.content;
  527. }
  528. const urlMatch = window.location.href.match(/channel\/(UC[\w-]+)/);
  529. if (urlMatch && urlMatch[1]) {
  530. return urlMatch[1];
  531. }
  532. throw new Error('Could not determine channel ID');
  533. }
  534. }
  535. async function fetchChannelStats(channelId) {
  536. try {
  537. let retries = 3;
  538. let lastError;
  539. while (retries > 0) {
  540. try {
  541. const stats = await fetchWithGM(
  542. `${STATS_API_URL}${channelId}`,
  543. {
  544. origin: "https://livecounts.io",
  545. referer: "https://livecounts.io/",
  546. }
  547. );
  548. if (!stats || typeof stats.followerCount === 'undefined') {
  549. throw new Error('Invalid stats response');
  550. }
  551. lastSuccessfulStats.set(channelId, stats);
  552. return stats;
  553. } catch (e) {
  554. lastError = e;
  555. retries--;
  556. if (retries > 0) {
  557. await new Promise(resolve => setTimeout(resolve, 1000));
  558. }
  559. }
  560. }
  561. if (lastSuccessfulStats.has(channelId)) {
  562. return lastSuccessfulStats.get(channelId);
  563. }
  564. const fallbackStats = {
  565. followerCount: 0,
  566. bottomOdos: [0, 0],
  567. error: true
  568. };
  569. const subCountElem = document.querySelector('#subscriber-count');
  570. if (subCountElem) {
  571. const subText = subCountElem.textContent;
  572. const subMatch = subText.match(/[\d,]+/);
  573. if (subMatch) {
  574. fallbackStats.followerCount = parseInt(subMatch[0].replace(/,/g, ''));
  575. }
  576. }
  577. return fallbackStats;
  578. } catch (error) {
  579. throw error;
  580. }
  581. }
  582.  
  583. function clearExistingOverlay() {
  584. const existingOverlay = document.querySelector('.channel-banner-overlay');
  585. if (existingOverlay) {
  586. existingOverlay.remove();
  587. }
  588. if (intervalId) {
  589. clearInterval(intervalId);
  590. intervalId = null;
  591. }
  592. lastSuccessfulStats.clear();
  593. previousStats.clear();
  594. isUpdating = false;
  595. overlay = null;
  596. }
  597.  
  598. function createDigitElement() {
  599. const digit = document.createElement('span');
  600. Object.assign(digit.style, {
  601. display: 'inline-block',
  602. width: '0.6em',
  603. textAlign: 'center',
  604. marginRight: '0.025em',
  605. marginLeft: '0.025em'
  606. });
  607. return digit;
  608. }
  609.  
  610. function createCommaElement() {
  611. const comma = document.createElement('span');
  612. comma.textContent = ',';
  613. Object.assign(comma.style, {
  614. display: 'inline-block',
  615. width: '0.3em',
  616. textAlign: 'center'
  617. });
  618. return comma;
  619. }
  620.  
  621. function createNumberContainer() {
  622. const container = document.createElement('div');
  623. Object.assign(container.style, {
  624. display: 'flex',
  625. justifyContent: 'center',
  626. alignItems: 'center',
  627. letterSpacing: '0.025em'
  628. });
  629. return container;
  630. }
  631.  
  632. function updateDigits(container, newValue) {
  633. const newValueStr = newValue.toString();
  634. const digits = [];
  635. for (let i = newValueStr.length - 1; i >= 0; i -= 3) {
  636. const start = Math.max(0, i - 2);
  637. digits.unshift(newValueStr.slice(start, i + 1));
  638. }
  639. while (container.firstChild) {
  640. container.removeChild(container.firstChild);
  641. }
  642. let digitIndex = 0;
  643. for (let i = 0; i < digits.length; i++) {
  644. const group = digits[i];
  645. for (let j = 0; j < group.length; j++) {
  646. const digitElement = createDigitElement();
  647. digitElement.textContent = group[j];
  648. container.appendChild(digitElement);
  649. digitIndex++;
  650. }
  651. if (i < digits.length - 1) {
  652. container.appendChild(createCommaElement());
  653. }
  654. }
  655. let elementIndex = 0;
  656. for (let i = 0; i < digits.length; i++) {
  657. const group = digits[i];
  658. for (let j = 0; j < group.length; j++) {
  659. const digitElement = container.children[elementIndex];
  660. const newDigit = parseInt(group[j]);
  661. const currentDigit = parseInt(digitElement.textContent || '0');
  662. if (currentDigit !== newDigit) {
  663. animateDigit(digitElement, currentDigit, newDigit);
  664. }
  665. elementIndex++;
  666. }
  667. if (i < digits.length - 1) {
  668. elementIndex++;
  669. }
  670. }
  671. }
  672.  
  673. function animateDigit(element, start, end) {
  674. const duration = 1000;
  675. const startTime = performance.now();
  676.  
  677. function update(currentTime) {
  678. const elapsed = currentTime - startTime;
  679. const progress = Math.min(elapsed / duration, 1);
  680. const easeOutQuart = 1 - Math.pow(1 - progress, 4);
  681. const current = Math.round(start + (end - start) * easeOutQuart);
  682. element.textContent = current;
  683.  
  684. if (progress < 1) {
  685. requestAnimationFrame(update);
  686. }
  687. }
  688.  
  689. requestAnimationFrame(update);
  690. }
  691.  
  692. function showContent(overlay) {
  693. const spinnerContainer = overlay.querySelector('.spinner-container');
  694. if (spinnerContainer) {
  695. spinnerContainer.remove();
  696. }
  697.  
  698. const containers = overlay.querySelectorAll('div[style*="visibility: hidden"]');
  699. containers.forEach(container => {
  700. container.style.visibility = 'visible';
  701. });
  702.  
  703. const icons = overlay.querySelectorAll('svg[style*="display: none"]');
  704. icons.forEach(icon => {
  705. icon.style.display = 'block';
  706. });
  707. }
  708.  
  709. function updateDifferenceElement(element, currentValue, previousValue) {
  710. if (!previousValue) return;
  711. const difference = currentValue - previousValue;
  712. if (difference === 0) {
  713. element.textContent = '';
  714. return;
  715. }
  716. const sign = difference > 0 ? '+' : '';
  717. element.textContent = `${sign}${difference.toLocaleString()}`;
  718. element.style.color = difference > 0 ? '#1ed760' : '#f3727f';
  719. setTimeout(() => {
  720. element.textContent = '';
  721. }, 1000);
  722. }
  723.  
  724. function updateDisplayState() {
  725. const overlay = document.querySelector('.channel-banner-overlay');
  726. if (!overlay) return;
  727. const statContainers = overlay.querySelectorAll('div[style*="width"]');
  728. if (!statContainers.length) return;
  729. let visibleCount = 0;
  730. const visibleContainers = [];
  731. statContainers.forEach(container => {
  732. const numberContainer = container.querySelector('[class$="-number"]');
  733. if (!numberContainer) return;
  734. const type = numberContainer.className.replace('-number', '');
  735. const isVisible = localStorage.getItem(`show-${type}`) !== 'false';
  736. if (isVisible) {
  737. container.style.display = 'flex';
  738. visibleCount++;
  739. visibleContainers.push(container);
  740. } else {
  741. container.style.display = 'none';
  742. }
  743. });
  744. visibleContainers.forEach(container => {
  745. container.style.width = '';
  746. container.style.margin = '';
  747. switch (visibleCount) {
  748. case 1:
  749. container.style.width = '100%';
  750. break;
  751. case 2:
  752. container.style.width = '50%';
  753. break;
  754. case 3:
  755. container.style.width = '33.33%';
  756. break;
  757. default:
  758. container.style.display = 'none';
  759. }
  760. });
  761. overlay.style.display = 'flex';
  762. }
  763.  
  764. async function updateOverlayContent(overlay, channelName) {
  765. if (isUpdating || channelName !== currentChannelName) return;
  766. isUpdating = true;
  767. try {
  768. const channelId = await fetchChannelId(channelName);
  769. const stats = await fetchChannelStats(channelId);
  770. if (channelName !== currentChannelName) {
  771. isUpdating = false;
  772. return;
  773. }
  774. if (stats.error) {
  775. const containers = overlay.querySelectorAll('[class$="-number"]');
  776. containers.forEach(container => {
  777. if (container.classList.contains('subscribers-number') && stats.followerCount > 0) {
  778. updateDigits(container, stats.followerCount);
  779. } else {
  780. container.textContent = '---';
  781. }
  782. });
  783. return;
  784. }
  785.  
  786. const updateElement = (className, value, label) => {
  787. const numberContainer = overlay.querySelector(`.${className}-number`);
  788. const differenceElement = overlay.querySelector(`.${className}-difference`);
  789. const labelElement = overlay.querySelector(`.${className}-label`);
  790. if (numberContainer) {
  791. updateDigits(numberContainer, value);
  792. }
  793. if (differenceElement && previousStats.has(channelId)) {
  794. const previousValue = className === 'subscribers' ?
  795. previousStats.get(channelId).followerCount :
  796. previousStats.get(channelId).bottomOdos[className === 'views' ? 0 : 1];
  797. updateDifferenceElement(differenceElement, value, previousValue);
  798. }
  799. if (labelElement) {
  800. labelElement.textContent = label;
  801. }
  802. };
  803. updateElement('subscribers', stats.followerCount, 'Subscribers');
  804. updateElement('views', stats.bottomOdos[0], 'Views');
  805. updateElement('videos', stats.bottomOdos[1], 'Videos');
  806. if (!previousStats.has(channelId)) {
  807. showContent(overlay);
  808. }
  809. previousStats.set(channelId, stats);
  810. } catch (error) {
  811. const containers = overlay.querySelectorAll('[class$="-number"]');
  812. containers.forEach(container => {
  813. container.textContent = '---';
  814. });
  815. } finally {
  816. isUpdating = false;
  817. }
  818. }
  819.  
  820. function addOverlay(bannerElement) {
  821. const channelName = window.location.pathname.split("/")[1].replace("@", "");
  822.  
  823. if (channelName === currentChannelName && overlay) {
  824. return;
  825. }
  826.  
  827. currentChannelName = channelName;
  828. overlay = createOverlay(bannerElement);
  829.  
  830. if (overlay) {
  831. if (intervalId) {
  832. clearInterval(intervalId);
  833. }
  834.  
  835. intervalId = setInterval(() => {
  836. updateOverlayContent(overlay, channelName);
  837. }, updateInterval);
  838.  
  839. updateOverlayContent(overlay, channelName);
  840. }
  841. }
  842.  
  843. function isChannelPage() {
  844. return window.location.pathname.startsWith("/@") ||
  845. window.location.pathname.startsWith("/channel/") ||
  846. window.location.pathname.startsWith("/c/");
  847. }
  848.  
  849. function observePageChanges() {
  850. const observer = new MutationObserver((mutations) => {
  851. for (const mutation of mutations) {
  852. if (mutation.type === 'childList') {
  853. const bannerElement = document.getElementById('page-header-banner-sizer');
  854. if (bannerElement && isChannelPage()) {
  855. addOverlay(bannerElement);
  856. break;
  857. }
  858. }
  859. }
  860. });
  861.  
  862. observer.observe(document.body, {
  863. childList: true,
  864. subtree: true
  865. });
  866. }
  867.  
  868. function addNavigationListener() {
  869. window.addEventListener("yt-navigate-finish", () => {
  870. if (!isChannelPage()) {
  871. clearExistingOverlay();
  872. currentChannelName = null;
  873. } else {
  874. const bannerElement = document.getElementById('page-header-banner-sizer');
  875. if (bannerElement) {
  876. addOverlay(bannerElement);
  877. }
  878. }
  879. });
  880. }
  881.  
  882. init();
  883. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址