HTML5 thingie for Spotify Web Player

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

目前为 2015-12-17 提交的版本。查看 最新版本

  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 2015.12.17
  8. // @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. /* SWFobject.js faker, sneaky! */
  23. Object.defineProperty(window, 'swfobject',
  24. {
  25. configurable: false,
  26. writable: false,
  27. value:
  28. {
  29. getFlashPlayerVersion: function(){ console.log("-<-< getFlashPlayerVersion =>", arguments); return { major: 11, minor: 2, release: 202 }; },
  30. hasFlashPlayerVersion: function(){ console.log("-<-< hasFlashPlayerVersion =>", arguments); return true; },
  31. embedSWF: function(swf, parent_id, unk_a, unk_b, req_flash_ver, unk_c, properties)
  32. {
  33. console.log("-<-< embedSWF =>", arguments, arguments[9].toString(), this, properties);
  34.  
  35. function get_pong(ping)
  36. {
  37. // 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
  38.  
  39. console.log("ping-pong", ping);
  40. var xhr = new XMLHttpRequest();
  41. xhr.open("GET", "https://crossorigin.me/http://ping-pong.spotify.nodestuff.net/" + ping.replace(/ /g,"-"), true);
  42. xhr.responseType = "json";
  43. xhr.send();
  44. /* believe me, I know this sucks */
  45. while (xhr.readyState != xhr.DONE){}
  46. console.log("pong", xhr);
  47. return xhr.response.pong.replace(/-/g," ");
  48. }
  49.  
  50. /* create our audio player in substitution of the swf abobination */
  51. var dummy = document.createElement("audio");
  52. dummy.id = properties.id;
  53. dummy["instanceid"] = properties.instanceId;
  54. dummy.loop = false;
  55. dummy.preload = 'auto';
  56. dummy.autoplay = false;
  57. dummy.volume = 1.0;
  58. dummy.sp_run = function(proof) { console.log("-<-< sp_run =>", arguments); return get_pong(proof); };
  59. dummy.sp_hasSound = function() { return true };
  60. dummy.sp_load = function(player_id, raw_uri, options)
  61. {
  62. /* for some reason we have to massage the url format to remove the 'mp3:' protocol
  63. prefix from the uri, and the '/cfx/st' from the server string */
  64. // 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"
  65. // options.server: "http://dsu0uct5x2puz.cloudfront.net/cfx/st"
  66.  
  67. var uri = options.server.match(/(.+\/\/[^\/]+?)\//)[1] + raw_uri.split(":")[1];
  68. console.log("sp_load =>", uri, arguments, this);
  69. this.src = uri;
  70.  
  71. if (options.startFrom !== 0)
  72. this.currentTime = Math.floor(0.001 * options.startFrom);
  73.  
  74. if (options.autoplay)
  75. this.play();
  76. console.log(this.paused)
  77.  
  78. unsafeWindow.Spotify.Instances.get(this.instanceid).audioManager.getPlayerById(player_id).trigger('LOAD', {}, {id: player_id});
  79. };
  80. dummy.sp_setVolume = function(player_id, vol) { console.log("=> volume", arguments); if (player_id == "main:A" && vol !== 0) this.volume = vol };
  81. 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 };
  82. dummy.sp_seek = function(player_id, pos) { console.log("=> seek", arguments); this.currentTime = Math.floor(0.001 * pos) };
  83. dummy.sp_pause = function(player_id) { console.log("=> pause", arguments); this.pause() };
  84. dummy.sp_resume = function(player_id) { console.log("=> pause", arguments); this.play() };
  85. dummy.sp_playerState = function(player_id)
  86. {
  87. return {
  88. volume: this.volume,
  89. position: Math.floor(1000 * this.currentTime),
  90. duration: Math.floor(1000 * this.duration),
  91. isPlaying: !this.paused,
  92. isStopped: false,
  93. isPaused: this.paused
  94. };
  95. };
  96. dummy.sp_addPlayer = function(player_index, player_id, player_protocol)
  97. {
  98. if (player_id != "main:A")
  99. return;
  100. var sp_html5_event_listeners =
  101. {
  102. canplay: 'LOAD',
  103. playing: 'PLAYING',
  104. pause: 'PAUSED',
  105. ended: 'TRACK_ENDED',
  106. durationchange: 'DURATION',
  107. progress: 'PROGRESS',
  108. error: 'PLAYBACK_FAILED'
  109. };
  110.  
  111. function sp_html5_generic_callback(e)
  112. {
  113. console.log("sp html5 audio «" + e.type + "» event", e, sp_html5_event_listeners[e.type]);
  114. var ret;
  115. switch(sp_html5_event_listeners[e.type])
  116. {
  117. case 'DURATION':
  118. ret = {duration: Math.floor(1000 * this.duration)};
  119. break;
  120. case 'PROGRESS':
  121. ret = {position: Math.floor(1000 * this.currentTime)};
  122. break;
  123.  
  124. default:
  125. ret = {};
  126. }
  127.  
  128. unsafeWindow.Spotify.Instances.get(this.instanceid).audioManager.getPlayerById(player_id).trigger(sp_html5_event_listeners[e.type], ret, {id: player_id});
  129. }
  130.  
  131. for (var i_event in sp_html5_event_listeners)
  132. this.addEventListener(i_event, sp_html5_generic_callback);
  133.  
  134. return true;
  135. };
  136. dummy.__noSuchMethod__ = function(name, params) { console.log('==>>== > invalid function call', name, params); };
  137.  
  138. /* necessary for the spotify framework to find it in the right place */
  139. window.document[properties.id] = dummy;
  140.  
  141. /* insert our dummy audio player in the requested element */
  142. document.getElementById(parent_id).appendChild(dummy);
  143.  
  144. /* tell the spotify framework that the swf embed is ready */
  145. arguments[9].apply(this, [{success: true}]);
  146. // act as the swf bridge and tell it that the Flash-backend is ready
  147. // JSInterface.notify(ApplicationEvents.READY,null,1);
  148. //Spotify.Instances.get(properties.instanceId).audioManager.getInterface()._triggerDeferred('FLASH_AVAILABLE', null);
  149. Spotify.Instances.get(properties.instanceId).audioManager.getInterface()._triggerDeferred('READY', null);
  150. console.log("AUDIOMANAGER-INTERFACE", properties.id, window.document[properties.id], Spotify.Instances.get(properties.instanceId).audioManager.getInterface(),
  151. Spotify.Instances.get(properties.instanceId).audioManager.getInterface().hasSound());
  152. }
  153. }
  154. });
  155.  
  156.  
  157. function when_external_loaded()
  158. {
  159. // ---
  160. /* wait until the page is ready for the code snippet to run */
  161. document.addEventListener('DOMContentLoaded', function()
  162. {
  163. console.log("!!! DOMContentLoaded");
  164.  
  165. WebSocket.prototype.sond = WebSocket.prototype.send;
  166. WebSocket.prototype.send = function(msg)
  167. {
  168. if (this.onmessage && this.sucks !== true)
  169. {
  170. callback = this.onmessage;
  171. console.log("orig prev callback:", callback);
  172.  
  173. this.onmessage = function(message)
  174. {
  175. var json_msg = JSON.parse(message.data);
  176. if (json_msg.id === window.last_track_msg)
  177. console.info("<- ws recv: ", window.last_track_msg, message.data);
  178. //if (json_msg && json_msg.result && json_msg.result.uri)
  179. //open(json_msg.result.uri);
  180. callback(message);
  181. }
  182. this.sucks = true;
  183. }
  184.  
  185. var json_msg = JSON.parse(msg);
  186.  
  187. // get the http link instead or rtmp, thankies!
  188. if (json_msg && json_msg.name == 'sp/track_uri')
  189. {
  190. arguments[0] = msg.replace(',"rtmp"', '');
  191. window.last_track_msg = json_msg.id;
  192. console.info("-> ws send: ", json_msg.id, msg);
  193. }
  194. //dec_msg = json_msg.name === 'sp/hm_b64' ? atob(json_msg.args[0]) : null;
  195. //console.info("-> ws send: ", msg); //json_msg, dec_msg);
  196. //if (json_msg.name !== 'sp/log')
  197. return WebSocket.prototype.sond.apply(this, arguments);
  198. }
  199. });
  200. // ---
  201. }
  202.  
  203. /* inject this cleaning function right in the page to avoid silly sandbox-related greasemonkey limitations */
  204. window.document.head.appendChild(
  205. inject_fn = document.createElement("script")
  206. );
  207.  
  208. inject_fn.innerHTML = '(' + when_external_loaded.toString() + ')()';