YouTube Subscriptions Scroller

Написан с нуля. Нативный IntersectionObserver. Счетчик по умолчанию 1. Безопасный вывод 3 видео ряда. Сетка не ломается.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         YouTube Subscriptions Scroller
// @namespace    http://tampermonkey.net
// @version      1.0
// @description  Написан с нуля. Нативный IntersectionObserver. Счетчик по умолчанию 1. Безопасный вывод 3 видео ряда. Сетка не ломается.
// @author       Wind173 with Google AI
// @match        https://www.youtube.com/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // Создаем абсолютно независимый и чистый каркас панели
    const container = document.createElement('div');
    container.id = 'yt-modern-counter-widget';
    container.style.cssText = 'position: fixed !important; top: 70px !important; left: 260px !important; z-index: 2147483647 !important; display: flex !important; align-items: center !important; background-color: #1f1f1f !important; color: #ffffff !important; padding: 6px 12px !important; border-radius: 20px !important; box-shadow: 0 6px 20px rgba(0,0,0,0.8) !important; border: 1px solid #444 !important; font-family: Roboto, Arial, sans-serif !important; gap: 8px !important;';

    // Инпут: значение по умолчанию строго 1
    const input = document.createElement('input');
    input.type = 'number';
    input.value = '1';
    input.min = '1';
    input.style.cssText = 'width: 55px !important; padding: 5px 8px !important; border-radius: 12px !important; border: 1px solid #555 !important; background-color: #0f0f0f !important; color: #fff !important; text-align: center !important; font-size: 13px !important; outline: none !important;';

    const btn = document.createElement('button');
    btn.innerText = 'Go ⬇️';
    btn.style.cssText = 'padding: 6px 14px !important; background-color: #ff0000 !important; color: #fff !important; border: none !important; border-radius: 12px !important; cursor: pointer !important; font-weight: bold !important; font-size: 13px !important;';

    // Единое информационное поле, куда мы запишем три чистых номера текущего ряда
    const positionBadge = document.createElement('div');
    positionBadge.innerText = '№ 1, 2, 3';
    positionBadge.style.cssText = 'padding: 5px 10px !important; border-radius: 12px !important; background-color: #0088ff !important; color: #fff !important; font-size: 13px !important; font-weight: bold !important; min-width: 95px !important; text-align: center !important;';

    container.appendChild(input);
    container.appendChild(btn);
    container.appendChild(positionBadge);

    let scrollInterval = null;
    let observer = null;
    let visibleVideos = new Map(); // Хранилище для видимых на экране роликов

    // Чистая и быстрая фильтрация видео без Shorts
    function getValidVideos() {
        const items = document.querySelectorAll('ytd-rich-item-renderer, ytd-grid-video-renderer, ytd-video-renderer');
        const videos = [];
        for (let i = 0; i < items.length; i++) {
            const item = items[i];
            const isShorts = item.closest('ytd-rich-shelf-renderer') || item.closest('ytd-reel-shelf-renderer');
            if (!isShorts) {
                const link = item.querySelector('a#video-title-link, a#video-title, a[href*="/watch"]');
                if (!link || !link.getAttribute('href').includes('/shorts/')) {
                    videos.push(item);
                }
            }
        }
        return videos;
    }

    // Современный и легкий трекер видимости элементов на экране
    function initObserver() {
        if (observer) observer.disconnect();
        visibleVideos.clear();

        observer = new IntersectionObserver((entries) => {
            entries.forEach(entry => {
                if (entry.isIntersecting) {
                    visibleVideos.set(entry.target, entry.boundingClientRect.top);
                } else {
                    visibleVideos.delete(entry.target);
                }
            });

            // Определяем, какие ролики сейчас находятся ближе всего к центру видимой зоны
            const allCurrentVideos = getValidVideos();
            if (allCurrentVideos.length === 0) return;

            let earliestVisibleIdx = Infinity;

            visibleVideos.forEach((top, element) => {
                const idx = allCurrentVideos.indexOf(element);
                if (idx !== -1 && idx < earliestVisibleIdx) {
                    earliestVisibleIdx = idx;
                }
            });

            if (earliestVisibleIdx !== Infinity) {
                let n1 = earliestVisibleIdx + 1;
                let n2 = n1 + 1;
                let n3 = n1 + 2;
                
                // Записываем номера трех роликов текущего видимого ряда
                positionBadge.innerText = '№ ' + n1 + ', ' + n2 + ', ' + n3;
            }
        }, {
            threshold: 0.1 // Срабатывает, если видно хотя бы 10% карточки ролика
        });

        // Подключаем слежку ко всем существующим видео
        getValidVideos().forEach(video => observer.observe(video));
    }

    // Автоматическое добавление новых подгружаемых роликов под наблюдение видеокарты
    const mutationObserver = new MutationObserver(() => {
        if (!window.location.href.includes('/feed/subscriptions')) return;
        if (observer) {
            getValidVideos().forEach(video => observer.observe(video));
        }
    });

    // Функция плавной промотки ленты до цели
    function startAutoScroll() {
        const targetNum = parseInt(input.value, 10);
        if (isNaN(targetNum) || targetNum <= 0) return;

        let lastHeight = document.documentElement.scrollHeight;
        let noChangeCount = 0;

        clearInterval(scrollInterval);
        input.disabled = true;
        btn.style.backgroundColor = '#333';

        scrollInterval = setInterval(() => {
            const videos = getValidVideos();
            btn.innerText = '⏳ ' + videos.length + '/' + targetNum;

            // Если цель достигнута — останавливаемся и центрируем ролик на экране
            if (videos.length >= targetNum) {
                clearInterval(scrollInterval);
                btn.innerText = 'Go ⬇️';
                btn.style.backgroundColor = '#ff0000';
                input.disabled = false;
                
                if (videos[targetNum - 1]) {
                    videos[targetNum - 1].scrollIntoView({ behavior: 'smooth', block: 'center' });
                }
                return;
            }

            // Иначе — продолжаем листать вниз
            window.scrollTo(0, document.documentElement.scrollHeight);
            const app = document.querySelector('ytd-app');
            if (app) app.scrollTop = app.scrollHeight;

            let currentHeight = document.documentElement.scrollHeight;
            if (currentHeight === lastHeight) {
                noChangeCount++;
                if (noChangeCount >= 15) { // Конец ленты подписок
                    clearInterval(scrollInterval);
                    btn.innerText = 'Go ⬇️';
                    btn.style.backgroundColor = '#ff0000';
                    input.disabled = false;
                }
            } else {
                noChangeCount = 0;
                lastHeight = currentHeight;
            }
        }, 250);
    }

    btn.onclick = function(e) {
        e.preventDefault();
        startAutoScroll();
    };

    // Контроль присутствия интерфейса при переходах по страницам YouTube
    setInterval(() => {
        const isSubscriptions = window.location.href.includes('/feed/subscriptions');
        const exists = document.getElementById('yt-modern-counter-widget');

        if (isSubscriptions && !exists) {
            document.body.appendChild(container);
            initObserver();
            mutationObserver.observe(document.body, { childList: true, subtree: true });
        } else if (!isSubscriptions && exists) {
            clearInterval(scrollInterval);
            if (observer) observer.disconnect();
            mutationObserver.disconnect();
            container.remove();
        }
    }, 1000);

})();