Greasy Fork is available in English.

HTML5 thingie for Spotify Web Player

Play music on the browser without having to install Flash Player. Yes, I know why SWF bridges exist.

// ==UserScript==
// @name        HTML5 thingie for Spotify Web Player
// @description Play music on the browser without having to install Flash Player. Yes, I know why SWF bridges exist.
// @author      Swyter
// @namespace   https://greasyfork.org/users/4813-swyter
// @match       https://play.spotify.com/*
// @version     2016.01.30
// /* turns out we now need to run on frames too to make our flashless context menu copying mechanism work */ @noframes
// @icon        https://i.imgur.com/LHkCkka.png
// @grant       none
// @run-at      document-start
// ==/UserScript==

  /* it's random, trust me, I'm Sony! :) */
  Object.defineProperty(window.Math, 'random',
  {
    configurable: false,
    writable: false,
    value: function(a) { return 0.1337; }
  });

  /* ZeroClipboard.js HTML5 shim, no ridiculous SWF overlay required, sneaky! */
  Object.defineProperty(window, 'ZeroClipboard',
  {
    configurable: false,
    writable: false,
    value: function(e)
    {
      console.log("ZC constructor", arguments);

      e.addEventListener('click', function (x)
      {
        x.preventDefault();

        /* add a dummy input to our page, fill it with our text, select it,
           copy it, remove our dummy item; voila, typical js kludge! */
        input = document.createElement('input');
        input.value = this.dataset.clipboardText;

        document.body.appendChild(input);

        input.select(); document.execCommand('copy');

        document.body.removeChild(input);

        console.log("ZC -> click", this, this.dataset.clipboardText, input, input.value);
      });

      return {
        on: function(e, f)
        {
          console.log("ZC -> on", arguments, this, this.elem);
          this.elem.addEventListener(e, f);
        },

        elem: e
      };
    }
  });

  ZeroClipboard.config = function(){ console.log("ZC -> config", arguments) };


  /* SWFobject.js faker, sneaky! */
  Object.defineProperty(window, 'swfobject',
  {
    configurable: false,
    writable: false,
    value:
    {
      getFlashPlayerVersion: function(){ console.log("-<-< getFlashPlayerVersion =>", arguments); return { major: 11, minor: 2, release: 202 }; },
      hasFlashPlayerVersion: function(){ console.log("-<-< hasFlashPlayerVersion =>", arguments); return true; },
      embedSWF:              function(swf, parent_id, unk_a, unk_b, req_flash_ver, unk_c, properties)
      {
        console.log("-<-< embedSWF =>", arguments, arguments[9].toString(), this, properties);

        /* create our audio player in substitution of the swf abobination */
        var dummy = document.createElement("audio");

        dummy.id             = properties.id;
        dummy["instanceid"]  = properties.instanceId;

        dummy.loop           = false;
        dummy.preload        = 'auto';
        dummy.autoplay       = false;
        dummy.volume         = 1.0;

        dummy.sp_run         = function(proof) { console.log("-<-< sp_run =>", arguments); return unsafeWindow.proof; };
        dummy.sp_hasSound    = function()      { return true };
        dummy.sp_load        = function(player_id, raw_uri, options)
        {
          /* for some reason we have to massage the url format to remove the 'mp3:' protocol
             prefix from the uri, and the '/cfx/st' from the server string */
          // uri :                                          "mp3:/mp3/6b381db769d31beb544ba67eb7cbc3ce4fc8ab4c.mp3?Expires=1450045402&Signature=dgvyYa~K2P-v6ArrdVBmRxAF44JTJhpk6PJqQXzHbMOmtcHw~eY~E1C0GgviL~O63-EhejMzCB~dLjlgaug-TQej8mCjvroY8crd776GRsBx0AJz4pnp3ZH03T3PnUecBHRwMrg28pjAbi1xWmuybyNvwWpitB9Q~hiCKxMzUhnXRjqpWJKZVrLDY7~iXB2GlptZNz8RZoapexeEkNA2kgjnYXk4JTe4CNTdRSmn~Uf9YHvxJdA4ttlRfDt353eSxCDTXKQdA7GkEBTKJfDvN6NgXyw8~Tm8tBHk9VYYn7jZMLYckwqi3OJAonof2SZZlHZoepgympEYxK8BdkvZMg__&Key-Pair-Id=APKAJXKSII4ED2EOGZZA"
          // options.server: "http://dsu0uct5x2puz.cloudfront.net/cfx/st"

          var uri = options.server.match(/(.+\/\/[^\/]+?)\//)[1] + raw_uri.split(":")[1];

          console.log("sp_load =>", uri, arguments, this);
          this.src = uri;

          if (options.startFrom !== 0)
            this.currentTime = Math.floor(0.001 * options.startFrom);

          if (options.autoplay)
            this.play();

          console.log(this.paused)

          unsafeWindow.Spotify.Instances.get(this.instanceid).audioManager.getPlayerById(player_id).trigger('LOAD', {}, {id: player_id});
        };

        /* dummy functions after __noSuchMethod__ was deprecated in Firefox 44 */
        dummy.sp_initializePlayerById = function(player_id)      { console.log("=> sp_initializePlayerById",  arguments); };
        dummy.sp_stop                 = function(player_id)      { console.log("=> sp_stop",                  arguments); };

        /* actual reimplementations of functions that embody the internal SWF interface */
        dummy.sp_setVolume   = function(player_id, vol) { console.log("=> volume", arguments); if (player_id == "main:A" && vol !== 0) this.volume = vol };
        dummy.sp_getVolume   = function(player_id, vol) { console.log("=> golume", arguments); if (player_id == "main:A" && vol !== 0) return parseFloat(this.volume); else return 0 };
        dummy.sp_seek        = function(player_id, pos) { console.log("=> seek",   arguments); this.currentTime = Math.floor(0.001 * pos) };
        dummy.sp_pause       = function(player_id)      { console.log("=> pause",  arguments); this.pause() };
        dummy.sp_resume      = function(player_id)      { console.log("=> pause",  arguments); this.play() };

        dummy.sp_playerState = function(player_id)
        {
          return {
            volume: this.volume,
            position: Math.floor(1000 * this.currentTime),
            duration: Math.floor(1000 * this.duration),
            isPlaying: !this.paused,
            isStopped: false,
            isPaused: this.paused
          };
        };

        dummy.sp_addPlayer   = function(player_index, player_id, player_protocol)
        {
          if (player_id != "main:A")
            return;

          var sp_html5_event_listeners =
          {
                   canplay: 'LOAD',
                   playing: 'PLAYING',
                     pause: 'PAUSED',
                     ended: 'TRACK_ENDED',
            durationchange: 'DURATION',
                  progress: 'PROGRESS',
                     error: 'PLAYBACK_FAILED'
          };

          function sp_html5_generic_callback(e)
          {
            console.log("sp html5 audio «" + e.type + "» event", e, sp_html5_event_listeners[e.type]);

            var ret;

            switch(sp_html5_event_listeners[e.type])
            {
              case 'DURATION':
                ret = {duration: Math.floor(1000 * this.duration)};
                break;

              case 'PROGRESS':
                ret = {position: Math.floor(1000 * this.currentTime)};
                break;

              default:
                ret = {};
            }

            unsafeWindow.Spotify.Instances.get(this.instanceid).audioManager.getPlayerById(player_id).trigger(sp_html5_event_listeners[e.type], ret, {id: player_id});
          }

          for (var i_event in sp_html5_event_listeners)
            this.addEventListener(i_event, sp_html5_generic_callback);

          return true;
        };

        dummy.__noSuchMethod__  = function(name, params) { console.log('==>>== > invalid function call', name, params); };

        /* necessary for the spotify framework to find it in the right place */
        window.document[properties.id] = dummy;

        /* insert our dummy audio player in the requested element */
        document.getElementById(parent_id).appendChild(dummy);

        /* tell the spotify framework that the swf embed is ready */
        arguments[9].apply(this, [{success: true}]);

        // act as the swf bridge and tell it that the Flash-backend is ready
        // JSInterface.notify(ApplicationEvents.READY,null,1);
        //Spotify.Instances.get(properties.instanceId).audioManager.getInterface()._triggerDeferred('FLASH_AVAILABLE', null);
        Spotify.Instances.get(properties.instanceId).audioManager.getInterface()._triggerDeferred('READY', null);

        console.log("AUDIOMANAGER-INTERFACE", properties.id, window.document[properties.id], Spotify.Instances.get(properties.instanceId).audioManager.getInterface(),
                    Spotify.Instances.get(properties.instanceId).audioManager.getInterface().hasSound());
      }
    }
  });


function when_external_loaded()
{
// ---


 function get_pong(ping)
 {
   // http://crossorigin.me/http://ping-pong.spotify.nodestuff.net/64-104-120-204-164-75-214-221-224-109-28-127-73-236-239-150-88-238-177-90

   console.log("ping-pong", ping);

   var xhr = new XMLHttpRequest();
   xhr.open("GET", "https://crossorigin.me/http://ping-pong.spotify.nodestuff.net/" + ping.replace(/ /g,"-"), true);
   xhr.responseType = "json";

   xhr.onloadend = function()
   {
     if (xhr.readyState != xhr.DONE)
       return;

     console.log("pong", xhr);
     window.proof = xhr.response.pong.replace(/-/g," ");

     if (!sp_ws)
       return;

     sp_ws.send(`{"id":` + (window.proof_id || 2) + `,"name":"sp/pong_flash2","args":["` + window.proof + `"]}`);
     //sp_ws.send(`{"id":2,"name":"sp/work_done","args":["undefined"]}`);

   }

   xhr.send();
 }

/* wait until the page is ready for the code snippet to run */
document.addEventListener('DOMContentLoaded', function()
{
  console.log("!!! DOMContentLoaded");

  WebSocket.prototype.sond = WebSocket.prototype.send;
  WebSocket.prototype.send = function(msg)
  {
    window.sp_ws = this;

    if (this.onmessage && this.sucks !== true)
    {
      callback = this.onmessage;

      console.log("orig prev callback:", callback);

      this.onmessage = function(message)
      {
        var json_msg = JSON.parse(message.data);

        if (json_msg && json_msg.message && json_msg.message[0] == 'ping_flash2' && json_msg.message[1])
        {
          console.log("getting preventive pong", json_msg);
          get_pong(json_msg.message[1]);
        }

        //if (json_msg.id === window.last_track_msg)
          console.info("<- ws recv: ", window.last_track_msg, message.data);

        //if (json_msg && json_msg.result && json_msg.result.uri)
          //open(json_msg.result.uri);

        callback(message);
      }

      this.sucks = true;
    }

    var json_msg = JSON.parse(msg);

    // block the flash pong reply until we have the correct reply for the challenge
    if (typeof window.proof !== "string" && json_msg && (json_msg.name == 'sp/pong_flash2')) // || json_msg.name == 'sp/work_done'))
    {
      console.info("-> blocking sent message until we have challenge: ",  json_msg.id, msg);

      window.proof_id = json_msg.id;

      return;
    }

    // get the http link instead or rtmp, thankies!
    if (json_msg && json_msg.name == 'sp/track_uri')
    {
      arguments[0] = msg.replace(',"rtmp"', '');
      window.last_track_msg = json_msg.id;
      console.info("-> ws send: ",  json_msg.id, msg);
    }
    //dec_msg = json_msg.name === 'sp/hm_b64' ? atob(json_msg.args[0]) : null;

    console.info("-> ws send: ", msg); //json_msg, dec_msg);

    //if (json_msg.name !== 'sp/log')
     return WebSocket.prototype.sond.apply(this, arguments);
  }

});

// ---
}

if (window != window.parent)
  throw "This should only execute in the main context frame...";

/* inject this cleaning function right in the page to avoid silly sandbox-related greasemonkey limitations */
window.document.head.appendChild(
  inject_fn = document.createElement("script")
);

inject_fn.innerHTML = '(' + when_external_loaded.toString() + ')()';