HTML5 thingie for Spotify Web Player

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

  1. // ==UserScript==
  2. // @name HTML5 thingie for Spotify Web Player
  3. // @description Play music on the browser without having to install Flash Player. Yes, I know why SWF bridges exist.
  4. // @author Swyter
  5. // @namespace https://greasyfork.org/users/4813-swyter
  6. // @match https://play.spotify.com/*
  7. // @version 2016.01.30
  8. // /* turns out we now need to run on frames too to make our flashless context menu copying mechanism work */ @noframes
  9. // @icon https://i.imgur.com/LHkCkka.png
  10. // @grant none
  11. // @run-at document-start
  12. // ==/UserScript==
  13.  
  14. /* it's random, trust me, I'm Sony! :) */
  15. Object.defineProperty(window.Math, 'random',
  16. {
  17. configurable: false,
  18. writable: false,
  19. value: function(a) { return 0.1337; }
  20. });
  21.  
  22. /* ZeroClipboard.js HTML5 shim, no ridiculous SWF overlay required, sneaky! */
  23. Object.defineProperty(window, 'ZeroClipboard',
  24. {
  25. configurable: false,
  26. writable: false,
  27. value: function(e)
  28. {
  29. console.log("ZC constructor", arguments);
  30.  
  31. e.addEventListener('click', function (x)
  32. {
  33. x.preventDefault();
  34.  
  35. /* add a dummy input to our page, fill it with our text, select it,
  36. copy it, remove our dummy item; voila, typical js kludge! */
  37. input = document.createElement('input');
  38. input.value = this.dataset.clipboardText;
  39.  
  40. document.body.appendChild(input);
  41.  
  42. input.select(); document.execCommand('copy');
  43.  
  44. document.body.removeChild(input);
  45.  
  46. console.log("ZC -> click", this, this.dataset.clipboardText, input, input.value);
  47. });
  48.  
  49. return {
  50. on: function(e, f)
  51. {
  52. console.log("ZC -> on", arguments, this, this.elem);
  53. this.elem.addEventListener(e, f);
  54. },
  55.  
  56. elem: e
  57. };
  58. }
  59. });
  60.  
  61. ZeroClipboard.config = function(){ console.log("ZC -> config", arguments) };
  62.  
  63.  
  64. /* SWFobject.js faker, sneaky! */
  65. Object.defineProperty(window, 'swfobject',
  66. {
  67. configurable: false,
  68. writable: false,
  69. value:
  70. {
  71. getFlashPlayerVersion: function(){ console.log("-<-< getFlashPlayerVersion =>", arguments); return { major: 11, minor: 2, release: 202 }; },
  72. hasFlashPlayerVersion: function(){ console.log("-<-< hasFlashPlayerVersion =>", arguments); return true; },
  73. embedSWF: function(swf, parent_id, unk_a, unk_b, req_flash_ver, unk_c, properties)
  74. {
  75. console.log("-<-< embedSWF =>", arguments, arguments[9].toString(), this, properties);
  76.  
  77. /* create our audio player in substitution of the swf abobination */
  78. var dummy = document.createElement("audio");
  79.  
  80. dummy.id = properties.id;
  81. dummy["instanceid"] = properties.instanceId;
  82.  
  83. dummy.loop = false;
  84. dummy.preload = 'auto';
  85. dummy.autoplay = false;
  86. dummy.volume = 1.0;
  87.  
  88. dummy.sp_run = function(proof) { console.log("-<-< sp_run =>", arguments); return unsafeWindow.proof; };
  89. dummy.sp_hasSound = function() { return true };
  90. dummy.sp_load = function(player_id, raw_uri, options)
  91. {
  92. /* for some reason we have to massage the url format to remove the 'mp3:' protocol
  93. prefix from the uri, and the '/cfx/st' from the server string */
  94. // 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"
  95. // options.server: "http://dsu0uct5x2puz.cloudfront.net/cfx/st"
  96.  
  97. var uri = options.server.match(/(.+\/\/[^\/]+?)\//)[1] + raw_uri.split(":")[1];
  98.  
  99. console.log("sp_load =>", uri, arguments, this);
  100. this.src = uri;
  101.  
  102. if (options.startFrom !== 0)
  103. this.currentTime = Math.floor(0.001 * options.startFrom);
  104.  
  105. if (options.autoplay)
  106. this.play();
  107.  
  108. console.log(this.paused)
  109.  
  110. unsafeWindow.Spotify.Instances.get(this.instanceid).audioManager.getPlayerById(player_id).trigger('LOAD', {}, {id: player_id});
  111. };
  112.  
  113. /* dummy functions after __noSuchMethod__ was deprecated in Firefox 44 */
  114. dummy.sp_initializePlayerById = function(player_id) { console.log("=> sp_initializePlayerById", arguments); };
  115. dummy.sp_stop = function(player_id) { console.log("=> sp_stop", arguments); };
  116.  
  117. /* actual reimplementations of functions that embody the internal SWF interface */
  118. dummy.sp_setVolume = function(player_id, vol) { console.log("=> volume", arguments); if (player_id == "main:A" && vol !== 0) this.volume = vol };
  119. 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 };
  120. dummy.sp_seek = function(player_id, pos) { console.log("=> seek", arguments); this.currentTime = Math.floor(0.001 * pos) };
  121. dummy.sp_pause = function(player_id) { console.log("=> pause", arguments); this.pause() };
  122. dummy.sp_resume = function(player_id) { console.log("=> pause", arguments); this.play() };
  123.  
  124. dummy.sp_playerState = function(player_id)
  125. {
  126. return {
  127. volume: this.volume,
  128. position: Math.floor(1000 * this.currentTime),
  129. duration: Math.floor(1000 * this.duration),
  130. isPlaying: !this.paused,
  131. isStopped: false,
  132. isPaused: this.paused
  133. };
  134. };
  135.  
  136. dummy.sp_addPlayer = function(player_index, player_id, player_protocol)
  137. {
  138. if (player_id != "main:A")
  139. return;
  140.  
  141. var sp_html5_event_listeners =
  142. {
  143. canplay: 'LOAD',
  144. playing: 'PLAYING',
  145. pause: 'PAUSED',
  146. ended: 'TRACK_ENDED',
  147. durationchange: 'DURATION',
  148. progress: 'PROGRESS',
  149. error: 'PLAYBACK_FAILED'
  150. };
  151.  
  152. function sp_html5_generic_callback(e)
  153. {
  154. console.log("sp html5 audio «" + e.type + "» event", e, sp_html5_event_listeners[e.type]);
  155.  
  156. var ret;
  157.  
  158. switch(sp_html5_event_listeners[e.type])
  159. {
  160. case 'DURATION':
  161. ret = {duration: Math.floor(1000 * this.duration)};
  162. break;
  163.  
  164. case 'PROGRESS':
  165. ret = {position: Math.floor(1000 * this.currentTime)};
  166. break;
  167.  
  168. default:
  169. ret = {};
  170. }
  171.  
  172. unsafeWindow.Spotify.Instances.get(this.instanceid).audioManager.getPlayerById(player_id).trigger(sp_html5_event_listeners[e.type], ret, {id: player_id});
  173. }
  174.  
  175. for (var i_event in sp_html5_event_listeners)
  176. this.addEventListener(i_event, sp_html5_generic_callback);
  177.  
  178. return true;
  179. };
  180.  
  181. dummy.__noSuchMethod__ = function(name, params) { console.log('==>>== > invalid function call', name, params); };
  182.  
  183. /* necessary for the spotify framework to find it in the right place */
  184. window.document[properties.id] = dummy;
  185.  
  186. /* insert our dummy audio player in the requested element */
  187. document.getElementById(parent_id).appendChild(dummy);
  188.  
  189. /* tell the spotify framework that the swf embed is ready */
  190. arguments[9].apply(this, [{success: true}]);
  191.  
  192. // act as the swf bridge and tell it that the Flash-backend is ready
  193. // JSInterface.notify(ApplicationEvents.READY,null,1);
  194. //Spotify.Instances.get(properties.instanceId).audioManager.getInterface()._triggerDeferred('FLASH_AVAILABLE', null);
  195. Spotify.Instances.get(properties.instanceId).audioManager.getInterface()._triggerDeferred('READY', null);
  196.  
  197. console.log("AUDIOMANAGER-INTERFACE", properties.id, window.document[properties.id], Spotify.Instances.get(properties.instanceId).audioManager.getInterface(),
  198. Spotify.Instances.get(properties.instanceId).audioManager.getInterface().hasSound());
  199. }
  200. }
  201. });
  202.  
  203.  
  204. function when_external_loaded()
  205. {
  206. // ---
  207.  
  208.  
  209. function get_pong(ping)
  210. {
  211. // 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
  212.  
  213. console.log("ping-pong", ping);
  214.  
  215. var xhr = new XMLHttpRequest();
  216. xhr.open("GET", "https://crossorigin.me/http://ping-pong.spotify.nodestuff.net/" + ping.replace(/ /g,"-"), true);
  217. xhr.responseType = "json";
  218.  
  219. xhr.onloadend = function()
  220. {
  221. if (xhr.readyState != xhr.DONE)
  222. return;
  223.  
  224. console.log("pong", xhr);
  225. window.proof = xhr.response.pong.replace(/-/g," ");
  226.  
  227. if (!sp_ws)
  228. return;
  229.  
  230. sp_ws.send(`{"id":` + (window.proof_id || 2) + `,"name":"sp/pong_flash2","args":["` + window.proof + `"]}`);
  231. //sp_ws.send(`{"id":2,"name":"sp/work_done","args":["undefined"]}`);
  232.  
  233. }
  234.  
  235. xhr.send();
  236. }
  237.  
  238. /* wait until the page is ready for the code snippet to run */
  239. document.addEventListener('DOMContentLoaded', function()
  240. {
  241. console.log("!!! DOMContentLoaded");
  242.  
  243. WebSocket.prototype.sond = WebSocket.prototype.send;
  244. WebSocket.prototype.send = function(msg)
  245. {
  246. window.sp_ws = this;
  247.  
  248. if (this.onmessage && this.sucks !== true)
  249. {
  250. callback = this.onmessage;
  251.  
  252. console.log("orig prev callback:", callback);
  253.  
  254. this.onmessage = function(message)
  255. {
  256. var json_msg = JSON.parse(message.data);
  257.  
  258. if (json_msg && json_msg.message && json_msg.message[0] == 'ping_flash2' && json_msg.message[1])
  259. {
  260. console.log("getting preventive pong", json_msg);
  261. get_pong(json_msg.message[1]);
  262. }
  263.  
  264. //if (json_msg.id === window.last_track_msg)
  265. console.info("<- ws recv: ", window.last_track_msg, message.data);
  266.  
  267. //if (json_msg && json_msg.result && json_msg.result.uri)
  268. //open(json_msg.result.uri);
  269.  
  270. callback(message);
  271. }
  272.  
  273. this.sucks = true;
  274. }
  275.  
  276. var json_msg = JSON.parse(msg);
  277.  
  278. // block the flash pong reply until we have the correct reply for the challenge
  279. if (typeof window.proof !== "string" && json_msg && (json_msg.name == 'sp/pong_flash2')) // || json_msg.name == 'sp/work_done'))
  280. {
  281. console.info("-> blocking sent message until we have challenge: ", json_msg.id, msg);
  282.  
  283. window.proof_id = json_msg.id;
  284.  
  285. return;
  286. }
  287.  
  288. // get the http link instead or rtmp, thankies!
  289. if (json_msg && json_msg.name == 'sp/track_uri')
  290. {
  291. arguments[0] = msg.replace(',"rtmp"', '');
  292. window.last_track_msg = json_msg.id;
  293. console.info("-> ws send: ", json_msg.id, msg);
  294. }
  295. //dec_msg = json_msg.name === 'sp/hm_b64' ? atob(json_msg.args[0]) : null;
  296.  
  297. console.info("-> ws send: ", msg); //json_msg, dec_msg);
  298.  
  299. //if (json_msg.name !== 'sp/log')
  300. return WebSocket.prototype.sond.apply(this, arguments);
  301. }
  302.  
  303. });
  304.  
  305. // ---
  306. }
  307.  
  308. if (window != window.parent)
  309. throw "This should only execute in the main context frame...";
  310.  
  311. /* inject this cleaning function right in the page to avoid silly sandbox-related greasemonkey limitations */
  312. window.document.head.appendChild(
  313. inject_fn = document.createElement("script")
  314. );
  315.  
  316. inject_fn.innerHTML = '(' + when_external_loaded.toString() + ')()';
  317.