/* BeejBløg */

Aug 13, 2016 - 5 minute read - Comments - Uncategorized

[Solved] Greasemonkey/Tampermonkey jQuery sideload and setInterval

i was having a heck of a time keeping a reliable handle on jQuery in the Pandora page… it would be there upon initial Greasemonkey script execution but then upon subsequent setInterval executions, the jQuery global variable was undefined… fascinating…

notable: as i was debugging, i started to see that Chrome was cycling through four ( 4 ! ) different VMxxxx “copies” of the greasemonkey script upon each setInterval execution… questions like why? and why 4? abound if anyone cares to enlighten me

so it struck me that i just need to make sure jQuery is available in each one of those “sessions”…

noteable: the “sideload” is accomplished via jQuery’s native “noConflict” facility… this post explains how it works… the gist is that each load of jQuery does indeed replace “$” BUT it also saves the previous into _$, such that $.noConflict can restore “$” to the previous version… this is what allows Pandora’s copy of jQuery to remain as-is… crucial in this case because Pandora depends on additional add-ons that it loads as expando properties on its instance of jQuery.

after that was in the bag, i couldn’t help dwelling on what else might be possible and had another aha moment… from tracing the pandora js execution i learned that there were pretty obvious variables getting set for allowed features (e.g. “allowSkipTrackWithoutLimit”)… i banged around quite a bit trying to replace the main pandora.js script with one where those values were tweaked… blocking the original script via AdBlockPlus was easy as well as loading the tweaked pandora.js inline <script> but that approach ran aground on not being able to load other dependency scripts in proper sequence with the replacement… Chrome doesn’t implement the crucial window.beforescriptexecute event which would probably make this feasible… the main pandora.js is wrappered in a self contained function call so we can’t monkey patch its innards…

but then it struck me, jQuery is global… and what if they’re getting these values via jQuery.ajax… such that i could override and tweak… sure enough, that approach panned all the way out!

update – after that last round, i realized the whole thing about sideloading jQuery was unnecessary, i just needed to use the inline script approach to make sure my code executed on the page context vs whatever weird context TamperMonkey normally does… so the following script now reflects the cleaner evolved approach

  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
// ==UserScript==
// @name          Pandora - "still listening" click
// @author        Brent Anderson
// @homepage      /2016/08/solved-greasemonkey-jquery-sideload-and-setinterval.html
// @match         https://www.pandora.com/*
// @grants        none
// @run-at        document-end
// ==/UserScript==

function recurringTweaks() {
  //this click, remove, click sequence skips embedded video ads and gets the tunes playing again
  var stillListeningButton = $("#still_listening_ignore");
  if (stillListeningButton.is(":visible")) {
    stillListeningButton.click();
    $("#videoPlayerContainer").remove();
    stillListeningButton.click();
    $(".playButton").click();
    //above brute force video ad skip leaves player controls disabled, this resolves that side effect
    $(".disabled").removeClass("disabled");
  }

  var adContainer = $("#ad_container");
  if (adContainer.length) {
    //remove right side ad section...
    $("#ad_container").remove();
    //and allow the album covers area to fill the space
    $(".contentContainer").css("width", "100%");
    $("#adLayout").css("width", "80%");

    //remove some other "upgrade" bits
    $(".registeredUser").remove();
    $("#rightColumnDivider").remove();
    $(".audioAdInfo").remove();
  }
}


// monkey patch jQuery.ajax so we can override some nice stuff =)
var hijax = function() {
  if (typeof $ !== 'undefined') {
    var oldAjax = $.ajax;
    var newAjax = function(a, b) {
      var oldSuccess = a.success;
      a.success = function(data, textStatus, jqHXR) {

        // infinite skip! =)
        $(data).find('name:contains(allowSkipTrackWithoutLimit) + value > boolean').replaceWith('<boolean>1</boolean>');

        //auto skip ads
        if (a.url.indexOf("method=registerImpression") !== -1) {
          $(".skipButton a").click();
        }

        //debug: console.log('url: ' + a.url + ', data: '+(''+data === '[object XMLDocument]' ? data.children[0].innerHTML : data));
        oldSuccess(data, textStatus, jqHXR);
      };
      oldAjax(a, b);
    };
    $.ajax = newAjax;

    setInterval(recurringTweaks, 2000);

  }

};

