sync/gdrive-userscript/cytube-google-drive.user.js

244 lines
7.8 KiB
JavaScript

// ==UserScript==
// @name Google Drive Video Player for {SITENAME}
// @namespace gdcytube
// @description Play Google Drive videos on {SITENAME}
// {INCLUDE_BLOCK}
// @grant unsafeWindow
// @grant GM_xmlhttpRequest
// @grant GM.xmlHttpRequest
// @connect docs.google.com
// @run-at document-end
// @version 1.7.0
// ==/UserScript==
try {
function debug(message) {
try {
unsafeWindow.console.log('[Drive]', message);
} catch (error) {
unsafeWindow.console.error(error);
}
}
function httpRequest(opts) {
if (typeof GM_xmlhttpRequest === 'undefined') {
// Assume GM4.0
debug('Using GM4.0 GM.xmlHttpRequest');
GM.xmlHttpRequest(opts);
} else {
debug('Using old-style GM_xmlhttpRequest');
GM_xmlhttpRequest(opts);
}
}
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/get_video_info?authuser='
+ '&docid=' + id
+ '&sle=true'
+ '&hl=en';
debug('Fetching ' + url);
httpRequest({
method: 'GET',
url: url,
onload: function (res) {
try {
debug('Got response ' + res.responseText);
if (res.status !== 200) {
debug('Response status not 200: ' + res.status);
return cb(
'Google Drive request failed: HTTP ' + res.status
);
}
var data = {};
var error;
// Google Santa sometimes eats login cookies and gets mad if there aren't any.
if(/accounts\.google\.com\/ServiceLogin/.test(res.responseText)){
error = 'Google Docs request failed: ' +
'This video requires you be logged into a Google account. ' +
'Open your Gmail in another tab and then refresh video.';
return cb(error);
}
res.responseText.split('&').forEach(function (kv) {
var pair = kv.split('=');
data[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]);
});
if (data.status === 'fail') {
error = 'Google Drive request failed: ' +
unescape(data.reason).replace(/\+/g, ' ');
return cb(error);
}
if (!data.fmt_stream_map) {
error = (
'Google has removed the video streams associated' +
' with this item. It can no longer be played.'
);
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);
}
},
onerror: function () {
var error = 'Google Drive 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;
}
/*
* 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.
*/
/*
* 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);
}
/*
* 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);
}
var TM_COMPATIBLES = [
'Tampermonkey',
'Violentmonkey' // https://github.com/calzoneman/sync/issues/713
];
function isTampermonkeyCompatible() {
try {
return TM_COMPATIBLES.indexOf(GM_info.scriptHandler) >= 0;
} catch (error) {
return false;
}
}
if (isTampermonkeyCompatible()) {
unsafeWindow.getGoogleDriveMetadata = getVideoInfo;
} else {
debug('Using non-TM polling workaround');
unsafeWindow.getGoogleDriveMetadata = exportFunction(
getGoogleDriveMetadata_GM, unsafeWindow);
setupGDPoll();
}
unsafeWindow.console.log('Initialized userscript Google Drive player');
unsafeWindow.hasDriveUserscript = true;
// Checked against GS_VERSION from data.js
unsafeWindow.driveUserscriptVersion = '1.7';
} catch (error) {
unsafeWindow.console.error(error);
}