Can you please tell me if this script is safe?

Hello World,

Not a Fedora/Linux related topic so sorry if that pisses some of you off, but I am strongly considering adding a script to my browser to fix a youtube issue that has been driving me out of my mind!!!.. :face_with_symbols_on_mouth:

However since I know diddly about this stuff I thought I’d better try and ask can you guys pretty please check this code and let me know if this script is safe to use? :folded_hands:
And are there any lines I need to delete or modifications to make to stop it from updating itself without my knowledge or stuff like that?

Thank you very much in advance. :face_blowing_a_kiss:

// ==UserScript==
// @name         YouTube Smaller Thumbnails
// @namespace    http://greasyfork.org
// @version      0.0.6
// @description  Adds additional thumbnails per row
// @author       you
// @license      MIT
// @match        *://www.youtube.com/*
// @match        *://youtube.com/*
// @run-at       document-start
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_addValueChangeListener
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @require https://update.greasyfork.org/scripts/470224/1506547/Tampermonkey%20Config.js
// @downloadURL https://update.greasyfork.org/scripts/533610/YouTube%20Smaller%20Thumbnails.user.js
// @updateURL https://update.greasyfork.org/scripts/533610/YouTube%20Smaller%20Thumbnails.meta.js
// ==/UserScript==
(function() {
    'use strict';
    const DEFAULT_MAX_COLUMNS = 6; // Maximum amount of columns.
    const DEFAULT_MAX_SHORTS_COLUMNS = 12; // Maximum amount of columns for shorts.

    let cfg

    if (
        typeof GM_registerMenuCommand === 'undefined' ||
        typeof GM_unregisterMenuCommand === 'undefined' ||
        typeof GM_addValueChangeListener === 'undefined' ||
        typeof GM_getValue === 'undefined' ||
        typeof GM_setValue === 'undefined' ||
        typeof GM_deleteValue === 'undefined'
    ) {
        cfg = {
            params: {
                'columns': DEFAULT_MAX_COLUMNS,
                'shortsColumns': DEFAULT_MAX_SHORTS_COLUMNS,
                'shortsScale': 10,
                'applyStyles': true
            },
            get: function (key) {
                return typeof this.params[key] !== 'undefined' ? this.params[key] : null;
            }
        }
    } else {
        cfg = new GM_config({
            columns: {
                type: 'int',
                name: 'Videos Per Row',
                value: DEFAULT_MAX_COLUMNS,
                min: 1,
                max: 20
            },
            shortsColumns: {
                type: 'int',
                name: 'Shorts Per Row',
                value: DEFAULT_MAX_SHORTS_COLUMNS,
                min: 1,
                max: 20
            },
            shortsScale: {
                type: 'int',
                name: 'Shorts Scale (in %)',
                min: 10,
                max: 200,
                value: 10
            },
            applyStyles: {
                type: 'boolean',
                name: 'Apply Styles',
                value: true
            }
        })
    }

    function debug(...args) {
        console.log('%c[YouTube Smaller Thumbnails]', 'background: #111; color: green; font-weight: bold;', ...args)
    }

    function applyStyles() {
        if (!cfg.get('applyStyles')) {
            return
        }

        var style = document.createElement('style');
        style.appendChild(document.createTextNode(`
ytd-rich-item-renderer[is-slim-media] {
  width: ${cfg.get('shortsScale')}% !important;
}
    	`));
        document.body.appendChild(style);
        debug('Applied styles')
    }

    document.addEventListener("DOMContentLoaded", applyStyles);
    document.addEventListener("load", applyStyles);


    function installStyle(contents) {
        var style = document.createElement('style');
        style.innerHTML = contents;
        document.body.appendChild(style);
    }

    function getTargetValue() {
        return currentOrDefault(+cfg.get('columns'), DEFAULT_MAX_COLUMNS)
    }

    function getShortsTargetValue() {
        return currentOrDefault(+cfg.get('shortsColumns'), DEFAULT_MAX_SHORTS_COLUMNS)
    }

    function currentOrDefault(value, defaultValue) {
        const num = parseInt(value, 10);
        if (!isNaN(num) && num.toString() === String(value).trim() && num > 0 && num < 100) {
            return num
        }
        return defaultValue
    }

    function isShorts(itemElement) {
        return null !== itemElement.getAttribute('is-slim-media')
    }

    function modifyGridStyle(gridElement) {
        const currentStyle = gridElement.getAttribute('style');
        if (!currentStyle) {
            return;
        }

        const itemsPerRowMatch = currentStyle.match(/--ytd-rich-grid-items-per-row:\s*(\d+)/);
        if (!itemsPerRowMatch) {
            return;
        }

        const currentValue = parseInt(itemsPerRowMatch[1], 10);

        if (isNaN(currentValue)) {
            return;
        }

        const newValue = getTargetValue();

        if (currentValue === newValue) {
            return;
        }

        const newStyle = currentStyle.replace(
            /--ytd-rich-grid-items-per-row:\s*\d+/,
            `--ytd-rich-grid-items-per-row: ${newValue}`
        );

        gridElement.setAttribute('style', newStyle);
        debug(`Modified items per row: ${currentValue} -> ${newValue}`);
    }

    function modifyItemsPerRow(itemElement) {
        const currentValue = parseInt(itemElement.getAttribute('items-per-row'), 10);

        if (isNaN(currentValue)) {
            return;
        }

        const newValue = isShorts(itemElement) ?
            getShortsTargetValue() :
            getTargetValue();

        if (currentValue === newValue) {
            return;
        }

        itemElement.setAttribute('items-per-row', newValue);
        debug(`Modified items per row: ${currentValue} -> ${newValue}`);
    }

    function modifyShortHidden(itemElement) {
        if (!isShorts(itemElement)) {
            return;
        }

        if (null === itemElement.getAttribute('hidden')) {
            return
        }

        itemElement.removeAttribute('hidden');
        debug(`Modified hidden`);
    }

    function modifyShelfRenderer(itemElement) {
        const currentStyle = itemElement.getAttribute('style');
        if (!currentStyle) {
            return;
        }

        const itemsCountMatch = currentStyle.match(/--ytd-rich-shelf-items-count:\s*(\d+)/);
        if (!itemsCountMatch) {
            return;
        }

        const currentValue = parseInt(itemElement.getAttribute('elements-per-row'), 10);
        if (isNaN(currentValue)) {
            return;
        }

        const newValue = getShortsTargetValue()
        if (currentValue === newValue) {
            return;
        }

        const newStyle = currentStyle.replace(
            /--ytd-rich-shelf-items-count:\s*\d+/,
            `--ytd-rich-shelf-items-count: ${newValue}`
        );

        itemElement.setAttribute('style', newStyle);
        itemElement.setAttribute('elements-per-row', newValue);
        debug(`Modified elements per row: ${currentValue} -> ${newValue}`);
    }

    function processExistingElements() {
        document.querySelectorAll('ytd-rich-grid-renderer').forEach(gridElement => {
            modifyGridStyle(gridElement);
        });

        document.querySelectorAll('ytd-rich-item-renderer').forEach(itemElement => {
            modifyItemsPerRow(itemElement);
            modifyShortHidden(itemElement);
        });
    }

    const observer = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
            if (mutation.addedNodes && mutation.addedNodes.length > 0) {
                mutation.addedNodes.forEach((node) => {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        if (node.tagName === 'YTD-RICH-GRID-RENDERER') {
                            modifyGridStyle(node);
                        }
                        if (node.tagName === 'YTD-RICH-ITEM-RENDERER') {
                            modifyItemsPerRow(node);
                        }
                        if (node.tagName === 'YTD-RICH-SHELF-RENDERER') {
                            modifyShelfRenderer(node);
                        }

                        node.querySelectorAll('ytd-rich-grid-renderer').forEach(gridElement => {
                            modifyGridStyle(gridElement);
                        });
                        node.querySelectorAll('ytd-rich-item-renderer').forEach(itemElement => {
                            modifyItemsPerRow(itemElement);
                            modifyShortHidden(itemElement);
                        });
                        node.querySelectorAll('ytd-rich-shelf-renderer').forEach(itemElement => {
                            modifyShelfRenderer(itemElement);
                        });
                    }
                });
            }

            if (mutation.type === 'attributes') {
                const target = mutation.target;

                if (target.tagName === 'YTD-RICH-GRID-RENDERER' && mutation.attributeName === 'style') {
                    modifyGridStyle(target);
                }
                if (target.tagName === 'YTD-RICH-ITEM-RENDERER' && mutation.attributeName === 'items-per-row') {
                    if (mutation.attributeName === 'items-per-row') {
                        modifyItemsPerRow(target);
                    }

                    if (mutation.attributeName === 'hidden') {
                        modifyShortHidden(target);
                    }

                }
                if (target.tagName === 'YTD-RICH-SHELF-RENDERER' && mutation.attributeName === 'elements-per-row') {
                    modifyShelfRenderer(target);
                }
            }
        });
    });

    function startObserver() {
        processExistingElements();
        observer.observe(document.documentElement, {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: ['style', 'hidden', 'items-per-row', 'elements-per-row']
        });

        debug('Observer started');
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', startObserver);
    } else {
        startObserver();
    }

    setInterval(processExistingElements, 3000);
})();

