Firefox extensions: How to run function on every video change on YouTube - javascript

I have an extension that injects js code into YouTube pages. I've used the following declaration in manifest.json:
"content_scripts": [
{
"matches": [
"*://*.youtube.com/*"
],
"js": [
"background.js"
]
}
]
I would like to define the function that prints the name of the video, number of likes and dislikes to console when I move to another video.
I've written this in background.js:
window.onhashchange = function () {
console.log(
document.querySelector("h1.title > yt-formatted-string:nth-child(1)").innerHTML, "\n",
document.querySelector("ytd-toggle-button-renderer.ytd-menu-renderer:nth-child(1) > a:nth-child(1) > yt-formatted-string:nth-child(2)").getAttribute("aria-label"), "\n",
document.querySelector("ytd-toggle-button-renderer.style-scope:nth-child(2) > a:nth-child(1) > yt-formatted-string:nth-child(2)").getAttribute("aria-label"), "\n",
)
}
But it runs only once. If I select new video from "Recommended" it does not work. I also tried .onload, .onunload, etc.
UPD: Now the only way I found is to use .setInterval.

Several possible solutions using the WebExtensions API, all require a background script that will send messages to your content script. Modify your manifest.json to include:
"background": {
"scripts": ["background.js"]
}
I've named the background script background.js here, that would collide with what you currently have - you might want to consider to rename your content script to something like contentscript.js, so you don't get the two confused.
In the contentscript.js you have the message listener
browser.runtime.onMessage.addListener(message => {
if (message.videoChanged) {
// do stuff
}
});
Using tabs.onUpdated
Needed permission in the manifest.json
"permissions": [
"tabs"
]
In the background.js
browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (!changeInfo.url) {
// url didn't change
return;
}
const url = new URL(changeInfo.url);
if (!url.href.startsWith('https://www.youtube.com/watch?') ||
!url.searchParams.get('v')) {
// not a youtube video
return;
}
browser.tabs.sendMessage(tabId, {videoChanged: true});
});
This method will message the content script on first visits, while on-site navigation or auto play.
Using webNavigation.onHistoryStateUpdated
Needed permission in the manifest.json
"permissions": [
"webNavigation"
]
In the background.js
browser.webNavigation.onHistoryStateUpdated.addListener(history => {
const url = new URL(history.url);
if (!url.searchParams.get('v')) {
// not a video
return;
}
browser.tabs.sendMessage(history.tabId, {videoChanged: true});
},
{url: [{urlMatches: '^https://www.youtube.com/watch\?'}]}
);
This method messages the content script while on-site navigation or auto play.
Using webRequest.onBeforeRequest or webRequest.onCompleted
YouTube makes a xmlhttrequest when the video changes. You can see the requests by opening the Developer Tools (Ctrl+Shift+I), selecting the Network tab, select XHR, filter by watch? and then let YT switch to the next video. You'll see that two requests occur for the next video, one with a prefetch parameter in the URL shortly before the video changes, and one when the video actually changes without the prefetch parameter.
Needed permissions in the manifest.json
"permissions": [
"https://www.youtube.com/watch?*",
"webRequest"
]
In the background.js
browser.webRequest.onBeforeRequest.addListener(request => {
const url = new URL(request.url);
if (!url.searchParams.get('v') || url.searchParams.get('prefetch')) {
// not a video or it's prefetch
return;
}
browser.tabs.sendMessage(request.tabId, {videoChanged: true});
},
{urls: ['https://www.youtube.com/watch?*'], types: ['xmlhttprequest']}
);
onBeforeRequest might be a bit too fast and send the message to the content script before the new video actually finished loading. In this case you could just replace it with onCompleted.
This method messages the content script while on-site navigation or auto play.

Well the idea is to find a way to periodically check for URL change so the trick I use is to take advantage of the user's need to click on things like the play/pause button and of course on other videos to watch.
So inside your page onload event... (W being your iframe ID)
if(W.contentWindow.document.URL.indexOf('www.youtube.com/watch?v=')>-1){ // You may want to add some more permissable URL types here
W.contentWindow.document.body.addEventListener('click',function(e){ CheckForURLChange(W.contentWindow.document.title,W.contentWindow.document.location); },false);
}
And further down with the rest of your functions...
function CheckForURLChange(Title,URL){
// Your logic to test for URL change and take any required steps
if(StoredURL!==URL){}
}
It's not the best solution but it does work.

