YouTube Supercharged: Grid Search & Channel Preloader

Combines "YouTube Grid Auto-Scroll & Search" for finding videos in grids/feeds with "YouTube Channel Full Video Preloader" for loading all videos on a channel page and opening them.

  1. // ==UserScript==
  2. // @name YouTube Supercharged: Grid Search & Channel Preloader
  3. // @namespace https://greasyfork.org/users/1435316
  4. // @version 3.4
  5. // @description Combines "YouTube Grid Auto-Scroll & Search" for finding videos in grids/feeds with "YouTube Channel Full Video Preloader" for loading all videos on a channel page and opening them.
  6. // @author Your Name & AI Assistant
  7. // @match https://www.youtube.com/*
  8. // @grant GM_addStyle
  9. // @grant GM_openInTab
  10. // @run-at document-idle
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. // --- Variables for YouTube Grid Auto-Scroll & Search ---
  18. let targetText = "";
  19. let searchBox;
  20. let isSearching = false;
  21. let searchInput;
  22. let searchButton;
  23. let stopButton;
  24. let prevButton;
  25. let nextButton;
  26. let scrollContinuationTimeout;
  27. let overallSearchTimeout;
  28. const SEARCH_TIMEOUT_MS = 20000;
  29. const SCROLL_DELAY_MS = 750;
  30. const MAX_SEARCH_LENGTH = 255;
  31. let highlightedElements = [];
  32. let currentHighlightIndex = -1;
  33. let lastScrollHeight = 0;
  34. let hasScrolledToResultThisSession = false;
  35.  
  36. // --- Variables for YouTube Channel Full Video Preloader ---
  37. let preloadButtonPreloader; // Renamed to avoid any potential abstract conflict
  38. let isLoadingStatePreloader = false; // Renamed for clarity
  39.  
  40. // --- Combined Styles ---
  41. GM_addStyle(`
  42. /* Styles for YouTube Grid Auto-Scroll & Search */
  43. #floating-search-box {
  44. background-color: #222;
  45. padding: 5px;
  46. border: 1px solid #444;
  47. border-radius: 5px;
  48. display: flex;
  49. align-items: center;
  50. margin-left: 10px;
  51. }
  52. @media (max-width: 768px) {
  53. #floating-search-box input[type="text"] {
  54. width: 150px;
  55. }
  56. }
  57. #floating-search-box input[type="text"] {
  58. background-color: #333;
  59. color: #fff;
  60. border: 1px solid #555;
  61. padding: 3px 5px;
  62. border-radius: 3px;
  63. margin-right: 5px;
  64. width: 200px;
  65. height: 30px;
  66. }
  67. #floating-search-box input[type="text"]:focus {
  68. outline: none;
  69. border-color: #065fd4;
  70. }
  71. #floating-search-box button {
  72. background-color: #065fd4;
  73. color: white;
  74. border: none;
  75. padding: 3px 8px;
  76. border-radius: 3px;
  77. cursor: pointer;
  78. height: 30px;
  79. }
  80. #floating-search-box button:hover {
  81. background-color: #0549a8;
  82. }
  83. #floating-search-box button:focus {
  84. outline: none;
  85. }
  86. #stop-search-button {
  87. background-color: #aa0000;
  88. }
  89. #stop-search-button:hover {
  90. background-color: #800000;
  91. }
  92. #prev-result-button, #next-result-button {
  93. background-color: #444;
  94. color: white;
  95. margin: 0 3px;
  96. }
  97. #prev-result-button:hover, #next-result-button:hover {
  98. background-color: #555;
  99. }
  100. .highlighted-text {
  101. position: relative;
  102. z-index: 1;
  103. }
  104. .highlighted-text::before {
  105. content: '';
  106. position: absolute;
  107. top: -2px;
  108. left: -2px;
  109. right: -2px;
  110. bottom: -2px;
  111. border: 2px solid transparent;
  112. border-radius: 8px;
  113. background: linear-gradient(to right, red, orange, yellow, green, blue, indigo, violet);
  114. background-size: 400% 400%;
  115. animation: gradientAnimation 5s ease infinite;
  116. z-index: -1;
  117. }
  118. @keyframes gradientAnimation {
  119. 0% { background-position: 0% 50%; }
  120. 50% { background-position: 100% 50%; }
  121. 100% { background-position: 0% 50%; }
  122. }
  123. #search-error-message {
  124. color: red;
  125. font-weight: bold;
  126. padding: 5px;
  127. position: fixed;
  128. top: 50px;
  129. left: 50%;
  130. transform: translateX(-50%);
  131. background-color: rgba(0, 0, 0, 0.8);
  132. color: white;
  133. border-radius: 5px;
  134. z-index: 10000;
  135. display: none;
  136. }
  137. #search-no-results-message {
  138. color: #aaa;
  139. padding: 5px;
  140. position: fixed;
  141. top: 50px;
  142. left: 50%;
  143. transform: translateX(-50%);
  144. background-color: rgba(0, 0, 0, 0.8);
  145. border-radius: 5px;
  146. z-index: 10000;
  147. display: none;
  148. }
  149.  
  150. /* Styles for YouTube Channel Full Video Preloader */
  151. #yt-channel-full-preload-button {
  152. position: fixed;
  153. bottom: 70px;
  154. right: 20px;
  155. z-index: 9999; /* Slightly lower than search messages */
  156. padding: 12px 20px;
  157. background-color: #007bff;
  158. color: white;
  159. border: none;
  160. border-radius: 5px;
  161. cursor: pointer;
  162. font-size: 14px;
  163. box-shadow: 0px 4px 8px rgba(0,0,0,0.3);
  164. transition: background-color 0.3s ease, opacity 0.3s ease;
  165. }
  166. #yt-channel-full-preload-button:hover {
  167. background-color: #0056b3;
  168. }
  169. #yt-channel-full-preload-button:disabled {
  170. background-color: #AAAAAA;
  171. cursor: not-allowed;
  172. opacity: 0.7;
  173. }
  174. `);
  175.  
  176. // --- Functions for YouTube Grid Auto-Scroll & Search ---
  177. function ensureSearchBoxAttached() {
  178. if (searchBox && !document.body.contains(searchBox)) {
  179. console.log("YouTube Grid Auto-Scroll: Search box detached, attempting to re-attach.");
  180. const mastheadEnd = document.querySelector('#end.ytd-masthead');
  181. const buttonsContainer = document.querySelector('#end #buttons');
  182. if (mastheadEnd) {
  183. if (buttonsContainer) {
  184. mastheadEnd.insertBefore(searchBox, buttonsContainer);
  185. } else {
  186. mastheadEnd.appendChild(searchBox);
  187. }
  188. } else {
  189. console.error("YouTube Grid Auto-Scroll: Could not find masthead to re-attach search box.");
  190. if (!document.body.contains(searchBox)) {
  191. document.body.insertBefore(searchBox, document.body.firstChild);
  192. }
  193. }
  194. }
  195. }
  196.  
  197. function createSearchBox() {
  198. if (document.getElementById('floating-search-box')) return; // Already exists
  199.  
  200. searchBox = document.createElement('div');
  201. searchBox.id = 'floating-search-box';
  202. searchBox.setAttribute('role', 'search');
  203.  
  204. searchInput = document.createElement('input');
  205. searchInput.type = 'text';
  206. searchInput.placeholder = 'Поиск для прокрутки...';
  207. searchInput.value = '';
  208. searchInput.setAttribute('aria-label', 'Search within YouTube grid');
  209. searchInput.maxLength = MAX_SEARCH_LENGTH;
  210.  
  211. searchButton = document.createElement('button');
  212. searchButton.textContent = 'Поиск';
  213. searchButton.addEventListener('click', () => {
  214. stopSearch();
  215. isSearching = true;
  216. hasScrolledToResultThisSession = false;
  217. currentHighlightIndex = -1;
  218. searchAndScroll();
  219. });
  220. searchButton.setAttribute('aria-label', 'Start search');
  221.  
  222. prevButton = document.createElement('button');
  223. prevButton.textContent = 'Пред.';
  224. prevButton.id = 'prev-result-button';
  225. prevButton.addEventListener('click', () => navigateResults(-1));
  226. prevButton.setAttribute('aria-label', 'Previous result');
  227. prevButton.disabled = true;
  228.  
  229. nextButton = document.createElement('button');
  230. nextButton.textContent = 'След.';
  231. nextButton.id = 'next-result-button';
  232. nextButton.addEventListener('click', () => navigateResults(1));
  233. nextButton.disabled = true;
  234.  
  235. stopButton = document.createElement('button');
  236. stopButton.textContent = 'Стоп';
  237. stopButton.id = 'stop-search-button';
  238. stopButton.addEventListener('click', stopSearch);
  239. stopButton.setAttribute('aria-label', 'Stop search');
  240.  
  241. searchBox.appendChild(searchInput);
  242. searchBox.appendChild(searchButton);
  243. searchBox.appendChild(prevButton);
  244. searchBox.appendChild(nextButton);
  245. searchBox.appendChild(stopButton);
  246.  
  247. const mastheadEnd = document.querySelector('#end.ytd-masthead');
  248. const buttonsContainer = document.querySelector('#end #buttons');
  249.  
  250. if (mastheadEnd) {
  251. if(buttonsContainer){
  252. mastheadEnd.insertBefore(searchBox, buttonsContainer);
  253. } else{
  254. mastheadEnd.appendChild(searchBox);
  255. }
  256. } else {
  257. console.error("Could not find the YouTube masthead's end element for search box.");
  258. showErrorMessageGridSearch("Не удалось найти шапку YouTube. Блок поиска размещен вверху страницы.");
  259. document.body.insertBefore(searchBox, document.body.firstChild);
  260. }
  261. }
  262.  
  263. function showErrorMessageGridSearch(message) { // Renamed to avoid conflict if another part used same name
  264. let errorDiv = document.getElementById('search-error-message');
  265. if (!errorDiv) {
  266. errorDiv = document.createElement('div');
  267. errorDiv.id = 'search-error-message';
  268. document.body.appendChild(errorDiv);
  269. }
  270. errorDiv.textContent = message;
  271. errorDiv.style.display = 'block';
  272. setTimeout(() => { if(errorDiv) errorDiv.style.display = 'none'; }, 5000);
  273. }
  274.  
  275. function showNoResultsMessageGridSearch() { // Renamed
  276. let noResultsDiv = document.getElementById('search-no-results-message');
  277. if (!noResultsDiv) {
  278. noResultsDiv = document.createElement('div');
  279. noResultsDiv.id = 'search-no-results-message';
  280. noResultsDiv.textContent = "Совпадений не найдено.";
  281. document.body.appendChild(noResultsDiv);
  282. }
  283. noResultsDiv.style.display = 'block';
  284. setTimeout(() => { if(noResultsDiv) noResultsDiv.style.display = 'none'; }, 5000);
  285. }
  286.  
  287. function stopSearch() {
  288. isSearching = false;
  289. clearTimeout(scrollContinuationTimeout);
  290. clearTimeout(overallSearchTimeout);
  291. currentHighlightIndex = -1;
  292. highlightedElements = [];
  293. document.querySelectorAll('.highlighted-text').forEach(el => {
  294. el.classList.remove('highlighted-text');
  295. });
  296. updateNavButtons();
  297. }
  298.  
  299. function navigateResults(direction) {
  300. if (highlightedElements.length === 0) return;
  301. currentHighlightIndex += direction;
  302. if (currentHighlightIndex < 0) currentHighlightIndex = highlightedElements.length - 1;
  303. else if (currentHighlightIndex >= highlightedElements.length) currentHighlightIndex = 0;
  304.  
  305. if (highlightedElements[currentHighlightIndex]) {
  306. highlightedElements[currentHighlightIndex].scrollIntoView({ behavior: 'auto', block: 'center' });
  307. hasScrolledToResultThisSession = true;
  308. }
  309. updateNavButtons();
  310. }
  311.  
  312. function updateNavButtons() {
  313. if (prevButton && nextButton) { // Ensure buttons are created
  314. prevButton.disabled = highlightedElements.length <= 1;
  315. nextButton.disabled = highlightedElements.length <= 1;
  316. }
  317. }
  318.  
  319. function searchAndScroll() {
  320. if (searchBox && !document.body.contains(searchBox)) {
  321. console.warn("YouTube Grid Auto-Scroll: Search box is not in the document. Stopping script operations.");
  322. showErrorMessageGridSearch("UI поиска потерян. Пожалуйста, перезагрузите страницу или попробуйте переустановить скрипт.");
  323. stopSearch();
  324. return;
  325. }
  326. if (!isSearching) {
  327. clearTimeout(scrollContinuationTimeout);
  328. clearTimeout(overallSearchTimeout);
  329. return;
  330. }
  331. clearTimeout(scrollContinuationTimeout);
  332. targetText = searchInput.value.trim().toLowerCase();
  333. if (!targetText) {
  334. stopSearch();
  335. return;
  336. }
  337. clearTimeout(overallSearchTimeout);
  338. overallSearchTimeout = setTimeout(() => {
  339. if (isSearching) {
  340. showErrorMessageGridSearch("Поиск прерван по таймауту.");
  341. stopSearch();
  342. }
  343. }, SEARCH_TIMEOUT_MS);
  344.  
  345. document.querySelectorAll('.highlighted-text').forEach(el => {
  346. el.classList.remove('highlighted-text');
  347. });
  348.  
  349. const mediaElements = Array.from(document.querySelectorAll('ytd-rich-grid-media:not([style*="display: none"])'));
  350. let newlyFoundHighlightedElements = [];
  351.  
  352. for (let i = 0; i < mediaElements.length; i++) {
  353. const titleElement = mediaElements[i].querySelector('#video-title');
  354. if (titleElement && titleElement.textContent.toLowerCase().includes(targetText)) {
  355. mediaElements[i].classList.add('highlighted-text');
  356. newlyFoundHighlightedElements.push(mediaElements[i]);
  357. }
  358. }
  359. highlightedElements = newlyFoundHighlightedElements;
  360. updateNavButtons();
  361.  
  362. let elementToScrollTo = null;
  363. let newActiveHighlightIndex = -1;
  364.  
  365. if (highlightedElements.length > 0) {
  366. if (currentHighlightIndex === -1 || currentHighlightIndex >= highlightedElements.length) {
  367. newActiveHighlightIndex = 0;
  368. } else {
  369. newActiveHighlightIndex = (currentHighlightIndex + 1) % highlightedElements.length;
  370. }
  371. elementToScrollTo = highlightedElements[newActiveHighlightIndex];
  372. }
  373.  
  374. if (elementToScrollTo) {
  375. elementToScrollTo.scrollIntoView({ behavior: 'auto', block: 'center' });
  376. currentHighlightIndex = newActiveHighlightIndex;
  377. hasScrolledToResultThisSession = true;
  378. isSearching = false;
  379. clearTimeout(overallSearchTimeout);
  380. } else {
  381. if (!isSearching) {
  382. clearTimeout(overallSearchTimeout);
  383. return;
  384. }
  385. lastScrollHeight = document.documentElement.scrollHeight;
  386. window.scrollTo({ top: lastScrollHeight, behavior: 'auto' });
  387.  
  388. scrollContinuationTimeout = setTimeout(() => {
  389. if (!isSearching) return;
  390. if (document.documentElement.scrollHeight === lastScrollHeight) {
  391. const searchWasActiveBeforeStop = isSearching;
  392. stopSearch();
  393. if (searchWasActiveBeforeStop && !hasScrolledToResultThisSession && targetText) {
  394. showNoResultsMessageGridSearch();
  395. }
  396. } else {
  397. searchAndScroll();
  398. }
  399. }, SCROLL_DELAY_MS);
  400. }
  401. }
  402.  
  403. document.addEventListener('visibilitychange', function() {
  404. if (!document.hidden) {
  405. ensureSearchBoxAttached();
  406. if (isSearching || (searchInput && searchInput.value.trim() && currentHighlightIndex !== -1) ) {
  407. const wasSearchingBeforeVisibilityChange = isSearching;
  408. const currentSearchTermInBox = searchInput ? searchInput.value : "";
  409. if (wasSearchingBeforeVisibilityChange) {
  410. console.log("YouTube Grid Auto-Scroll: Tab became visible during an active search. Restarting search process.");
  411. stopSearch();
  412. if (searchInput) searchInput.value = currentSearchTermInBox;
  413. if (currentSearchTermInBox.trim()) {
  414. isSearching = true;
  415. hasScrolledToResultThisSession = false;
  416. currentHighlightIndex = -1;
  417. searchAndScroll();
  418. }
  419. }
  420. }
  421. }
  422. });
  423.  
  424.  
  425. // --- Functions for YouTube Channel Full Video Preloader ---
  426. function createPreloadButtonPreloader() {
  427. if (document.getElementById('yt-channel-full-preload-button')) {
  428. return; // Button already exists
  429. }
  430.  
  431. preloadButtonPreloader = document.createElement('button');
  432. preloadButtonPreloader.textContent = 'Загрузить ВСЕ видео (со скроллом)';
  433. preloadButtonPreloader.id = 'yt-channel-full-preload-button';
  434.  
  435. preloadButtonPreloader.addEventListener('click', startFullPreloadPreloader);
  436. document.body.appendChild(preloadButtonPreloader);
  437. }
  438.  
  439. async function scrollToBottomPreloader() {
  440. return new Promise(resolve => {
  441. preloadButtonPreloader.disabled = true;
  442. isLoadingStatePreloader = true;
  443. preloadButtonPreloader.textContent = 'Прокрутка... (0 видео)';
  444.  
  445. let lastHeight = 0;
  446. let currentHeight = document.documentElement.scrollHeight;
  447. let consecutiveNoChange = 0;
  448. const maxConsecutiveNoChange = 5;
  449. let videosFound = 0;
  450.  
  451. const scrollInterval = setInterval(() => {
  452. videosFound = document.querySelectorAll('ytd-rich-item-renderer a#video-title-link').length;
  453. preloadButtonPreloader.textContent = `Прокрутка... (${videosFound} видео)`;
  454.  
  455. window.scrollTo(0, document.documentElement.scrollHeight);
  456. lastHeight = currentHeight;
  457. currentHeight = document.documentElement.scrollHeight;
  458.  
  459. if (lastHeight === currentHeight) {
  460. consecutiveNoChange++;
  461. if (consecutiveNoChange >= maxConsecutiveNoChange) {
  462. clearInterval(scrollInterval);
  463. preloadButtonPreloader.textContent = `Прокрутка завершена (${videosFound} видео)`;
  464. isLoadingStatePreloader = false;
  465. setTimeout(resolve, 1000);
  466. }
  467. } else {
  468. consecutiveNoChange = 0;
  469. }
  470. }, 1000);
  471. });
  472. }
  473.  
  474. async function startFullPreloadPreloader() {
  475. if (isLoadingStatePreloader) {
  476. alert("Процесс уже запущен.");
  477. return;
  478. }
  479.  
  480. const startScroll = confirm("Скрипт начнет прокручивать страницу вниз до конца, чтобы загрузить все видео. Это может занять некоторое время. Продолжить?");
  481. if (!startScroll) {
  482. return;
  483. }
  484.  
  485. await scrollToBottomPreloader();
  486.  
  487. preloadButtonPreloader.disabled = false;
  488. const videoItems = document.querySelectorAll('ytd-rich-item-renderer');
  489. let videoLinks = [];
  490.  
  491. videoItems.forEach(item => {
  492. const linkElement = item.querySelector('a#video-title-link');
  493. if (linkElement && linkElement.href) {
  494. videoLinks.push(linkElement.href);
  495. }
  496. });
  497.  
  498. if (videoLinks.length === 0) {
  499. alert('Видео для предзагрузки не найдены на этой странице.');
  500. preloadButtonPreloader.textContent = 'Загрузить ВСЕ видео (со скроллом)';
  501. return;
  502. }
  503.  
  504. const confirmation = confirm(`Найдено ${videoLinks.length} видео. Хотите открыть их все в фоновых вкладках? Это может занять некоторое время и потребовать много ресурсов.`);
  505.  
  506. if (confirmation) {
  507. preloadButtonPreloader.disabled = true;
  508. preloadButtonPreloader.textContent = `Открытие... (0/${videoLinks.length})`;
  509. let openedCount = 0;
  510.  
  511. videoLinks.forEach((url, index) => {
  512. setTimeout(() => {
  513. GM_openInTab(url, { active: false, insert: true });
  514. openedCount++;
  515. preloadButtonPreloader.textContent = `Открытие... (${openedCount}/${videoLinks.length})`;
  516. if (openedCount === videoLinks.length) {
  517. alert('Все ссылки на видео отправлены на открытие в фоновых вкладках.');
  518. preloadButtonPreloader.disabled = false;
  519. preloadButtonPreloader.textContent = 'Загрузить ВСЕ видео (со скроллом)';
  520. }
  521. }, index * 300);
  522. });
  523. } else {
  524. preloadButtonPreloader.textContent = 'Загрузить ВСЕ видео (со скроллом)';
  525. }
  526. }
  527.  
  528. function initOrReinitPreloaderButton() {
  529. const oldButton = document.getElementById('yt-channel-full-preload-button');
  530. if (oldButton) {
  531. oldButton.remove();
  532. }
  533. // Check if on a /videos page
  534. if (window.location.pathname.endsWith('/videos') || /\/@.+\/videos/.test(window.location.pathname)) {
  535. // Delay to ensure page elements are settled, especially on SPA navigation
  536. setTimeout(createPreloadButtonPreloader, 1500); // Adjusted delay
  537. }
  538. }
  539.  
  540. // --- Initialization ---
  541.  
  542. // Initialize Grid Search UI
  543. createSearchBox();
  544.  
  545. // Initialize Preloader Button (conditionally)
  546. initOrReinitPreloaderButton();
  547.  
  548. // MutationObserver for SPA navigation to handle Preloader Button visibility
  549. let lastUrlPreloader = location.href;
  550. new MutationObserver(() => {
  551. const url = location.href;
  552. if (url !== lastUrlPreloader) {
  553. lastUrlPreloader = url;
  554. isLoadingStatePreloader = false; // Reset preloader's specific loading state
  555. initOrReinitPreloaderButton(); // Check and add/remove preloader button
  556. }
  557. }).observe(document, {subtree: true, childList: true});
  558.  
  559. })();