Youtube Scrollable Right Side Description

Moves and expands YouTube description on the right, making it scrollable and dynamically adjusting height to video.

As of 14. 04. 2025. See the latest version.

// ==UserScript==
// @name         Youtube Scrollable Right Side Description
// @description  Moves and expands YouTube description on the right, making it scrollable and dynamically adjusting height to video.
// @version      3.0
// @author       SH3LL
// @license      MIT
// @match        *://*.youtube.com/*
// @grant        none
// @run-at       document-end
// @namespace https://greatest.deepsurf.us/users/762057
// ==/UserScript==

(function() {
    'use strict';

    let descriptionExpander;
    const moveEl = (parent, child) => parent?.prepend(child);
    const setElStyle = (el, styles) => el?.setAttribute('style', styles);
    const qSel = sel => document.querySelector(sel);
    const getElById = id => document.getElementById(id);

    const ownerObserver = new MutationObserver((mutationsList, observer) => {
        if (getElById('owner')) {
            observer.disconnect();
            console.log('"owner" found, executing.');
            executeScript();
        }
    });
    ownerObserver.observe(document.body, { childList: true, subtree: true });

    const updateDescHeight = height => {
        descriptionExpander && (descriptionExpander.style.maxHeight = `${height - 70}px`);
    };

    const videoHeightObserver = new MutationObserver(mutations => {
        for (const m of mutations) {
            if (m.type === 'attributes' && m.attributeName === 'style') {
                const heightMatch = m.target.style.height.match(/^(\d+)px$/);
                heightMatch && updateDescHeight(parseInt(heightMatch[1], 10));
            }
        }
    });

    const observeVideoHeight = () => {
        const videoContent = qSel('.ytp-iv-video-content');
        if (videoContent) {
            videoHeightObserver.observe(videoContent, { attributes: true, attributeFilter: ['style'] });
            const initialHeight = videoContent.style.height.match(/^(\d+)px$/);
            initialHeight && updateDescHeight(parseInt(initialHeight[1], 10));
        } else {
            console.warn('No .ytp-iv-video-content found.');
        }
    };

    function executeScript() {
        const related = qSel('#related');
        const bottomRow = getElById('bottom-row');
        const owner = getElById('owner');
        const below = qSel('#below');
        const infoContainer = getElById('info-container');
        descriptionExpander = getElById('description-inline-expander');
        const description = getElById('description');
        const descriptionInner = getElById('description-inner');
        const descStyle = `margin-left: 0; overflow: auto; max-width: 100%; font-size: 1.3rem; line-height: normal; width: auto; padding: 8px; border-bottom-width: 0; --yt-endpoint-text-decoration: underline; background-color: var(--yt-playlist-background-item)`;

        moveEl(related, bottomRow);
        owner && (moveEl(related, owner), setElStyle(owner, 'margin:0'));
        moveEl(below, infoContainer);
        setElStyle(infoContainer, 'color:white; font-size: 12px');

        if (descriptionExpander && description && descriptionInner) {
            setElStyle(descriptionExpander, descStyle);
            setElStyle(description, 'margin: 0');
            setElStyle(descriptionInner, 'margin: 0');
            descriptionExpander.setAttribute('is-expanded', '');
            observeVideoHeight();
        }

        const removalObserver = new MutationObserver(mutations => {
            const targets = ['expand', 'collapse', 'ytd-watch-info-text', 'snippet'];
            for (const m of mutations) {
                if (m.type === 'childList') {
                    targets.forEach(id => getElById(id)?.remove());
                    !targets.some(getElById) && (observer.disconnect(), console.log('Removal observer done.'));
                }
            }
        });

        const watchInfoFlex = qSel('ytd-watch-metadata > div');
        watchInfoFlex && removalObserver.observe(watchInfoFlex, { childList: true, subtree: true });
    }
})();