Related

How do you detect changes in url with a chrome extension?

I am learning to make chrome extensions. I ran into a problem where context scripts that I want to run, even just alert("test");, are unable to when onload is not activated. This also occurs when you press the back arrow to visit the last visited page. I notice that the url changed, but nothing activates. How do I detect this? If the answer is with service workers, a detailed explanation would be greatly appreciated.
maifest version 2.0
Try using chrome.tabs.onUpdated.addListener((id, change, tab)=>{}). This should run every time the URL changes! Here is a minimalistic example of some code that injects js to a site when the URL changes.
background.js:
// inject code on change
chrome.tabs.onUpdated.addListener((id, change, tab) => {
// inject js file called 'inject.js'
chrome.tabs.executeScript(id, {
file: 'inject.js'
});
});
mainfest version 3.0
You can do it by using chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {}). However, this will actually trigger multiple times when a page URL is changed So you need to add a check for the URL in the changeInfo variable, so it only triggers once!
manifest.json:
{
"name": "URL change detector",
"description": "detect a URL change in a tab, and inject a script to the page!",
"version": "1.0",
"manifest_version": 3,
"permissions": [
"scripting",
"tabs"
],
"host_permissions": [
"http://*/*",
"https://*/*"
],
"background": {
"service_worker": "background.js"
}
}
background.js:
// function that injects code to a specific tab
function injectScript(tabId) {
chrome.scripting.executeScript(
{
target: {tabId: tabId},
files: ['inject.js'],
}
);
}
// adds a listener to tab change
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
// check for a URL in the changeInfo parameter (url is only added when it is changed)
if (changeInfo.url) {
// calls the inject function
injectScript(tabId);
}
});
inject.js:
// you can write the code here that you want to inject
alert('Hello world!');

Detect page changes that'd don't refresh in Chrome Extension Javascript + Clay.js

