您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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.
// ==UserScript== // @name YouTube Supercharged: Grid Search & Channel Preloader // @namespace https://greasyfork.org/users/1435316 // @version 3.4 // @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. // @author Your Name & AI Assistant // @match https://www.youtube.com/* // @grant GM_addStyle // @grant GM_openInTab // @run-at document-idle // @license MIT // ==/UserScript== (function() { 'use strict'; // --- Variables for YouTube Grid Auto-Scroll & Search --- let targetText = ""; let searchBox; let isSearching = false; let searchInput; let searchButton; let stopButton; let prevButton; let nextButton; let scrollContinuationTimeout; let overallSearchTimeout; const SEARCH_TIMEOUT_MS = 20000; const SCROLL_DELAY_MS = 750; const MAX_SEARCH_LENGTH = 255; let highlightedElements = []; let currentHighlightIndex = -1; let lastScrollHeight = 0; let hasScrolledToResultThisSession = false; // --- Variables for YouTube Channel Full Video Preloader --- let preloadButtonPreloader; // Renamed to avoid any potential abstract conflict let isLoadingStatePreloader = false; // Renamed for clarity // --- Combined Styles --- GM_addStyle(` /* Styles for YouTube Grid Auto-Scroll & Search */ #floating-search-box { background-color: #222; padding: 5px; border: 1px solid #444; border-radius: 5px; display: flex; align-items: center; margin-left: 10px; } @media (max-width: 768px) { #floating-search-box input[type="text"] { width: 150px; } } #floating-search-box input[type="text"] { background-color: #333; color: #fff; border: 1px solid #555; padding: 3px 5px; border-radius: 3px; margin-right: 5px; width: 200px; height: 30px; } #floating-search-box input[type="text"]:focus { outline: none; border-color: #065fd4; } #floating-search-box button { background-color: #065fd4; color: white; border: none; padding: 3px 8px; border-radius: 3px; cursor: pointer; height: 30px; } #floating-search-box button:hover { background-color: #0549a8; } #floating-search-box button:focus { outline: none; } #stop-search-button { background-color: #aa0000; } #stop-search-button:hover { background-color: #800000; } #prev-result-button, #next-result-button { background-color: #444; color: white; margin: 0 3px; } #prev-result-button:hover, #next-result-button:hover { background-color: #555; } .highlighted-text { position: relative; z-index: 1; } .highlighted-text::before { content: ''; position: absolute; top: -2px; left: -2px; right: -2px; bottom: -2px; border: 2px solid transparent; border-radius: 8px; background: linear-gradient(to right, red, orange, yellow, green, blue, indigo, violet); background-size: 400% 400%; animation: gradientAnimation 5s ease infinite; z-index: -1; } @keyframes gradientAnimation { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } } #search-error-message { color: red; font-weight: bold; padding: 5px; position: fixed; top: 50px; left: 50%; transform: translateX(-50%); background-color: rgba(0, 0, 0, 0.8); color: white; border-radius: 5px; z-index: 10000; display: none; } #search-no-results-message { color: #aaa; padding: 5px; position: fixed; top: 50px; left: 50%; transform: translateX(-50%); background-color: rgba(0, 0, 0, 0.8); border-radius: 5px; z-index: 10000; display: none; } /* Styles for YouTube Channel Full Video Preloader */ #yt-channel-full-preload-button { position: fixed; bottom: 70px; right: 20px; z-index: 9999; /* Slightly lower than search messages */ padding: 12px 20px; background-color: #007bff; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 14px; box-shadow: 0px 4px 8px rgba(0,0,0,0.3); transition: background-color 0.3s ease, opacity 0.3s ease; } #yt-channel-full-preload-button:hover { background-color: #0056b3; } #yt-channel-full-preload-button:disabled { background-color: #AAAAAA; cursor: not-allowed; opacity: 0.7; } `); // --- Functions for YouTube Grid Auto-Scroll & Search --- function ensureSearchBoxAttached() { if (searchBox && !document.body.contains(searchBox)) { console.log("YouTube Grid Auto-Scroll: Search box detached, attempting to re-attach."); const mastheadEnd = document.querySelector('#end.ytd-masthead'); const buttonsContainer = document.querySelector('#end #buttons'); if (mastheadEnd) { if (buttonsContainer) { mastheadEnd.insertBefore(searchBox, buttonsContainer); } else { mastheadEnd.appendChild(searchBox); } } else { console.error("YouTube Grid Auto-Scroll: Could not find masthead to re-attach search box."); if (!document.body.contains(searchBox)) { document.body.insertBefore(searchBox, document.body.firstChild); } } } } function createSearchBox() { if (document.getElementById('floating-search-box')) return; // Already exists searchBox = document.createElement('div'); searchBox.id = 'floating-search-box'; searchBox.setAttribute('role', 'search'); searchInput = document.createElement('input'); searchInput.type = 'text'; searchInput.placeholder = 'Поиск для прокрутки...'; searchInput.value = ''; searchInput.setAttribute('aria-label', 'Search within YouTube grid'); searchInput.maxLength = MAX_SEARCH_LENGTH; searchButton = document.createElement('button'); searchButton.textContent = 'Поиск'; searchButton.addEventListener('click', () => { stopSearch(); isSearching = true; hasScrolledToResultThisSession = false; currentHighlightIndex = -1; searchAndScroll(); }); searchButton.setAttribute('aria-label', 'Start search'); prevButton = document.createElement('button'); prevButton.textContent = 'Пред.'; prevButton.id = 'prev-result-button'; prevButton.addEventListener('click', () => navigateResults(-1)); prevButton.setAttribute('aria-label', 'Previous result'); prevButton.disabled = true; nextButton = document.createElement('button'); nextButton.textContent = 'След.'; nextButton.id = 'next-result-button'; nextButton.addEventListener('click', () => navigateResults(1)); nextButton.disabled = true; stopButton = document.createElement('button'); stopButton.textContent = 'Стоп'; stopButton.id = 'stop-search-button'; stopButton.addEventListener('click', stopSearch); stopButton.setAttribute('aria-label', 'Stop search'); searchBox.appendChild(searchInput); searchBox.appendChild(searchButton); searchBox.appendChild(prevButton); searchBox.appendChild(nextButton); searchBox.appendChild(stopButton); const mastheadEnd = document.querySelector('#end.ytd-masthead'); const buttonsContainer = document.querySelector('#end #buttons'); if (mastheadEnd) { if(buttonsContainer){ mastheadEnd.insertBefore(searchBox, buttonsContainer); } else{ mastheadEnd.appendChild(searchBox); } } else { console.error("Could not find the YouTube masthead's end element for search box."); showErrorMessageGridSearch("Не удалось найти шапку YouTube. Блок поиска размещен вверху страницы."); document.body.insertBefore(searchBox, document.body.firstChild); } } function showErrorMessageGridSearch(message) { // Renamed to avoid conflict if another part used same name let errorDiv = document.getElementById('search-error-message'); if (!errorDiv) { errorDiv = document.createElement('div'); errorDiv.id = 'search-error-message'; document.body.appendChild(errorDiv); } errorDiv.textContent = message; errorDiv.style.display = 'block'; setTimeout(() => { if(errorDiv) errorDiv.style.display = 'none'; }, 5000); } function showNoResultsMessageGridSearch() { // Renamed let noResultsDiv = document.getElementById('search-no-results-message'); if (!noResultsDiv) { noResultsDiv = document.createElement('div'); noResultsDiv.id = 'search-no-results-message'; noResultsDiv.textContent = "Совпадений не найдено."; document.body.appendChild(noResultsDiv); } noResultsDiv.style.display = 'block'; setTimeout(() => { if(noResultsDiv) noResultsDiv.style.display = 'none'; }, 5000); } function stopSearch() { isSearching = false; clearTimeout(scrollContinuationTimeout); clearTimeout(overallSearchTimeout); currentHighlightIndex = -1; highlightedElements = []; document.querySelectorAll('.highlighted-text').forEach(el => { el.classList.remove('highlighted-text'); }); updateNavButtons(); } function navigateResults(direction) { if (highlightedElements.length === 0) return; currentHighlightIndex += direction; if (currentHighlightIndex < 0) currentHighlightIndex = highlightedElements.length - 1; else if (currentHighlightIndex >= highlightedElements.length) currentHighlightIndex = 0; if (highlightedElements[currentHighlightIndex]) { highlightedElements[currentHighlightIndex].scrollIntoView({ behavior: 'auto', block: 'center' }); hasScrolledToResultThisSession = true; } updateNavButtons(); } function updateNavButtons() { if (prevButton && nextButton) { // Ensure buttons are created prevButton.disabled = highlightedElements.length <= 1; nextButton.disabled = highlightedElements.length <= 1; } } function searchAndScroll() { if (searchBox && !document.body.contains(searchBox)) { console.warn("YouTube Grid Auto-Scroll: Search box is not in the document. Stopping script operations."); showErrorMessageGridSearch("UI поиска потерян. Пожалуйста, перезагрузите страницу или попробуйте переустановить скрипт."); stopSearch(); return; } if (!isSearching) { clearTimeout(scrollContinuationTimeout); clearTimeout(overallSearchTimeout); return; } clearTimeout(scrollContinuationTimeout); targetText = searchInput.value.trim().toLowerCase(); if (!targetText) { stopSearch(); return; } clearTimeout(overallSearchTimeout); overallSearchTimeout = setTimeout(() => { if (isSearching) { showErrorMessageGridSearch("Поиск прерван по таймауту."); stopSearch(); } }, SEARCH_TIMEOUT_MS); document.querySelectorAll('.highlighted-text').forEach(el => { el.classList.remove('highlighted-text'); }); const mediaElements = Array.from(document.querySelectorAll('ytd-rich-grid-media:not([style*="display: none"])')); let newlyFoundHighlightedElements = []; for (let i = 0; i < mediaElements.length; i++) { const titleElement = mediaElements[i].querySelector('#video-title'); if (titleElement && titleElement.textContent.toLowerCase().includes(targetText)) { mediaElements[i].classList.add('highlighted-text'); newlyFoundHighlightedElements.push(mediaElements[i]); } } highlightedElements = newlyFoundHighlightedElements; updateNavButtons(); let elementToScrollTo = null; let newActiveHighlightIndex = -1; if (highlightedElements.length > 0) { if (currentHighlightIndex === -1 || currentHighlightIndex >= highlightedElements.length) { newActiveHighlightIndex = 0; } else { newActiveHighlightIndex = (currentHighlightIndex + 1) % highlightedElements.length; } elementToScrollTo = highlightedElements[newActiveHighlightIndex]; } if (elementToScrollTo) { elementToScrollTo.scrollIntoView({ behavior: 'auto', block: 'center' }); currentHighlightIndex = newActiveHighlightIndex; hasScrolledToResultThisSession = true; isSearching = false; clearTimeout(overallSearchTimeout); } else { if (!isSearching) { clearTimeout(overallSearchTimeout); return; } lastScrollHeight = document.documentElement.scrollHeight; window.scrollTo({ top: lastScrollHeight, behavior: 'auto' }); scrollContinuationTimeout = setTimeout(() => { if (!isSearching) return; if (document.documentElement.scrollHeight === lastScrollHeight) { const searchWasActiveBeforeStop = isSearching; stopSearch(); if (searchWasActiveBeforeStop && !hasScrolledToResultThisSession && targetText) { showNoResultsMessageGridSearch(); } } else { searchAndScroll(); } }, SCROLL_DELAY_MS); } } document.addEventListener('visibilitychange', function() { if (!document.hidden) { ensureSearchBoxAttached(); if (isSearching || (searchInput && searchInput.value.trim() && currentHighlightIndex !== -1) ) { const wasSearchingBeforeVisibilityChange = isSearching; const currentSearchTermInBox = searchInput ? searchInput.value : ""; if (wasSearchingBeforeVisibilityChange) { console.log("YouTube Grid Auto-Scroll: Tab became visible during an active search. Restarting search process."); stopSearch(); if (searchInput) searchInput.value = currentSearchTermInBox; if (currentSearchTermInBox.trim()) { isSearching = true; hasScrolledToResultThisSession = false; currentHighlightIndex = -1; searchAndScroll(); } } } } }); // --- Functions for YouTube Channel Full Video Preloader --- function createPreloadButtonPreloader() { if (document.getElementById('yt-channel-full-preload-button')) { return; // Button already exists } preloadButtonPreloader = document.createElement('button'); preloadButtonPreloader.textContent = 'Загрузить ВСЕ видео (со скроллом)'; preloadButtonPreloader.id = 'yt-channel-full-preload-button'; preloadButtonPreloader.addEventListener('click', startFullPreloadPreloader); document.body.appendChild(preloadButtonPreloader); } async function scrollToBottomPreloader() { return new Promise(resolve => { preloadButtonPreloader.disabled = true; isLoadingStatePreloader = true; preloadButtonPreloader.textContent = 'Прокрутка... (0 видео)'; let lastHeight = 0; let currentHeight = document.documentElement.scrollHeight; let consecutiveNoChange = 0; const maxConsecutiveNoChange = 5; let videosFound = 0; const scrollInterval = setInterval(() => { videosFound = document.querySelectorAll('ytd-rich-item-renderer a#video-title-link').length; preloadButtonPreloader.textContent = `Прокрутка... (${videosFound} видео)`; window.scrollTo(0, document.documentElement.scrollHeight); lastHeight = currentHeight; currentHeight = document.documentElement.scrollHeight; if (lastHeight === currentHeight) { consecutiveNoChange++; if (consecutiveNoChange >= maxConsecutiveNoChange) { clearInterval(scrollInterval); preloadButtonPreloader.textContent = `Прокрутка завершена (${videosFound} видео)`; isLoadingStatePreloader = false; setTimeout(resolve, 1000); } } else { consecutiveNoChange = 0; } }, 1000); }); } async function startFullPreloadPreloader() { if (isLoadingStatePreloader) { alert("Процесс уже запущен."); return; } const startScroll = confirm("Скрипт начнет прокручивать страницу вниз до конца, чтобы загрузить все видео. Это может занять некоторое время. Продолжить?"); if (!startScroll) { return; } await scrollToBottomPreloader(); preloadButtonPreloader.disabled = false; const videoItems = document.querySelectorAll('ytd-rich-item-renderer'); let videoLinks = []; videoItems.forEach(item => { const linkElement = item.querySelector('a#video-title-link'); if (linkElement && linkElement.href) { videoLinks.push(linkElement.href); } }); if (videoLinks.length === 0) { alert('Видео для предзагрузки не найдены на этой странице.'); preloadButtonPreloader.textContent = 'Загрузить ВСЕ видео (со скроллом)'; return; } const confirmation = confirm(`Найдено ${videoLinks.length} видео. Хотите открыть их все в фоновых вкладках? Это может занять некоторое время и потребовать много ресурсов.`); if (confirmation) { preloadButtonPreloader.disabled = true; preloadButtonPreloader.textContent = `Открытие... (0/${videoLinks.length})`; let openedCount = 0; videoLinks.forEach((url, index) => { setTimeout(() => { GM_openInTab(url, { active: false, insert: true }); openedCount++; preloadButtonPreloader.textContent = `Открытие... (${openedCount}/${videoLinks.length})`; if (openedCount === videoLinks.length) { alert('Все ссылки на видео отправлены на открытие в фоновых вкладках.'); preloadButtonPreloader.disabled = false; preloadButtonPreloader.textContent = 'Загрузить ВСЕ видео (со скроллом)'; } }, index * 300); }); } else { preloadButtonPreloader.textContent = 'Загрузить ВСЕ видео (со скроллом)'; } } function initOrReinitPreloaderButton() { const oldButton = document.getElementById('yt-channel-full-preload-button'); if (oldButton) { oldButton.remove(); } // Check if on a /videos page if (window.location.pathname.endsWith('/videos') || /\/@.+\/videos/.test(window.location.pathname)) { // Delay to ensure page elements are settled, especially on SPA navigation setTimeout(createPreloadButtonPreloader, 1500); // Adjusted delay } } // --- Initialization --- // Initialize Grid Search UI createSearchBox(); // Initialize Preloader Button (conditionally) initOrReinitPreloaderButton(); // MutationObserver for SPA navigation to handle Preloader Button visibility let lastUrlPreloader = location.href; new MutationObserver(() => { const url = location.href; if (url !== lastUrlPreloader) { lastUrlPreloader = url; isLoadingStatePreloader = false; // Reset preloader's specific loading state initOrReinitPreloaderButton(); // Check and add/remove preloader button } }).observe(document, {subtree: true, childList: true}); })();