How to intercept modifications to the thumbnails on the YouTube website? - javascript

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

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

Strange error with asynchronous code in Mutation Observer

I'm working on an third party addon for a WordPress page builder for which the code is based on Vue. Concretely, I'm working on an Emmet styled command line to add new elements to the UI with commands like: > section > container > button.
So far, so good. Since this Page Builder does not currently provide an API to interact inside the Vue context / state, I need to be creative. Currently it is a mixture between event dispatching and mutation observers. I manually go down each click path and thus create the elements as I need them.
This works, but only to a limited extent. I can't get any further at one point. The code you find below only goes up to the second layer.
section > container will work. The code will produce a section element with a nested container element inside. But: section > container > button will not work. The result here also is the same as before. The third element (button) is missing.
Here is the code:
async addElements(command) {
try {
// Split elements by one or more spaces
let elements = command.split(">");
// Remove items from array which are empty strings
elements = elements.filter((item) => item);
// Remove whitespace from all elements in the array
elements = elements.map((item) => item.replace(/\s+/, ""));
for (let i = 0; i < elements.length; i++) {
// Remove whitespace
elements[i] = elements[i].replace(" ", "");
// Add Element
await this.addElement({
element: elements[i],
index: i,
elementsArray: elements,
});
}
} catch (e) {
console.log(e);
}
}
async addElement({ element, elementsArray, index }) {
// if is empty string, skip
if (element == "") {
return;
}
let newCreatedElement;
let newCreatedElements = [];
let mutationCountMax = elementsArray.length;
// Add Mutation Observer to wait for the element to be created
let observer = new MutationObserver(async (mutations) => {
if (this.mutationCount >= mutationCountMax) {
observer.disconnect();
return;
}
this.mutationCount++;
// Create a list from all li elements from the mutation.addedNodes
let liList = [];
mutations.forEach((mutation) => {
if (mutation.addedNodes && mutation.addedNodes.length) {
liList.push(
...mutation.addedNodes[0].querySelectorAll("li")
);
}
});
// Remove mutations which have no added nodes
mutations = mutations.filter(
(mutation) => mutation.addedNodes.length > 0
);
// Remove mutations which have #builder-panel-header as parent
mutations = mutations.filter(
(mutation) =>
mutation.addedNodes[0].parentElement.id !=
"builder-panel-header"
);
// Remove mutations which target is not tag UL or tag LI
mutations = mutations.filter(
(mutation) =>
mutation.addedNodes[0].tagName == "UL" ||
mutation.addedNodes[0].tagName == "LI"
);
let addedNode = mutations[0].addedNodes[0];
newCreatedElement = [addedNode];
// if newCreatedElement is UL, get the first LI
newCreatedElement = newCreatedElement[0].querySelectorAll("li");
newCreatedElement.forEach((element) => {
// Add click event to the element
element
.querySelector(".builder-draggable-handle")
.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
/**
* Wait for the nested LI Elements to be added to the page
*/
let liObserver = new MutationObserver(async (liMutations) => {
// Wait until the nested li elements have been added to the page
if (
liMutations.some(
(liMutation) => liMutation.addedNodes.length > 0
)
) {
// Get all of the nested li elements
let liElements = addedNode.querySelectorAll("li li");
console.log(liElements);
console.log("Detected Nested LI Elements");
liElements.forEach((element) => {
// Add click event to the element
element
.querySelector(".builder-draggable-handle")
.dispatchEvent(
new MouseEvent("click", { bubbles: true })
);
});
// Disconnect the observer once the li elements have been found
liObserver.disconnect();
}
});
// Observe the addedNode element for changes
liObserver.observe(addedNode, {
childList: true,
subtree: true,
});
/**
* Create Element
*/
if (this.mutationCount != 1) {
await this.createElement(element);
}
observer.disconnect();
});
// Observe the body
observer.observe(document.querySelector("#builder-structure"), {
childList: true,
// Not observing attributes
attributes: false,
// Also deep elements
subtree: true,
});
if (index == 0) {
this.createElement(element);
}
}
async createElement(element) {
console.log("Create Element: " + element);
// Code to add the element to the UI structure
// ...
}
The console.log output for the command section > container > button is the following:
Create Element: section
Create Element: container
Create Element: button
NodeList [li#element-aqtufh
Detected Nested LI Elements
NodeList [li#element-aqtufh
Detected Nested LI Elements
NodeList [li#element-aqtufh
Detected Nested LI Elements
I think the problem lies in the asynchrony. The output in the console also seems to be not quite correct.
In that case I need the second mutation observer, because in the first one strangely the nested LI elements can't be found - I don't understand why. So it works, but only with the above mentioned problem.
Where is the error in my code?

FancyTree w/ ExtPersist - If no LocalStorage value for expanded, set defaults

I inherited some code and I've been asked to add a new 'feature'. The existing code sets the expanded state to 'true' for all top-level nodes not named "Favorites", but the "fancytree-1-expanded" Value in LocalStorage is not set until the user explicitly collapses and expands another top-level node. I understand that this is the expected behavior, but I need to bypass this and set the "fancytree-1-expanded" LocalStorage Value is none exists (I know the values of the top-level nodes, so I can name them easily).
Here is my FancyTree init code with persist (and several remarks I've used to try to identify the data and get this going):
// start of UI integration
buildMenu = function() {
$("#" + Menu).fancytree({
extensions: ["filter", "glyph", "persist"],
quicksearch: true,
//debugLevel: 4,
source: getandformatdata,
filter: {
autoApply: true, // Re-apply last filter if lazy data is loaded
autoExpand: true, // Expand all branches that contain matches while filtered
counter: true, // Show a badge with number of matching child nodes near parent icons
fuzzy: false, // Match single characters in order, e.g. 'fb' will match 'FooBar'
hideExpandedCounter: true, // Hide counter badge if parent is expanded
hideExpanders: false, // Hide expanders if all child nodes are hidden by filter
highlight: true, // Highlight matches by wrapping inside <mark> tags
leavesOnly: true, // Match end nodes only
nodata: true, // Display a 'no data' status node if result is empty
mode: "hide" // "dimm" = Grayout unmatched nodes (pass "hide" to remove unmatched node instead)
},
glyph: {
preset: "awesome4",
map: {
expanderClosed: "expandIconSidebar",
expanderLazy: "expandIconSidebar",
expanderOpen: "expandedIconSidebar",
}
},
click: function (event, data) {
//needed to reapply classes
/*if($(data.originalEvent.target).hasClass('fancytree-expander')) {
//cannot apply class if not expanded
data.node.setExpanded(true);
//Add leaf node classes
$('.fancytree-node:not(.fancytree-folder)').closest('li').addClass('leafNode');
$('.fancytree-folder').closest('li').addClass("folderNode");
data.node.setExpanded(false);
}*/
if(data.node.isFolder()) {
//if(data.node.title==='Favorites') {
// $('#actGoHome').click();
//}
//else
return;
}
},
persist: {
store: "auto" // 'cookie', 'local': use localStore, 'session': sessionStore
},
postProcess: function (event, data) {
//if persist is not set then expand all folders except Favorite
//console.log("postdata", data);
//console.log("persist",$.ui.fancytree.getTree('#' + Menu).getPersistData());
let persistExpanded = $.ui.fancytree.getTree('#' + Menu).getPersistData().expanded[0];
//console.log("Expanded: "+persistExpanded);
let foldersExpanded = data.response.filter(d => { return persistExpanded.split("~").some(f => {return d.title === f})});
//console.log("foldersExpanded", foldersExpanded);
if (foldersExpanded.length == 0) {
data.response.forEach((d) => {
if (d.title != 'Favorites') {
d.expanded = true;
}
})
}
},
});
}
I've tried to look at using localStorage.getItem/setItem, but my site is in an iframe in another port from the parent app/site. This appears to be problematic for trying to directly get and set the LocalStorage, so I'm hoping to edit my FancyTree's ExtPersist code to set the LocalStorage value if none exists.
The following JSFiddle can be used if someone knows of a way to default in the Value for "fancytree-1-expanded" of "b9052f9a-1c45-47ef-9d66-8c2bded1230e~tr" if no Value exists - https://jsfiddle.net/vt8mrLt0/1/

Detect css changes (mutationObserver on styleSheet css)

Is there a way to detect stylesheet changes ? MutationObserver only tracks inline css changes on the element.
html
<div class="exampleClass"></div>
js
let config = {
attributes: true,
// attributeFilter: ["style"],
};
let mutationCallback = function(mutationsList) {
mutationsList.forEach((mutation, i) => {
console.log(mutation);
});
};
let observer = new MutationObserver(mutationCallback);
observer.observe(document.querySelector('.exampleClass'), config);
If I modify the element throught js with
document.querySelector(‘exampleClass’).style.top = '10px'
or the webconsole inspector directly on the node, the mutation observer callback is called, but if the class (not the node itself) is modified in the webconsole inspector there is no callback

Mutation Observer or DOMNodeInserted

I have a script where I´ve use on the first slide of Adobe Captivate, to automate the task ok creating, courses, the script create the UX, navigation elements, intro/end motions, a game, insert spritesheets with characters, etc...
I´ve used DOMNodeInserted until know to check the modifications on the slide, when the user go to the next slide, the elements are added to the DOM and the page content is changed I´ve used this timer until now to call the function:
function detectChange(){
var slideName = document.getElementById('div_Slide')
slideName.addEventListener("DOMNodeInserted", detectChange, false);
updateSlideElements();
setTimeout(updateSlideElements, 100);
}
So I´m trying to use mutation Observer now:
var observer = new MutationObserver(function(mutations, observer) {
updateSlideElements();
});
observer.observe(document.getElementById('div_Slide').firstChild, {
attributes: true,
childList:true
});
But this is what´s happening, before with setTimeout I could reach the following element:
var motionText2 = document.querySelectorAll('div[id*=motion][class=cp-accessibility]');
This element is the firstChild of:
And the element can be found:
But now with mutationObserver the console returns empty:
I´ve just use a setTimeout inside the observer and watch the parent container not the firstChild:
var observer = new MutationObserver(function(mutations, observer) {
setTimeout(updateSlideElements, 100);
});
observer.observe(document.getElementById('div_Slide'), {
attributes: true,
childList:true
//subtree:true
});

MutationObserver not showing all mutations?

I've got a 3rd-party script that loads a photo gallery on my page with the images coming from off-site.
My page starts as empty:
<div class="container-fluid" id="cincopa">
</div>
The 3rd-party script then adds other stuff (like the frame of the photo gallery):
<div class="container-fluid" id="cincopa">
<div id="cp_widget_38cc1320-f4a4-407a-a80e-1747bd339b64">
</div>
</div>
Then finally the images load:
<div class="container-fluid" id="cincopa">
<div id="cp_widget_38cc1320-f4a4-407a-a80e-1747bd339b64">
<div class="galleria_images">
<div class="galleria_image">SomeImage</div>
<div class="galleria_image">SomeImage</div>
<div class="galleria_image">SomeImage</div>
</div>
</div>
</div>
I want to:
display a loading animation
set a MutationObserver on $('#cincopa')
when it detects that $('.galleria_image') has been created, it means images have been loaded, so I can
remove the loading animation
Code:
var target = document.querySelector('#cincopa');
// create an observer instance
var observer = new MutationObserver(function(mutations) {
console.log(mutations);
mutations.forEach(function(mutation) {
console.log(mutation.type);
});
});
// configuration of the observer:
var config = { attributes: true, childList: true, characterData: true };
// start the observer, pass in the target node, as well as the observer options
observer.observe(target, config);
The problem is that the MutationObserver only console logs one mutation and the MutationRecord only has one mutation in its array. I would expect numerous mutations as the 3rd-party script creates DOM elements.
Am I misunderstanding how MutationObserver works?
Here's the solution
// This is MeteorJS creating the loading spinning thing
var loadingView = Blaze.render(Template.loading, $('#cincopa')[0]);
// select the target node
var target = document.querySelector('#cincopa');
// create an observer instance
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if(mutation.target.className === "galleria_image"){
// a image has been loaded, so remove the loading spinner and
// kill the observer
Blaze.remove(loadingView);
observer.disconnect();
}
});
});
// configuration of the observer:
var config = { attributes: true, childList: true, characterData: true, subtree: true };
// start the observer, pass in the target node, as well as the observer options
observer.observe(target, config);
Updated Solution
.forEach is dumb and doesn't have a good way to break out of the loop, which meant that I was getting multiple commands to Blaze.remove() and observer.disconnect(), even after .galleria_image had been found.
So I used underscore instead:
// create an observer instance
var observer = new MutationObserver(function(mutations) {
var loaded = _.find(mutations, function(mutation){
console.log("observer running");
return mutation.target.className === "galleria-image";
});
if(loaded){
Blaze.remove(loadingView);
observer.disconnect();
console.log("observer stopped");
};
});
There's an option to allow you to do exactly what you want: observe the subtree of an element. Just add subtree: true to your config for the MutationObserver.
// ...
// In this case case only these two are needed, I believe.
var config = {
childList: true,
subtree: true
};
// ...observe
This should allow you to figure when .gallaria_images has been inserted. As a side note, you (OP) should also double check that images are loaded when that happens.

Categories