0

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

dcferreira
  • 120
  • 7
  • A MutationObserver should work for both cases. We would need to see your code to debug why it isn't. – Rory McCrossan Nov 03 '22 at 11:49
  • See [How to detect page navigation on YouTube and modify its appearance seamlessly?](https://stackoverflow.com/a/34100952). – wOxxOm Nov 03 '22 at 11:53
  • @RoryMcCrossan I edited my post to include how I'm using the MutationObserver. – dcferreira Nov 03 '22 at 12:45
  • @wOxxOm Thanks, I'd ran into that before. While it was helpful in giving me more insight into YouTube, it didn't really help me address the issue here. I tried just now to use `document.addEventListener('yt-navigate-finish', replaceCurrentThumbnailTimes);`, but it seems to trigger before the thumbnails are updated. The channel names appearing in the video length are out of date with the actual thumbnails – dcferreira Nov 03 '22 at 12:48
  • Your callback only checks addedNodes but apparently the site keeps the node and only changes its attributes so you need to check mutation.type and if it's for `attributes` or `characterData` then process the `target` accordingly. – wOxxOm Nov 03 '22 at 17:18
  • It might be I'm missing something very obvious, but I'm now trying to catch all attribute/characterData mutations, and it's never triggered (`if (mutation.type === 'attributes') console.log("ATTRIBUTES")`). But if that were the case, wouldn't the time in the thumbnails be overwritten on page change? They don't seem to be overwritten after I change them once, for some reason. – dcferreira Nov 03 '22 at 19:34
  • I guess you didn't reload the extension or added the check in the wrong place because I see such mutations. – wOxxOm Nov 04 '22 at 10:43
  • Indeed something like that happened, and you're right! I still don't exactly understand why the text of the time isn't changed after I change it, but monitoring the attributes' changes is enough for my case. Thanks a lot! I'll write a short answer for this simplified case. – dcferreira Nov 04 '22 at 11:29

1 Answers1

0

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();
dcferreira
  • 120
  • 7