Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2bc7e41b2 | ||
|
|
a7bc34df0d | ||
|
|
b2ecd05586 | ||
|
|
8b9f9a5e1c | ||
|
|
376ff09632 | ||
|
|
7926b2fd9a | ||
|
|
3f2a920e1e | ||
|
|
1555229635 | ||
|
|
7c085caaa0 | ||
|
|
cc0bf4dfb1 | ||
|
|
00857ba46f | ||
|
|
b77a35e0e9 | ||
|
|
ca59e3b2fd | ||
|
|
95c0c4d6d3 | ||
|
|
0d2d36251a | ||
|
|
48a58ffca6 | ||
|
|
505e6c67ce | ||
|
|
83e8a5e08c | ||
|
|
c82319a2c6 | ||
|
|
1fe7c697c5 | ||
|
|
239d1c60a3 | ||
|
|
bdfc96d56e | ||
|
|
8f7babd809 | ||
|
|
c708df574b | ||
|
|
401a511668 | ||
|
|
969879921e | ||
|
|
546ba9b45a | ||
|
|
27bdf54782 | ||
|
|
beed932e93 | ||
|
|
e5c4c08e66 | ||
|
|
2b201b0785 | ||
|
|
fdf5805911 | ||
|
|
9191075f48 | ||
|
|
9f73e4f865 | ||
|
|
1f20e028e1 | ||
|
|
da0fb17015 | ||
|
|
2aa71832b3 | ||
|
|
876def4392 | ||
|
|
0ba51b7270 | ||
|
|
7da4289097 | ||
|
|
ce5038cf08 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
dist/
|
||||
node_modules/
|
||||
package-lock.json
|
||||
|
||||
@@ -3,6 +3,11 @@ sudo: false
|
||||
node_js:
|
||||
- 8
|
||||
- 9
|
||||
- 10
|
||||
- 11
|
||||
- 12
|
||||
# - 13
|
||||
# - 14
|
||||
before_install:
|
||||
- npm install --only=dev
|
||||
script:
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
# Crunchy: a fork of Deathspike/CrunchyRoll.js
|
||||
|
||||
[](http://issuestats.com/github/Godzil/Crunchy) [](https://travis-ci.org/Godzil/Crunchy) [](https://codeclimate.com/github/Godzil/Crunchy/maintainability)
|
||||
[](https://travis-ci.org/Godzil/Crunchy)
|
||||
[](https://codeclimate.com/github/Godzil/Crunchy/maintainability)
|
||||

|
||||

|
||||

|
||||
|
||||
*Crunchy* is capable of downloading *anime* episodes from the popular *CrunchyRoll* streaming service. An episode is stored in the original video format (often H.264 in a MP4 container) and the configured subtitle format (ASS or SRT).The two output files are then merged into a single MKV file.
|
||||
|
||||
|
||||
3034
package-lock.json
generated
3034
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
47
package.json
47
package.json
@@ -15,40 +15,41 @@
|
||||
"engines": {
|
||||
"node": ">=5.0"
|
||||
},
|
||||
"version": "1.4.1",
|
||||
"version": "1.5.1",
|
||||
"bin": {
|
||||
"crunchy": "./bin/crunchy",
|
||||
"crunchy.sh": "./bin/crunchy.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"big-integer": "^1.6.32",
|
||||
"bluebird": "^3.5.1",
|
||||
"big-integer": "^1.6.48",
|
||||
"bluebird": "^3.7.2",
|
||||
"brotli": "^1.3.2",
|
||||
"cheerio": "^0.22.0",
|
||||
"cloudscraper": "^1.5.0",
|
||||
"commander": "^2.16.0",
|
||||
"fs-extra": "^7.0.0",
|
||||
"mkdirp": "^0.5.0",
|
||||
"cloudscraper": "^4.6.0",
|
||||
"commander": "^5.0.0",
|
||||
"fs-extra": "^9.0.0",
|
||||
"mkdirp": "^1.0.4",
|
||||
"pjson": "^1.0.9",
|
||||
"request": "^2.87.0",
|
||||
"request-promise": "^4.2.2",
|
||||
"request": "^2.88.2",
|
||||
"request-promise": "^4.2.5",
|
||||
"tough-cookie-file-store": "^1.2.0",
|
||||
"uuid": "^3.3.2",
|
||||
"xml2js": "^0.4.5"
|
||||
"uuid": "^7.0.3",
|
||||
"xml2js": "^0.4.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bluebird": "^3.5.23",
|
||||
"@types/cheerio": "^0.22.8",
|
||||
"@types/fs-extra": "^5.0.4",
|
||||
"@types/mkdirp": "^0.5.2",
|
||||
"@types/node": "^10.5.3",
|
||||
"@types/request": "^2.47.1",
|
||||
"@types/request-promise": "^4.1.42",
|
||||
"@types/uuid": "^3.4.3",
|
||||
"@types/xml2js": "^0.4.3",
|
||||
"npm-check": "^5.7.1",
|
||||
"@types/bluebird": "^3.5.30",
|
||||
"@types/cheerio": "^0.22.17",
|
||||
"@types/fs-extra": "^8.1.0",
|
||||
"@types/mkdirp": "^1.0.0",
|
||||
"@types/node": "^13.11.1",
|
||||
"@types/request": "^2.48.4",
|
||||
"@types/request-promise": "^4.1.46",
|
||||
"@types/uuid": "^7.0.2",
|
||||
"@types/xml2js": "^0.4.5",
|
||||
"npm-check": "^5.9.2",
|
||||
"tsconfig-lint": "^0.12.0",
|
||||
"tslint": "^5.11.0",
|
||||
"typescript": "^2.9.2"
|
||||
"tslint": "^6.1.1",
|
||||
"typescript": "^3.8.3"
|
||||
},
|
||||
"scripts": {
|
||||
"prepublishOnly": "npm run build",
|
||||
|
||||
66
src/batch.ts
66
src/batch.ts
@@ -33,6 +33,11 @@ export default function(args: string[], done: (err?: Error) => void)
|
||||
batchPath = path.normalize(path.join(process.cwd(), config.batch));
|
||||
}
|
||||
|
||||
if (config.nametmpl === undefined)
|
||||
{
|
||||
config.nametmpl = '{SERIES_TITLE} - s{SEASON_NUMBER}e{EPISODE_NUMBER} - {EPISODE_TITLE} - [{TAG}]';
|
||||
}
|
||||
|
||||
// Update the config file with new parameters
|
||||
cfg.save(config);
|
||||
|
||||
@@ -88,7 +93,7 @@ export default function(args: string[], done: (err?: Error) => void)
|
||||
return done(err);
|
||||
}
|
||||
|
||||
if (tasksArr[0].address === '')
|
||||
if (!tasksArr || !tasksArr[0] || (tasksArr[0].address === ''))
|
||||
{
|
||||
return done();
|
||||
}
|
||||
@@ -195,7 +200,7 @@ function split(value: string): string[]
|
||||
return pieces;
|
||||
}
|
||||
|
||||
function get_min_filter(filter: string): number
|
||||
function get_min_filter(filter: string): IEpisodeNumber
|
||||
{
|
||||
if (filter !== undefined)
|
||||
{
|
||||
@@ -209,13 +214,41 @@ function get_min_filter(filter: string): number
|
||||
|
||||
if (tok[0] !== '')
|
||||
{
|
||||
return parseInt(tok[0], 10);
|
||||
/* If first item is not empty, ie '10-20' */
|
||||
if (tok[0].includes('e'))
|
||||
{
|
||||
/* include a e so we probably have something like 5e10
|
||||
aka season 5 episode 10
|
||||
*/
|
||||
const tok2 = tok[0].split('else');
|
||||
if (tok2.length > 2)
|
||||
{
|
||||
log.error('Invalid episode filter \'' + filter + '\'');
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
if (tok[0] !== '')
|
||||
{
|
||||
/* So season is properly filled */
|
||||
return {season: parseInt(tok2[0], 10), episode: parseInt(tok2[1], 10)};
|
||||
}
|
||||
else
|
||||
{
|
||||
/* we have 'e10' */
|
||||
return {season: 0, episode: parseInt(tok2[1], 10)};
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return {season: 0, episode: parseInt(tok[0], 10)};
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
/* First item is empty, ie '-20' */
|
||||
return {season: 0, episode: 0};
|
||||
}
|
||||
|
||||
function get_max_filter(filter: string): number
|
||||
function get_max_filter(filter: string): IEpisodeNumber
|
||||
{
|
||||
if (filter !== undefined)
|
||||
{
|
||||
@@ -230,15 +263,15 @@ function get_max_filter(filter: string): number
|
||||
if ((tok.length > 1) && (tok[1] !== ''))
|
||||
{
|
||||
/* We have a max value */
|
||||
return parseInt(tok[1], 10);
|
||||
return {season: +Infinity, episode: parseInt(tok[1], 10)};
|
||||
}
|
||||
else if ((tok.length === 1) && (tok[0] !== ''))
|
||||
{
|
||||
/* A single episode has been requested */
|
||||
return parseInt(tok[0], 10);
|
||||
return {season: +Infinity, episode: parseInt(tok[0], 10) + 1};
|
||||
}
|
||||
}
|
||||
return +Infinity;
|
||||
return {season: +Infinity, episode: +Infinity};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -252,7 +285,15 @@ function checkURL(address: string): boolean
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (address.startsWith('http:\/\/'))
|
||||
if (address.startsWith('https:\/\/'))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (address.startsWith('@http:\/\/'))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (address.startsWith('@https:\/\/'))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -277,7 +318,7 @@ function tasks(config: IConfigLine, batchPath: string, done: (err: Error, tasks?
|
||||
episode_min: get_min_filter(config.episodes), episode_max: get_max_filter(config.episodes)};
|
||||
}
|
||||
|
||||
return {address: '', retry: 0, episode_min: 0, episode_max: 0};
|
||||
return {address: '', retry: 0, episode_min: {season: 0, episode: 0}, episode_max: {season: 0, episode: 0}};
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -341,17 +382,18 @@ function parse(args: string[]): IConfigLine
|
||||
// Episode filter
|
||||
.option('-e, --episodes <s>', 'Episode list. Read documentation on how to use')
|
||||
// Settings
|
||||
.option('-l, --crlang <s>', 'CR page language (valid: en, fr, es, it, pt, de, ru).')
|
||||
.option('-f, --format <s>', 'The subtitle format.', 'ass')
|
||||
.option('-o, --output <s>', 'The output path.')
|
||||
.option('-s, --series <s>', 'The series name override.')
|
||||
.option('--ignoredub', 'Experimental: Ignore all seasons where the title end with \'Dub)\'')
|
||||
.option('-n, --nametmpl <s>', 'Output name template', '{SERIES_TITLE} - s{SEASON_NUMBER}e{EPISODE_NUMBER} - {EPISODE_TITLE} - [{TAG}]')
|
||||
.option('-n, --nametmpl <s>', 'Output name template')
|
||||
.option('-t, --tag <s>', 'The subgroup.', 'CrunchyRoll')
|
||||
.option('-r, --resolution <s>', 'The video resolution. (valid: 360, 480, 720, 1080)', '1080')
|
||||
.option('-b, --batch <s>', 'Batch file', 'CrunchyRoll.txt')
|
||||
.option('--verbose', 'Make tool verbose')
|
||||
.option('--debug', 'Create a debug file. Use only if requested!')
|
||||
.option('--rebuildcrp', 'Rebuild the crpersistant file.')
|
||||
.option('--retry <i>', 'Number or time to retry fetching an episode.', 5)
|
||||
.option('--retry <i>', 'Number or time to retry fetching an episode.', '5')
|
||||
.parse(args);
|
||||
}
|
||||
|
||||
23
src/cli.ts
23
src/cli.ts
@@ -11,18 +11,25 @@ log.info('Crunchy version ' + current_version);
|
||||
request.get({ uri: 'https://box.godzil.net/getVersion.php?tool=crunchy&v=' + current_version },
|
||||
(error: Error, response: any, body: any) =>
|
||||
{
|
||||
const onlinepkg = JSON.parse(body);
|
||||
if (onlinepkg.status === 'ok')
|
||||
if (response && (response.statusCode === 200))
|
||||
{
|
||||
let tmp = current_version.split('.');
|
||||
const cur = (Number(tmp[0]) * 10000) + (Number(tmp[1]) * 100) + Number(tmp[2]);
|
||||
tmp = onlinepkg.version.split('.');
|
||||
const dist = (Number(tmp[0]) * 10000) + (Number(tmp[1]) * 100) + Number(tmp[2]);
|
||||
if (dist > cur)
|
||||
const onlinepkg = JSON.parse(body);
|
||||
if (onlinepkg.status === 'ok')
|
||||
{
|
||||
log.warnMore('There is a newer version of crunchy (v' + onlinepkg.version + '), you should update!');
|
||||
let tmp = current_version.split('.');
|
||||
const cur = (Number(tmp[0]) * 10000) + (Number(tmp[1]) * 100) + Number(tmp[2]);
|
||||
tmp = onlinepkg.version.split('.');
|
||||
const dist = (Number(tmp[0]) * 10000) + (Number(tmp[1]) * 100) + Number(tmp[2]);
|
||||
if (dist > cur)
|
||||
{
|
||||
log.warnMore('There is a newer version of crunchy (v' + onlinepkg.version + '), you should update!');
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
log.info('Error while checking for the current version.');
|
||||
}
|
||||
});
|
||||
|
||||
batch(process.argv, (err: any) =>
|
||||
|
||||
@@ -45,6 +45,27 @@ export function save(config: IConfig)
|
||||
tmp.args = undefined;
|
||||
tmp.commands = undefined;
|
||||
tmp._allowUnknownOption = undefined;
|
||||
tmp.parent = undefined;
|
||||
tmp._scriptPath = undefined;
|
||||
tmp._optionValues = undefined;
|
||||
tmp._storeOptionsAsProperties = undefined;
|
||||
tmp._passCommandToAction = undefined;
|
||||
tmp._actionResults = undefined;
|
||||
tmp._actionHandler = undefined;
|
||||
tmp._executableHandler = undefined;
|
||||
tmp._executableFile = undefined;
|
||||
tmp._defaultCommandName = undefined;
|
||||
tmp._exitCallback = undefined;
|
||||
tmp._alias = undefined;
|
||||
tmp._noHelp = undefined;
|
||||
tmp._helpFlags = undefined;
|
||||
tmp._helpDescription = undefined;
|
||||
tmp._helpShortFlag = undefined;
|
||||
tmp._helpLongFlag = undefined;
|
||||
tmp._hasImplicitHelpCommand = undefined;
|
||||
tmp._helpCommandName = undefined;
|
||||
tmp._helpCommandnameAndArgs = undefined;
|
||||
tmp._helpCommandDescription = undefined;
|
||||
|
||||
// Things we don't want to save
|
||||
tmp.cache = undefined;
|
||||
|
||||
@@ -66,13 +66,15 @@ function fileExist(path: string)
|
||||
|
||||
function sanitiseFileName(str: string)
|
||||
{
|
||||
return str.replace(/[\/':\?\*"<>\\\.]/g, '_');
|
||||
const sanitized = str.replace(/[\/':\?\*"<>\\\.\|]/g, '_');
|
||||
|
||||
return sanitized.replace(/{DIR_SEPARATOR}/g, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the subtitle and video.
|
||||
*/
|
||||
function download(config: IConfig, page: IEpisodePage, player: IEpisodePlayer, done: (err: Error, ign: boolean) => void)
|
||||
function download(config: IConfig, page: IEpisodePage, player: IEpisodePlayer, done: (err: Error | string, ign: boolean) => void)
|
||||
{
|
||||
const serieFolder = sanitiseFileName(config.series || page.series);
|
||||
|
||||
@@ -109,14 +111,9 @@ function download(config: IConfig, page: IEpisodePage, player: IEpisodePlayer, d
|
||||
return done(null, true);
|
||||
}
|
||||
|
||||
mkdirp(path.dirname(filePath), (errM: Error) =>
|
||||
const ret = mkdirp(path.dirname(filePath));
|
||||
if (ret)
|
||||
{
|
||||
if (errM)
|
||||
{
|
||||
log.dispEpisode(fileName, 'Error...', true);
|
||||
return done(errM, false);
|
||||
}
|
||||
|
||||
log.dispEpisode(fileName, 'Fetching...', false);
|
||||
downloadSubtitle(config, player, filePath, (errDS) =>
|
||||
{
|
||||
@@ -164,13 +161,18 @@ function download(config: IConfig, page: IEpisodePage, player: IEpisodePlayer, d
|
||||
done(null, true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
log.dispEpisode(fileName, 'Error creating folder \'" + filePath + "\'...', true);
|
||||
return done('Cannot create folder', false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the subtitles to disk.
|
||||
*/
|
||||
function downloadSubtitle(config: IConfig, player: IEpisodePlayer, filePath: string, done: (err?: Error) => void)
|
||||
function downloadSubtitle(config: IConfig, player: IEpisodePlayer, filePath: string, done: (err?: Error | string) => void)
|
||||
{
|
||||
const enc = player.subtitle;
|
||||
|
||||
@@ -336,15 +338,15 @@ function scrapePlayer(config: IConfig, address: string, id: number, done: (err:
|
||||
return done(new Error('Invalid address.'));
|
||||
}
|
||||
|
||||
my_request.post(config, {
|
||||
form: {
|
||||
current_page: address,
|
||||
video_format: config.video_format,
|
||||
video_quality: config.video_quality,
|
||||
media_id: id
|
||||
},
|
||||
url: url[1] + '/xml/?req=RpcApiVideoPlayer_GetStandardConfig&media_id=' + id,
|
||||
}, (err, result) =>
|
||||
const postForm = {
|
||||
current_page: address,
|
||||
video_format: config.video_format,
|
||||
video_quality: config.video_quality,
|
||||
media_id: id
|
||||
};
|
||||
|
||||
my_request.post(config, url[1] + '/xml/?req=RpcApiVideoPlayer_GetStandardConfig&media_id=' + id, postForm,
|
||||
(err, result) =>
|
||||
{
|
||||
if (err)
|
||||
{
|
||||
|
||||
2
src/interface/AuthError.d.ts
vendored
2
src/interface/AuthError.d.ts
vendored
@@ -1,3 +1,5 @@
|
||||
interface IAuthError extends Error {
|
||||
name: string;
|
||||
message: string;
|
||||
authError: boolean;
|
||||
}
|
||||
|
||||
1
src/interface/IConfig.d.ts
vendored
1
src/interface/IConfig.d.ts
vendored
@@ -7,6 +7,7 @@ interface IConfig {
|
||||
merge?: boolean;
|
||||
episodes?: string;
|
||||
// Settings
|
||||
crlang?: string;
|
||||
format?: string;
|
||||
output?: string;
|
||||
series?: string;
|
||||
|
||||
4
src/interface/IConfigTask.d.ts
vendored
4
src/interface/IConfigTask.d.ts
vendored
@@ -1,6 +1,6 @@
|
||||
interface IConfigTask {
|
||||
address: string;
|
||||
retry: number;
|
||||
episode_min: number;
|
||||
episode_max: number;
|
||||
episode_min: IEpisodeNumber;
|
||||
episode_max: IEpisodeNumber;
|
||||
}
|
||||
|
||||
4
src/interface/IEpisodeNumber.d.ts
vendored
Normal file
4
src/interface/IEpisodeNumber.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
interface IEpisodeNumber {
|
||||
season: number,
|
||||
episode: number
|
||||
}
|
||||
66
src/languages.ts
Normal file
66
src/languages.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
'use strict';
|
||||
|
||||
const localeCC: { [id: string]: string; } =
|
||||
{
|
||||
enUS: 'en', enGB: 'en',
|
||||
esLA: 'es', esES: 'es',
|
||||
ptPT: 'pt', ptBR: 'pt',
|
||||
frFR: 'fr',
|
||||
deDE: 'de',
|
||||
itIT: 'it',
|
||||
ruRU: 'ru',
|
||||
};
|
||||
|
||||
export function localeToCC(locale: string): string
|
||||
{
|
||||
let ret = localeCC.enGB;
|
||||
|
||||
if (locale in localeCC)
|
||||
{
|
||||
ret = localeCC[locale];
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
const dubignore_regexp: { [id: string]: RegExp; } =
|
||||
{
|
||||
en: /\(.*Dub(?:bed)?.*\)|(?:\(RU\))/i,
|
||||
fr: /\(.*Dub(?:bed)?.*\)|(?:\(RU\))|\(?Doublage.*\)?/,
|
||||
de: /\(.*isch\)|\(Dubbed\)|\(RU\)/
|
||||
};
|
||||
|
||||
export function get_diregexp(config: IConfig): RegExp
|
||||
{
|
||||
let ret = dubignore_regexp.en;
|
||||
|
||||
if (config.crlang in dubignore_regexp)
|
||||
{
|
||||
ret = dubignore_regexp[config.crlang];
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
const episodes_regexp: { [id: string]: RegExp; } =
|
||||
{
|
||||
en: /Episode\s+((OVA)|(PV )?[S0-9][\-P0-9.]*[a-fA-F]?)\s*$/i,
|
||||
fr: /Épisode\s+((OVA)|(PV )?[S0-9][\-P0-9.]*[a-fA-F]?)\s*$/i,
|
||||
de: /Folge\s+((OVA)|(PV )?[S0-9][\-P0-9.]*[a-fA-F]?)\s*$/i,
|
||||
es: /Episodio\s+((OVA)|(PV )?[S0-9][\-P0-9.]*[a-fA-F]?)\s*$/i,
|
||||
it: /Episodio\s+((OVA)|(PV )?[S0-9][\-P0-9.]*[a-fA-F]?)\s*$/i,
|
||||
pt: /Episódio\s+((OVA)|(PV )?[S0-9][\-P0-9.]*[a-fA-F]?)\s*$/i,
|
||||
ru: /Серия\s+((OVA)|(PV )?[S0-9][\-P0-9.]*[a-fA-F]?)\s*$/i,
|
||||
};
|
||||
|
||||
export function get_epregexp(config: IConfig): RegExp
|
||||
{
|
||||
let ret = episodes_regexp.en;
|
||||
|
||||
if (config.crlang in episodes_regexp)
|
||||
{
|
||||
ret = episodes_regexp[config.crlang];
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import Promise = require('bluebird');
|
||||
import uuid = require('uuid');
|
||||
import path = require('path');
|
||||
import fs = require('fs-extra');
|
||||
import languages = require('./languages');
|
||||
import log = require('./log');
|
||||
|
||||
import { RequestPromise } from 'request-promise';
|
||||
@@ -13,8 +14,6 @@ import { Response } from 'request';
|
||||
|
||||
// tslint:disable-next-line:no-var-requires
|
||||
const cookieStore = require('tough-cookie-file-store');
|
||||
// tslint:disable-next-line:no-var-requires
|
||||
const cloudscraper = require('cloudscraper');
|
||||
|
||||
const CR_COOKIE_DOMAIN = 'http://crunchyroll.com';
|
||||
|
||||
@@ -23,12 +22,10 @@ let isPremium = false;
|
||||
|
||||
let j: request.CookieJar;
|
||||
|
||||
const defaultHeaders: request.Headers =
|
||||
{
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36',
|
||||
'Connection': 'keep-alive',
|
||||
'Referer': 'https://www.crunchyroll.com/login',
|
||||
};
|
||||
// tslint:disable-next-line:no-var-requires
|
||||
import cloudscraper = require('cloudscraper');
|
||||
let currentOptions: any;
|
||||
let optionsSet = false;
|
||||
|
||||
function AuthError(msg: string): IAuthError
|
||||
{
|
||||
@@ -62,7 +59,7 @@ function startSession(config: IConfig): Promise<any>
|
||||
});
|
||||
}
|
||||
|
||||
function login(config: IConfig, sessionId: string, user: string, pass: string): Promise<any>
|
||||
function APIlogin(config: IConfig, sessionId: string, user: string, pass: string): Promise<any>
|
||||
{
|
||||
return rp(
|
||||
{
|
||||
@@ -85,24 +82,14 @@ function login(config: IConfig, sessionId: string, user: string, pass: string):
|
||||
});
|
||||
}
|
||||
|
||||
function checkIfUserIsAuth(config: IConfig, done: (err: Error) => void): void
|
||||
function checkIfUserIsAuth(config: IConfig, done: (err: any) => void): void
|
||||
{
|
||||
if (j === undefined)
|
||||
{
|
||||
loadCookies(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* The main page give us some information about the user
|
||||
*/
|
||||
const options =
|
||||
{
|
||||
headers: defaultHeaders,
|
||||
jar: j,
|
||||
url: 'http://www.crunchyroll.com/',
|
||||
method: 'GET',
|
||||
};
|
||||
cloudscraper.request(options, (err: Error, rep: string, body: string) =>
|
||||
const url = 'http://www.crunchyroll.com/';
|
||||
|
||||
cloudscraper.get(url, getOptions(config, null), (err: any, rep: Response, body: string) =>
|
||||
{
|
||||
if (err)
|
||||
{
|
||||
@@ -111,6 +98,22 @@ function checkIfUserIsAuth(config: IConfig, done: (err: Error) => void): void
|
||||
|
||||
const $ = cheerio.load(body);
|
||||
|
||||
/* As we are here, try to detect which locale CR tell us */
|
||||
const localeRE = /LOCALE = "([a-zA-Z]+)",/g;
|
||||
const locale = localeRE.exec($('script').text())[1];
|
||||
const countryCode = languages.localeToCC(locale);
|
||||
|
||||
if (config.crlang === undefined)
|
||||
{
|
||||
log.info('No locale set. Setting to the one reported by CR: "' + countryCode + '"');
|
||||
config.crlang = countryCode;
|
||||
}
|
||||
else if (config.crlang !== countryCode)
|
||||
{
|
||||
log.warn('Crunchy is configured for locale "' + config.crlang + '" but CR report "' + countryCode + '" (LOCALE = ' + locale + ')');
|
||||
log.warn('Check if it is correct or rerun (once) with "-l ' + countryCode + '" to correct.');
|
||||
}
|
||||
|
||||
/* Check if auth worked */
|
||||
const regexps = /ga\('set', 'dimension[5-8]', '([^']*)'\);/g;
|
||||
const dims = regexps.exec($('script').text());
|
||||
@@ -131,6 +134,11 @@ function checkIfUserIsAuth(config: IConfig, done: (err: Error) => void): void
|
||||
if (isAuthenticated === false)
|
||||
{
|
||||
const error = $('ul.message, li.error').text();
|
||||
log.warn('Authentication failed: ' + error);
|
||||
|
||||
log.dumpToDebug('not auth rep', rep);
|
||||
log.dumpToDebug('not auth body', body);
|
||||
|
||||
return done(AuthError('Authentication failed: ' + error));
|
||||
}
|
||||
else
|
||||
@@ -175,24 +183,14 @@ export function eatCookies(config: IConfig)
|
||||
|
||||
export function getUserAgent(): string
|
||||
{
|
||||
return defaultHeaders['User-Agent'];
|
||||
return currentOptions.headers['User-Agent'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a GET request for the resource.
|
||||
*/
|
||||
export function get(config: IConfig, options: string|request.Options, done: (err: any, result?: string) => void)
|
||||
export function get(config: IConfig, url: string, done: (err: any, result?: string) => void)
|
||||
{
|
||||
if (j === undefined)
|
||||
{
|
||||
loadCookies(config);
|
||||
}
|
||||
|
||||
if (config.userAgent)
|
||||
{
|
||||
defaultHeaders['User-Agent'] = config.userAgent;
|
||||
}
|
||||
|
||||
authenticate(config, (err) =>
|
||||
{
|
||||
if (err)
|
||||
@@ -200,7 +198,7 @@ export function get(config: IConfig, options: string|request.Options, done: (err
|
||||
return done(err);
|
||||
}
|
||||
|
||||
cloudscraper.request(modify(options, 'GET'), (error: any, response: any, body: any) =>
|
||||
cloudscraper.get(url, getOptions(config, null), (error: any, response: any, body: any) =>
|
||||
{
|
||||
if (error) return done(error);
|
||||
|
||||
@@ -212,18 +210,8 @@ export function get(config: IConfig, options: string|request.Options, done: (err
|
||||
/**
|
||||
* Performs a POST request for the resource.
|
||||
*/
|
||||
export function post(config: IConfig, options: request.Options, done: (err: Error, result?: string) => void)
|
||||
export function post(config: IConfig, url: string, form: any, done: (err: any, result?: string) => void)
|
||||
{
|
||||
if (j === undefined)
|
||||
{
|
||||
loadCookies(config);
|
||||
}
|
||||
|
||||
if (config.userAgent)
|
||||
{
|
||||
defaultHeaders['User-Agent'] = config.userAgent;
|
||||
}
|
||||
|
||||
authenticate(config, (err) =>
|
||||
{
|
||||
if (err)
|
||||
@@ -231,7 +219,7 @@ export function post(config: IConfig, options: request.Options, done: (err: Erro
|
||||
return done(err);
|
||||
}
|
||||
|
||||
cloudscraper.request(modify(options, 'POST'), (error: Error, response: any, body: any) =>
|
||||
cloudscraper.post(url, getOptions(config, form), (error: Error, response: any, body: any) =>
|
||||
{
|
||||
if (error)
|
||||
{
|
||||
@@ -242,10 +230,130 @@ export function post(config: IConfig, options: request.Options, done: (err: Erro
|
||||
});
|
||||
}
|
||||
|
||||
function authUsingCookies(config: IConfig, done: (err: any) => void)
|
||||
{
|
||||
j.setCookie(request.cookie('c_userid=' + config.crUserId + '; Domain=crunchyroll.com; HttpOnly; hostOnly=false;'),
|
||||
CR_COOKIE_DOMAIN);
|
||||
j.setCookie(request.cookie('c_userkey=' + config.crUserKey + '; Domain=crunchyroll.com; HttpOnly; hostOnly=false;'),
|
||||
CR_COOKIE_DOMAIN);
|
||||
|
||||
checkIfUserIsAuth(config, (errCheckAuth2) =>
|
||||
{
|
||||
if (isAuthenticated)
|
||||
{
|
||||
return done(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
return done(errCheckAuth2);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function authUsingApi(config: IConfig, done: (err: any) => void)
|
||||
{
|
||||
if (!config.pass || !config.user)
|
||||
{
|
||||
log.error('You need to give login/password to use Crunchy');
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
if (config.crDeviceId === undefined)
|
||||
{
|
||||
config.crDeviceId = uuid.v4();
|
||||
}
|
||||
|
||||
if (!config.crSessionUrl || !config.crDeviceType || !config.crAPIVersion ||
|
||||
!config.crLocale || !config.crLoginUrl)
|
||||
{
|
||||
return done(AuthError('Invalid API configuration, please check your config file.'));
|
||||
}
|
||||
|
||||
startSession(config)
|
||||
.then((sessionId: string) =>
|
||||
{
|
||||
// defaultHeaders['Cookie'] = `sess_id=${sessionId}; c_locale=enUS`;
|
||||
return APIlogin(config, sessionId, config.user, config.pass);
|
||||
})
|
||||
.then((userData) =>
|
||||
{
|
||||
checkIfUserIsAuth(config, (errCheckAuth2) =>
|
||||
{
|
||||
if (isAuthenticated)
|
||||
{
|
||||
return done(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
return done(errCheckAuth2);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((errInChk) =>
|
||||
{
|
||||
return done(AuthError(errInChk.message));
|
||||
});
|
||||
}
|
||||
|
||||
function authUsingForm(config: IConfig, done: (err: any) => void)
|
||||
{
|
||||
/* So if we are here now, that mean we are not authenticated so do as usual */
|
||||
if (!config.pass || !config.user)
|
||||
{
|
||||
log.error('You need to give login/password to use Crunchy');
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
/* First get https://www.crunchyroll.com/login to get the login token */
|
||||
cloudscraper.get('https://www.crunchyroll.com/login', getOptions(config, null), (err: any, rep: Response, body: string) =>
|
||||
{
|
||||
if (err) return done(err);
|
||||
|
||||
const $ = cheerio.load(body);
|
||||
|
||||
/* Get the token from the login page */
|
||||
const token = $('input[name="login_form[_token]"]').attr('value');
|
||||
if (token === '')
|
||||
{
|
||||
return done(AuthError('Can\'t find token!'));
|
||||
}
|
||||
|
||||
/* Now call the page again with the token and credentials */
|
||||
const paramForm =
|
||||
{
|
||||
'login_form[name]': config.user,
|
||||
'login_form[password]': config.pass,
|
||||
'login_form[redirect_url]': '/',
|
||||
'login_form[_token]': token
|
||||
};
|
||||
|
||||
cloudscraper.post('https://www.crunchyroll.com/login', getOptions(config, paramForm), (err: any, rep: Response, body: string) =>
|
||||
{
|
||||
if (err)
|
||||
{
|
||||
return done(err);
|
||||
}
|
||||
|
||||
/* Now let's check if we are authentificated */
|
||||
checkIfUserIsAuth(config, (errCheckAuth2) =>
|
||||
{
|
||||
if (isAuthenticated)
|
||||
{
|
||||
return done(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
return done(errCheckAuth2);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates using the configured pass and user.
|
||||
*/
|
||||
function authenticate(config: IConfig, done: (err: Error) => void)
|
||||
function authenticate(config: IConfig, done: (err: any) => void)
|
||||
{
|
||||
if (isAuthenticated)
|
||||
{
|
||||
@@ -260,155 +368,57 @@ function authenticate(config: IConfig, done: (err: Error) => void)
|
||||
return done(null);
|
||||
}
|
||||
|
||||
/* So if we are here now, that mean we are not authenticated so do as usual */
|
||||
if (!config.pass || !config.user)
|
||||
{
|
||||
log.error('You need to give login/password to use Crunchy');
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
log.info('Seems we are not currently logged. Let\'s login!');
|
||||
|
||||
if (config.logUsingApi)
|
||||
{
|
||||
if (config.crDeviceId === undefined)
|
||||
{
|
||||
config.crDeviceId = uuid.v4();
|
||||
}
|
||||
|
||||
if (!config.crSessionUrl || !config.crDeviceType || !config.crAPIVersion ||
|
||||
!config.crLocale || !config.crLoginUrl)
|
||||
{
|
||||
return done(AuthError('Invalid API configuration, please check your config file.'));
|
||||
}
|
||||
|
||||
startSession(config)
|
||||
.then((sessionId: string) =>
|
||||
{
|
||||
defaultHeaders.Cookie = `sess_id=${sessionId}; c_locale=enUS`;
|
||||
return login(config, sessionId, config.user, config.pass);
|
||||
})
|
||||
.then((userData) =>
|
||||
{
|
||||
checkIfUserIsAuth(config, (errCheckAuth2) =>
|
||||
{
|
||||
if (isAuthenticated)
|
||||
{
|
||||
return done(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
return done(errCheckAuth2);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((errInChk) =>
|
||||
{
|
||||
return done(AuthError(errInChk.message));
|
||||
});
|
||||
return authUsingApi(config, done);
|
||||
}
|
||||
else if (config.logUsingCookie)
|
||||
{
|
||||
j.setCookie(request.cookie('c_userid=' + config.crUserId + '; Domain=crunchyroll.com; HttpOnly; hostOnly=false;'),
|
||||
CR_COOKIE_DOMAIN);
|
||||
j.setCookie(request.cookie('c_userkey=' + config.crUserKey + '; Domain=crunchyroll.com; HttpOnly; hostOnly=false;'),
|
||||
CR_COOKIE_DOMAIN);
|
||||
|
||||
checkIfUserIsAuth(config, (errCheckAuth2) =>
|
||||
{
|
||||
if (isAuthenticated)
|
||||
{
|
||||
return done(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
return done(errCheckAuth2);
|
||||
}
|
||||
});
|
||||
return authUsingCookies(config, done);
|
||||
}
|
||||
else
|
||||
{
|
||||
/* First get https://www.crunchyroll.com/login to get the login token */
|
||||
const options =
|
||||
{
|
||||
headers: defaultHeaders,
|
||||
jar: j,
|
||||
gzip: false,
|
||||
method: 'GET',
|
||||
url: 'https://www.crunchyroll.com/login'
|
||||
};
|
||||
|
||||
cloudscraper.request(options, (err: Error, rep: string, body: string) =>
|
||||
{
|
||||
if (err) return done(err);
|
||||
|
||||
const $ = cheerio.load(body);
|
||||
|
||||
/* Get the token from the login page */
|
||||
const token = $('input[name="login_form[_token]"]').attr('value');
|
||||
if (token === '')
|
||||
{
|
||||
return done(AuthError('Can\'t find token!'));
|
||||
}
|
||||
|
||||
/* Now call the page again with the token and credentials */
|
||||
const options =
|
||||
{
|
||||
headers: defaultHeaders,
|
||||
form:
|
||||
{
|
||||
'login_form[name]': config.user,
|
||||
'login_form[password]': config.pass,
|
||||
'login_form[redirect_url]': '/',
|
||||
'login_form[_token]': token
|
||||
},
|
||||
jar: j,
|
||||
gzip: false,
|
||||
method: 'POST',
|
||||
url: 'https://www.crunchyroll.com/login'
|
||||
};
|
||||
|
||||
cloudscraper.request(options, (err: Error, rep: string, body: string) =>
|
||||
{
|
||||
if (err)
|
||||
{
|
||||
return done(err);
|
||||
}
|
||||
|
||||
/* Now let's check if we are authentificated */
|
||||
checkIfUserIsAuth(config, (errCheckAuth2) =>
|
||||
{
|
||||
if (isAuthenticated)
|
||||
{
|
||||
return done(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
return done(errCheckAuth2);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
return authUsingForm(config, done);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies the options to use the authenticated cookie jar.
|
||||
*/
|
||||
function modify(options: string|request.Options, reqMethod: string): request.Options
|
||||
function getOptions(config: IConfig, form: any)
|
||||
{
|
||||
if (typeof options !== 'string')
|
||||
if (!optionsSet)
|
||||
{
|
||||
options.jar = j;
|
||||
options.headers = defaultHeaders;
|
||||
options.method = reqMethod;
|
||||
return options;
|
||||
currentOptions = {};
|
||||
currentOptions.headers = {};
|
||||
|
||||
if (config.userAgent)
|
||||
{
|
||||
currentOptions.headers['User-Agent'] = config.userAgent;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentOptions.headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:75.0) Gecko/20100101 Firefox/75.0';
|
||||
}
|
||||
|
||||
if (j === undefined)
|
||||
{
|
||||
loadCookies(config);
|
||||
}
|
||||
|
||||
currentOptions.decodeEmails = true;
|
||||
currentOptions.jar = j;
|
||||
|
||||
optionsSet = true;
|
||||
}
|
||||
return {
|
||||
jar: j,
|
||||
headers: defaultHeaders,
|
||||
url: options.toString(),
|
||||
method: reqMethod
|
||||
};
|
||||
}
|
||||
|
||||
currentOptions.form = {};
|
||||
|
||||
if (form !== null)
|
||||
{
|
||||
currentOptions.form = form;
|
||||
}
|
||||
|
||||
|
||||
return currentOptions;
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
'use strict';
|
||||
import cheerio = require('cheerio');
|
||||
import episode from './episode';
|
||||
// import fs = require('fs');
|
||||
import fs = require('fs-extra');
|
||||
import my_request = require('./my_request');
|
||||
import path = require('path');
|
||||
import url = require('url');
|
||||
import log = require('./log');
|
||||
import languages = require('./languages');
|
||||
|
||||
const persistent = '.crpersistent';
|
||||
|
||||
/**
|
||||
@@ -143,9 +144,10 @@ function download(cache: {[address: string]: number}, config: IConfig,
|
||||
done: (err: any, ign: boolean) => void)
|
||||
{
|
||||
const episodeNumber = parseInt(item.episode, 10);
|
||||
const seasonNumber = item.volume;
|
||||
|
||||
if ( (episodeNumber < task.episode_min) ||
|
||||
(episodeNumber > task.episode_max) )
|
||||
if ( (episodeNumber < task.episode_min.episode) ||
|
||||
(episodeNumber > task.episode_max.episode) )
|
||||
{
|
||||
return done(null, false);
|
||||
}
|
||||
@@ -197,7 +199,7 @@ function pageScrape(config: IConfig, task: IConfigTask, done: (err: any, result?
|
||||
}
|
||||
|
||||
const $ = cheerio.load(result);
|
||||
const title = $('span[itemprop=name]').text();
|
||||
const title = $('meta[itemprop=name]').attr('content');
|
||||
|
||||
if (config.debug)
|
||||
{
|
||||
@@ -234,11 +236,13 @@ function pageScrape(config: IConfig, task: IConfigTask, done: (err: any, result?
|
||||
|
||||
const season_name = $(el).closest('ul').prev('a').text();
|
||||
const volume = /([0-9]+)\s*$/.exec($(el).closest('ul').prev('a').text());
|
||||
const regexp = /Episode\s+((PV )?[S0-9][\-P0-9.]*[a-fA-F]?)\s*$/i;
|
||||
const regexp = languages.get_epregexp(config);
|
||||
const episode = regexp.exec($(el).children('.series-title').text());
|
||||
const url = $(el).attr('href');
|
||||
|
||||
if (config.ignoredub && (season_name.endsWith('Dub)') || season_name.endsWith('dub)')))
|
||||
const igndub_re = languages.get_diregexp(config);
|
||||
|
||||
if (config.ignoredub && (igndub_re.exec(season_name)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
96
ts.js
96
ts.js
@@ -1,96 +0,0 @@
|
||||
'use strict';
|
||||
var childProcess = require('child_process');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var isTest = process.argv[2] === '--only-test';
|
||||
|
||||
// TODO: This build task should be removed upon release of TypeScript 1.5 with
|
||||
// the support for `tsconfig.json`. Invoking `tsc` from `package.json` will then
|
||||
// read the configuration and compile accordingly. It seems that `TSLint` will,
|
||||
// eventually, support this mechanism too. That prevents the need for any kind
|
||||
// of build task and will run entirely based on instructions from `npm`.
|
||||
//
|
||||
// Reference #1: https://github.com/Microsoft/TypeScript/issues/1667
|
||||
// Reference #2: https://github.com/palantir/tslint/issues/281
|
||||
|
||||
read(function(err, fileNames) {
|
||||
clean(fileNames, function() {
|
||||
var hasLintError = false;
|
||||
compile(fileNames, function(err) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return process.exit(1);
|
||||
}
|
||||
lint(fileNames, function(message) {
|
||||
process.stdout.write(message);
|
||||
hasLintError = true;
|
||||
}, function() {
|
||||
process.exit(Number(hasLintError));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Clean the files.
|
||||
* @param {Array.<string>} filePaths
|
||||
* @param {function()} done
|
||||
*/
|
||||
function clean(filePaths, done) {
|
||||
if (isTest) return done();
|
||||
var i = -1;
|
||||
(function next() {
|
||||
i += 1;
|
||||
if (i >= filePaths.length) return done();
|
||||
var filePath = filePaths[i];
|
||||
if (/\.d\.ts$/.test(filePath)) return next();
|
||||
var mapName = filePath.substring(4, filePath.length - 2) + 'js.map';
|
||||
var mapPath = path.join('dist', mapName);
|
||||
if (fs.existsSync(mapPath)) fs.unlinkSync(mapPath);
|
||||
next();
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile the files.
|
||||
* @param {Array.<string>} filePaths
|
||||
* @param {function(Error)} done
|
||||
*/
|
||||
function compile(filePaths, done) {
|
||||
if (isTest) return done(null);
|
||||
var execPath = path.join(__dirname, 'node_modules/.bin/tsc');
|
||||
var options = '--declaration --module CommonJS --noImplicitAny --outDir dist --target ES5';
|
||||
childProcess.exec([execPath, options].concat(filePaths).join(' '), function(err, stdout) {
|
||||
if (stdout) return done(new Error(stdout));
|
||||
done(null);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Lint the files.
|
||||
* @param {Array.<string>} filePaths
|
||||
* @param {function(string)} handler
|
||||
* @param {function()} done
|
||||
*/
|
||||
function lint(filePaths, handler, done) {
|
||||
var i = -1;
|
||||
var execPath = path.join(__dirname, 'node_modules/.bin/tslint');
|
||||
(function next() {
|
||||
i += 1;
|
||||
if (i >= filePaths.length) return done();
|
||||
var filePath = filePaths[i];
|
||||
if (/\.d\.ts$/.test(filePath)) return next();
|
||||
childProcess.exec(execPath + ' -f ' + filePath, function(err, stdout) {
|
||||
if (stdout) handler(stdout);
|
||||
next();
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the files from the project file.
|
||||
* @param {function(Error, Array.<string>)} done
|
||||
*/
|
||||
function read(done) {
|
||||
done(null, JSON.parse(fs.readFileSync('tsconfig.json', 'utf8')).files);
|
||||
}
|
||||
Reference in New Issue
Block a user