YouTube Enchantments

Automatically likes videos of channels you're subscribed to and automatically scrolls down on Youtube with a toggle button. Also removes the ad-blocking warning dialog.

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

  1. // ==UserScript==
  2. // @name YouTube Enchantments
  3. // @namespace Based on YouTube Auto-Liker by HatScripts and Youtube Auto Scroll Down
  4. // @version 0.3
  5. // @description Automatically likes videos of channels you're subscribed to and automatically scrolls down on Youtube with a toggle button. Also removes the ad-blocking warning dialog.
  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. // Selectors for various YouTube elements
  22. const SELECTORS = {
  23. PLAYER: '#movie_player',
  24. SUBSCRIBE_BUTTON: '#subscribe-button > ytd-subscribe-button-renderer, ytd-reel-player-overlay-renderer #subscribe-button',
  25. LIKE_BUTTON: '#menu .YtLikeButtonViewModelHost button, #segmented-like-button button, #like-button button',
  26. DISLIKE_BUTTON: '#menu .YtDislikeButtonViewModelHost button, #segmented-dislike-button button, #dislike-button button'
  27. };
  28.  
  29. // Set to store video IDs that have been auto-liked
  30. const autoLikedVideoIds = new Set();
  31. let isScrolling = false;
  32. let scrollInterval;
  33.  
  34. // Default settings for the script
  35. const defaultSettings = {
  36. debugMode: false,
  37. checkFrequency: 5000,
  38. watchThreshold: 0,
  39. likeIfNotSubscribed: false,
  40. autoLikeLiveStreams: false
  41. };
  42.  
  43. // Load settings from storage or use default settings
  44. const settings = loadSettings();
  45.  
  46. // Function to load settings from storage or use default settings
  47. function loadSettings() {
  48. const savedSettings = GM_getValue('settings', null);
  49. return savedSettings ? Object.assign({}, defaultSettings, savedSettings) : defaultSettings;
  50. }
  51.  
  52. // Function to save settings to storage
  53. function saveSettings() {
  54. GM_setValue('settings', settings);
  55. }
  56.  
  57. // Function to toggle a specific setting
  58. function toggleSetting(settingName) {
  59. if (settingName === 'watchThreshold') {
  60. showWatchThresholdDropdown();
  61. } else {
  62. settings[settingName] = !settings[settingName];
  63. saveSettings();
  64. }
  65. }
  66.  
  67. // Function called when the script is initialized
  68. function onInit() {
  69. const DEBUG = new Debugger(GM_info.script.name, settings.debugMode);
  70. setInterval(wait, settings.checkFrequency, DEBUG);
  71. createSettingsMenu();
  72. }
  73.  
  74. // Function to create the settings menu
  75. function createSettingsMenu() {
  76. GM_registerMenuCommand('YouTube Enchantments Settings', showSettingsDialog);
  77. }
  78.  
  79. // Function to show the settings dialog
  80. function showSettingsDialog() {
  81. const settingsDialog = createSettingsDialog();
  82. document.body.appendChild(settingsDialog);
  83. }
  84.  
  85. // Function to create the settings dialog element
  86. function createSettingsDialog() {
  87. const dialog = document.createElement('div');
  88. dialog.style.position = 'fixed';
  89. dialog.style.top = '50%';
  90. dialog.style.left = '50%';
  91. dialog.style.transform = 'translate(-50%, -50%)';
  92. dialog.style.backgroundColor = 'white';
  93. dialog.style.padding = '20px';
  94. dialog.style.border = '1px solid black';
  95. dialog.style.zIndex = '9999';
  96.  
  97. const title = document.createElement('h2');
  98. title.textContent = 'YouTube Enchantments Settings';
  99. dialog.appendChild(title);
  100.  
  101. const settingsList = document.createElement('ul');
  102. settingsList.style.listStyleType = 'none';
  103. settingsList.style.padding = '0';
  104.  
  105. // Create a setting item for each setting
  106. for (const setting in settings) {
  107. const settingItem = createSettingItem(setting);
  108. settingsList.appendChild(settingItem);
  109. }
  110.  
  111. dialog.appendChild(settingsList);
  112.  
  113. const closeButton = document.createElement('button');
  114. closeButton.textContent = 'Close';
  115. closeButton.addEventListener('click', () => {
  116. document.body.removeChild(dialog);
  117. });
  118. dialog.appendChild(closeButton);
  119.  
  120. return dialog;
  121. }
  122.  
  123. // Function to create a setting item element
  124. function createSettingItem(settingName) {
  125. const listItem = document.createElement('li');
  126. listItem.style.marginBottom = '10px';
  127.  
  128. const label = document.createElement('label');
  129. label.style.cursor = 'pointer';
  130.  
  131. const checkbox = document.createElement('input');
  132. checkbox.type = 'checkbox';
  133. checkbox.checked = settings[settingName];
  134. checkbox.addEventListener('change', () => {
  135. toggleSetting(settingName);
  136. });
  137.  
  138. const settingText = document.createElement('span');
  139. settingText.textContent = settingName.replace(/([A-Z])/g, ' $1').trim();
  140.  
  141. label.appendChild(checkbox);
  142. label.appendChild(settingText);
  143. listItem.appendChild(label);
  144.  
  145. return listItem;
  146. }
  147.  
  148. // Function to show the watch threshold dropdown
  149. function showWatchThresholdDropdown() {
  150. const dropdown = document.createElement('select');
  151. const watchThresholdOptions = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
  152.  
  153. watchThresholdOptions.forEach(option => {
  154. const optionElement = document.createElement('option');
  155. optionElement.value = option;
  156. optionElement.textContent = `${option}%`;
  157. if (option === settings.watchThreshold) {
  158. optionElement.selected = true;
  159. }
  160. dropdown.appendChild(optionElement);
  161. });
  162.  
  163. const dialogContainer = document.createElement('div');
  164. dialogContainer.style.position = 'fixed';
  165. dialogContainer.style.top = '50%';
  166. dialogContainer.style.left = '50%';
  167. dialogContainer.style.transform = 'translate(-50%, -50%)';
  168. dialogContainer.style.backgroundColor = 'white';
  169. dialogContainer.style.padding = '20px';
  170. dialogContainer.style.border = '1px solid black';
  171. dialogContainer.style.zIndex = '9999';
  172.  
  173. const title = document.createElement('h2');
  174. title.textContent = 'Select Watch Threshold';
  175. dialogContainer.appendChild(title);
  176. dialogContainer.appendChild(dropdown);
  177.  
  178. const closeButton = document.createElement('button');
  179. closeButton.textContent = 'Close';
  180. closeButton.addEventListener('click', () => {
  181. document.body.removeChild(dialogContainer);
  182. });
  183. dialogContainer.appendChild(closeButton);
  184.  
  185. dropdown.addEventListener('change', () => {
  186. settings.watchThreshold = parseInt(dropdown.value);
  187. saveSettings();
  188. document.body.removeChild(dialogContainer);
  189. });
  190.  
  191. document.body.appendChild(dialogContainer);
  192. }
  193.  
  194. // Function to remove the ad-blocking dialog
  195. function removeAdBlockingDialog() {
  196. const dialog = document.querySelector('tp-yt-paper-dialog');
  197. if (dialog) {
  198. const dialogText = dialog.querySelector('#title yt-attributed-string')?.textContent;
  199. if (dialogText?.includes('Los bloqueadores de anuncios no se permiten en YouTube') || dialogText?.includes("Ad blockers aren't allowed on YouTube")) {
  200. dialog.remove();
  201. }
  202. }
  203. }
  204.  
  205. // Call the function to remove the ad-blocking dialog when the script loads
  206. removeAdBlockingDialog();
  207.  
  208. // Clear the set of auto-liked video IDs when the page state changes
  209. function clearAutoLikedVideoIds() {
  210. autoLikedVideoIds.clear();
  211. }
  212.  
  213. window.addEventListener('popstate', clearAutoLikedVideoIds);
  214.  
  215. // Get the current video ID
  216. function getVideoId() {
  217. const watchFlexyElem = document.querySelector('#page-manager > ytd-watch-flexy');
  218. if (watchFlexyElem && watchFlexyElem.hasAttribute('video-id')) {
  219. return watchFlexyElem.getAttribute('video-id');
  220. } else {
  221. const urlParams = new URLSearchParams(window.location.search);
  222. return urlParams.get('v');
  223. }
  224. }
  225.  
  226. // Check if the watch threshold has been reached
  227. function watchThresholdReached(DEBUG) {
  228. const player = document.querySelector(SELECTORS.PLAYER);
  229. if (player) {
  230. const watched = player.getCurrentTime() / player.getDuration();
  231. const watchedTarget = settings.watchThreshold / 100;
  232. if (watched < watchedTarget) {
  233. DEBUG.info(`Waiting until watch threshold reached (${watched.toFixed(2)}/${watchedTarget})...`);
  234. return false;
  235. }
  236. }
  237. return true;
  238. }
  239.  
  240. // Check if the user is subscribed to the channel
  241. function isSubscribed(DEBUG) {
  242. DEBUG.info('Checking whether subscribed...');
  243. const subscribeButton = document.querySelector(SELECTORS.SUBSCRIBE_BUTTON);
  244. if (!subscribeButton) {
  245. DEBUG.warn('Couldn\'t find sub button');
  246. return false;
  247. }
  248. const subscribed = subscribeButton.hasAttribute('subscribe-button-invisible') || subscribeButton.hasAttribute('subscribed');
  249. DEBUG.info(subscribed ? 'We are subscribed' : 'We are not subscribed');
  250. return subscribed;
  251. }
  252.  
  253. // Function to check if video should be liked and perform liking
  254. function wait(DEBUG) {
  255. if (watchThresholdReached(DEBUG)) {
  256. try {
  257. if (settings.likeIfNotSubscribed || isSubscribed(DEBUG)) {
  258. if (settings.autoLikeLiveStreams || window.getComputedStyle(document.querySelector('.ytp-live-badge')).display === 'none') {
  259. like(DEBUG);
  260. }
  261. }
  262. } catch (e) {
  263. DEBUG.warn(`Failed to like video: ${e}. Will try again in ${settings.checkFrequency} ms...`);
  264. }
  265. }
  266. }
  267.  
  268. // Check if a like or dislike button is pressed
  269. function isButtonPressed(button) {
  270. return button.classList.contains('style-default-active') || button.getAttribute('aria-pressed') === 'true';
  271. }
  272.  
  273. // Function to like the current video
  274. function like(DEBUG) {
  275. DEBUG.info('Trying to like video...');
  276. const likeButton = document.querySelector(SELECTORS.LIKE_BUTTON);
  277. const dislikeButton = document.querySelector(SELECTORS.DISLIKE_BUTTON);
  278. if (!likeButton) {
  279. throw Error('Couldn\'t find like button');
  280. }
  281. if (!dislikeButton) {
  282. throw Error('Couldn\'t find dislike button');
  283. }
  284. const videoId = getVideoId();
  285. if (isButtonPressed(likeButton)) {
  286. DEBUG.info('Like button has already been clicked');
  287. autoLikedVideoIds.add(videoId);
  288. } else if (isButtonPressed(dislikeButton)) {
  289. DEBUG.info('Dislike button has already been clicked');
  290. } else if (autoLikedVideoIds.has(videoId)) {
  291. DEBUG.info('Video has already been auto-liked. User must have un-liked it, so we won\'t like it again');
  292. } else {
  293. DEBUG.info('Found like button. It\'s unclicked. Clicking it...');
  294. likeButton.click();
  295. if (isButtonPressed(likeButton)) {
  296. autoLikedVideoIds.add(videoId);
  297. DEBUG.info('Successfully liked video');
  298. } else {
  299. DEBUG.info('Failed to like video');
  300. }
  301. }
  302. }
  303.  
  304. // Debugger class for logging messages
  305. class Debugger {
  306. constructor(name, enabled) {
  307. this.debug = {};
  308. if (!window.console) {
  309. return () => { };
  310. }
  311. Object.getOwnPropertyNames(window.console).forEach(key => {
  312. if (typeof window.console[key] === 'function') {
  313. this.debug[key] = enabled ? window.console[key].bind(window.console, name + ': ') : () => { };
  314. }
  315. });
  316. return this.debug;
  317. }
  318. }
  319.  
  320. // Function to toggle automatic scrolling
  321. function toggleScrolling() {
  322. if (isScrolling) {
  323. clearInterval(scrollInterval);
  324. isScrolling = false;
  325. } else {
  326. isScrolling = true;
  327. scrollInterval = setInterval(scrollDown, 20);
  328. }
  329. }
  330.  
  331. // Function to perform scrolling
  332. function scrollDown() {
  333. var scrollAmount = 50;
  334. window.scrollBy(0, scrollAmount);
  335. }
  336.  
  337. // Event listener for keydown to toggle scrolling
  338. document.addEventListener("keydown", function (event) {
  339. if (event.key === "PageDown") {
  340. toggleScrolling();
  341. } else if (event.key === "PageUp") {
  342. clearInterval(scrollInterval);
  343. isScrolling = false;
  344. }
  345. });
  346.  
  347. // Observe DOM changes to detect the ad-blocking dialog
  348. const observer = new MutationObserver((mutations) => {
  349. mutations.forEach((mutation) => {
  350. if (mutation.addedNodes.length > 0) {
  351. mutation.addedNodes.forEach((node) => {
  352. if (node.nodeName === 'TP-YT-PAPER-DIALOG') {
  353. removeAdBlockingDialog();
  354. }
  355. });
  356. }
  357. });
  358. });
  359.  
  360. // Start observing the body element for changes
  361. observer.observe(document.body, { childList: true, subtree: true });
  362.  
  363. // Call the functions to initialize the script
  364. onInit();
  365. })();