28

I'm making a simple Chrome extension to add up the length of each video in a YouTube playlist and insert the total length in the page. I've succeeded at that, but my script only works after refreshing a page but not when the site is navigated. That's not very convenient though.

Is it possible to detect page navigation on YouTube and insert HTML into the page before it's rendered in the browser so that the added content is shown immediately without any delay and no page refresh is required?

Example link: https://www.youtube.com/playlist?list=PL_8APVyhfhpdgMJ3J80YQxWBMUhbwXw8B

P.S. My question is not the same as Modify elements immediately after they are displayed (not after page completely loads) in greasemonkey script? because I've tried MutationObserver and the problem is the same - a refresh is needed to show changes to the web page:

var observer = new MutationObserver(function(mutations) {
    for (var i=0; i<mutations.length; i++) {
        var mutationAddedNodes = mutations[i].addedNodes;
        for (var j=0; j<mutationAddedNodes.length; j++) {
            var node = mutationAddedNodes[j];
            if (node.classList && node.classList.contains("timestamp")) {
                var videoLength = node.firstElementChild.innerText;
                observer.disconnect();    

                var lengthNode = document.createElement("li");
                var lengthNodeText = document.createTextNode(videoLength);
                lengthNode.appendChild(lengthNodeText);
                document.getElementsByClassName("pl-header-details")[0].appendChild(lengthNode);

                return;
            }
        }
    }
});
observer.observe(document, {childList: true, subtree: true});
wOxxOm
  • 65,848
  • 11
  • 132
  • 136
Lauren
  • 883
  • 2
  • 9
  • 7
  • Hmm, I tried that but it doesn't work for my purpose. The part of the page I want to insert the combined video length into is already loaded by the time the timestamp for each video is loaded so I have the same problem, the page needs to be refreshed to show the injected HTML. Updated my question since it was marked as a duplicate. – Lauren Dec 04 '15 at 23:58
  • Just tried that and no, that didn't fix it. A refresh is still required. – Lauren Dec 05 '15 at 01:17
  • Thank you for trying to help so far. Here is the script, with the manifest at the bottom as well: http://pastie.org/private/yyc7piosnsna4yq7oymia – Lauren Dec 05 '15 at 01:37

2 Answers2

55

YouTube site doesn't reload pages on navigation, it replaces the history state.

The extension's content scripts aren't reinjected when URL changes without a page being reloaded. Naturally when you reload a page manually the content script is executed.

There are several methods to detect page transitions on Youtube site:

  • using a background page (or MV3 service worker) script: webNavigation API, tabs API

  • using a content script and navigatesuccess event in modern Chrome

  • using a content script and site's own event for video navigation

    So, to find it, let's run getEventListeners(document) in devtools console:

    enter image description here

    • yt-navigate-start is what we need in this case.
    • yt-navigate-finish may be better for other cases.

manifest.json:

{
  "name": "YouTube Playlist Length",
  "version": "0.0.1",
  "manifest_version": 2,
  "description": ".............",
  "content_scripts": [{
      "matches": [ "*://*.youtube.com/*" ],
      "js": [ "content.js" ],
      "run_at": "document_start"
  }]
}

Note: the matches key encompasses the entire youtube.com domain so that the content script runs when the user first opens the home youtube page then navigates to a watch page.

content.js:

document.addEventListener('yt-navigate-start', process);
// Choose a different event depending on when you want to apply the change
// document.addEventListener('yt-navigate-finish', process);

if (document.body) process();
else document.addEventListener('DOMContentLoaded', process);

The process function will alter the page.
Note, the specified element classes and structure will change in the future.

function process() {
  if (!location.pathname.startsWith('/playlist')) {
    return;
  }
  var seconds = [].reduce.call(
    document.getElementsByClassName('timestamp'),
    function (sum, ts) {
      var minsec = ts.textContent.split(':');
      return sum + minsec[0] * 60 + minsec[1] * 1;
    },
    0,
  );
  if (!seconds) {
    console.warn('Got no timestamps. Empty playlist?');
    return;
  }
  var timeHMS = new Date(seconds * 1000).toUTCString().split(' ')[4]
    .replace(/^[0:]+/, ''); // trim leading zeroes
  document.querySelector('.pl-header-details')
    .insertAdjacentHTML('beforeend', '<li>Length: ' + timeHMS + '</li>');
}
wOxxOm
  • 65,848
  • 11
  • 132
  • 136
  • For reference, here is an example of the background script approach: http://stackoverflow.com/a/21071357/72321 – Dennis Jul 31 '16 at 22:25
  • 1
    This solution works fine, but unfortunately only for YouTube old design. – Oleg Popov May 30 '17 at 17:27
  • There's a new `yt-navigate-start` event on the site shown to some users. Also, I've slightly generalized the answer to explain how one can find the actual event. – wOxxOm Jul 24 '17 at 11:10
  • 1
    Are you sure they do use `pushState` ? I had a really quick look a few days ago because of a question (I shall find it back to VTC as dupe of this one btw), but overwritting `history.pushState` and `history.replaceState`, in order to include an event emitter on these methods didn't worked at all, and this even if I did overwrote before the page had loaded. – Kaiido Jul 24 '17 at 11:19
  • 1
    @Kaiido, yes, they simply use the hack to restore the original API bindings via an iframe: https://puu.sh/wRIZg/6a3ad30828.png – wOxxOm Jul 24 '17 at 11:26
  • Wow clever, I never saw (nor though of course) of such an hack. Do you know if this would even work if the overwritings were done on the History proto itself ? – Kaiido Jul 24 '17 at 11:30
  • Yes, this hack would work if you create a new iframe because it'll use the original browser bindings. – wOxxOm Jul 24 '17 at 11:32
  • Great answer, any chance to use event listener approach for mobile version of YouTube (as it seems there are no beforementioned listeners available)? – Johncze Mar 10 '18 at 15:31
19

2017 answer:

I use this for new Material Design version Youtube

body.addEventListener("yt-navigate-finish", function(event) {
    // code here
});

and this for old Youtube

window.addEventListener("spfdone", function(e) {
    // code here
});

code come from 2 script I wrote call
"Youtube subtitle downloader" and "Youtube auto subtitle downloader".

both of them work, I tested.

if you are interested in my script and want know more detail:
https://github.com/1c7/Youtube-Auto-Subtitle-Download

NamNamNam
  • 1,190
  • 3
  • 13
  • 22
  • Of note, this information _is_ covered by the existing answer (though it uses `yt-navigate-start`) – Xan Aug 30 '17 at 09:27
  • 1
    yes, this one didn't talk about principle but give out code right the way, I think this answer still have some value, so I would still keep this answer. :D thanks for mention it @Xan – NamNamNam Aug 30 '17 at 10:57