60 Commits

Author SHA1 Message Date
Godzil
fc119acb1c 1.6.0 2020-04-27 23:06:04 +01:00
Godzil
2db34c3ed8 Update readme with some of the new informations 2020-04-27 23:05:16 +01:00
Godzil
64200a1da9 Add support for the NEW Way.
It started as just a way to fix subtitles download. It ended in a
complete new way to get info about video stream AND subtitles.

Lots of things have change on CR since the last major update, and on
some pages, the old subtitle fetch fail. They changed the player on the
page from the old flash one to a HTML based one and albeit most scrapped
info are still valid, some are no longer working on some new releases.

It should be more reliable, but there are some drawback. I'm currently
unable to select the resolution, it is 1080 by default. It will probably
not work for non premium account, but, you know, I ask clearly for you
to only use that tool if you have a premium account, so well, I'm not
going to try to support non premium account if it does not work.

Oh, and it add the possibility to download subtitles in the languages of
your choice!

The old mechanism is still there as fallback, but may be removed in the
futur to clean up the code.
2020-04-27 22:48:24 +01:00
Godzil
8655874097 Remove no longer used entry for the config. 2020-04-27 22:39:51 +01:00
Godzil
99ba051b7f Remove some too verbose messages when an error occur and we are not
in verbose mode.
2020-04-27 22:39:30 +01:00
Godzil
d692199d07 CF don't like us if we go to fast. Let's add some delays. 2020-04-27 22:39:03 +01:00
Godzil
e058b8e699 We want to save the video format/quality infos in the config file. 2020-04-27 22:37:23 +01:00
Godzil
b3a96cd0e7 Let's make the headers a bit better. 2020-04-27 22:34:13 +01:00
Godzil
f25a62234c Add more to the dub ignore regexp. 2020-04-27 22:33:16 +01:00
Manoël Trapier
e8a7856e44 Merge pull request #116 from elisha464/master
fix logUsingCookie
2020-04-13 20:27:08 +01:00
Manoël Trapier
f086b3593e Merge branch 'master' into master 2020-04-13 20:25:37 +01:00
Godzil
d2bc7e41b2 1.5.1 2020-04-13 20:16:37 +01:00
Godzil
a7bc34df0d New way to specify episode range. Work in progress, may not work well. 2020-04-13 20:14:28 +01:00
Godzil
b2ecd05586 Trying to clean file name in a better way. 2020-04-13 20:13:23 +01:00
Godzil
8b9f9a5e1c The new version of the command line parser leave a lot of new things.
Let's remove them...
2020-04-13 20:12:32 +01:00
Godzil
376ff09632 Change my_request to be more clean and try to fix the login issue. 2020-04-13 20:11:56 +01:00
Godzil
7926b2fd9a Make the code compile again. 2020-04-13 20:09:30 +01:00
Godzil
3f2a920e1e Update dependencies 2020-04-13 20:06:41 +01:00
elisha464
fa4c68c239 fix logUsingCookie
* it seems that the site only cares about the cookie `session_id`
* when checking if the user is authenticated the cookie jar was not being used
2019-11-30 16:32:24 +02:00
Manoël Trapier
1555229635 Update .travis.yml
Travis currently don't support Node 13 and 14.
2019-07-31 16:52:08 +01:00
Godzil
7c085caaa0 Add more travis target 2019-07-31 17:22:52 +02:00
Godzil
cc0bf4dfb1 1.5.0 2019-07-31 17:20:53 +02:00
Godzil
00857ba46f Update dependencies 2019-07-31 17:20:47 +02:00
Godzil
b77a35e0e9 Remove non production logs 2019-07-31 17:16:30 +02:00
Manoël Trapier
ca59e3b2fd Merge pull request #108 from Ronserruya/fix_title_scrape
Fix login and only direct links issues.

