YouTube Enchantments

Automatically likes videos of channels you're subscribed to, scrolls down on Youtube with a toggle button, and bypasses the AdBlock ban.

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

  1. // ==UserScript==
  2. // @name YouTube Enchantments
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.8
  5. // @description Automatically likes videos of channels you're subscribed to, scrolls down on Youtube with a toggle button, and bypasses the AdBlock ban.
  6. // @author JJJ
  7. // @match https://www.youtube.com/*
  8. // @exclude https://www.youtube.com/*/community
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
  10. // @grant GM_getValue
  11. // @grant GM_setValue
  12. // @grant GM_registerMenuCommand
  13. // @run-at document-idle
  14. // @noframes
  15. // @license MIT
  16. // ==/UserScript==
  17.  
  18. (() => {
  19. 'use strict';
  20.  
  21. const SELECTORS = {
  22. PLAYER: '#movie_player',
  23. SUBSCRIBE_BUTTON: '#subscribe-button > ytd-subscribe-button-renderer, ytd-reel-player-overlay-renderer #subscribe-button',
  24. LIKE_BUTTON: '#menu .YtLikeButtonViewModelHost button, #segmented-like-button button, #like-button button',
  25. DISLIKE_BUTTON: '#menu .YtDislikeButtonViewModelHost button, #segmented-dislike-button button, #dislike-button button',
  26. PLAYER_CONTAINER: '#player-container-outer',
  27. ERROR_SCREEN: '#error-screen',
  28. PLAYABILITY_ERROR: '.yt-playability-error-supported-renderers',
  29. LIVE_BADGE: '.ytp-live-badge'
  30. };
  31.  
  32. const CONSTANTS = {
  33. IFRAME_ID: 'adblock-bypass-player',
  34. STORAGE_KEY: 'youtubeEnchantmentsSettings',
  35. DELAY: 200,
  36. MAX_TRIES: 100,
  37. DUPLICATE_CHECK_INTERVAL: 5000
  38. };
  39.  
  40. const defaultSettings = {
  41. autoLikeEnabled: true,
  42. autoLikeLiveStreams: false,
  43. likeIfNotSubscribed: false,
  44. watchThreshold: 0,
  45. checkFrequency: 5000
  46. };
  47.  
  48. let settings = loadSettings();
  49. const autoLikedVideoIds = new Set();
  50. let isScrolling = false;
  51. let scrollInterval;
  52. let currentPageUrl = window.location.href;
  53. let tries = 0;
  54.  
  55. const worker = createWorker();
  56.  
  57. const urlUtils = {
  58. extractParams(url) {
  59. try {
  60. const params = new URL(url).searchParams;
  61. return {
  62. videoId: params.get('v'),
  63. playlistId: params.get('list'),
  64. index: params.get('index')
  65. };
  66. } catch (e) {
  67. console.error('Failed to extract URL params:', e);
  68. return {};
  69. }
  70. },
  71.  
  72. getTimestampFromUrl(url) {
  73. try {
  74. const timestamp = new URL(url).searchParams.get('t');
  75. if (timestamp) {
  76. const timeArray = timestamp.split(/h|m|s/).map(Number);
  77. const timeInSeconds = timeArray.reduce((acc, time, index) =>
  78. acc + time * Math.pow(60, 2 - index), 0);
  79. return `&start=${timeInSeconds}`;
  80. }
  81. } catch (e) {
  82. console.error('Failed to extract timestamp:', e);
  83. }
  84. return '';
  85. }
  86. };
  87.  
  88. const playerManager = {
  89. createIframe(url) {
  90. const { videoId, playlistId, index } = urlUtils.extractParams(url);
  91. if (!videoId) return null;
  92.  
  93. const iframe = document.createElement('iframe');
  94. const commonArgs = 'autoplay=1&modestbranding=1';
  95. const embedUrl = playlistId
  96. ? `https://www.youtube-nocookie.com/embed/${videoId}?${commonArgs}&list=${playlistId}&index=${index}`
  97. : `https://www.youtube-nocookie.com/embed/${videoId}?${commonArgs}${urlUtils.getTimestampFromUrl(url)}`;
  98.  
  99. this.setIframeAttributes(iframe, embedUrl);
  100. return iframe;
  101. },
  102.  
  103. setIframeAttributes(iframe, url) {
  104. iframe.id = CONSTANTS.IFRAME_ID;
  105. iframe.src = url;
  106. iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share';
  107. iframe.allowFullscreen = true;
  108. iframe.style.cssText = 'height:100%; width:100%; border:none; border-radius:12px;';
  109. },
  110.  
  111. replacePlayer(url) {
  112. const playerContainer = document.querySelector(SELECTORS.ERROR_SCREEN);
  113. if (!playerContainer) return;
  114.  
  115. let iframe = document.getElementById(CONSTANTS.IFRAME_ID);
  116. if (iframe) {
  117. this.setIframeAttributes(iframe, url);
  118. } else {
  119. iframe = this.createIframe(url);
  120. if (iframe) {
  121. playerContainer.appendChild(iframe);
  122. }
  123. }
  124. this.bringToFront(CONSTANTS.IFRAME_ID);
  125. },
  126.  
  127. bringToFront(elementId) {
  128. const element = document.getElementById(elementId);
  129. if (element) {
  130. const maxZIndex = Math.max(
  131. ...Array.from(document.querySelectorAll('*'))
  132. .map(e => parseInt(window.getComputedStyle(e).zIndex) || 0)
  133. );
  134. element.style.zIndex = maxZIndex + 1;
  135. }
  136. },
  137.  
  138. removeDuplicates() {
  139. const iframes = document.querySelectorAll(`#${CONSTANTS.IFRAME_ID}`);
  140. if (iframes.length > 1) {
  141. Array.from(iframes).slice(1).forEach(iframe => iframe.remove());
  142. }
  143. }
  144. };
  145.  
  146. function createWorker() {
  147. const workerBlob = new Blob([`
  148. let checkInterval;
  149.  
  150. self.onmessage = function(e) {
  151. if (e.data.type === 'startCheck') {
  152. clearInterval(checkInterval);
  153. checkInterval = setInterval(() => {
  154. self.postMessage({ type: 'check' });
  155. }, e.data.checkFrequency);
  156. } else if (e.data.type === 'stopCheck') {
  157. clearInterval(checkInterval);
  158. }
  159. };
  160. `], { type: 'text/javascript' });
  161.  
  162. const worker = new Worker(URL.createObjectURL(workerBlob));
  163.  
  164. worker.onmessage = function (e) {
  165. if (e.data.type === 'check') {
  166. checkAndLikeVideo();
  167. }
  168. };
  169.  
  170. return worker;
  171. }
  172.  
  173. function loadSettings() {
  174. const savedSettings = GM_getValue(CONSTANTS.STORAGE_KEY, {});
  175. return Object.keys(defaultSettings).reduce((acc, key) => {
  176. acc[key] = key in savedSettings ? savedSettings[key] : defaultSettings[key];
  177. return acc;
  178. }, {});
  179. }
  180.  
  181. function saveSettings() {
  182. GM_setValue(CONSTANTS.STORAGE_KEY, settings);
  183. }
  184.  
  185. function createSettingsMenu() {
  186. GM_registerMenuCommand('YouTube Enchantments Settings', showSettingsDialog);
  187. }
  188.  
  189. function showSettingsDialog() {
  190. let dialog = document.getElementById('youtube-enchantments-settings');
  191. if (!dialog) {
  192. dialog = createSettingsDialog();
  193. document.body.appendChild(dialog);
  194. }
  195. dialog.style.display = 'block';
  196. }
  197.  
  198. function createSettingsDialog() {
  199. const dialog = document.createElement('div');
  200. dialog.id = 'youtube-enchantments-settings';
  201. dialog.style.cssText = `
  202. position: fixed;
  203. top: 50%;
  204. left: 50%;
  205. transform: translate(-50%, -50%);
  206. background-color: #030d22;
  207. padding: 20px;
  208. border: 1px solid black;
  209. z-index: 9999;
  210. font-family: Arial, sans-serif;
  211. box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  212. border-radius: 8px;
  213. `;
  214.  
  215. dialog.innerHTML = `
  216. <h2 style="margin-top: 0; color: white; font-weight: bold;">YouTube Enchantments Settings</h2>
  217. <ul style="list-style-type: none; padding: 0;">
  218. ${Object.entries(settings).map(([setting, value]) =>
  219. setting === 'watchThreshold'
  220. ? `
  221. <li style="margin-bottom: 15px;">
  222. <label style="display: flex; align-items: center; color: white; font-weight: bold;">
  223. <span style="margin-right: 10px;">${formatSettingName(setting)}:</span>
  224. <input type="range" min="0" max="100" step="10" value="${value}" data-setting="${setting}" style="width: 200px;">
  225. <span style="margin-left: 10px;" id="watchThresholdValue">${value}%</span>
  226. </label>
  227. </li>
  228. `
  229. : `
  230. <li style="margin-bottom: 15px;">
  231. <label style="cursor: pointer; display: flex; align-items: center; color: white; font-weight: bold;">
  232. <input type="checkbox" ${value ? 'checked' : ''} data-setting="${setting}" style="margin-right: 10px;">
  233. <span>${formatSettingName(setting)}</span>
  234. </label>
  235. </li>
  236. `
  237. ).join('')}
  238. </ul>
  239. <button id="close-settings" style="background-color: #cc0000; color: white; border: none; padding: 10px 15px; cursor: pointer; border-radius: 4px;">Close</button>
  240. `;
  241.  
  242. dialog.addEventListener('change', handleSettingChange);
  243. dialog.addEventListener('input', handleSliderInput);
  244. dialog.querySelector('#close-settings').addEventListener('click', hideSettingsDialog);
  245.  
  246. return dialog;
  247. }
  248.  
  249. function hideSettingsDialog() {
  250. const dialog = document.getElementById('youtube-enchantments-settings');
  251. if (dialog) {
  252. dialog.style.display = 'none';
  253. }
  254. }
  255.  
  256. function formatSettingName(setting) {
  257. return setting.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase());
  258. }
  259.  
  260. function handleSettingChange(e) {
  261. if (e.target.dataset.setting) {
  262. if (e.target.type === 'checkbox') {
  263. toggleSetting(e.target.dataset.setting);
  264. } else if (e.target.type === 'range') {
  265. updateNumericSetting(e.target.dataset.setting, e.target.value);
  266. }
  267. }
  268. }
  269.  
  270. function handleSliderInput(e) {
  271. if (e.target.type === 'range') {
  272. const value = e.target.value;
  273. document.getElementById('watchThresholdValue').textContent = `${value}%`;
  274. updateNumericSetting(e.target.dataset.setting, value);
  275. }
  276. }
  277.  
  278. function toggleSetting(settingName) {
  279. settings[settingName] = !settings[settingName];
  280. saveSettings();
  281. }
  282.  
  283. function updateNumericSetting(settingName, value) {
  284. settings[settingName] = parseInt(value, 10);
  285. saveSettings();
  286. }
  287.  
  288. function startBackgroundCheck() {
  289. worker.postMessage({ type: 'startCheck', checkFrequency: settings.checkFrequency });
  290. }
  291.  
  292. function checkAndLikeVideo() {
  293. console.log('Checking if video should be liked...');
  294. if (watchThresholdReached()) {
  295. console.log('Watch threshold reached.');
  296. if (settings.autoLikeEnabled) {
  297. console.log('Auto-like is enabled.');
  298. if (settings.likeIfNotSubscribed || isSubscribed()) {
  299. console.log('User is subscribed or likeIfNotSubscribed is enabled.');
  300. if (settings.autoLikeLiveStreams || !isLiveStream()) {
  301. console.log('Video is not a live stream or auto-like for live streams is enabled.');
  302. likeVideo();
  303. } else {
  304. console.log('Video is a live stream and auto-like for live streams is disabled.');
  305. }
  306. } else {
  307. console.log('User is not subscribed and likeIfNotSubscribed is disabled.');
  308. }
  309. } else {
  310. console.log('Auto-like is disabled.');
  311. }
  312. } else {
  313. console.log('Watch threshold not reached.');
  314. }
  315. }
  316.  
  317. function watchThresholdReached() {
  318. const player = document.querySelector(SELECTORS.PLAYER);
  319. if (player) {
  320. const watched = player.getCurrentTime() / player.getDuration();
  321. const watchedTarget = settings.watchThreshold / 100;
  322. if (watched < watchedTarget) {
  323. console.log(`Waiting until watch threshold reached (${watched.toFixed(2)}/${watchedTarget})...`);
  324. return false;
  325. }
  326. }
  327. return true;
  328. }
  329.  
  330. function isSubscribed() {
  331. const subscribeButton = document.querySelector(SELECTORS.SUBSCRIBE_BUTTON);
  332. return subscribeButton && (subscribeButton.hasAttribute('subscribe-button-invisible') || subscribeButton.hasAttribute('subscribed'));
  333. }
  334.  
  335. function isLiveStream() {
  336. const liveBadge = document.querySelector(SELECTORS.LIVE_BADGE);
  337. return liveBadge && window.getComputedStyle(liveBadge).display !== 'none';
  338. }
  339.  
  340. function likeVideo() {
  341. console.log('Attempting to like the video...');
  342. const likeButton = document.querySelector(SELECTORS.LIKE_BUTTON);
  343. const dislikeButton = document.querySelector(SELECTORS.DISLIKE_BUTTON);
  344. const videoId = getVideoId();
  345.  
  346. if (!likeButton || !dislikeButton || !videoId) {
  347. console.log('Like button, dislike button, or video ID not found.');
  348. return;
  349. }
  350.  
  351. if (!isButtonPressed(likeButton) && !isButtonPressed(dislikeButton) && !autoLikedVideoIds.has(videoId)) {
  352. console.log('Liking the video...');
  353. likeButton.click();
  354. if (isButtonPressed(likeButton)) {
  355. console.log('Video liked successfully.');
  356. autoLikedVideoIds.add(videoId);
  357. } else {
  358. console.log('Failed to like the video.');
  359. }
  360. } else {
  361. console.log('Video already liked or disliked, or already auto-liked.');
  362. }
  363. }
  364.  
  365. function isButtonPressed(button) {
  366. return button.classList.contains('style-default-active') || button.getAttribute('aria-pressed') === 'true';
  367. }
  368.  
  369. function getVideoId() {
  370. const watchFlexyElem = document.querySelector('#page-manager > ytd-watch-flexy');
  371. if (watchFlexyElem && watchFlexyElem.hasAttribute('video-id')) {
  372. return watchFlexyElem.getAttribute('video-id');
  373. }
  374. const urlParams = new URLSearchParams(window.location.search);
  375. return urlParams.get('v');
  376. }
  377.  
  378. function handleAdBlockError() {
  379. const playabilityError = document.querySelector(SELECTORS.PLAYABILITY_ERROR);
  380. if (playabilityError) {
  381. playabilityError.remove();
  382. playerManager.replacePlayer(window.location.href);
  383. } else if (tries < CONSTANTS.MAX_TRIES) {
  384. tries++;
  385. setTimeout(handleAdBlockError, CONSTANTS.DELAY);
  386. }
  387. }
  388.  
  389. function handleKeyPress(event) {
  390. switch (event.key) {
  391. case 'F2':
  392. toggleSettingsDialog();
  393. break;
  394. case 'PageDown':
  395. toggleScrolling();
  396. break;
  397. case 'PageUp':
  398. handlePageUp();
  399. break;
  400. }
  401. }
  402.  
  403. function toggleSettingsDialog() {
  404. const dialog = document.getElementById('youtube-enchantments-settings');
  405. if (dialog && dialog.style.display === 'block') {
  406. hideSettingsDialog();
  407. } else {
  408. showSettingsDialog();
  409. }
  410. }
  411.  
  412. function toggleScrolling() {
  413. if (isScrolling) {
  414. clearInterval(scrollInterval);
  415. isScrolling = false;
  416. } else {
  417. isScrolling = true;
  418. scrollInterval = setInterval(() => window.scrollBy(0, 50), 20);
  419. }
  420. }
  421.  
  422. function handlePageUp() {
  423. if (isScrolling) {
  424. clearInterval(scrollInterval);
  425. isScrolling = false;
  426. } else {
  427. window.scrollTo(0, 0);
  428. }
  429. }
  430.  
  431. function setupEventListeners() {
  432. window.addEventListener('beforeunload', () => {
  433. currentPageUrl = window.location.href;
  434. });
  435.  
  436. document.addEventListener('yt-navigate-finish', () => {
  437. const newUrl = window.location.href;
  438. if (newUrl !== currentPageUrl) {
  439. if (newUrl.endsWith('.com/')) {
  440. const iframe = document.getElementById(CONSTANTS.IFRAME_ID);
  441. iframe?.remove();
  442. } else {
  443. handleAdBlockError();
  444. }
  445. currentPageUrl = newUrl;
  446. }
  447. });
  448.  
  449. document.addEventListener('keydown', handleKeyPress);
  450.  
  451. const observer = new MutationObserver((mutations) => {
  452. for (const mutation of mutations) {
  453. if (mutation.type === 'childList') {
  454. for (const node of mutation.addedNodes) {
  455. if (node.nodeType === Node.ELEMENT_NODE &&
  456. node.matches(SELECTORS.PLAYABILITY_ERROR)) {
  457. handleAdBlockError();
  458. return;
  459. }
  460. }
  461. }
  462. }
  463. });
  464. observer.observe(document.body, { childList: true, subtree: true });
  465.  
  466. setInterval(() => playerManager.removeDuplicates(), CONSTANTS.DUPLICATE_CHECK_INTERVAL);
  467. }
  468.  
  469. function initScript() {
  470. createSettingsMenu();
  471. setupEventListeners();
  472. startBackgroundCheck();
  473. }
  474.  
  475. initScript();
  476. })();