|
1 | 1 | // ==UserScript==
|
2 | 2 | // @name Grabber
|
3 | 3 | // @namespace https://github.com/lap00zza/
|
4 |
| -// @version 0.5.1 |
| 4 | +// @version 0.6.0 |
5 | 5 | // @description Grab links from 9anime!
|
6 | 6 | // @author Jewel Mahanta
|
7 | 7 | // @icon https://image.ibb.co/fnOY7k/icon48.png
|
|
18 | 18 |
|
19 | 19 | (function () {
|
20 | 20 | 'use strict'
|
| 21 | + // TODO: convert anime names to Title Case |
| 22 | + // TODO: add a option to select quality for 9anime server. |
| 23 | + // right now it just grabs everything. |
| 24 | + |
21 | 25 | console.log('Grabber ' + GM_info.script.version + ' is now running!')
|
22 | 26 | var dlInProgress = false // global switch to indicate dl status
|
23 | 27 | var dlEpisodeIds = [] // list of id's currently being grabbed
|
24 |
| - var dlServerType = '' // FIXME: cant queue different server types together |
| 28 | + var dlServerType = '' |
25 | 29 | var dlAggregateLinks = '' // stores all the grabbed links as a single string
|
26 | 30 | var ts = document.getElementsByTagName('body')[0].dataset['ts'] // ts is needed to send API requests
|
27 | 31 | var animeName = document.querySelectorAll('h1.title')[0].innerText
|
|
114 | 118 | }
|
115 | 119 |
|
116 | 120 | /********************************************************************************************************************/
|
| 121 | + // Utility functions |
117 | 122 | /**
|
118 | 123 | * Just as the function name says!
|
119 |
| - * We replace the illegal characters with underscore (_) |
| 124 | + * We remove the illegal characters. |
120 | 125 | * @param filename
|
121 | 126 | * @returns {string}
|
122 | 127 | */
|
123 | 128 | function generateFileSafeString (filename) {
|
124 | 129 | var re = /[\\/<>*?:"|]/gi
|
125 |
| - return filename.replace(re, '_') |
| 130 | + return filename.replace(re, '') |
126 | 131 | }
|
127 | 132 |
|
| 133 | + // metadataUrl is a part of createMetadataFile |
128 | 134 | var metadataUrl = null
|
129 | 135 | /**
|
130 | 136 | * This functions generates the blob for the `metadata.json`
|
|
157 | 163 | }
|
158 | 164 | }
|
159 | 165 |
|
| 166 | + /** |
| 167 | + * Generate the query parameters from an object. |
| 168 | + * @param {object} params |
| 169 | + * @returns {string} |
| 170 | + */ |
| 171 | + function generateQParams (params) { |
| 172 | + var qParams = '' |
| 173 | + var dKeys = Object.keys(params) |
| 174 | + for (var i = 0; i < dKeys.length; i++) { |
| 175 | + if (i === 0) { |
| 176 | + qParams += dKeys[i] + '=' + params[dKeys[i]] |
| 177 | + } else { |
| 178 | + qParams += '&' + dKeys[i] + '=' + params[dKeys[i]] |
| 179 | + } |
| 180 | + } |
| 181 | + return qParams |
| 182 | + } |
| 183 | + |
| 184 | + // parser is a part of getURL |
| 185 | + var parser = document.createElement('a') |
| 186 | + /** |
| 187 | + * Get a url from a uri string. |
| 188 | + * Credits to jlong for this implementation idea: |
| 189 | + * https://gist.github.com/jlong/2428561 |
| 190 | + * @param {string} uriString |
| 191 | + * @returns {string} |
| 192 | + */ |
| 193 | + function getURL (uriString) { |
| 194 | + parser.href = uriString |
| 195 | + return parser.protocol + '//' + parser.hostname + parser.pathname |
| 196 | + } |
| 197 | + |
| 198 | + /** |
| 199 | + * Converts the searchParams in then uri string to |
| 200 | + * an object. |
| 201 | + * @param {string} uriString |
| 202 | + * @returns {object} |
| 203 | + */ |
| 204 | + function searchParams2Obj (uriString) { |
| 205 | + parser.href = uriString |
| 206 | + // HTMLHyperlinkElementUtils.search returns a search |
| 207 | + // string, also called a query string containing a '?' |
| 208 | + // followed by the parameters of the URL. We don't need |
| 209 | + // the '?' so we slice it. |
| 210 | + var searchParams = parser.search.slice(1) |
| 211 | + // All search params are delimited by '&'. |
| 212 | + // So we split them into an array and iterate |
| 213 | + // through it to get the keys and values. |
| 214 | + var search = searchParams.split('&') |
| 215 | + var searchObj = {} |
| 216 | + for (var i = 0; i < search.length; i++) { |
| 217 | + var searchSplit = search[i].split('=') |
| 218 | + if (searchSplit[0] !== '' && searchSplit[1] !== undefined) { |
| 219 | + searchObj[searchSplit[0]] = searchSplit[1] |
| 220 | + } |
| 221 | + } |
| 222 | + return searchObj |
| 223 | + } |
| 224 | + |
| 225 | + /** |
| 226 | + * A simple helper function that merges 2 objects. |
| 227 | + * @param {object} obj1 |
| 228 | + * @param {object} obj2 |
| 229 | + * @returns {object} |
| 230 | + */ |
| 231 | + function mergeObject (obj1, obj2) { |
| 232 | + var obj3 = {} |
| 233 | + for (var a in obj1) { |
| 234 | + if (obj1.hasOwnProperty(a)) { |
| 235 | + obj3[a] = obj1[a] |
| 236 | + } |
| 237 | + } |
| 238 | + for (var b in obj2) { |
| 239 | + if (obj2.hasOwnProperty(b)) { |
| 240 | + obj3[b] = obj2[b] |
| 241 | + } |
| 242 | + } |
| 243 | + return obj3 |
| 244 | + } |
| 245 | + |
| 246 | + /********************************************************************************************************************/ |
160 | 247 | /**
|
161 | 248 | * Generates the name of the original mp4 file (RapidVideo).
|
162 | 249 | * @param url
|
|
214 | 301 | })
|
215 | 302 | }
|
216 | 303 |
|
| 304 | + /** |
| 305 | + * Fetch 9anime video links for Server F4 etc. |
| 306 | + * @param {string} url |
| 307 | + * The 9anime url to grab videos |
| 308 | + * @param {object} params |
| 309 | + * A list of query parameters to send to the API. |
| 310 | + * @returns {Promise} |
| 311 | + */ |
| 312 | + function getVideoLinks9a (url, params) { |
| 313 | + return new Promise(function (resolve, reject) { |
| 314 | + var xhr = new window.XMLHttpRequest() |
| 315 | + xhr.open('GET', url + '?' + generateQParams(params), true) |
| 316 | + xhr.onload = function () { |
| 317 | + // Some error codes don't trigger the |
| 318 | + // onerror. So we make sure that we only |
| 319 | + // parse the response text for 200. |
| 320 | + if (xhr.status === 200) { |
| 321 | + try { |
| 322 | + resolve(JSON.parse(xhr.responseText)) |
| 323 | + } catch (e) { |
| 324 | + // This is when there is an error |
| 325 | + // parsing the response text |
| 326 | + reject(e) |
| 327 | + } |
| 328 | + } else { |
| 329 | + reject(xhr.statusText) |
| 330 | + } |
| 331 | + } |
| 332 | + xhr.onerror = function () { |
| 333 | + console.log('error') |
| 334 | + reject(xhr.statusText) |
| 335 | + } |
| 336 | + xhr.send() |
| 337 | + }) |
| 338 | + } |
| 339 | + |
217 | 340 | /**
|
218 | 341 | * Get the grabber info from the 9anime API.
|
219 |
| - * @param {string} qParams |
| 342 | + * @param {object} params |
220 | 343 | * A list of query parameters to send to the API.
|
221 | 344 | * @returns {Promise}
|
222 | 345 | */
|
223 |
| - function getGrabber (qParams) { |
| 346 | + function getGrabber (params) { |
224 | 347 | return new Promise(function (resolve, reject) {
|
225 | 348 | var xhr = new window.XMLHttpRequest()
|
226 |
| - xhr.open('GET', '/ajax/episode/info?' + qParams, true) |
| 349 | + xhr.open('GET', '/ajax/episode/info' + '?' + generateQParams(params), true) |
227 | 350 | xhr.onload = function () {
|
228 | 351 | // Some error codes don't trigger the
|
229 | 352 | // onerror. So we make sure that we only
|
|
290 | 413 | var ep = dlEpisodeIds.shift()
|
291 | 414 | grabberStatus.innerHTML = 'Fetching ' + ep.num
|
292 | 415 |
|
293 |
| - var data = { |
| 416 | + var params = { |
294 | 417 | ts: ts,
|
295 | 418 | id: ep.id,
|
296 | 419 | update: 0
|
297 | 420 | }
|
298 |
| - data['_'] = generateToken(data) |
| 421 | + params['_'] = generateToken(params) |
299 | 422 |
|
300 |
| - // Generate the query parameters |
301 |
| - var qParams = '' |
302 |
| - var dKeys = Object.keys(data) |
303 |
| - for (var i = 0; i < dKeys.length; i++) { |
304 |
| - if (i === 0) { |
305 |
| - qParams += dKeys[i] + '=' + data[dKeys[i]] |
306 |
| - } else { |
307 |
| - qParams += '&' + dKeys[i] + '=' + data[dKeys[i]] |
308 |
| - } |
309 |
| - } |
310 |
| - getGrabber(qParams) |
| 423 | + getGrabber(params) |
311 | 424 | .then(function (resp) {
|
312 |
| - if (dlServerType === 'RapidVideo') { |
313 |
| - getVideoLinksRV(resp['target']) |
314 |
| - .then(function (resp) { |
315 |
| - dlAggregateLinks += resp[0]['file'] + '\n' |
316 |
| - var fileSafeName = generateFileSafeString(animeName + '-ep_' + ep.num + '-' + resp[0]['label']) + '.mp4' |
317 |
| - // Metadata only for RapidVideo |
318 |
| - metadata.files.push({ |
319 |
| - original: generateRVOriginal(resp[0]['file']), |
320 |
| - real: fileSafeName.toLowerCase() |
| 425 | + switch (dlServerType) { |
| 426 | + case 'RapidVideo': |
| 427 | + getVideoLinksRV(resp['target']) |
| 428 | + .then(function (resp) { |
| 429 | + dlAggregateLinks += resp[0]['file'] + '\n' |
| 430 | + var fileSafeName = generateFileSafeString(animeName + '-ep_' + ep.num + '-' + resp[0]['label']) + '.mp4' |
| 431 | + // Metadata only for RapidVideo |
| 432 | + metadata.files.push({ |
| 433 | + original: generateRVOriginal(resp[0]['file']), |
| 434 | + real: fileSafeName.toLowerCase() |
| 435 | + }) |
| 436 | + grabberStatus.innerHTML = 'Completed ' + ep.num |
| 437 | + requeue() |
| 438 | + }) |
| 439 | + .catch(function (e) { |
| 440 | + console.debug(e) |
| 441 | + grabberStatus.innerHTML = '<span class="grabber--fail">Failed ' + ep.num + '</span>' |
| 442 | + requeue() |
| 443 | + }) |
| 444 | + break |
| 445 | + |
| 446 | + case '9anime': |
| 447 | + var data = { |
| 448 | + ts: ts, |
| 449 | + id: resp['params']['id'], |
| 450 | + options: resp['params']['options'], |
| 451 | + token: resp['params']['token'], |
| 452 | + mobile: 0 |
| 453 | + } |
| 454 | + var url = getURL(resp['grabber']) |
| 455 | + // The grabber url has additional search params |
| 456 | + // we need to add those to 'data' before generating |
| 457 | + // the token. |
| 458 | + var sParams = searchParams2Obj(resp['grabber']) |
| 459 | + var merged = mergeObject(data, sParams) |
| 460 | + var initState = s(a(DD + url, '')) |
| 461 | + merged['_'] = generateToken(merged, initState) |
| 462 | + getVideoLinks9a(url, merged) |
| 463 | + .then(function (resp) { |
| 464 | + // resp is of the format |
| 465 | + // {data: [{file: '', label: '', type: ''}], error: null, token: ''} |
| 466 | + // data contains the files array. |
| 467 | + var data = resp['data'] |
| 468 | + for (var i = 0; i < data.length; i++) { |
| 469 | + var title = generateFileSafeString(animeName + '-ep_' + ep.num + '-' + data[i]['label']) |
| 470 | + dlAggregateLinks += data[i]['file'] + '?&title=' + title.toLowerCase() + |
| 471 | + '&type=video/' + data[i]['type'] + '\n' |
| 472 | + } |
| 473 | + grabberStatus.innerHTML = 'Completed ' + ep.num |
| 474 | + requeue() |
| 475 | + }) |
| 476 | + .catch(function (e) { |
| 477 | + console.debug(e) |
| 478 | + grabberStatus.innerHTML = '<span class="grabber--fail">Failed ' + ep.num + '</span>' |
| 479 | + requeue() |
321 | 480 | })
|
322 |
| - grabberStatus.innerHTML = 'Completed ' + ep.num |
323 |
| - requeue() |
324 |
| - }) |
325 |
| - .catch(function () { |
326 |
| - grabberStatus.innerHTML = '<span class="grabber--fail">Failed ' + ep.num + '</span>' |
327 |
| - requeue() |
328 |
| - }) |
| 481 | + break |
329 | 482 | }
|
330 | 483 | })
|
331 | 484 | .catch(function () {
|
|
376 | 529 | // Attach the 'Grab All' button to RapidVideo for now.
|
377 | 530 | var serverLabels = document.querySelectorAll('.server.row > label')
|
378 | 531 | for (var i = 0; i < serverLabels.length; i++) {
|
| 532 | + // Remove the leading and trailing whitespace |
| 533 | + // from the server labels. |
379 | 534 | var serverLabel = serverLabels[i].innerText.trim()
|
380 |
| - if (/^RapidVideo$/i.test(serverLabel)) { |
381 |
| - console.log(serverLabels[i]) |
| 535 | + if (/RapidVideo/i.test(serverLabel)) { |
382 | 536 | serverLabels[i].appendChild(generateDlBtn('RapidVideo'))
|
| 537 | + } else if (/Server\s+F/i.test(serverLabel)) { |
| 538 | + serverLabels[i].appendChild(generateDlBtn('9anime')) |
383 | 539 | }
|
384 | 540 | }
|
385 | 541 | })()
|
0 commit comments