Google Drive Video Player for Synchtube

Play Google Drive videos on Synchtube

  1. // ==UserScript==
  2. // @name Google Drive Video Player for Synchtube
  3. // @namespace gdcytube
  4. // @description Play Google Drive videos on Synchtube
  5. // @include http://synchtu.be/r/*
  6. // @include https://synchtu.be/r/*
  7. // @grant unsafeWindow
  8. // @grant GM_xmlhttpRequest
  9. // @connect docs.google.com
  10. // @run-at document-end
  11. // @version 1.1.0
  12. // ==/UserScript==
  13.  
  14. try {
  15. function debug(message) {
  16. if (!unsafeWindow.enableCyTubeGoogleDriveUserscriptDebug) {
  17. return;
  18. }
  19.  
  20. try {
  21. unsafeWindow.console.log(message);
  22. } catch (error) {
  23. unsafeWindow.console.error(error);
  24. }
  25. }
  26.  
  27. var ITAG_QMAP = {
  28. 37: 1080,
  29. 46: 1080,
  30. 22: 720,
  31. 45: 720,
  32. 59: 480,
  33. 44: 480,
  34. 35: 480,
  35. 18: 360,
  36. 43: 360,
  37. 34: 360
  38. };
  39.  
  40. var ITAG_CMAP = {
  41. 43: 'video/webm',
  42. 44: 'video/webm',
  43. 45: 'video/webm',
  44. 46: 'video/webm',
  45. 18: 'video/mp4',
  46. 22: 'video/mp4',
  47. 37: 'video/mp4',
  48. 59: 'video/mp4',
  49. 35: 'video/flv',
  50. 34: 'video/flv'
  51. };
  52.  
  53. function getVideoInfo(id, cb) {
  54. var url = 'https://docs.google.com/file/d/' + id + '/get_video_info';
  55. debug('Fetching ' + url);
  56.  
  57. GM_xmlhttpRequest({
  58. method: 'GET',
  59. url: url,
  60. onload: function (res) {
  61. try {
  62. debug('Got response ' + res.responseText);
  63. var data = {};
  64. var error;
  65. res.responseText.split('&').forEach(function (kv) {
  66. var pair = kv.split('=');
  67. data[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]);
  68. });
  69.  
  70. if (data.status === 'fail') {
  71. error = new Error('Google Docs request failed: ' +
  72. 'metadata indicated status=fail');
  73. error.response = res.responseText;
  74. error.reason = 'RESPONSE_STATUS_FAIL';
  75. return cb(error);
  76. }
  77.  
  78. if (!data.fmt_stream_map) {
  79. error = new Error('Google Docs request failed: ' +
  80. 'metadata lookup returned no valid links');
  81. error.response = res.responseText;
  82. error.reason = 'MISSING_LINKS';
  83. return cb(error);
  84. }
  85.  
  86. data.links = {};
  87. data.fmt_stream_map.split(',').forEach(function (item) {
  88. var pair = item.split('|');
  89. data.links[pair[0]] = pair[1];
  90. });
  91. data.videoMap = mapLinks(data.links);
  92.  
  93. cb(null, data);
  94. } catch (error) {
  95. unsafeWindow.console.error(error);
  96. }
  97. },
  98.  
  99. onerror: function () {
  100. var error = new Error('Google Docs request failed: ' +
  101. 'metadata lookup HTTP request failed');
  102. error.reason = 'HTTP_ONERROR';
  103. return cb(error);
  104. }
  105. });
  106. }
  107.  
  108. function mapLinks(links) {
  109. var videos = {
  110. 1080: [],
  111. 720: [],
  112. 480: [],
  113. 360: []
  114. };
  115.  
  116. Object.keys(links).forEach(function (itag) {
  117. itag = parseInt(itag, 10);
  118. if (!ITAG_QMAP.hasOwnProperty(itag)) {
  119. return;
  120. }
  121.  
  122. videos[ITAG_QMAP[itag]].push({
  123. itag: itag,
  124. contentType: ITAG_CMAP[itag],
  125. link: links[itag]
  126. });
  127. });
  128.  
  129. return videos;
  130. }
  131.  
  132. /*
  133. * Greasemonkey 2.0 has this wonderful sandbox that attempts
  134. * to prevent script developers from shooting themselves in
  135. * the foot by removing the trigger from the gun, i.e. it's
  136. * impossible to cross the boundary between the browser JS VM
  137. * and the privileged sandbox that can run GM_xmlhttpRequest().
  138. *
  139. * So in this case, we have to resort to polling a special
  140. * variable to see if getGoogleDriveMetadata needs to be called
  141. * and deliver the result into another special variable that is
  142. * being polled on the browser side.
  143. */
  144.  
  145. /*
  146. * Browser side function -- sets gdUserscript.pollID to the
  147. * ID of the Drive video to be queried and polls
  148. * gdUserscript.pollResult for the result.
  149. */
  150. function getGoogleDriveMetadata_GM(id, callback) {
  151. debug('Setting GD poll ID to ' + id);
  152. unsafeWindow.gdUserscript.pollID = id;
  153. var tries = 0;
  154. var i = setInterval(function () {
  155. if (unsafeWindow.gdUserscript.pollResult) {
  156. debug('Got result');
  157. clearInterval(i);
  158. var result = unsafeWindow.gdUserscript.pollResult;
  159. unsafeWindow.gdUserscript.pollResult = null;
  160. callback(result.error, result.result);
  161. } else if (++tries > 100) {
  162. // Took longer than 10 seconds, give up
  163. clearInterval(i);
  164. }
  165. }, 100);
  166. }
  167.  
  168. /*
  169. * Sandbox side function -- polls gdUserscript.pollID for
  170. * the ID of a Drive video to be queried, looks up the
  171. * metadata, and stores it in gdUserscript.pollResult
  172. */
  173. function setupGDPoll() {
  174. unsafeWindow.gdUserscript = cloneInto({}, unsafeWindow);
  175. var pollInterval = setInterval(function () {
  176. if (unsafeWindow.gdUserscript.pollID) {
  177. var id = unsafeWindow.gdUserscript.pollID;
  178. unsafeWindow.gdUserscript.pollID = null;
  179. debug('Polled and got ' + id);
  180. getVideoInfo(id, function (error, data) {
  181. unsafeWindow.gdUserscript.pollResult = cloneInto({
  182. error: error,
  183. result: data
  184. }, unsafeWindow);
  185. });
  186. }
  187. }, 1000);
  188. }
  189.  
  190. function isRunningTampermonkey() {
  191. try {
  192. return GM_info.scriptHandler === 'Tampermonkey';
  193. } catch (error) {
  194. return false;
  195. }
  196. }
  197.  
  198. if (isRunningTampermonkey()) {
  199. unsafeWindow.getGoogleDriveMetadata = getVideoInfo;
  200. } else {
  201. debug('Using non-TM polling workaround');
  202. unsafeWindow.getGoogleDriveMetadata = exportFunction(
  203. getGoogleDriveMetadata_GM, unsafeWindow);
  204. setupGDPoll();
  205. }
  206.  
  207. unsafeWindow.console.log('Initialized userscript Google Drive player');
  208. unsafeWindow.hasDriveUserscript = true;
  209. } catch (error) {
  210. unsafeWindow.console.error(error);
  211. }
  212.