Home > Net >  How to intercept modifications to the thumbnails on the YouTube website?
How to intercept modifications to the thumbnails on the YouTube website?

Time:11-05

I'm making a simple Chrome extension that modifies some information shown on the thumbnails of the recommended videos on YouTube.

For a simplification, let's say I want to replace the video length (e.g., "14:32") with the name of the channel (e.g., "PewDiePie").

Let's say I'm in the page of any YouTube video (video player in the center, list of thumbnails on the right side). I can do this replacement once:

function processNode(node: HTMLElement) {
    const channelName = node
        .closest('ytd-thumbnail')
        ?.parentElement?.querySelector('.ytd-channel-name')
        ?.querySelector('yt-formatted-string');

    if (channelName?.textContent) node.textContent = channelName?.textContent;
}

async function replaceCurrentThumbnailTimes(): Promise<void> {
    for (const node of document.querySelectorAll(
        'span.ytd-thumbnail-overlay-time-status-renderer',
    )) {
        processNode(node as HTMLElement);
    }
}

void replaceCurrentThumbnailTimes();

This works, but then if I navigate to a new page---for example by clicking any video in the list of recommended---the video lengths are not updated. The values I changed remain the same, despite the thumbnails being updated to refer to a new video.

As an example, let's say I open a YouTube video and the first thumbnail on the side is a video by Alice. The time on the thumbnail is replaced by Alice, as I wanted. Next, I click in some other video, and the first thumbnail is now a video by Bob. The time on that thumbnail is still Alice, despite that being out of date.

I tried using the MutationObserver API, and that works when new thumbnails are added to the DOM (e.g., when scrolling down the page), but it also doesn't work for when the existing thumbnail elements are modified. This is what I tried:

async function replaceFutureThumbnailTimes(): Promise<void> {
    const observer = new MutationObserver((mutations) => {
        // For each new node added, check if it's a video thumbnail time
        for (const mutation of mutations) {
            for (const node of mutation.addedNodes) {
                if (
                    node instanceof HTMLElement &&
                    node.classList.contains(
                        'ytd-thumbnail-overlay-time-status-renderer',
                    ) &&
                    node.getAttribute('id') === 'text'
                ) {
                    processNode(node);
                }
            }
        }
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true,
        characterData: true,
        attributes: true,
    });
}

void replaceFutureThumbnailTimes();

I think it might have something to do with the shadow/shady DOM, but I can't figure out how to go around it.

PS: to make it simpler for others to reproduce, I put the same code in pure javascript on pastebin, so that you can just copy it into the chrome console: https://pastebin.com/NWKfzCwQ

CodePudding user response:

As @RoryMcCrossan and @wOxxOm suggested in the comments to the question, indeed the MutationObserver works, and I was just misusing it. Thanks to both of them!

In this case, I needed to monitor for attributes changes, and check for changes in the aria-label, for nodes with id text.

Here is the code in javascript which accomplishes this:

async function replaceFutureThumbnailTimes() {
    const observer = new MutationObserver((mutations) => {
        for (const mutation of mutations) {
            // If attributes were changed, check if it's the thumbnail time
            if (
                mutation.type === 'attributes' && 
                mutation.attributeName === 'aria-label' && 
                mutation.target.getAttribute('id') === 'text') {
                    processNode(mutation.target);
            }
            // For each new node added, check if it's a video thumbnail time
            for (const node of mutation.addedNodes) {
                if (
                    node instanceof HTMLElement &&
                    node.classList.contains(
                        'ytd-thumbnail-overlay-time-status-renderer',
                    ) &&
                    node.getAttribute('id') === 'text'
                ) {
                    processNode(node);
                }
            }
        }
    });
 
    observer.observe(document.body, {
        childList: true,
        subtree: true,
        characterData: false,
        attributes: true,
    });
}
 
void replaceFutureThumbnailTimes();
  • Related