Still need to understand what is happening.
2019-07-31 16:09:27 +01:00
ronserruya
95c0c4d6d3 Linter stuff 2019-07-31 14:29:49 +03:00
ronserruya
0d2d36251a Fix title fetching 2019-07-31 14:26:22 +03:00
ronserruya
48a58ffca6 Fix login issue 2019-07-31 14:26:13 +03:00
Manoël Trapier
505e6c67ce Add one more badge 2019-05-07 13:39:04 +01:00
Manoël Trapier
83e8a5e08c Change issue badge 2019-05-07 13:28:01 +01:00
Godzil
c82319a2c6 Make code (somwhat) compliant with latest version of CloudScraper and add some instrumentation to try to understand what is happening with web based login. Still unclear for now.. 2019-05-07 13:13:08 +02:00
Godzil
1fe7c697c5 Remove the old and unneeded ts.js 2019-05-07 13:11:03 +02:00
Godzil
239d1c60a3 Update some dependencies 2019-05-07 13:10:34 +02:00
Godzil
bdfc96d56e Remove package-lock.json as it is not needed 2019-04-30 16:57:25 +02:00
Godzil
8f7babd809 1.4.6 2019-03-04 18:51:27 +01:00
Godzil
c708df574b Update deps 2019-03-04 18:49:29 +01:00
Godzil
401a511668 Add Node 10 and 11 2019-03-04 18:44:41 +01:00
Godzil
969879921e 1.4.5 2018-10-04 20:08:15 +01:00
Godzil
546ba9b45a Add a warn when login failed to be more explicit 2018-10-04 20:07:54 +01:00
Godzil
27bdf54782 Solve issue with redirection (now it should follow automatically) 2018-10-04 20:02:28 +01:00
Godzil
beed932e93 Javascript: I hate you.
(fix a **** stupid bug while doing version checking)
2018-08-27 18:16:23 +01:00
Godzil
e5c4c08e66 1.4.4 2018-08-27 16:46:59 +01:00
Godzil
2b201b0785 Fix #94 2018-08-27 13:16:22 +01:00
Godzil
fdf5805911 Fix for #88 2018-08-27 13:11:06 +01:00
Godzil
9191075f48 Fix for #92 when the version server is not answering properly 2018-08-27 13:08:01 +01:00
Godzil
9f73e4f865 Update `ignoredub` to support more form
(and also make it work with multiple languages)
2018-08-17 00:56:50 +01:00
Manoël Trapier
1f20e028e1 Merge pull request #87 from TheDammedGamer/master
Filtering out Pipe Symbol in file names
2018-08-13 15:36:35 +01:00
Liam Townsend
da0fb17015 Filtering out Pipe Symbol in file names
updated sanitiseFileName to include the pipe symbol as this character is not allowed on windows, the error thast it currently throws is: {"errno":-4058,"code":"ENOENT","syscall":"open","path":"K:\\MediaDwn\\Is It Wrong to Try to Pick Up Girls in a Dungeon_\\Is It Wrong to Try to Pick Up Girls in a Dungeon_ - s01e01 - Bell Cranel | Adventurer - [CrunchyRoll].ass"}
2018-08-13 15:25:06 +01:00
Godzil
2aa71832b3 1.4.3 2018-08-11 20:50:02 +01:00
Godzil
876def4392 Add code to check what langage CR is serving the page, and try to adapt
some regexp to that. The langage can be forced by the user

