YT Feed Sorter

Sorts the YouTube feed so that all scheduled streams come before uploaded videos.

As of 2023-06-30. See the latest version.

// ==UserScript==
// @name         YT Feed Sorter
// @namespace    YTFeedSorter
// @version      0.6.2
// @description  Sorts the YouTube feed so that all scheduled streams come before uploaded videos.
// @match        *://*.youtube.com/*
// @author       KFP
// ==/UserScript==

(function() {
    'use strict';

    let feedSorted = false;
    const feedGridSelector = 'ytd-browse[page-subtype="subscriptions"][role="main"] #contents.ytd-rich-grid-renderer';
    const feedListSelector = 'ytd-browse[page-subtype="subscriptions"][role="main"] #contents.ytd-section-list-renderer';
    const liveSelector = '.badge-style-type-live-now-alternate';
    const soonSelector = 'ytd-toggle-button-renderer';

    const sortFeed = feed => {
        try{
            const isList = feed.classList.contains('ytd-section-list-renderer');
            if (isList) {
                [...feed.children].sort((a, b) => {
                    const aInner = a.root.querySelector('ytd-shelf-renderer').root.querySelector('ytd-expanded-shelf-contents-renderer').root.querySelector('ytd-video-renderer');
                    const bInner = b.root.querySelector('ytd-shelf-renderer').root.querySelector('ytd-expanded-shelf-contents-renderer').root.querySelector('ytd-video-renderer');
                    const aLive = aInner.querySelector('ytd-badge-supported-renderer:not([hidden])')?.root.querySelector(liveSelector);
                    const aSoon = aInner.querySelector(soonSelector);
                    const bLive = bInner.querySelector('ytd-badge-supported-renderer:not([hidden])')?.root.querySelector(liveSelector);
                    const bSoon = bInner.querySelector(soonSelector);
                    const ai = aLive ? 2 : aSoon ? 1 : 0;
                    const bi = bLive ? 2 : bSoon ? 1 : 0;
                    return (ai > bi) ? -1 : (ai < bi) ? 1 : 0;
                }).forEach(item => feed.appendChild(item));
            } else {
                const rows = feed.querySelectorAll('ytd-rich-grid-row');
                let currentRowI = 0;
                let currentRow = rows[0].querySelector('#contents');
                let rowLength = currentRow.children.length;
                let items = [];
                rows.forEach(row => {
                    const rowItems = row.querySelectorAll('ytd-rich-item-renderer');
                    items = items.concat([...rowItems]);
                });
                items.sort((a, b) => {
                    const aInner = a.querySelector('ytd-rich-grid-media');
                    const bInner = b.querySelector('ytd-rich-grid-media');
                    const aLive = aInner.querySelector('.video-badge:not([hidden])')?.root.querySelector(liveSelector);
                    const aSoon = aInner.querySelector(soonSelector);
                    const bLive = bInner.querySelector('.video-badge:not([hidden])')?.root.querySelector(liveSelector);
                    const bSoon = bInner.querySelector(soonSelector);
                    const ai = aLive ? 2 : aSoon ? 1 : 0;
                    const bi = bLive ? 2 : bSoon ? 1 : 0;
                    return (ai > bi) ? -1 : (ai < bi) ? 1 : 0;
                }).forEach((item, i) => {
                    currentRow.appendChild(item);
                    if (i && ((i + 1) % rowLength === 0)) {
                        currentRowI++;
                        if (rows[currentRowI]) {
                            currentRow = rows[currentRowI].querySelector('#contents');
                        }
                    }
                });
            }
        } catch(e) {
            console.log(e);
        }
    };

    const gridObserver = new MutationObserver(mutations => {
        for (const mut of mutations) {
            if (mut.removedNodes?.length) {
                setTimeout(() => sortFeed(mut.target), 200);
                setTimeout(() => sortFeed(mut.target), 2000);
                setTimeout(() => sortFeed(mut.target), 4000);
            }
        }
    });

    let listObserverTimer = 0;
    const listObserver = new MutationObserver(mutations => {
        for (const mut of mutations) {
            if (mut.addedNodes?.length) {
                const now = Date.now();
                if (now - listObserverTimer > 5200) {
                    setTimeout(() => sortFeed(mut.target), 200);
                    setTimeout(() => sortFeed(mut.target), 5000);
                    listObserverTimer = now;
                }
            }
        }
    });

    let pageChanged = false;
    setInterval(() => {
        let isList = false;
        let feed = document.querySelector(feedGridSelector);
        if (!feed) {
            feed = document.querySelector(feedListSelector);
            if (feed) isList = true;
        }
        if (feed) {
            if (feedSorted) return;
            if (isList) {
                setTimeout(() => sortFeed(feed), 200);
                setTimeout(() => sortFeed(feed), 2000);
                listObserver.observe(feed, {childList: true});
            } else {
                if (pageChanged) {
                    setTimeout(() => sortFeed(feed), 1000);
                    setTimeout(() => sortFeed(feed), 3000);
                    setTimeout(() => sortFeed(feed), 6000);
                } else {
                    setTimeout(() => sortFeed(feed), 1);
                }
                gridObserver.observe(feed, {childList: true});
            }
            feedSorted = true;
        } else if (feedSorted) {
            gridObserver.disconnect();
            listObserver.disconnect();
            pageChanged = true;
            feedSorted = false;
        }
    }, 100);
})();