Background
I am trying to write a Google Chrome extension to detect whenever a user scrolls down on Facebook, and if they successfully load a new set of posts, change the icon for one of the reaction options for all posts. I am using clay.js to detect if the div that contains the Facebook feed has resized, which means more posts have loaded / comments have been posted. This works fine.
Problem
The problem arises when you swap between pages on Facebook without refreshing. For example, if you start on your Home page, this will work fine. However, when you swap to your profile, the script no longer runs, until you refresh the page. Once refreshed, the script works perfectly again. I know I'm missing something about how my file is being loaded, so my question is: how do I run my script on every Facebook page, without having to refresh between each type of page?
Relevant Code (reaction-changer.js)
const fbContentId = "#content"
// on DOM load, watch for future feed scrolling
document.addEventListener('DOMContentLoaded', checkFeedUpdate(), false);
function checkFeedUpdate(){
let currFeed = new Clay(fbContentId)
// resize occurs whenever the user scrolls down or a comment loads
// on a prexisting post
currFeed.on('resize', function() {
switchAllIcons()
});
}
Manifest (some elements omitted for simplicity, notated by ...). change-icons.js is the script that actually changes icons, which will run fine, if the reaction-changer.js script actually runs.
{
...
"version": "1.0",
"manifest_version": 2,
"content_scripts": [
{
"matches": ["https://www.facebook.com/*"],
"js": ["extension/clay.js", "change-icons.js", "reaction-changer.js"],
"all_frames": true
}
],
"web_accessible_resources": [
"img/*.png"
]
...
}
Any help would be greatly appreciated! I've read the Chrome Extension documentation, as well as a bunch of other stack overflow posts, but must have missed a solution somewhere.
Alrighty, I spent the last 2 hours working on this, and I found a solution that I'm happy with for now (albeit not content with -- but what'll ya do). Basically, the big question that I had in my OP was:
how do I run my script on every Facebook page, without having to refresh between each type of page?
Well, what I realized is that, yes, refreshing is the solution. So... what if we force a refresh on Facebook's end, allowing the DOM to refresh, and the code to run as expected? I believe that this PROBABLY is actually an underlying issue with how the Clay.js library I'm using is implemented. Anyway, I basically approached the solution by:
First, creating a background.js file that takes advantage of chrome.tabs.onUpdated.addListener -- this function basically let me detect if a tab changed or if the page status was "completed" indicating it has loaded.
If it loaded, then I run the function checkFeedUpdate() exactly as above.
If it changed to a new page (e.g., user clicked from Home to Profile), I force a reload, and then wait for point 2 above to fire.
'background.jsis detecting whether or not these states have happened yet, and relaying the information toreaction-changer.js`.
Here's the updated bit of reaction-changer.js (in place of document.addEventListener):
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
// listen for messages sent from background.js
if (request.message === 'reload') {
location.reload();
} else if (request.message === 'start'){
checkFeedUpdate()
}
});
Here's the updated manifest:
"background": {
"scripts": ["background.js"]
},
"content_scripts": [
{
"matches": ["https://www.facebook.com/*"],
"js": ["extension/clay.js", "change-icons.js", "reaction-changer.js"],
"all_frames": true
}
],
"permissions": [
"tabs"
],
"web_accessible_resources": [
"img/*.png"
]
And here's what I created for background.js:
chrome.tabs.onUpdated.addListener(
function(tabId, changeInfo) {
// read changeInfo data and do something with it
if (changeInfo.url) {
chrome.tabs.sendMessage( tabId, {
message: 'reload'
})
} else if (changeInfo.status === 'complete'){
chrome.tabs.sendMessage( tabId, {
message: 'start'
})
}
}
);
If anyone ends up facing a similar issue (it seems like refreshing does the trick, but you can't get it to work without refreshing), it seems that just forcing a refresh might be a good solution. If there's a better one, please let me know!

Cannot run my chrome extension in other broswer/machine except mine : some mismatch in unpacked distribution?

Sorry for my poor English, i hope you can understand the issue.
I'm new to chrome extension development,and for sure in my code there are a lot of
thing to change or optimize;
anyway i've written a simple code that, (seems) works at least from my chrome.
The code clicks a button every X minutes in specific page, then wait and parse the result in page.
I've :
a content script (loaded from manifest.json) which "inject" some button and text Input box in page, so user can sets some "filter params" before click a "start button"; the start button then sendMessage() to background.js to set Alarm Event for the click ;
an eventPage (which is set persistent true in actually ) which handle the request from tabs and set a countdown alarm for each tab; when X min are passed fire a message to the interested tab;
I also have a popup.html e popup.js not important here (i think).
I've to distribuite this extension manually, so i would distribuite a zip that user can load with "developer mode ".
*Now the issue is: why the code was working only on my Chrome ? *
I've tested with others 2-3 laptop with Chrome, the background script is loaded (i can see the background page printint console log)
but in webpage the contents.js seems no way executed .
In my chrome works well: i can see in console some initial output (i print the name of dir extension to check) and
the dynamic created element (button,input box ect.) in page.
And all is working, i can fire the start button and receive results of parsing.
During the development i've never run the extension on other machine. Yesterday i've succssfully tested on 2-3 laptop.. then i made only few change but nothing serious.
Today i can run only in my chrome.
In other pc nothing, neither the simple console.log output first line of script.
I can read in console log :
"Unchecked runtime.lastError: Could not establish connection. Receiving end does not exist."
but this also in my working istance in my laptop chrome .
The zip file is the same and the extraction is good, in fact i can actually load the extension and i see the background page debug console.log() sentences
In some case, in laptop where it dosen't work, i've received a message relative jQuery and the fact that chrome.runtime.sendMessage() is not defined; and it points to code in webpage, not mine.
I've see that in webpage code there is something like:
var extid = "mcmhdskbnejjjdjsdkksmeadjaibo";
var extVer = "1.5";
var extStatus = 0;
$(document).ready(function () {
///...
chrome.runtime.sendMessage(extid, {message: "version"},
function (reply) {
if (reply) {
if (reply.version) {
if (reply.version == extVer) {
if (reply.gminfo != 'OK') {
extStatus = 1; /// ...
Seems that chrome.runtime is undefined, and the webpage can't call the sendMessage().
EDIT: this undefined occurs only when my extension is loaded
Maybe there is some conflict when i load my extension? But in my chrome browser works...
Can some expert indicate in where direction i've to investigate?
Thanks a lot for any suggestions.
My Manifest.json :
{"manifest_version": 2,
"name": "myAlarm",
"description": "This extension alerts.",
"version": "0.1",
"permissions": [
"alarms",
"system.cpu",
"storage",
"tabs",
"webNavigation",
"https://www.mytargetsite.com/subUrl/"
],
"web_accessible_resources": [
"icon.png",
"vanillaSelectBox.css"],
"content_scripts": [
{
"matches": ["https://www.mytargetsite.com/subUrl/"],
"css": ["vanillaSelectBox.css"],
"js": ["jquery-3.3.1.min.js","vanillaSelectBox.js","taffy-min.js","content.js"],
"run_at": "document_end"
}
],
"background": {
"scripts": ["eventPage.js"],
"persistent": true
},
"browser_action": {
"default_icon": "icon.png",
"default_popup": "popup.html"
},
"icons": {
....
}
}
My contents,js (stripped):
chrome.runtime.onMessage.addListener(
function(request, sender) {
// here i parse message "time'up" from background js
});
window.addEventListener('load', function() {
var pt=chrome.runtime.getURL('filterOff.wav');
var p=pt.split("/");
console.log("[myAlarm v0.1] started" );
console.log("[myAlarm v0.1] folder : ("+p[2]+")");
// here i start an active wait for the presence in page of button with ID= btntarget_id
waitForElementToDisplay("#btntarget_id", 500); //when function find button then create and add button and input text to webpage
});
My eventPage.js :
var curr_alarms =[];
chrome.extension.onMessage.addListener(function(request, sender)
{ /// here receive start countdown message from content.js and set alarm ...
}
chrome.alarms.onAlarm.addListener(function(alarm) {
/// here i manage each alarm for each tab
});
chrome.tabs.onRemoved.addListener(function(tabid, removed) {
// ...
});
chrome.tabs.onUpdated.addListener(function
(tabId, changeInfo, tab) {
//
});
edit : in browser where it dosen't work i can read also :
Access to XMLHttpRequest at 'https://mytargetsite.com/suburl/grid.php' (redirected from 'https://mytargetsite.com/suburl/grid.php') from origin 'https://mytargetsite.com' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: Redirect is not allowed for a preflight request.
The fact that the declared content script runs or not, should be verified by inspecting in devtools => sources sub-tab => content scripts sub-sub-tab. If it really doesn't run, there can be just two explanations: the URL is different (for example not https) or extensions are blocked by their domain admin via runtime_blocked_hosts which you can see in chrome://policy.
Your development mode extension's id will be different on a different machine unless you pin it by adding a "key" in manifest.json
To use chrome.runtime to send messages to your extension from a webpage code (not from a content script!) your extension's manifest should declare "externally_connectable" and use a different event onMessageExternal, see also sending messages from web pages.
The CORS error may be irrelevant to your code (you can investigate the source of the error by expanding the error's call stack in devtools console).

Can't successfully run executeScript from the background script unless I load the popup page/script first

I've trying to run execute script from my background script using keyboard shortcuts, it doesn't work and returns:
Error: No window matching {"matchesHost":[]}
But if I just open the popup page, close it, and do the same, everything works.
I've recreated the problem in using the Beastify example with minimal changes. Here's the code:
manifest.json
{
... (not interesting part, same as in beastify)
"permissions": [
"activeTab"
],
"browser_action": {
"default_icon": "icons/beasts-32.png",
"default_title": "Beastify",
"default_popup": "popup/choose_beast.html"
},
"web_accessible_resources": [
"beasts/frog.jpg",
"beasts/turtle.jpg",
"beasts/snake.jpg"
],
My additions start here:
"background": {
"scripts": ["background_scripts/background_script.js"]
},
"commands": {
"run_content_test": {
"suggested_key": {
"default": "Alt+Shift+W"
}
}
}
}
popup/choose_beast.js (same as in original)
/*
Given the name of a beast, get the URL to the corresponding image.
*/
function beastNameToURL(beastName) {
switch (beastName) {
case "Frog":
return browser.extension.getURL("beasts/frog.jpg");
case "Snake":
return browser.extension.getURL("beasts/snake.jpg");
case "Turtle":
return browser.extension.getURL("beasts/turtle.jpg");
}
}
/*
Listen for clicks in the popup.
If the click is on one of the beasts:
Inject the "beastify.js" content script in the active tab.
Then get the active tab and send "beastify.js" a message
containing the URL to the chosen beast's image.
If it's on a button wich contains class "clear":
Reload the page.
Close the popup. This is needed, as the content script malfunctions after page reloads.
*/
document.addEventListener("click", (e) => {
if (e.target.classList.contains("beast")) {
var chosenBeast = e.target.textContent;
var chosenBeastURL = beastNameToURL(chosenBeast);
browser.tabs.executeScript(null, {
file: "/content_scripts/beastify.js"
});
var gettingActiveTab = browser.tabs.query({active: true, currentWindow: true});
gettingActiveTab.then((tabs) => {
browser.tabs.sendMessage(tabs[0].id, {beastURL: chosenBeastURL});
});
}
else if (e.target.classList.contains("clear")) {
browser.tabs.reload();
window.close();
return;
}
});
background_scripts/background_script.js (added by me)
browser.commands.onCommand.addListener(function(command) {
var executing = browser.tabs.executeScript(
null,
{file: "/content_scripts/content_test.js"});
executing.then(
function (res){
console.log("started content_test.js: " + res);
},
function (err){
console.log("haven't started, error: " + err);
});
});
content_scripts/content_test.js (added by me)
alert("0");
I'm skipping the whole content_scripts/beastify.js cause it has nothing to do with it (IMO), but it can be found here.
Now, I know that the background script runs and receives the messages even when the popup page hasn't been opened before, because I see it failing executing the script. I have no idea what causes this behavior and if there's a way to fix it.
Note: I tried adding permissions such as "tabs" and even "all_urls", but it didn't change anything.
Note 2: I'm running the add-on as a temporary add-on from the about:debugging page, but I'm trying to execute the script on a normal non-restricted page (on this page for example I can recreate the problem).
Thanks a lot guys!
// in manifest.json
"permissions": [
"<all_urls>",
"activeTab"
],
DOES work for me (Firefox 50, Mac OS X 10.11.6).
I had gotten the exact same error message you described when I had used the original
"permissions": [
"activeTab"
],
So the addition of "<all_urls>" seems to fix the problem. However, you said that you were still experiencing the issue when you included "all_urls" in your permissions, so I am not sure whether the way I did it fixes the issue in your own setup.
edit: Whether giving any webextension such broad permissions would be wise in terms of the security risks it might pose is a separate, important consideration, I would imagine.
(I would have posted this as a comment, but I don't have enough reputation yet to be able to add comments.)

Chrome Extension Page Action not showing next to omnibar

Manifest:
{
"manifest_version":2,
"name":"Optimize url",
"description":"Optimize url",
"page_action":{
"default_icon":{
"19":"url-icon16.png",
"38":"url-icon48.png"
},
"default_title":"Optimize url"
},
"background":{
"scripts":["background.js"]
},
"version":"0.1",
"permissions":[
"tabs",
"https://url.com/*"
]
}
Background JS:
function checkURL(){
var host = parseURL(tab.url).host;
if (host.indexOf("url.com") >= 0) {
chrome.pageAction.show(tabId);
}
}
chrome.tabs.onUpdated.addListener(checkURL);
Yet when I add it to the developing Extensions page. It doesn't show up anywhere. I was originally going to have this as a browser action but it made more since to use it as a page action since it's only going to be focused for one website only.
Can anyone explain to me what I may be doing wrong?
There are the following problems with your code:
The variable tab, which is used in checkURL, is nowhere defined.
The function parseURL is also nowhere defined (it is not a built-in function as you seem to assume).
It is, also, a good idea to filter the onUpdated events looking for status: 'complete', because several onUpdated events are triggered during a single tab update.
So, replace your background.js code with the following:
var hostRegex = /^[^:]+:\/\/[^\/]*url.com/i;
function checkURL(tabId, info, tab) {
if (info.status === "complete") {
if (hostRegex.test(tab.url)) {
chrome.pageAction.show(tabId);
}
}
}
chrome.tabs.onUpdated.addListener(checkURL);

Categories

Resources