Fix #1 and Fix #76
2018-08-11 20:42:12 +01:00
Godzil
0ba51b7270 1.4.2 2018-08-05 11:01:03 +01:00
Godzil
7da4289097 1.4.2-0 2018-08-05 10:57:09 +01:00
Godzil
ce5038cf08 @ URL was broken since 1.3.7 doh! 2018-08-05 10:46:27 +01:00
Godzil
1b0f53a88c 1.4.1 2018-08-05 10:34:00 +01:00
Godzil
d19992f0d3 Make linter happy (and fix a mistake) 2018-08-05 10:30:22 +01:00
Godzil
a44d1ae668 Use a more stable and futur proof URL to get current version information 2018-08-05 09:30:14 +01:00
Godzil
14fd18479e Try to tweak ffmpeg setting, but there are still some stalling issues.. 2018-08-05 09:29:39 +01:00
Godzil
1106a28288 Make possible georestrictions messages more clear 2018-08-05 09:28:53 +01:00
Godzil
f1a5d1b6a8 Add a new warning type (more "strong" but still a warning) 2018-08-05 09:28:28 +01:00
Godzil
4193643306 Fix a missing invalid char for filename for windows ('\') 2018-08-05 09:27:46 +01:00
23 changed files with 804 additions and 3499 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
dist/
node_modules/
package-lock.json

View File

@@ -3,6 +3,11 @@ sudo: false
node_js:
- 8
- 9
- 10
- 11
- 12
# - 13
# - 14
before_install:
- npm install --only=dev
script:

View File

@@ -1,6 +1,10 @@
# Crunchy: a fork of Deathspike/CrunchyRoll.js
[![Issue Stats](http://issuestats.com/github/Godzil/Crunchy/badge/issue)](http://issuestats.com/github/Godzil/Crunchy) [![Travis CI](https://travis-ci.org/Godzil/Crunchy.svg?branch=master)](https://travis-ci.org/Godzil/Crunchy) [![Maintainability](https://api.codeclimate.com/v1/badges/413c7ca11c0805b1ef3e/maintainability)](https://codeclimate.com/github/Godzil/Crunchy/maintainability)
[![Travis CI](https://travis-ci.org/Godzil/Crunchy.svg?branch=master)](https://travis-ci.org/Godzil/Crunchy)
[![Maintainability](https://api.codeclimate.com/v1/badges/413c7ca11c0805b1ef3e/maintainability)](https://codeclimate.com/github/Godzil/Crunchy/maintainability)
![npm](https://img.shields.io/npm/dy/crunchy.svg)
![Issue Count](https://img.shields.io/github/issues/Godzil/Crunchy.svg)
![npm](https://img.shields.io/npm/v/crunchy.svg?label=Last%20published%20version)
*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.
@@ -75,7 +79,6 @@ The [command-line interface](http://en.wikipedia.org/wiki/Command-line_interface
-e, --episodes <s> Episode list. Read documentation on how to use
-f, --format <s> The subtitle format. (default: ass)
-o, --output <s> The output path.
-s, --series <s> The series name override.
--ignoredub Experimental: Ignore all seasons where the title end with 'Dub)'
-n, --nametmpl <s> Output name template (default: {SERIES_TITLE} - s{SEASON_NUMBER}e{EPISODE_NUMBER} - [{TAG}])
-t, --tag <s> The subgroup. (default: CrunchyRoll)
@@ -84,6 +87,8 @@ The [command-line interface](http://en.wikipedia.org/wiki/Command-line_interface
--verbose Make tool verbose
--rebuildcrp Rebuild the crpersistant file.
--retry <i> Number or time to retry fetching an episode. (default: 5)
-s, --sublang <items> Select the subtitle languages, multiple value separated by a comma are accepted (like: frFR,enUS )
--sleepTime <i> Minimum wait time between each http requests.
-h, --help output usage information
#### Batch-mode
@@ -94,7 +99,7 @@ When no sequence of series addresses is provided, the batch-mode source file wil
Starting from version 1.4.0, Crunchy store some information in a config.json file. The file which is use have to be in the folder you are calling Crunchy. This is partly by design and a limitation on where Crunchy can find files.
This file store some informations like your username and password.
This file store some information like your username and password.
You don't need to create that file as Crunchy will create it for you, the first time you run it. Each run will update the content of the file, so it you run crunchy with your credential on the command line, it will add them to config file.
@@ -111,6 +116,7 @@ Here are the list of valid parameter in the config file:
* `nametmpl` see `--nametmpl`
* `tag` see `--tag`
* `resolution` see `--resolution`
* `sublang` see `--sublang`
- Login related options:
* `pass` see `--user`
@@ -124,14 +130,15 @@ Here are the list of valid parameter in the config file:
* `crLocale`
* `crSessionKey`
* `crLoginUrl`
* `crUserId`
* `crUserKey`
- Other options:
* `sleepTime`: minimum delay (in ms) between each page load
- Generated values: don't touch them:
* `crDeviceId`
* `crSessionId`
Some of theses login related options are not going to be documented on what to put there for _legal_ reason.
Some of these login related options are not going to be documented on what to put there for _legal_ reason.
Crunchy will also create a `.cookie.jar` file in the output folder (by default the current folder) it is the file used by Crunchy to store the web cookies.
@@ -181,7 +188,24 @@ Download episodes starting from 42 to the last available of *Tail Fairy*:
crunchy -u login -p password http://www.cr.com/tail-fairy -e 42-
Download episode up to 42 (included) of *Tail Fairy* with italian subtitles:
crunchy -u login -p password http://www.cr.com/tail-fairy -e -42 -s itIT
Download episode up to 42 (included) of *Tail Fairy* with italian subtitles and fallback to english if no available:
crunchy -u login -p password http://www.cr.com/tail-fairy -e -42 -s itIT,enUS
#### Known valid subtitles language:
- `enUS` : English
- `frFR` : French
- `ptBR` : Portuguese (Brazil)
- `esES` : Spanish (Spain)
- `deDE` : German
- `esLA` : Spanish (Latin America)
- `itIT` : Italian
- `arME` : Armenian
- `ptPT` : Portuguese (Portugal)
#### Command line parameters
@@ -242,5 +266,5 @@ More information will be added at a later point. For now the recommendations are
* Atom with `atom-typescript` and `linter-tslint` (and dependencies).
Since this project uses TypeScript, compile with `node run compile` to build the tool and `npm run test` to run the linter.
Since this project uses TypeScript, compile with `node run build` to build the tool and `npm run test` to run the linter.

3034
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,40 +15,41 @@
"engines": {
"node": ">=5.0"
},
"version": "1.4.0",
"version": "1.6.0",
"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",

View File

@@ -33,19 +33,19 @@ export default function(args: string[], done: (err?: Error) => void)
batchPath = path.normalize(path.join(process.cwd(), config.batch));
}
// Update the config file with new parameters
cfg.save(config);
if (config.unlog)
if (config.nametmpl === undefined)
{
config.crDeviceId = undefined;
config.user = undefined;
config.pass = undefined;
my_request.eatCookies(config);
cfg.save(config);
log.info('Unlogged!');
config.nametmpl = '{SERIES_TITLE} - s{SEASON_NUMBER}e{EPISODE_NUMBER} - {EPISODE_TITLE} - [{TAG}]';
}
process.exit(0);
if (config.tag === undefined)
{
config.tag = 'CrunchyRoll';
}
if (config.sublang === undefined)
{
config.sublang = [ 'enUS' ];
}
// set resolution
@@ -70,6 +70,21 @@ export default function(args: string[], done: (err?: Error) => void)
config.video_quality = resol_table['1080'].quality;
}
// Update the config file with new parameters
cfg.save(config);
if (config.unlog)
{
config.crDeviceId = undefined;
config.user = undefined;
config.pass = undefined;
my_request.eatCookies(config);
cfg.save(config);
log.info('Unlogged!');
process.exit(0);
}
if (config.debug)
{
/* Ugly but meh */
@@ -88,7 +103,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();
}
@@ -128,7 +143,10 @@ export default function(args: string[], done: (err?: Error) => void)
}
else if (tasksArr[i].retry <= 0)
{
log.error(JSON.stringify(errin));
if (config.verbose)
{
log.error(JSON.stringify(errin));
}
if (config.debug)
{
log.dumpToDebug('BatchGiveUp', JSON.stringify(errin));
@@ -155,7 +173,7 @@ export default function(args: string[], done: (err?: Error) => void)
{
i += 1;
}
next();
setTimeout(next, config.sleepTime);
});
})();
});
@@ -195,7 +213,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 +227,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 +276,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 +298,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 +331,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}};
}));
}
@@ -325,6 +379,10 @@ function tasks(config: IConfigLine, batchPath: string, done: (err: Error, tasks?
});
}
function commaSeparatedList(value: any, dummyPrevious: any) {
return value.split(',');
}
/**
* Parses the arguments and returns a configuration.
*/
@@ -341,17 +399,21 @@ 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('-s, --sublang <items>', 'Select the subtitle languages, multiple value separated by a comma ' +
'are accepted (like: frFR,enUS )', commaSeparatedList)
.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('-t, --tag <s>', 'The subgroup.', 'CrunchyRoll')
.option('-r, --resolution <s>', 'The video resolution. (valid: 360, 480, 720, 1080)', '1080')
.option('-n, --nametmpl <s>', 'Output name template')
.option('-t, --tag <s>', 'The subgroup.')
.option('-r, --resolution <s>', 'The video resolution. (valid: 360, 480, 720, 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')
.option('--sleepTime <i>', 'Minimum wait time between each http requests.')
.parse(args);
}

