diff --git a/gdrive-userscript/cytube-google-drive.user.js b/gdrive-userscript/cytube-google-drive.user.js index 1d674f65..d8fa6655 100644 --- a/gdrive-userscript/cytube-google-drive.user.js +++ b/gdrive-userscript/cytube-google-drive.user.js @@ -7,16 +7,20 @@ // @grant GM_xmlhttpRequest // @connect docs.google.com // @run-at document-end -// @version 1.0.0 +// @version 1.1.0 // ==/UserScript== -(function () { +try { function debug(message) { if (!unsafeWindow.enableCyTubeGoogleDriveUserscriptDebug) { return; } - unsafeWindow.console.log.apply(unsafeWindow.console, arguments); + try { + unsafeWindow.console.log(message); + } catch (error) { + unsafeWindow.console.error(error); + } } var ITAG_QMAP = { @@ -53,36 +57,42 @@ method: 'GET', url: url, onload: function (res) { - var data = {}; - var error; - res.responseText.split('&').forEach(function (kv) { - var pair = kv.split('='); - data[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]); - }); + try { + debug('Got response ' + res.responseText); + var data = {}; + var error; + res.responseText.split('&').forEach(function (kv) { + var pair = kv.split('='); + data[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]); + }); - if (data.status === 'fail') { - error = new Error('Google Docs request failed: ' + - 'metadata indicated status=fail'); - error.response = res.responseText; - error.reason = 'RESPONSE_STATUS_FAIL'; - return cb(error); + if (data.status === 'fail') { + error = new Error('Google Docs request failed: ' + + 'metadata indicated status=fail'); + error.response = res.responseText; + error.reason = 'RESPONSE_STATUS_FAIL'; + return cb(error); + } + + if (!data.fmt_stream_map) { + error = new Error('Google Docs request failed: ' + + 'metadata lookup returned no valid links'); + error.response = res.responseText; + error.reason = 'MISSING_LINKS'; + return cb(error); + } + + data.links = {}; + data.fmt_stream_map.split(',').forEach(function (item) { + var pair = item.split('|'); + data.links[pair[0]] = pair[1]; + }); + data.videoMap = mapLinks(data.links); + + cb(null, data); + } catch (error) { + unsafeWindow.console.error(error); } - - if (!data.fmt_stream_map) { - error = new Error('Google Docs request failed: ' + - 'metadata lookup returned no valid links'); - error.response = res.responseText; - error.reason = 'MISSING_LINKS'; - return cb(error); - } - - data.links = {}; - data.fmt_stream_map.split(',').forEach(function (item) { - var pair = item.split('|'); - data.links[pair[0]] = pair[1]; - }); - - cb(null, data); }, onerror: function () { @@ -118,36 +128,83 @@ return videos; } - function GoogleDrivePlayer(data) { - if (!(this instanceof GoogleDrivePlayer)) { - return new GoogleDrivePlayer(data); - } + /* + * Greasemonkey 2.0 has this wonderful sandbox that attempts + * to prevent script developers from shooting themselves in + * the foot by removing the trigger from the gun, i.e. it's + * impossible to cross the boundary between the browser JS VM + * and the privileged sandbox that can run GM_xmlhttpRequest(). + * + * So in this case, we have to resort to polling a special + * variable to see if getGoogleDriveMetadata needs to be called + * and deliver the result into another special variable that is + * being polled on the browser side. + */ - this.setMediaProperties(data); - this.load(data); + /* + * Browser side function -- sets gdUserscript.pollID to the + * ID of the Drive video to be queried and polls + * gdUserscript.pollResult for the result. + */ + function getGoogleDriveMetadata_GM(id, callback) { + debug('Setting GD poll ID to ' + id); + unsafeWindow.gdUserscript.pollID = id; + var tries = 0; + var i = setInterval(function () { + if (unsafeWindow.gdUserscript.pollResult) { + debug('Got result'); + clearInterval(i); + var result = unsafeWindow.gdUserscript.pollResult; + unsafeWindow.gdUserscript.pollResult = null; + callback(result.error, result.result); + } else if (++tries > 100) { + // Took longer than 10 seconds, give up + clearInterval(i); + } + }, 100); } - GoogleDrivePlayer.prototype = Object.create(unsafeWindow.VideoJSPlayer.prototype); - - GoogleDrivePlayer.prototype.load = function (data) { - var self = this; - getVideoInfo(data.id, function (err, videoData) { - if (err) { - debug(err); - var alertBox = unsafeWindow.document.createElement('div'); - alertBox.className = 'alert alert-danger'; - alertBox.textContent = err.message; - document.getElementById('ytapiplayer').appendChild(alertBox); - return; + /* + * Sandbox side function -- polls gdUserscript.pollID for + * the ID of a Drive video to be queried, looks up the + * metadata, and stores it in gdUserscript.pollResult + */ + function setupGDPoll() { + unsafeWindow.gdUserscript = cloneInto({}, unsafeWindow); + var pollInterval = setInterval(function () { + if (unsafeWindow.gdUserscript.pollID) { + var id = unsafeWindow.gdUserscript.pollID; + unsafeWindow.gdUserscript.pollID = null; + debug('Polled and got ' + id); + getVideoInfo(id, function (error, data) { + unsafeWindow.gdUserscript.pollResult = cloneInto({ + error: error, + result: data + }, unsafeWindow); + }); } + }, 1000); + } - debug('Retrieved links: ' + JSON.stringify(videoData.links)); - data.meta.direct = mapLinks(videoData.links); - unsafeWindow.VideoJSPlayer.prototype.loadPlayer.call(self, data); - }); - }; + function isRunningTampermonkey() { + try { + return GM_info.scriptHandler === 'Tampermonkey'; + } catch (error) { + return false; + } + } + + if (isRunningTampermonkey()) { + unsafeWindow.getGoogleDriveMetadata = getVideoInfo; + } else { + debug('Using non-TM polling workaround'); + unsafeWindow.getGoogleDriveMetadata = exportFunction( + getGoogleDriveMetadata_GM, unsafeWindow); + setupGDPoll(); + } - unsafeWindow.GoogleDrivePlayer = GoogleDrivePlayer; unsafeWindow.console.log('Initialized userscript Google Drive player'); unsafeWindow.hasDriveUserscript = true; -})(); +} catch (error) { + unsafeWindow.console.error(error); +} diff --git a/player/gdrive-player.coffee b/player/gdrive-player.coffee index 34c8565f..026216bb 100644 --- a/player/gdrive-player.coffee +++ b/player/gdrive-player.coffee @@ -4,3 +4,19 @@ window.GoogleDrivePlayer = class GoogleDrivePlayer extends VideoJSPlayer return new GoogleDrivePlayer(data) super(data) + + load: (data) -> + if typeof window.getGoogleDriveMetadata is 'function' + window.getGoogleDriveMetadata(data.id, (error, metadata) => + if error + console.error(error) + alertBox = window.document.createElement('div') + alertBox.className = 'alert alert-danger' + alertBox.textContent = error.message + document.getElementById('ytapiplayer').appendChild(alertBox) + else + data.meta.direct = metadata.videoMap + super(data) + ) + else + super(data) diff --git a/player/update.coffee b/player/update.coffee index 291fe81f..1dde4123 100644 --- a/player/update.coffee +++ b/player/update.coffee @@ -33,7 +33,7 @@ window.loadMediaPlayer = (data) -> else if data.type is 'gd' try if data.meta.html5hack or window.hasDriveUserscript - window.PLAYER = new window.GoogleDrivePlayer(data) + window.PLAYER = new GoogleDrivePlayer(data) else window.PLAYER = new GoogleDriveYouTubePlayer(data) catch e diff --git a/player/videojs.coffee b/player/videojs.coffee index ff53e35b..4ad445ac 100644 --- a/player/videojs.coffee +++ b/player/videojs.coffee @@ -43,8 +43,7 @@ window.VideoJSPlayer = class VideoJSPlayer extends Player if not (this instanceof VideoJSPlayer) return new VideoJSPlayer(data) - @setMediaProperties(data) - @loadPlayer(data) + @load(data) loadPlayer: (data) -> waitUntilDefined(window, 'videojs', => diff --git a/www/js/player.js b/www/js/player.js index ec972b8f..11690df0 100644 --- a/www/js/player.js +++ b/www/js/player.js @@ -503,8 +503,7 @@ if (!(this instanceof VideoJSPlayer)) { return new VideoJSPlayer(data); } - this.setMediaProperties(data); - this.loadPlayer(data); + this.load(data); } VideoJSPlayer.prototype.loadPlayer = function(data) { @@ -676,6 +675,28 @@ GoogleDrivePlayer.__super__.constructor.call(this, data); } + GoogleDrivePlayer.prototype.load = function(data) { + if (typeof window.getGoogleDriveMetadata === 'function') { + return window.getGoogleDriveMetadata(data.id, (function(_this) { + return function(error, metadata) { + var alertBox; + if (error) { + console.error(error); + alertBox = window.document.createElement('div'); + alertBox.className = 'alert alert-danger'; + alertBox.textContent = error.message; + return document.getElementById('ytapiplayer').appendChild(alertBox); + } else { + data.meta.direct = metadata.videoMap; + return GoogleDrivePlayer.__super__.load.call(_this, data); + } + }; + })(this)); + } else { + return GoogleDrivePlayer.__super__.load.call(this, data); + } + }; + return GoogleDrivePlayer; })(VideoJSPlayer); @@ -1359,7 +1380,7 @@ } else if (data.type === 'gd') { try { if (data.meta.html5hack || window.hasDriveUserscript) { - return window.PLAYER = new window.GoogleDrivePlayer(data); + return window.PLAYER = new GoogleDrivePlayer(data); } else { return window.PLAYER = new GoogleDriveYouTubePlayer(data); }