From 4feee02e33a3d3741b2d1cc605d061cc91d8e2db Mon Sep 17 00:00:00 2001 From: Calvin Montgomery Date: Mon, 15 Aug 2016 21:00:56 -0700 Subject: [PATCH 1/5] Add initial userscript --- build-player.js | 1 + gdrive-userscript/cytube-google-drive.user.js | 155 ++++++++++++++++++ gdrive-userscript/generate-userscript.js | 19 +++ package.json | 3 +- player/gdrive-player.coffee | 6 + player/update.coffee | 4 +- www/js/data.js | 3 + www/js/player.js | 20 ++- 8 files changed, 205 insertions(+), 6 deletions(-) create mode 100644 gdrive-userscript/cytube-google-drive.user.js create mode 100644 gdrive-userscript/generate-userscript.js create mode 100644 player/gdrive-player.coffee diff --git a/build-player.js b/build-player.js index 9e233106..8ea29dc8 100644 --- a/build-player.js +++ b/build-player.js @@ -8,6 +8,7 @@ var order = [ 'youtube.coffee', 'dailymotion.coffee', 'videojs.coffee', + 'gdrive-player.coffee', 'raw-file.coffee', 'soundcloud.coffee', 'embed.coffee', diff --git a/gdrive-userscript/cytube-google-drive.user.js b/gdrive-userscript/cytube-google-drive.user.js new file mode 100644 index 00000000..6add91fa --- /dev/null +++ b/gdrive-userscript/cytube-google-drive.user.js @@ -0,0 +1,155 @@ +// ==UserScript== +// @name Google Drive Video Player for {SITENAME} +// @namespace gdcytube +// @description Play Google Drive videos on {SITENAME} +// {INCLUDE_BLOCK} +// @grant unsafeWindow +// @grant GM_xmlhttpRequest +// @connect docs.google.com +// @run-at document-end +// @version 1.0.0 +// ==/UserScript== + +(function () { + if (!unsafeWindow.enableCyTubeGoogleDriveUserscript) { + return; + } + + function debug(message) { + if (!unsafeWindow.enableCyTubeGoogleDriveUserscriptDebug) { + return; + } + + unsafeWindow.console.log.apply(unsafeWindow.console, arguments); + } + + var ITAG_QMAP = { + 37: 1080, + 46: 1080, + 22: 720, + 45: 720, + 59: 480, + 44: 480, + 35: 480, + 18: 360, + 43: 360, + 34: 360 + }; + + var ITAG_CMAP = { + 43: 'video/webm', + 44: 'video/webm', + 45: 'video/webm', + 46: 'video/webm', + 18: 'video/mp4', + 22: 'video/mp4', + 37: 'video/mp4', + 59: 'video/mp4', + 35: 'video/flv', + 34: 'video/flv' + }; + + function getVideoInfo(id, cb) { + var url = 'https://docs.google.com/file/d/' + id + '/get_video_info'; + debug('Fetching ' + url); + + GM_xmlhttpRequest({ + method: 'GET', + url: url, + onload: function (res) { + var data = {}; + res.responseText.split('&').forEach(function (kv) { + var pair = kv.split('='); + data[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]); + }); + + if (data.status === 'fail') { + var 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) { + var 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 () { + var error = new Error('Google Docs request failed: ' + + 'metadata lookup HTTP request failed'); + error.reason = 'HTTP_ONERROR'; + return cb(error); + } + }); + } + + function mapLinks(links) { + var videos = { + 1080: [], + 720: [], + 480: [], + 360: [] + }; + + Object.keys(links).forEach(function (itag) { + itag = parseInt(itag, 10); + if (!ITAG_QMAP.hasOwnProperty(itag)) { + return; + } + + videos[ITAG_QMAP[itag]].push({ + itag: itag, + contentType: ITAG_CMAP[itag], + link: links[itag] + }); + }); + + return videos; + } + + function GoogleDrivePlayer(data) { + if (!(this instanceof GoogleDrivePlayer)) { + return new GoogleDrivePlayer(data); + } + + this.setMediaProperties(data); + this.load(data); + } + + 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; + } + + debug('Retrieved links: ' + JSON.stringify(videoData.links)); + data.meta.direct = mapLinks(videoData.links); + unsafeWindow.VideoJSPlayer.prototype.loadPlayer.call(self, data); + }); + }; + + unsafeWindow.GoogleDrivePlayer = GoogleDrivePlayer; + unsafeWindow.console.log('Initialized userscript Google Drive player'); +})(); diff --git a/gdrive-userscript/generate-userscript.js b/gdrive-userscript/generate-userscript.js new file mode 100644 index 00000000..33b297e5 --- /dev/null +++ b/gdrive-userscript/generate-userscript.js @@ -0,0 +1,19 @@ +var fs = require('fs'); +var path = require('path'); + +var sitename = process.argv[2]; +var includes = process.argv.slice(3).map(function (include) { + return '// @include ' + include; +}).join('\n'); + +var lines = String(fs.readFileSync( + path.resolve(__dirname, 'cytube-google-drive.user.js'))).split('\n'); +lines.forEach(function (line) { + if (line.match(/\{INCLUDE_BLOCK\}/)) { + console.log(includes); + } else if (line.match(/\{SITENAME\}/)) { + console.log(line.replace(/\{SITENAME\}/, sitename)); + } else { + console.log(line); + } +}); diff --git a/package.json b/package.json index 7643b71c..9a65db52 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,8 @@ "build-player": "$npm_node_execpath build-player.js", "build-server": "babel -D --source-maps --loose es6.destructuring,es6.forOf --out-dir lib/ src/", "postinstall": "./postinstall.sh", - "server-dev": "babel -D --watch --source-maps --loose es6.destructuring,es6.forOf --out-dir lib/ src/" + "server-dev": "babel -D --watch --source-maps --loose es6.destructuring,es6.forOf --out-dir lib/ src/", + "generate-userscript": "$npm_node_execpath gdrive-userscript/generate-userscript $@ > www/js/cytube-google-drive.user.js" }, "devDependencies": { "coffee-script": "^1.9.2" diff --git a/player/gdrive-player.coffee b/player/gdrive-player.coffee new file mode 100644 index 00000000..34c8565f --- /dev/null +++ b/player/gdrive-player.coffee @@ -0,0 +1,6 @@ +window.GoogleDrivePlayer = class GoogleDrivePlayer extends VideoJSPlayer + constructor: (data) -> + if not (this instanceof GoogleDrivePlayer) + return new GoogleDrivePlayer(data) + + super(data) diff --git a/player/update.coffee b/player/update.coffee index 5504a14d..20b528cb 100644 --- a/player/update.coffee +++ b/player/update.coffee @@ -2,7 +2,7 @@ TYPE_MAP = yt: YouTubePlayer vi: VimeoPlayer dm: DailymotionPlayer - gd: GoogleDriveYouTubePlayer + gd: GoogleDrivePlayer gp: VideoJSPlayer fi: FilePlayer jw: FilePlayer @@ -33,7 +33,7 @@ window.loadMediaPlayer = (data) -> else if data.type is 'gd' try if data.meta.html5hack - window.PLAYER = new VideoJSPlayer(data) + window.PLAYER = new window.GoogleDrivePlayer(data) else window.PLAYER = new GoogleDriveYouTubePlayer(data) catch e diff --git a/www/js/data.js b/www/js/data.js index da23942b..8a38b061 100644 --- a/www/js/data.js +++ b/www/js/data.js @@ -216,3 +216,6 @@ function eraseCookie(name) { /* to be implemented in callbacks.js */ function setupCallbacks() { } + +window.enableCyTubeGoogleDriveUserscript = true; +window.enableCyTubeGoogleDriveUserscriptDebug = true; diff --git a/www/js/player.js b/www/js/player.js index ef333654..395dd53c 100644 --- a/www/js/player.js +++ b/www/js/player.js @@ -1,5 +1,5 @@ (function() { - var CUSTOM_EMBED_WARNING, CustomEmbedPlayer, DEFAULT_ERROR, DailymotionPlayer, EmbedPlayer, FilePlayer, GoogleDriveYouTubePlayer, HITBOX_ERROR, HLSPlayer, HitboxPlayer, ImgurPlayer, LivestreamPlayer, Player, RTMPPlayer, SoundCloudPlayer, TYPE_MAP, TwitchPlayer, USTREAM_ERROR, UstreamPlayer, VideoJSPlayer, VimeoPlayer, YouTubePlayer, codecToMimeType, genParam, sortSources, + var CUSTOM_EMBED_WARNING, CustomEmbedPlayer, DEFAULT_ERROR, DailymotionPlayer, EmbedPlayer, FilePlayer, GoogleDrivePlayer, GoogleDriveYouTubePlayer, HITBOX_ERROR, HLSPlayer, HitboxPlayer, ImgurPlayer, LivestreamPlayer, Player, RTMPPlayer, SoundCloudPlayer, TYPE_MAP, TwitchPlayer, USTREAM_ERROR, UstreamPlayer, VideoJSPlayer, VimeoPlayer, YouTubePlayer, codecToMimeType, genParam, sortSources, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; @@ -666,6 +666,20 @@ })(Player); + window.GoogleDrivePlayer = GoogleDrivePlayer = (function(superClass) { + extend(GoogleDrivePlayer, superClass); + + function GoogleDrivePlayer(data) { + if (!(this instanceof GoogleDrivePlayer)) { + return new GoogleDrivePlayer(data); + } + GoogleDrivePlayer.__super__.constructor.call(this, data); + } + + return GoogleDrivePlayer; + + })(VideoJSPlayer); + codecToMimeType = function(codec) { switch (codec) { case 'mov/h264': @@ -1308,7 +1322,7 @@ yt: YouTubePlayer, vi: VimeoPlayer, dm: DailymotionPlayer, - gd: GoogleDriveYouTubePlayer, + gd: GoogleDrivePlayer, gp: VideoJSPlayer, fi: FilePlayer, jw: FilePlayer, @@ -1345,7 +1359,7 @@ } else if (data.type === 'gd') { try { if (data.meta.html5hack) { - return window.PLAYER = new VideoJSPlayer(data); + return window.PLAYER = new window.GoogleDrivePlayer(data); } else { return window.PLAYER = new GoogleDriveYouTubePlayer(data); } From ba9fbea1a1e36d905103f8a51657fdca272113f8 Mon Sep 17 00:00:00 2001 From: Calvin Montgomery Date: Mon, 15 Aug 2016 21:09:43 -0700 Subject: [PATCH 2/5] Minor fixes/cleanup --- gdrive-userscript/cytube-google-drive.user.js | 5 +---- player/update.coffee | 2 +- www/js/data.js | 3 +-- www/js/player.js | 2 +- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/gdrive-userscript/cytube-google-drive.user.js b/gdrive-userscript/cytube-google-drive.user.js index 6add91fa..cc70a457 100644 --- a/gdrive-userscript/cytube-google-drive.user.js +++ b/gdrive-userscript/cytube-google-drive.user.js @@ -11,10 +11,6 @@ // ==/UserScript== (function () { - if (!unsafeWindow.enableCyTubeGoogleDriveUserscript) { - return; - } - function debug(message) { if (!unsafeWindow.enableCyTubeGoogleDriveUserscriptDebug) { return; @@ -152,4 +148,5 @@ unsafeWindow.GoogleDrivePlayer = GoogleDrivePlayer; unsafeWindow.console.log('Initialized userscript Google Drive player'); + unsafeWindow.hasDriveUserscript = true; })(); diff --git a/player/update.coffee b/player/update.coffee index 20b528cb..291fe81f 100644 --- a/player/update.coffee +++ b/player/update.coffee @@ -32,7 +32,7 @@ window.loadMediaPlayer = (data) -> console.error e else if data.type is 'gd' try - if data.meta.html5hack + if data.meta.html5hack or window.hasDriveUserscript window.PLAYER = new window.GoogleDrivePlayer(data) else window.PLAYER = new GoogleDriveYouTubePlayer(data) diff --git a/www/js/data.js b/www/js/data.js index 8a38b061..75acd277 100644 --- a/www/js/data.js +++ b/www/js/data.js @@ -217,5 +217,4 @@ function eraseCookie(name) { /* to be implemented in callbacks.js */ function setupCallbacks() { } -window.enableCyTubeGoogleDriveUserscript = true; -window.enableCyTubeGoogleDriveUserscriptDebug = true; +window.enableCyTubeGoogleDriveUserscriptDebug = getOrDefault("cytube_drive_debug", false); diff --git a/www/js/player.js b/www/js/player.js index 395dd53c..ec972b8f 100644 --- a/www/js/player.js +++ b/www/js/player.js @@ -1358,7 +1358,7 @@ } } else if (data.type === 'gd') { try { - if (data.meta.html5hack) { + if (data.meta.html5hack || window.hasDriveUserscript) { return window.PLAYER = new window.GoogleDrivePlayer(data); } else { return window.PLAYER = new GoogleDriveYouTubePlayer(data); From 8d3b2e59dfd14cd8a1947fea2e95ea49cd32d7ae Mon Sep 17 00:00:00 2001 From: Calvin Montgomery Date: Mon, 15 Aug 2016 21:16:14 -0700 Subject: [PATCH 3/5] Shut up tampermonkey about redefined variables --- gdrive-userscript/cytube-google-drive.user.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gdrive-userscript/cytube-google-drive.user.js b/gdrive-userscript/cytube-google-drive.user.js index cc70a457..1d674f65 100644 --- a/gdrive-userscript/cytube-google-drive.user.js +++ b/gdrive-userscript/cytube-google-drive.user.js @@ -54,13 +54,14 @@ 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]); }); if (data.status === 'fail') { - var error = new Error('Google Docs request failed: ' + + error = new Error('Google Docs request failed: ' + 'metadata indicated status=fail'); error.response = res.responseText; error.reason = 'RESPONSE_STATUS_FAIL'; @@ -68,7 +69,7 @@ } if (!data.fmt_stream_map) { - var error = new Error('Google Docs request failed: ' + + error = new Error('Google Docs request failed: ' + 'metadata lookup returned no valid links'); error.response = res.responseText; error.reason = 'MISSING_LINKS'; From 578d3fbb235dfe214ae847ffd40c4db9aae0c820 Mon Sep 17 00:00:00 2001 From: Calvin Montgomery Date: Sat, 20 Aug 2016 10:59:20 -0700 Subject: [PATCH 4/5] Add workaround for GM sandbox and refactor userscript a bit --- gdrive-userscript/cytube-google-drive.user.js | 169 ++++++++++++------ player/gdrive-player.coffee | 16 ++ player/update.coffee | 2 +- player/videojs.coffee | 3 +- www/js/player.js | 27 ++- 5 files changed, 155 insertions(+), 62 deletions(-) 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); } From 5a81ab7ce73d66f245244c3ca0cdf3fe72f192f6 Mon Sep 17 00:00:00 2001 From: Calvin Montgomery Date: Tue, 23 Aug 2016 21:50:18 -0700 Subject: [PATCH 5/5] Add a prompt explaining the situation as well as documentation --- NEWS.md | 13 ++++ docs/gdrive-userscript-serveradmins.md | 24 ++++++++ package.json | 2 +- player/gdrive-youtube.coffee | 28 +++++++++ player/videojs.coffee | 2 + src/web/pug.js | 3 + src/web/routes/google_drive_userscript.js | 7 +++ src/web/webserver.js | 1 + templates/google_drive_userscript.pug | 74 +++++++++++++++++++++++ www/js/player.js | 32 +++++++++- 10 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 docs/gdrive-userscript-serveradmins.md create mode 100644 src/web/routes/google_drive_userscript.js create mode 100644 templates/google_drive_userscript.pug diff --git a/NEWS.md b/NEWS.md index cc4eea5e..34dd755d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,16 @@ +2016-08-23 +========== + +A few weeks ago, the previous Google Drive player stopped working. This is +nothing new; Google Drive has consistently broken a few times a year ever since +support for it was added. However, it's becoming increasingly difficult and +complicated to provide good support for Google Drive, so I've made the decision +to phase out the native player and require a userscript for it, in order to +bypass CORS and allow each browser to request the video stream itself. + +See [the updated documentation](docs/gdrive-userscript-serveradmins.md) for +details on how to enable this for your users. + 2016-04-27 ========== diff --git a/docs/gdrive-userscript-serveradmins.md b/docs/gdrive-userscript-serveradmins.md new file mode 100644 index 00000000..3f4b19a6 --- /dev/null +++ b/docs/gdrive-userscript-serveradmins.md @@ -0,0 +1,24 @@ +# Google Drive Userscript Setup + +In response to increasing difficulty and complexity of maintaining Google Drive +support, the native player is being phased out in favor of requiring a +userscript to allow each client to fetch the video stream links for themselves. +Users will be prompted with a link to `/google_drive_userscript`, which explains +the situation and instructs how to install the userscript. + +As a server admin, you must generate the userscript from the template by using +the following command: + +```sh +npm run generate-userscript [...] +``` + +The first argument is the site name as it will appear in the userscript title. +The remaining arguments are the URL patterns on which the script will run. For +example, for cytu.be I use: + +```sh +npm run generate-userscript CyTube http://cytu.be/r/* https://cytu.be/r/* +``` + +This will generate `www/js/cytube-google-drive.user.js`. diff --git a/package.json b/package.json index 9a65db52..698ae34c 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "author": "Calvin Montgomery", "name": "CyTube", "description": "Online media synchronizer and chat", - "version": "3.19.0", + "version": "3.20.0", "repository": { "url": "http://github.com/calzoneman/sync" }, diff --git a/player/gdrive-youtube.coffee b/player/gdrive-youtube.coffee index c7461c03..bb463150 100644 --- a/player/gdrive-youtube.coffee +++ b/player/gdrive-youtube.coffee @@ -7,6 +7,7 @@ window.GoogleDriveYouTubePlayer = class GoogleDriveYouTubePlayer extends Player @init(data) init: (data) -> + window.promptToInstallDriveUserscript() embed = $('').attr( type: 'application/x-shockwave-flash' src: "https://www.youtube.com/get_player?docid=#{data.id}&ps=docs\ @@ -102,3 +103,30 @@ window.GoogleDriveYouTubePlayer = class GoogleDriveYouTubePlayer extends Player cb(@yt.getVolume() / 100) else cb(VOLUME) + +window.promptToInstallDriveUserscript = -> + if document.getElementById('prompt-install-drive-userscript') + return + alertBox = document.createElement('div') + alertBox.id = 'prompt-install-drive-userscript' + alertBox.className = 'alert alert-info' + alertBox.innerHTML = """ +Due to continual breaking changes making it increasingly difficult to +maintain Google Drive support, you can now install a userscript that +simplifies the code and has better compatibility. In the future, the +old player will be removed.""" + alertBox.appendChild(document.createElement('br')) + infoLink = document.createElement('a') + infoLink.className = 'btn btn-info' + infoLink.href = '/google_drive_userscript' + infoLink.textContent = 'Click here for details' + infoLink.target = '_blank' + alertBox.appendChild(infoLink) + + closeButton = document.createElement('button') + closeButton.className = 'close pull-right' + closeButton.innerHTML = '×' + closeButton.onclick = -> + alertBox.parentNode.removeChild(alertBox) + alertBox.insertBefore(closeButton, alertBox.firstChild) + document.getElementById('videowrap').appendChild(alertBox) diff --git a/player/videojs.coffee b/player/videojs.coffee index 4ad445ac..9a3820af 100644 --- a/player/videojs.coffee +++ b/player/videojs.coffee @@ -93,6 +93,8 @@ window.VideoJSPlayer = class VideoJSPlayer extends Player @player.src(@sources[@sourceIdx]) else console.error('Out of sources, video will not play') + if @mediaType is 'gd' and not window.hasDriveUserscript + window.promptToInstallDriveUserscript() ) @setVolume(VOLUME) @player.on('ended', -> diff --git a/src/web/pug.js b/src/web/pug.js index 60815539..b944ed0b 100644 --- a/src/web/pug.js +++ b/src/web/pug.js @@ -36,6 +36,9 @@ function getBaseUrl(res) { * Renders and serves a pug template */ function sendPug(res, view, locals) { + if (!locals) { + locals = {}; + } locals.loggedIn = locals.loggedIn || !!res.user; locals.loginName = locals.loginName || res.user ? res.user.name : false; locals.superadmin = locals.superadmin || res.user ? res.user.global_rank >= 255 : false; diff --git a/src/web/routes/google_drive_userscript.js b/src/web/routes/google_drive_userscript.js new file mode 100644 index 00000000..72515bb9 --- /dev/null +++ b/src/web/routes/google_drive_userscript.js @@ -0,0 +1,7 @@ +import { sendPug } from '../pug'; + +export default function initialize(app) { + app.get('/google_drive_userscript', (req, res) => { + return sendPug(res, 'google_drive_userscript') + }); +} diff --git a/src/web/webserver.js b/src/web/webserver.js index b428517f..60ff5f83 100644 --- a/src/web/webserver.js +++ b/src/web/webserver.js @@ -177,6 +177,7 @@ module.exports = { require('./account').init(app); require('./acp').init(app); require('../google2vtt').attach(app); + require('./routes/google_drive_userscript')(app); app.use(serveStatic(path.join(__dirname, '..', '..', 'www'), { maxAge: webConfig.getCacheTTL() })); diff --git a/templates/google_drive_userscript.pug b/templates/google_drive_userscript.pug new file mode 100644 index 00000000..f0744431 --- /dev/null +++ b/templates/google_drive_userscript.pug @@ -0,0 +1,74 @@ +doctype html +html(lang="en") + head + include head + +head() + body + #wrap + nav.navbar.navbar-inverse.navbar-fixed-top(role="navigation") + include nav + +navheader() + #nav-collapsible.collapse.navbar-collapse + ul.nav.navbar-nav + +navdefaultlinks("/google_drive_userscript") + +navloginlogout("/google_drive_userscript") + section#mainpage + .container + .col-md-8.col-md-offset-2 + h1 Google Drive Userscript + h2 Why? + p. + Since Google Drive support was launched in early 2014, it has broken + at least 4-5 times, requiring increasing effort to get it working again + and disrupting many channels. This is because there is no official API + for it like there is for YouTube videos, which means support for it + relies on undocumented tricks. In August 2016, the decision was made + to phase out the native support for Google Drive and instead require + users to install a userscript, which allows to bypass certain browser + restrictions and make the code easier, simpler, and less prone to failure + (it could still break due to future Google Drive changes, but is less + likely to be difficult to fix). + h2 How It Works + p. + The userscript is a short script that you can install using a browser + extension such as Greasemonkey or Tampermonkey that runs on the page + and provides additional functionality needed to play Google Drive + videos. + h2 Installation + ul + li + strong Chrome + | —Install Tampermonkey. + li + strong Firefox + | —Install Tampermonkey + | or Greasemonkey. + li + strong Other Browsers + | —Install the appropriate userscript plugin for your browser. + | Tampermonkey supports many browsers besides Chrome. + p. + Once you have installed the userscript manager addon for your browser, + you can + install the userscript. If this link 404s, it means the administrator + of this server hasn't generated it yet. + p. + You can find a guide with screenshots of the installation process + on GitHub. + + include footer + +footer() + script(type="text/javascript"). + function showEmail(btn, email, key) { + email = unescape(email); + key = unescape(key); + var dest = new Array(email.length); + for (var i = 0; i < email.length; i++) { + dest[i] = String.fromCharCode(email.charCodeAt(i) ^ key.charCodeAt(i % key.length)); + } + email = dest.join(""); + $("").attr("href", "mailto:" + email) + .text(email) + .insertBefore(btn); + $(btn).remove(); + } diff --git a/www/js/player.js b/www/js/player.js index 11690df0..3089fe24 100644 --- a/www/js/player.js +++ b/www/js/player.js @@ -558,7 +558,10 @@ if (_this.sourceIdx < _this.sources.length) { return _this.player.src(_this.sources[_this.sourceIdx]); } else { - return console.error('Out of sources, video will not play'); + console.error('Out of sources, video will not play'); + if (_this.mediaType === 'gd' && !window.hasDriveUserscript) { + return window.promptToInstallDriveUserscript(); + } } } }); @@ -1179,6 +1182,7 @@ GoogleDriveYouTubePlayer.prototype.init = function(data) { var embed; + window.promptToInstallDriveUserscript(); embed = $('').attr({ type: 'application/x-shockwave-flash', src: "https://www.youtube.com/get_player?docid=" + data.id + "&ps=docs&partnerid=30&enablejsapi=1&cc_load_policy=1&auth_timeout=86400000000", @@ -1308,6 +1312,32 @@ })(Player); + window.promptToInstallDriveUserscript = function() { + var alertBox, closeButton, infoLink; + if (document.getElementById('prompt-install-drive-userscript')) { + return; + } + alertBox = document.createElement('div'); + alertBox.id = 'prompt-install-drive-userscript'; + alertBox.className = 'alert alert-info'; + alertBox.innerHTML = "Due to continual breaking changes making it increasingly difficult to\nmaintain Google Drive support, you can now install a userscript that\nsimplifies the code and has better compatibility. In the future, the\nold player will be removed."; + alertBox.appendChild(document.createElement('br')); + infoLink = document.createElement('a'); + infoLink.className = 'btn btn-info'; + infoLink.href = '/google_drive_userscript'; + infoLink.textContent = 'Click here for details'; + infoLink.target = '_blank'; + alertBox.appendChild(infoLink); + closeButton = document.createElement('button'); + closeButton.className = 'close pull-right'; + closeButton.innerHTML = '×'; + closeButton.onclick = function() { + return alertBox.parentNode.removeChild(alertBox); + }; + alertBox.insertBefore(closeButton, alertBox.firstChild); + return document.getElementById('videowrap').appendChild(alertBox); + }; + window.HLSPlayer = HLSPlayer = (function(superClass) { extend(HLSPlayer, superClass);