Unfortunately, it references another script that is hosted remotely. It might be fine right now, but if the author wanted to add an exploit, he could do so at any time.

Edit:

It looks like you can change the @downloadURL parameter to none to prevent it from updating. You would also need to calculate the SHA256 for the script that is referenced on the @require line and then append the base64 encoded value to the current @require line. It would look something like @require https://update.greasyfork.org/.../Tampermonkey%20Config.js#sha256=23456....

Reference: Documentation | Tampermonkey

Of course, when the YouTube site changes and you need to use a newer version of the script, you would have to manually re-evaluate everything to get it working again.

1 Like

I see… :cry: And I’m guessing that if I take out the lines that “load”(is that even the right word?) that remote script then it won’t work anymore, or that they can’t even be taken out?

LE: Oh, ok cool, I’ll try to do those things then. Thank you very much, wish me luck. :wink:

You might be able to take out those require (load) lines if you also copy-and-paste the code from the remote script into the local copy.

https://update.greasyfork.org/scripts/470224/1506547/Tampermonkey%20Config.js

You’d want to just copy the part that starts class ... and paste it immediately above the function definition in the local script. That might work. You’d just have to try it and see.

What’s the issue?


I mainly use YouTube to upload/watch my own vids and quick music videos, but haven’t really seen anything annoying enough to want to block while just video watching.

