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-16 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube Enchantments
  3. // @namespace Based on YouTube Auto-Liker by HatScripts and Youtube Auto Scroll Down
  4. // @version 0.2
  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.  
  195. // Clear the set of auto-liked video IDs when the page state changes
  196. function clearAutoLikedVideoIds() {
  197. autoLikedVideoIds.clear();
  198. }
  199.  
  200. window.addEventListener('popstate', clearAutoLikedVideoIds);
  201.  
  202. // Get the current video ID
  203. function getVideoId() {
  204. const watchFlexyElem = document.querySelector('#page-manager > ytd-watch-flexy');
  205. if (watchFlexyElem && watchFlexyElem.hasAttribute('video-id')) {
  206. return watchFlexyElem.getAttribute('video-id');
  207. } else {
  208. const urlParams = new URLSearchParams(window.location.search);
  209. return urlParams.get('v');
  210. }
  211. }
  212.  
  213. // Check if the watch threshold has been reached
  214. function watchThresholdReached(DEBUG) {
  215. const player = document.querySelector(SELECTORS.PLAYER);
  216. if (player) {
  217. const watched = player.getCurrentTime() / player.getDuration();
  218. const watchedTarget = settings.watchThreshold / 100;
  219. if (watched < watchedTarget) {
  220. DEBUG.info(`Waiting until watch threshold reached (${watched.toFixed(2)}/${watchedTarget})...`);
  221. return false;
  222. }
  223. }
  224. return true;
  225. }
  226.  
  227. // Check if the user is subscribed to the channel
  228. function isSubscribed(DEBUG) {
  229. DEBUG.info('Checking whether subscribed...');
  230. const subscribeButton = document.querySelector(SELECTORS.SUBSCRIBE_BUTTON);
  231. if (!subscribeButton) {
  232. DEBUG.warn('Couldn\'t find sub button');
  233. return false;
  234. }
  235. const subscribed = subscribeButton.hasAttribute('subscribe-button-invisible') || subscribeButton.hasAttribute('subscribed');
  236. DEBUG.info(subscribed ? 'We are subscribed' : 'We are not subscribed');
  237. return subscribed;
  238. }
  239.  
  240. // Function to check if video should be liked and perform liking
  241. function wait(DEBUG) {
  242. if (watchThresholdReached(DEBUG)) {
  243. try {
  244. if (settings.likeIfNotSubscribed || isSubscribed(DEBUG)) {
  245. if (settings.autoLikeLiveStreams || window.getComputedStyle(document.querySelector('.ytp-live-badge')).display === 'none') {
  246. like(DEBUG);
  247. }
  248. }
  249. } catch (e) {
  250. DEBUG.warn(`Failed to like video: ${e}. Will try again in ${settings.checkFrequency} ms...`);
  251. }
  252. }
  253. }
  254.  
  255. // Check if a like or dislike button is pressed
  256. function isButtonPressed(button) {
  257. return button.classList.contains('style-default-active') || button.getAttribute('aria-pressed') === 'true';
  258. }
  259.  
  260. // Function to like the current video
  261. function like(DEBUG) {
  262. DEBUG.info('Trying to like video...');
  263. const likeButton = document.querySelector(SELECTORS.LIKE_BUTTON);
  264. const dislikeButton = document.querySelector(SELECTORS.DISLIKE_BUTTON);
  265. if (!likeButton) {
  266. throw Error('Couldn\'t find like button');
  267. }
  268. if (!dislikeButton) {
  269. throw Error('Couldn\'t find dislike button');
  270. }
  271. const videoId = getVideoId();
  272. if (isButtonPressed(likeButton)) {
  273. DEBUG.info('Like button has already been clicked');
  274. autoLikedVideoIds.add(videoId);
  275. } else if (isButtonPressed(dislikeButton)) {
  276. DEBUG.info('Dislike button has already been clicked');
  277. } else if (autoLikedVideoIds.has(videoId)) {
  278. DEBUG.info('Video has already been auto-liked. User must have un-liked it, so we won\'t like it again');
  279. } else {
  280. DEBUG.info('Found like button. It\'s unclicked. Clicking it...');
  281. likeButton.click();
  282. if (isButtonPressed(likeButton)) {
  283. autoLikedVideoIds.add(videoId);
  284. DEBUG.info('Successfully liked video');
  285. } else {
  286. DEBUG.info('Failed to like video');
  287. }
  288. }
  289. }
  290.  
  291. // Debugger class for logging messages
  292. class Debugger {
  293. constructor(name, enabled) {
  294. this.debug = {};
  295. if (!window.console) {
  296. return () => { };
  297. }
  298. Object.getOwnPropertyNames(window.console).forEach(key => {
  299. if (typeof window.console[key] === 'function') {
  300. this.debug[key] = enabled ? window.console[key].bind(window.console, name + ': ') : () => { };
  301. }
  302. });
  303. return this.debug;
  304. }
  305. }
  306.  
  307. // Function to toggle automatic scrolling
  308. function toggleScrolling() {
  309. if (isScrolling) {
  310. clearInterval(scrollInterval);
  311. isScrolling = false;
  312. } else {
  313. isScrolling = true;
  314. scrollInterval = setInterval(scrollDown, 20);
  315. }
  316. }
  317.  
  318. // Function to perform scrolling
  319. function scrollDown() {
  320. var scrollAmount = 50;
  321. window.scrollBy(0, scrollAmount);
  322. }
  323.  
  324. // Event listener for keydown to toggle scrolling
  325. document.addEventListener("keydown", function (event) {
  326. if (event.key === "PageDown") {
  327. toggleScrolling();
  328. } else if (event.key === "PageUp") {
  329. clearInterval(scrollInterval);
  330. isScrolling = false;
  331. }
  332. });
  333.  
  334. // Observe DOM changes to detect the ad-blocking dialog
  335. const observer = new MutationObserver((mutations) => {
  336. mutations.forEach((mutation) => {
  337. if (mutation.addedNodes.length > 0) {
  338. mutation.addedNodes.forEach((node) => {
  339. if (node.nodeName === 'TP-YT-PAPER-DIALOG') {
  340. const dialogText = node.querySelector('#title yt-attributed-string').textContent;
  341. if (dialogText.includes('Los bloqueadores de anuncios no se permiten en YouTube') || dialogText.includes("Ad blockers aren't allowed on YouTube")) {
  342. // Ad-blocking dialog detected, remove it
  343. node.remove();
  344. }
  345. }
  346. });
  347. }
  348. });
  349. });
  350.  
  351. // Start observing the body element for changes
  352. observer.observe(document.body, { childList: true, subtree: true });
  353.  
  354. // Call the functions to initialize the script
  355. onInit();
  356. })();