SHOW:
|
|
- or go back to the newest paste.
1 | // ==UserScript== | |
2 | // @name Google Drive Video Player for CyTube | |
3 | // @namespace gdcytube | |
4 | // @description Play Google Drive videos on CyTube | |
5 | // @include https://cytu.be/r/* | |
6 | - | // @include http://lolcow.tv/r/* |
6 | + | |
7 | - | // @include https://lolcow.tv/r/* |
7 | + | |
8 | // @grant GM_xmlhttpRequest | |
9 | // @grant GM.xmlHttpRequest | |
10 | // @connect docs.google.com | |
11 | // @run-at document-end | |
12 | // @version 1.7.0 | |
13 | // ==/UserScript== | |
14 | ||
15 | try { | |
16 | function debug(message) { | |
17 | try { | |
18 | unsafeWindow.console.log('[Drive]', message); | |
19 | } catch (error) { | |
20 | unsafeWindow.console.error(error); | |
21 | } | |
22 | } | |
23 | ||
24 | function httpRequest(opts) { | |
25 | if (typeof GM_xmlhttpRequest === 'undefined') { | |
26 | // Assume GM4.0 | |
27 | debug('Using GM4.0 GM.xmlHttpRequest'); | |
28 | GM.xmlHttpRequest(opts); | |
29 | } else { | |
30 | debug('Using old-style GM_xmlhttpRequest'); | |
31 | GM_xmlhttpRequest(opts); | |
32 | } | |
33 | } | |
34 | ||
35 | var ITAG_QMAP = { | |
36 | 37: 1080, | |
37 | 46: 1080, | |
38 | 22: 720, | |
39 | 45: 720, | |
40 | 59: 480, | |
41 | 44: 480, | |
42 | 35: 480, | |
43 | 18: 360, | |
44 | 43: 360, | |
45 | 34: 360 | |
46 | }; | |
47 | ||
48 | var ITAG_CMAP = { | |
49 | 43: 'video/webm', | |
50 | 44: 'video/webm', | |
51 | 45: 'video/webm', | |
52 | 46: 'video/webm', | |
53 | 18: 'video/mp4', | |
54 | 22: 'video/mp4', | |
55 | 37: 'video/mp4', | |
56 | 59: 'video/mp4', | |
57 | 35: 'video/flv', | |
58 | 34: 'video/flv' | |
59 | }; | |
60 | ||
61 | function getVideoInfo(id, cb) { | |
62 | var url = 'https://docs.google.com/get_video_info?authuser=' | |
63 | + '&docid=' + id | |
64 | + '&sle=true' | |
65 | + '&hl=en'; | |
66 | debug('Fetching ' + url); | |
67 | ||
68 | httpRequest({ | |
69 | method: 'GET', | |
70 | url: url, | |
71 | onload: function (res) { | |
72 | try { | |
73 | debug('Got response ' + res.responseText); | |
74 | ||
75 | if (res.status !== 200) { | |
76 | debug('Response status not 200: ' + res.status); | |
77 | return cb( | |
78 | 'Google Drive request failed: HTTP ' + res.status | |
79 | ); | |
80 | } | |
81 | ||
82 | var data = {}; | |
83 | var error; | |
84 | // Google Santa sometimes eats login cookies and gets mad if there aren't any. | |
85 | if(/accounts\.google\.com\/ServiceLogin/.test(res.responseText)){ | |
86 | error = 'Google Docs request failed: ' + | |
87 | 'This video requires you be logged into a Google account. ' + | |
88 | 'Open your Gmail in another tab and then refresh video.'; | |
89 | return cb(error); | |
90 | } | |
91 | ||
92 | res.responseText.split('&').forEach(function (kv) { | |
93 | var pair = kv.split('='); | |
94 | data[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]); | |
95 | }); | |
96 | ||
97 | if (data.status === 'fail') { | |
98 | error = 'Google Drive request failed: ' + | |
99 | unescape(data.reason).replace(/\+/g, ' '); | |
100 | return cb(error); | |
101 | } | |
102 | ||
103 | if (!data.fmt_stream_map) { | |
104 | error = ( | |
105 | 'Google has removed the video streams associated' + | |
106 | ' with this item. It can no longer be played.' | |
107 | ); | |
108 | ||
109 | return cb(error); | |
110 | } | |
111 | ||
112 | data.links = {}; | |
113 | data.fmt_stream_map.split(',').forEach(function (item) { | |
114 | var pair = item.split('|'); | |
115 | data.links[pair[0]] = pair[1]; | |
116 | }); | |
117 | data.videoMap = mapLinks(data.links); | |
118 | ||
119 | cb(null, data); | |
120 | } catch (error) { | |
121 | unsafeWindow.console.error(error); | |
122 | } | |
123 | }, | |
124 | ||
125 | onerror: function () { | |
126 | var error = 'Google Drive request failed: ' + | |
127 | 'metadata lookup HTTP request failed'; | |
128 | error.reason = 'HTTP_ONERROR'; | |
129 | return cb(error); | |
130 | } | |
131 | }); | |
132 | } | |
133 | ||
134 | function mapLinks(links) { | |
135 | var videos = { | |
136 | 1080: [], | |
137 | 720: [], | |
138 | 480: [], | |
139 | 360: [] | |
140 | }; | |
141 | ||
142 | Object.keys(links).forEach(function (itag) { | |
143 | itag = parseInt(itag, 10); | |
144 | if (!ITAG_QMAP.hasOwnProperty(itag)) { | |
145 | return; | |
146 | } | |
147 | ||
148 | videos[ITAG_QMAP[itag]].push({ | |
149 | itag: itag, | |
150 | contentType: ITAG_CMAP[itag], | |
151 | link: links[itag] | |
152 | }); | |
153 | }); | |
154 | ||
155 | return videos; | |
156 | } | |
157 | ||
158 | /* | |
159 | * Greasemonkey 2.0 has this wonderful sandbox that attempts | |
160 | * to prevent script developers from shooting themselves in | |
161 | * the foot by removing the trigger from the gun, i.e. it's | |
162 | * impossible to cross the boundary between the browser JS VM | |
163 | * and the privileged sandbox that can run GM_xmlhttpRequest(). | |
164 | * | |
165 | * So in this case, we have to resort to polling a special | |
166 | * variable to see if getGoogleDriveMetadata needs to be called | |
167 | * and deliver the result into another special variable that is | |
168 | * being polled on the browser side. | |
169 | */ | |
170 | ||
171 | /* | |
172 | * Browser side function -- sets gdUserscript.pollID to the | |
173 | * ID of the Drive video to be queried and polls | |
174 | * gdUserscript.pollResult for the result. | |
175 | */ | |
176 | function getGoogleDriveMetadata_GM(id, callback) { | |
177 | debug('Setting GD poll ID to ' + id); | |
178 | unsafeWindow.gdUserscript.pollID = id; | |
179 | var tries = 0; | |
180 | var i = setInterval(function () { | |
181 | if (unsafeWindow.gdUserscript.pollResult) { | |
182 | debug('Got result'); | |
183 | clearInterval(i); | |
184 | var result = unsafeWindow.gdUserscript.pollResult; | |
185 | unsafeWindow.gdUserscript.pollResult = null; | |
186 | callback(result.error, result.result); | |
187 | } else if (++tries > 100) { | |
188 | // Took longer than 10 seconds, give up | |
189 | clearInterval(i); | |
190 | } | |
191 | }, 100); | |
192 | } | |
193 | ||
194 | /* | |
195 | * Sandbox side function -- polls gdUserscript.pollID for | |
196 | * the ID of a Drive video to be queried, looks up the | |
197 | * metadata, and stores it in gdUserscript.pollResult | |
198 | */ | |
199 | function setupGDPoll() { | |
200 | unsafeWindow.gdUserscript = cloneInto({}, unsafeWindow); | |
201 | var pollInterval = setInterval(function () { | |
202 | if (unsafeWindow.gdUserscript.pollID) { | |
203 | var id = unsafeWindow.gdUserscript.pollID; | |
204 | unsafeWindow.gdUserscript.pollID = null; | |
205 | debug('Polled and got ' + id); | |
206 | getVideoInfo(id, function (error, data) { | |
207 | unsafeWindow.gdUserscript.pollResult = cloneInto({ | |
208 | error: error, | |
209 | result: data | |
210 | }, unsafeWindow); | |
211 | }); | |
212 | } | |
213 | }, 1000); | |
214 | } | |
215 | ||
216 | var TM_COMPATIBLES = [ | |
217 | 'Tampermonkey', | |
218 | 'Violentmonkey' // https://github.com/calzoneman/sync/issues/713 | |
219 | ]; | |
220 | ||
221 | function isTampermonkeyCompatible() { | |
222 | try { | |
223 | return TM_COMPATIBLES.indexOf(GM_info.scriptHandler) >= 0; | |
224 | } catch (error) { | |
225 | return false; | |
226 | } | |
227 | } | |
228 | ||
229 | if (isTampermonkeyCompatible()) { | |
230 | unsafeWindow.getGoogleDriveMetadata = getVideoInfo; | |
231 | } else { | |
232 | debug('Using non-TM polling workaround'); | |
233 | unsafeWindow.getGoogleDriveMetadata = exportFunction( | |
234 | getGoogleDriveMetadata_GM, unsafeWindow); | |
235 | setupGDPoll(); | |
236 | } | |
237 | ||
238 | unsafeWindow.console.log('Initialized userscript Google Drive player'); | |
239 | unsafeWindow.hasDriveUserscript = true; | |
240 | // Checked against GS_VERSION from data.js | |
241 | unsafeWindow.driveUserscriptVersion = '1.7'; | |
242 | } catch (error) { | |
243 | unsafeWindow.console.error(error); | |
244 | } |