View File

@@ -8,17 +8,27 @@ const current_version = pjson.version;
/* Check if the current version is the latest */
log.info('Crunchy version ' + current_version);
request.get({ uri: 'https://raw.githubusercontent.com/Godzil/Crunchy/master/package.json' },
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);
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)
if (response && (response.statusCode === 200))
{
log.warn('There is a newer version of crunchy (v' + onlinepkg.version + '), you should update!');
const onlinepkg = JSON.parse(body);
if (onlinepkg.status === 'ok')
{
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.');
}
});

View File

@@ -45,13 +45,32 @@ 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;
tmp.episodes = undefined;
tmp.series = undefined;
tmp.video_format = undefined;
tmp.video_quality = undefined;
tmp.rebuildcrp = undefined;
tmp.batch = undefined;
tmp.verbose = undefined;

View File

@@ -5,6 +5,7 @@ import mkdirp = require('mkdirp');
import my_request = require('./my_request');
import path = require('path');
import subtitle from './subtitle/index';
import vlos from './vlos';
import video from './video/index';
import xml2js = require('xml2js');
import log = require('./log');
@@ -21,15 +22,24 @@ export default function(config: IConfig, address: string, done: (err: Error, ign
return done(err, false);
}
scrapePlayer(config, address, page.id, (errS, player) =>
if (page.media != null)
{
if (errS)
/* No player to scrape */
download(config, page, null, done);
}
else
{
/* The old way */
scrapePlayer(config, address, page.id, (errS, player) =>
{
return done(errS, false);
}
if (errS)
{
return done(errS, false);
}
download(config, page, player, done);
});
download(config, page, player, done);
});
}
});
}
@@ -66,13 +76,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,16 +121,11 @@ 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) =>
downloadSubtitle(config, page, player, filePath, (errDS) =>
{
if (errDS)
{
@@ -127,7 +134,8 @@ function download(config: IConfig, page: IEpisodePage, player: IEpisodePlayer, d
}
const now = Date.now();
if (player.video.file !== undefined)
if ( ((page.media === null) && (player.video.file !== undefined))
|| ((page.media !== null) /* Do they still create page in advance for unreleased episodes? */) )
{
log.dispEpisode(fileName, 'Fetching video...', false);
downloadVideo(config, page, player, filePath, (errDV) =>
@@ -143,10 +151,28 @@ function download(config: IConfig, page: IEpisodePage, player: IEpisodePlayer, d
return complete(fileName, 'Finished!', now, done);
}
const isSubtited = Boolean(player.subtitle);
let isSubtitled = true;
if (page.media === null)
{
isSubtitled = Boolean(player.subtitle);
}
else
{
if (page.media.subtitles.length === 0)
{
isSubtitled = false;
}
}
let videoExt = '.mp4';
if ( (page.media === null) && (player.video.mode === 'RTMP'))
{
videoExt = path.extname(player.video.file);
}
log.dispEpisode(fileName, 'Merging...', false);
video.merge(config, isSubtited, player.video.file, filePath, player.video.mode, config.verbose, (errVM) =>
video.merge(config, isSubtitled, videoExt, filePath, config.verbose, (errVM) =>
{
if (errVM)
{
@@ -164,56 +190,136 @@ 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, page: IEpisodePage, player: IEpisodePlayer,
filePath: string, done: (err?: Error | string) => void)
{
const enc = player.subtitle;
if (!enc)
if (page.media !== null)
{
return done();
}
subtitle.decode(enc.id, enc.iv, enc.data, (errSD, data) =>
{
if (errSD)
const subs = page.media.subtitles;
if (subs.length === 0)
{
return done(errSD);
/* No downloadable subtitles */
console.warn('Can\'t find subtitle ?!');
return done();
}
if (config.debug)
{
log.dumpToDebug('SubtitlesXML', data);
}
let i;
let j;
const formats = subtitle.formats;
const format = formats[config.format] ? config.format : 'ass';
formats[format](config, data, (errF: Error, decodedSubtitle: string) =>
/* Find a proper subtitles */
for (j = 0; j < config.sublang.length; j++)
{
if (errF)
const reqSubLang = config.sublang[j];
for (i = 0; i < subs.length; i++)
{
return done(errF);
const curSub = subs[i];
if (curSub.format === 'ass' && curSub.language === reqSubLang)
{
my_request.get(config, curSub.url, (err, result) =>
{
if (err)
{
log.error('An error occured while fetching subtitles...');
return done(err);
}
fs.writeFile(filePath + '.ass', '\ufeff' + result, done);
});
/* Break from the first loop */
j = config.sublang.length;
break;
}
}
}
if (i >= subs.length)
{
done('Cannot find subtitles with requested language(s)');
}
}
else
{
const enc = player.subtitle;
if (!enc)
{
return done();
}
subtitle.decode(enc.id, enc.iv, enc.data, (errSD, data) =>
{
if (errSD)
{
log.error('An error occured while getting subtitles...');
return done(errSD);
}
fs.writeFile(filePath + '.' + format, '\ufeff' + decodedSubtitle, done);
if (config.debug)
{
log.dumpToDebug('SubtitlesXML', data);
}
const formats = subtitle.formats;
const format = formats[config.format] ? config.format : 'ass';
formats[format](config, data, (errF: Error, decodedSubtitle: string) =>
{
if (errF)
{
return done(errF);
}
fs.writeFile(filePath + '.' + format, '\ufeff' + decodedSubtitle, done);
});
});
});
}
}
/**
* Streams the video to disk.
*/
function downloadVideo(config: IConfig, page: IEpisodePage, player: IEpisodePlayer,
filePath: string, done: (err: Error) => void)
filePath: string, done: (err: any) => void)
{
video.stream(player.video.host, player.video.file, page.swf, filePath,
path.extname(player.video.file), player.video.mode, config.verbose, done);
if (player == null)
{
/* new way */
const streams = page.media.streams;
let i;
/* Find a proper subtitles */
for (i = 0; i < streams.length; i++)
{
if (streams[i].format === 'vo_adaptive_hls' && streams[i].audio_lang === 'jaJP' &&
streams[i].hardsub_lang === null)
{
video.stream('', streams[i].url, '', filePath,
'mp4', 'HLS', config.verbose, done);
break;
}
}
if (i >= streams.length)
{
done('Cannot find a valid stream');
}
}
else
{
/* Old way */
video.stream(player.video.host, player.video.file, page.swf, filePath,
path.extname(player.video.file), player.video.mode, config.verbose, done);
}
}
/**
@@ -275,51 +381,71 @@ function scrapePage(config: IConfig, address: string, done: (err: Error, page?:
}
const $ = cheerio.load(result);
const swf = /^([^?]+)/.exec($('link[rel=video_src]').attr('href'));
const regexp = /\s*([^\n\r\t\f]+)\n?\s*[^0-9]*([0-9][\-0-9.]*)?,?\n?\s\s*[^0-9]*((PV )?[S0-9][P0-9.]*[a-fA-F]?)/;
const look = $('#showmedia_about_media').text();
const seasonTitle = $('span[itemprop="title"]').text();
const episodeTitle = $('#showmedia_about_name').text().replace(/[“”]/g, '');
const data = regexp.exec(look);
/* First check if we have the new player */
const vlosScript = $('#vilos-iframe-container');
if (config.debug)
if (vlosScript)
{
log.dumpToDebug('episode page', $.html());
}
const pageMetadata = JSON.parse($('script[type="application/ld+json"]')[0].children[0].data);
const divScript = $('div[id="showmedia_video_box_wide"]');
const scripts = divScript.find('script').toArray();
const script = scripts[2].children[0].data;
let seasonNumber = '1';
let seasonTitle = '';
if (!swf || !data)
{
log.warn('Somethig unexpected in the page at ' + address + ' (data are: ' + look + ')');
log.warn('Setting Season to 0 and episode to 0...');
if (config.debug)
if (pageMetadata.partOfSeason)
{
log.dumpToDebug('episode unexpected', look);
seasonNumber = pageMetadata.partOfSeason.seasonNumber;
seasonTitle = pageMetadata.partOfSeason.name;
}
done(null, {
episode: '0',
id: epId,
series: seasonTitle,
season: seasonTitle,
title: episodeTitle,
swf: swf[1],
volume: '0',
filename: '',
});
done(null, vlos.getMedia(script, seasonTitle, seasonNumber));
}
else
{
done(null, {
episode: data[3],
id: epId,
series: data[1],
season: seasonTitle,
title: episodeTitle,
swf: swf[1],
volume: data[2] || '1',
filename: '',
});
/* Use the old way */
const swf = /^([^?]+)/.exec($('link[rel=video_src]').attr('href'));
const regexp = /\s*([^\n\r\t\f]+)\n?\s*[^0-9]*([0-9][\-0-9.]*)?,?\n?\s\s*[^0-9]*((PV )?[S0-9][P0-9.]*[a-fA-F]?)/;
const seasonTitle = $('span[itemprop="title"]').text();
const look = $('#showmedia_about_media').text();
const episodeTitle = $('#showmedia_about_name').text().replace(/[“”]/g, '');
const data = regexp.exec(look);
if (config.debug) {
log.dumpToDebug('episode page', $.html());
}
if (!swf || !data) {
log.warn('Somethig unexpected in the page at ' + address + ' (data are: ' + look + ')');
log.warn('Setting Season to 0 and episode to 0...');
if (config.debug) {
log.dumpToDebug('episode unexpected', look);
}
done(null, {
episode: '0',
id: epId,
series: seasonTitle,
season: seasonTitle,
title: episodeTitle,
swf: swf[1],
volume: '0',
filename: '',
media: null,
});
} else {
done(null, {
episode: data[3],
id: epId,
series: data[1],
season: seasonTitle,
title: episodeTitle,
swf: swf[1],
volume: data[2] || '1',
filename: '',
media: null,
});
}
}
});
}
@@ -336,15 +462,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)
{

View File

@@ -1,3 +1,5 @@
interface IAuthError extends Error {
name: string;
message: string;
authError: boolean;
}

View File

@@ -7,6 +7,8 @@ interface IConfig {
merge?: boolean;
episodes?: string;
// Settings
crlang?: string;
sublang?: any;
format?: string;
output?: string;
series?: string;
@@ -22,6 +24,7 @@ interface IConfig {
debug?: boolean;
unlog?: boolean;
retry?: number;
sleepTime?: number;
// Login options
userAgent?: string;
logUsingApi?: boolean;
@@ -32,9 +35,6 @@ interface IConfig {
crLocale?: string;
crSessionKey?: string;
crLoginUrl?: string;
// Third method, injecting data from cookies
crUserId?: string;
crUserKey?: string;
// Generated values
crDeviceId?: string;
crSessionId?: string;

View File

@@ -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
View File

@@ -0,0 +1,4 @@
interface IEpisodeNumber {
season: number,
episode: number
}

View File

@@ -7,4 +7,5 @@ interface IEpisodePage {
title: string;
swf: string;
filename: string;
media: IVlosScript;
}

14
src/interface/IVlosScript.d.ts vendored Normal file
View File

@@ -0,0 +1,14 @@
interface IVlosScript
{
metadata: {
episode_number: any;
id: any;
title: any;
};
confic: any;
subtitles: any;
streams: any;
series: {
title: any;
};
}

66
src/languages.ts Normal file
View 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\))|\(Russian\)/i,
fr: /\(.*Dub(?:bed)?.*\)|(?:\(RU\))|\(?Doublage.*\)|\(Russian\)?/,
de: /\(.*isch\)|\(Dubbed\)|\(RU\)|\(Russian\)/
};
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;
}

View File

@@ -26,6 +26,12 @@ export function warn(str: string)
console.log(' \x1B[1;33m* WARN \x1B[0m: ' + str);
}
export function warnMore(str: string)
{
/* Do fancy output */
console.log(' \x1B[1;38;5;166m* WARN \x1B[0m: ' + str);
}
export function dispEpisode(name: string, status: string, addNL: boolean)
{
/* Do fancy output */

View File

@@ -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
@@ -173,21 +181,16 @@ export function eatCookies(config: IConfig)
j = undefined;
}
export function getUserAgent(): string
{
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)
@@ -195,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);
@@ -207,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)
@@ -226,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)
{
@@ -237,10 +230,128 @@ export function post(config: IConfig, options: request.Options, done: (err: Erro
});
}
function authUsingCookies(config: IConfig, done: (err: any) => void)
{
j.setCookie(request.cookie('session_id=' + config.crSessionId + '; 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)
{
@@ -255,155 +366,59 @@ 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 = {};
currentOptions.headers['Cache-Control'] = 'private';
currentOptions.headers.Accept = 'application/xml,application/xhtml+xml,text/html;q=0.9, text/plain;q=0.8,image/png,*/*;q=0.5';
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;
}

View File

@@ -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';
/**
@@ -105,7 +106,8 @@ export default function(config: IConfig, task: IConfigTask, done: (err: any) =>
'" - Retry ' + page.episodes[i].retry + ' / ' + config.retry);
page.episodes[i].retry -= 1;
}
next();
setTimeout(next, config.sleepTime);
return;
}
else
{
@@ -120,13 +122,15 @@ export default function(config: IConfig, task: IConfigTask, done: (err: any) =>
}
i += 1;
next();
setTimeout(next, config.sleepTime);
return;
});
}
else
{
i += 1;
next();
setTimeout(next, config.sleepTime);
return;
}
}
});
@@ -143,9 +147,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 +202,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)
{
@@ -215,9 +220,16 @@ function pageScrape(config: IConfig, task: IConfigTask, done: (err: any, result?
log.info('Checking availability for ' + title);
const episodes: ISeriesEpisode[] = [];
if ($('.availability-notes-low').length || $('.availability-notes-high').length)
if ($('.availability-notes-low').length)
{
log.warn('This serie may have georestriction and some missings episode.');
log.warn('This serie may have georestriction and some missings episode (like some dubs)' +
' [Message: ' + $('.availability-notes-low').text() + '].');
}
if ($('.availability-notes-high').length)
{
log.warnMore('This serie probably have georestriction and will miss some episodes' +
' [Message: ' + $('.availability-notes-high').text() + '].');
}
$('.episode').each((i, el) => {
@@ -227,11 +239,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;
}

View File

@@ -9,21 +9,14 @@ import subtitle from '../subtitle/index';
/**
* Merges the subtitle and video files into a Matroska Multimedia Container.
*/
export default function(config: IConfig, isSubtitled: boolean, rtmpInputPath: string, filePath: string,
streamMode: string, verbose: boolean, done: (err: Error) => void)
export default function(config: IConfig, isSubtitled: boolean, videoFileExtention: string, filePath: string,
verbose: boolean, done: (err: Error) => void)
{
const subtitlePath = filePath + '.' + (subtitle.formats[config.format] ? config.format : 'ass');
let videoPath = filePath;
let cp;
if (streamMode === 'RTMP')
{
videoPath += path.extname(rtmpInputPath);
}
else
{
videoPath += '.mp4';
}
videoPath += videoFileExtention;
cp = childProcess.exec(command() + ' ' +
'-o "' + filePath + '.mkv" ' +

View File

@@ -3,6 +3,7 @@ import childProcess = require('child_process');
import os = require('os');
import path = require('path');
import my_request = require('../my_request');
import log = require('../log');
/**
@@ -24,7 +25,8 @@ export default function(rtmpUrl: string, rtmpInputPath: string, swfUrl: string,
else if (mode === 'HLS')
{
cmd = command('ffmpeg') + ' ' +
'-y -xerror ' +
'-user_agent "' + my_request.getUserAgent() + '" ' +
'-y -xerror -discard none ' +
'-i "' + rtmpInputPath + '" ' +
'-c copy -bsf:a aac_adtstoasc ' +
'"' + filePath + '.mp4"';

70
src/vlos.ts Normal file
View File

@@ -0,0 +1,70 @@
'use strict';
export default {getMedia};
function getMedia(vlosScript: string, seasonTitle: string, seasonNumber: string): IEpisodePage
{
let vlosMedia: IVlosScript;
function f(script: string) {
/* We need to scope things */
/* This is what will give us the medias */
function VilosPlayer() {
this.load = function(a: string, b: any, c: any)
{
vlosMedia = this.config.media;
vlosMedia.series = this.config.analytics.media_reporting_parent;
};
this.config = {};
this.config.player = {};
this.config.player.pause_screen = {};
this.config.language = '';
}
/* Let's stub what the script need */
const window = {
WM: {
UserConsent: {
getUserConsentAdvertisingState(): string { return ''; }
}
}
};
const document = {
getElementsByClassName(a: any): any { return {length: 0}; },
};
const localStorage = {
getItem(a: any): any { return null; },
};
const $ = {
cookie(a: any) { /* nothing */ },
};
/*
Evil ugly things. Need to run the script from a somewhat untrusted source.
Need to find a better way of doing.
*/
// tslint:disable-next-line:no-eval
eval(script);
}
f(vlosScript);
if (vlosMedia === undefined)
{
console.error('Error fetching vlos data - aborting - Please report the error if happen again.');
process.exit(-1);
}
return {
episode: vlosMedia.metadata.episode_number,
id: vlosMedia.metadata.id,
series: vlosMedia.series.title,
season: seasonTitle,
title: vlosMedia.metadata.title,
swf: '',
volume: seasonNumber,
filename: '',
media: vlosMedia,
};
}

96
ts.js
View File

@@ -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);
}