3
\$\begingroup\$

I felt that having a plain red favicon for every youtube tab is a little boring. So, I wrote a TamperMonkey Script with the new ES6 promises that replaces the favicon with the channel's logo. Link to the script here.

The script works by first extracting the channel logo's URL from the window.ytInitialData object. Then, a mutation listener is attached to the title, and whenever the title of the page changes, the script makes a GET request using the window.location.href and then extracts the logo's URL from the responseText.

Since this is my first time writing a userscript, I wanted to confirm that I am following best practices and not breaking anything on the site. Also, is there a better way of achieving the same result?

Thank you for your time.

My code:

// ==UserScript==
// @name        YouTube Favicon to Channel logo
// @version     1.1
// @description Changes the YouTube Favicon to the Channel logo
// @author      Kartik Soneji
// @updateURL   https://openuserjs.org/meta/KartikSoneji/YouTube_Favicon_to_Channel_logo.meta.js
// @match       https://www.youtube.com/*
// @exclude     https://www.youtube.com/
// @exclude     https://www.youtube.com/tv*
// @exclude     https://www.youtube.com/embed/*
// @exclude     https://www.youtube.com/live_chat*
// @grant       none
// @run-at      document-end
// @license     MIT
// ==/UserScript==

(function(){
    'use strict';

    const urlRegex = /https:\/\/yt3.ggpht.com\/a[/-]{1,2}[A-Za-z\d-_]+/;

    function isInIframe(){
        try{
            return window.self !== window.top;
        }
        catch(e){
            return true;
        }
    }
    if(isInIframe()) return;

    console.log("Started");

    roundImageToDataUrl(JSON.stringify(window.ytInitialData).match(urlRegex)[0]).then(setFavicon);

    function run(){ 
        getChannelLogo(window.location.href).then(roundImageToDataUrl).then(setFavicon);
    }

    async function getChannelLogo(url){
        return new Promise((resolve, reject) => {
            let xhr = new XMLHttpRequest();
            xhr.open("GET", url);
            xhr.onload = () =>{
                let u = xhr.responseText.match(urlRegex)[0];
                console.log(u);
                resolve(u);
            }
            xhr.send();
        });
    }

    let observer = new MutationObserver(run);

    function registerUrlChangeListener(){
        let e = document.querySelector("title");
        if(e){
            //observer.observe(e, {attributes: true});
            observer.observe(document.querySelector("title"), {attributes: true, characterData: true, childList: true});
            return;
        }

        setTimeout(registerUrlChangeListener, 250);
    }
    registerUrlChangeListener();

    async function roundImageToDataUrl(url){
        return new Promise((resolve, reject) => {
            if(url.src) url = url.src;

            let img = document.createElement("img");
            img.crossOrigin = "anonymous";
            img.src = url;
            img.style = "position: absolute; top: -100000px; left: -100000px;";

            img.onload = function(){
                let canvas = document.createElement("canvas"), g = canvas.getContext("2d");

                canvas.height = img.naturalHeight;
                canvas.width = img.naturalWidth;

                g.beginPath();
                g.arc(canvas.width/2, canvas.height/2, canvas.width/2, 0, Math.PI * 2);
                g.clip();

                g.drawImage(img, 0, 0);

                img.parentElement.removeChild(img);

                resolve(canvas.toDataURL());
            }

            document.body.appendChild(img);
        });
    }

    function setFavicon(url){
        let a = document.querySelectorAll("link[rel *= 'icon']");

        if(a.length == 0){
            let link = document.createElement('link');
            link.type = 'image/x-icon';
            link.rel = 'icon';
            document.head.appendChild(link);
            a = [link];
        }

        for(let i of a) i.href = url;
    }
})();
\$\endgroup\$
2
\$\begingroup\$
img.style = "position: absolute; top: -100000px; left: -100000px;";
document.body.appendChild(img);

img.parentElement.removeChild(img);

This seems weird. The canvas can be drawn from an unattached image and these lines can be removed.

  if(url.src) url = url.src;

  img.src = url;

A more idiomatic phrasing is img.src = url.src || url.

 .querySelectorAll("link[rel *= 'icon']");

Unless you have something specific in mind, I'd use [rel='icon'] here. It's faster, and not clear that matching an exotic future variant like <link rel="BiscottiConfig"> is going to be desirable.

let

Except for the final loop, every use of let in your script can be replaced by const. It's a good habit, even if the aesthetics are inferior to let or var.

const urlRegex = /https:\/\/yt3.ggpht.com\/a[/-]{1,2}[A-Za-z\d-_]+/;

roundImageToDataUrl(JSON.stringify(window.ytInitialData).match(urlRegex)[0]).then(setFavicon);

This looks like a misstep. Youtube avatars have id=img; why not just roundImageToDataUrl( document.getElementById("img").src )?

Dealing with future changes to the host page is a constant challenge with userscripts. There are many things that have to remain unchanged for this to work (the image host, the URL format, the name, content and encoding of ytInitialData) and I'll predict that some of them won't. Selecting the avatar itself with semantic markup is likely to be less brittle, and easier to fix if the markup changes (assuming there will always be an avatar and it will always have some semantics attached to it).

\$\endgroup\$
  • \$\begingroup\$ Thank you for your answer. I have removed the document.body.appendChild(img); I was not aware that off screen images could be drawn onto a canvas. I really do not like statements like img.src = url.src || url for some reason. They look like javascript "hacks". I am using .querySelectorAll("link[rel *= 'icon']"); because I need to match both: <link rel="icon"> and <link rel="shortcut icon">. I do not know of a better way to do this. I do not like to use const everywhere, especially while developing the script. You have to make a new variable every time. \$\endgroup\$ – Kartik Soneji May 6 at 8:53
  • \$\begingroup\$ About the Youtube avatars. The page does not load the image src till the page is focused. This means that pages in the background will not have a src value. Also, if a video plays with autoplay, then it's img is not loaded until the tab is focused again. This was the first approach that I tried, and almost gave up before realizing that the logo url was in the html of the page. Also, you can copy and paste the entire script into developer tools in a youtube page if you want to try it out. Also, please let me know if you have any suggestions. \$\endgroup\$ – Kartik Soneji May 6 at 9:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.