YouTube and other Google stuff changes too-wildly to script blocks around; a footer might be footer-8has in an hour because they want to annoy-adblocker-users-so-they-turn-it-off-cuz-broken-website block ad-blockers :stuck_out_tongue:

That script was too-large for me to want to read let alone trust.

The issue is an annoying horizontal scroll bar at the bottom of the screen that often appears in full screen, mainly in playlists when going from one video to another.
Once it appears I have to keep going back and forth to the next/prev video many many times until it disappears and then hope it won’t show up again in the very next video. And it’s driving me crazy!!! :exploding_head:

So, unless you have a better idea, I’m gonna try to make the changes @glb suggested today then test it for a few days to see if they work… :pray:

LE: Apparently, according to reddit, if I used an adblocker like uBlock all I’d need to do is add this line
youtube.com##body, ytd-app[scrolling]:style(overflow: hidden !important;)
somewhere inside it. But I use the Brave browser(for now, very exited about Orion coming to Linux sometime in the future, but we’ll have to see…) that has it’s own built in ad&tracker blocker and I have no idea how to even access let alone modify that. :sob:

Brave is a Chromium derivative.
All Chromium/Chrome browsers requires content blocking extensions to be “declarative”, meaning they cannot block anything by themselves, they must pass a list of “rules” to the browser and the browser takes care of the blocking.
As result, uBlock Origin comes in two versions, the “full” version is deprecated on Chromium and works only on Firefox. The “light” version was developed for Chromium but it is automated, meaning it doesn’t have any “inspect” and “rule set” function. So you probably could install “uBlock Lite” on Brave but, besides possibile conflicts with the provided “adblocker”, it would be useless for your own problem.

The rule from reddit is just cosmetic CSS, the trick is you overwrite the Web site CSS with your own. CSS does not change anything, it is like “make this text 12pixels, red and centered”. In our case, it sets some page element as “display:hidden”.

With the JS script you adopt a more powerful approach, basically you re-write parts of the Web pages. I mean while the CSS just passes parameters to the browser so it displays the page elements applying some properties, the script can add, change or remove parts of the page, even better/worse when the Web site is not “static” but it is generated runtime upon user request by a lot of other JS scripts.

I agree with Espionage724, Youtube and Google services in general are very complicated and it is a lot of work to keep up with them.