// load <script> inline to the page so it has access to jQuery "$" global vs TamperMonkey's alternative context
if (!document.getElementById("hijax")) {
  var hijaxScript = document.createElement("script");
  hijaxScript.setAttribute("id", "hijax");
  hijaxScript.innerHTML = recurringTweaks.toString() + "\r\n" + hijax.toString().replace(/^function.*{|}$/g, "");
  document.head.appendChild(hijaxScript);
}



///////////////////////////////////////////////////////////////////////////////////////////////////////
//sorry, turning this post into a catch all for stuff that might come in handy elsewhere
/*

//the original jquery "sideload" code
function loadJq() {
    if (!window.jq) {
        script = document.createElement("script");
        script.src = "https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.1/jquery.min.js";
        script.onload = function() { window.jq = $.noConflict(true); cosmetics(); };
        document.getElementsByTagName("head")[0].appendChild(script);
    }
    else cosmetics();
}

    //helpful: https://userscripts-mirror.org/scripts/show/125936

  window.addEventListener('beforescriptexecute', function(e) {
      if (e.target.src.indexOf("pandora.js") != -1) {
          e.preventDefault();
          //e.stopPropagation(); //??
          e.target.src = ''; //??
          e.target.innerHTML = "patched script";
      }
  }, true);

var a = document.getElementsByTagName("script");
for each (var e in a) {
  if (!e) continue; // oddly, this does sometimes grab null elements.
  var b = e.getAttribute("src");
  if (b && b.indexOf("pandora.js") != -1) {
    e.parentNode.removeChild(e);
    debugger;
    break;
  }
}
*/

// @grants         GM_xmlhttpRequest
/*GM_xmlhttpRequest({
  method: "GET",
  url: "https://rawgit.com/Beej126/567a36f2dd1e3ce613ad8ec5846a40d4/raw/fac20b4ab17681b5da41b07c2549676ff3571fc9/dorPanda.js", //"https://www.pandora.com/pandora.js?v=440211416",
  onload: function(response) {
    debugger;

    //here's the beef!
    //var tweaked = response.responseText.replace("this.PC=b.allowSkipTrackWithoutLimit", "this.PC = true;");
    //$("script[src*='/pandora.js'").af

    var tweaked = response.responseText;
    document.head.appendChild(document.createElement('script')).innerHTML = tweaked;
  }
});*

*/

starting the same hijinx for Spotify… they load MooTools into $ and for some reason the selector wasn’t finding obvious classes… i’ve never picked up MooTools so maybe the syntax is different than jQuery… so i just went back to the jQuery sideload approach on this one… after that, worked it down into pure DOM, no jQuery needed

 1 2 3 4 5 6 7 8 91011121314151617181920212223242526272829303132333435363738
// ==UserScript==
// @name          Spotify tweaks
// @author        Brent Anderson
// homepage      /2016/08/solved-greasemonkey-jquery-sideload-and-setinterval.html
// @match         https://play.spotify.com/*
// @grants        none
// @run-at        document-end
// ==/UserScript==

function terminator() {
  var target = document.getElementsByClassName("ads-leaderboard-container");
  if (target.length) {
    console.log("bye bye =)");
    target[0].remove();
    clearInterval(timerId); //kill the timer once the targeted element finally shows up
  }
}

//replace main.js with hacked version
//(block original with AdBlockPlus plugin)
//was easy to enable "next" button during ads but it sticks to the ad anyway, would take further effort and not worth it until they actually fire enough ads to be annoying
var scripts = document.getElementsByTagName("script");
for(var i = 0; i<scripts.length; i++) { if(scripts[i].src.indexOf("https://play.spotify.edgekey.net/apps/player/4.2.0/main.js") != -1) {
  //debugger;
  var mainjs = document.createElement("script");
  mainjs.crossorigin = "anonymous";
  mainjs.src = "https://rawgit.com/Beej126/1501d5acb4fd20a6fcdcfe6599ce0c5e/raw/2725727f297a00444ef51c490a6009458a513e07/SpotifyMain.js";
  document.body.appendChild(mainjs);
  break;
}}

//there were multiple iframes, targeting the one that actually gets the ads
if (document.body && document.body.classList.length && document.body.classList[0] === "non-mobile" && document.body.attributes.length === 1) {
  //setup a recurring check to see when ads get dynamically inserted into page
  var script = document.createElement("script");
  script.innerHTML = terminator.toString() + "\r\n" + "var timerId = setInterval(terminator, 2000);";
  document.head.appendChild(script);
}