Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de3f9a1e9e | ||
|
|
f26ea70ef8 | ||
|
|
baf15dc1b4 | ||
|
|
2c58e5e4ba | ||
|
|
c8f33e947d | ||
|
|
fc119acb1c | ||
|
|
2db34c3ed8 | ||
|
|
64200a1da9 | ||
|
|
8655874097 | ||
|
|
99ba051b7f | ||
|
|
d692199d07 | ||
|
|
e058b8e699 | ||
|
|
b3a96cd0e7 | ||
|
|
f25a62234c | ||
|
|
e8a7856e44 | ||
|
|
f086b3593e | ||
|
|
d2bc7e41b2 | ||
|
|
a7bc34df0d | ||
|
|
b2ecd05586 | ||
|
|
8b9f9a5e1c | ||
|
|
376ff09632 | ||
|
|
7926b2fd9a | ||
|
|
3f2a920e1e | ||
|
|
fa4c68c239 | ||
|
|
1555229635 | ||
|
|
7c085caaa0 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,3 +1,6 @@
|
||||
dist/
|
||||
node_modules/
|
||||
package-lock.json
|
||||
.idea/
|
||||
test/
|
||||
config.json
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
extras/
|
||||
node_modules/
|
||||
src/
|
||||
typings/
|
||||
ts.js
|
||||
tsconfig.json
|
||||
tsd.json
|
||||
tslint.json
|
||||
.idea/
|
||||
.github/
|
||||
test/
|
||||
config.json
|
||||
.travis.yml
|
||||
|
||||
@@ -5,6 +5,9 @@ node_js:
|
||||
- 9
|
||||
- 10
|
||||
- 11
|
||||
- 12
|
||||
# - 13
|
||||
# - 14
|
||||
before_install:
|
||||
- npm install --only=dev
|
||||
script:
|
||||
|
||||
32
README.md
32
README.md
@@ -79,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)
|
||||
@@ -88,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
|
||||
@@ -98,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.
|
||||
|
||||
@@ -115,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`
|
||||
@@ -128,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.
|
||||
|
||||
@@ -185,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
|
||||
|
||||
@@ -246,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.
|
||||
|
||||
|
||||
46
package.json
46
package.json
@@ -15,41 +15,41 @@
|
||||
"engines": {
|
||||
"node": ">=5.0"
|
||||
},
|
||||
"version": "1.5.0",
|
||||
"version": "1.6.1",
|
||||
"bin": {
|
||||
"crunchy": "./bin/crunchy",
|
||||
"crunchy.sh": "./bin/crunchy.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"big-integer": "^1.6.44",
|
||||
"bluebird": "^3.5.5",
|
||||
"big-integer": "^1.6.48",
|
||||
"bluebird": "^3.7.2",
|
||||
"brotli": "^1.3.2",
|
||||
"cheerio": "^0.22.0",
|
||||
"cloudscraper": "^4.1.2",
|
||||
"commander": "^2.20.0",
|
||||
"fs-extra": "^8.1.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.88.0",
|
||||
"request-promise": "^4.2.4",
|
||||
"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.27",
|
||||
"@types/cheerio": "^0.22.12",
|
||||
"@types/fs-extra": "^8.0.0",
|
||||
"@types/mkdirp": "^0.5.2",
|
||||
"@types/node": "^12.6.8",
|
||||
"@types/request": "^2.48.2",
|
||||
"@types/request-promise": "^4.1.44",
|
||||
"@types/uuid": "^3.4.5",
|
||||
"@types/xml2js": "^0.4.4",
|
||||
"npm-check": "^5.9.0",
|
||||
"@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.18.0",
|
||||
"typescript": "^3.5.3"
|
||||
"tslint": "^6.1.1",
|
||||
"typescript": "^3.8.3"
|
||||
},
|
||||
"scripts": {
|
||||
"prepublishOnly": "npm run build",
|
||||
|
||||
96
src/batch.ts
96
src/batch.ts
@@ -38,19 +38,14 @@ export default function(args: string[], done: (err?: Error) => void)
|
||||
config.nametmpl = '{SERIES_TITLE} - s{SEASON_NUMBER}e{EPISODE_NUMBER} - {EPISODE_TITLE} - [{TAG}]';
|
||||
}
|
||||
|
||||
// Update the config file with new parameters
|
||||
cfg.save(config);
|
||||
|
||||
if (config.unlog)
|
||||
if (config.tag === undefined)
|
||||
{
|
||||
config.crDeviceId = undefined;
|
||||
config.user = undefined;
|
||||
config.pass = undefined;
|
||||
my_request.eatCookies(config);
|
||||
cfg.save(config);
|
||||
log.info('Unlogged!');
|
||||
config.tag = 'CrunchyRoll';
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
if (config.sublang === undefined)
|
||||
{
|
||||
config.sublang = [ 'enUS' ];
|
||||
}
|
||||
|
||||
// set resolution
|
||||
@@ -75,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 */
|
||||
@@ -133,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));
|
||||
@@ -160,7 +173,7 @@ export default function(args: string[], done: (err?: Error) => void)
|
||||
{
|
||||
i += 1;
|
||||
}
|
||||
next();
|
||||
setTimeout(next, config.sleepTime);
|
||||
});
|
||||
})();
|
||||
});
|
||||
@@ -200,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)
|
||||
{
|
||||
@@ -214,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)
|
||||
{
|
||||
@@ -235,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};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -290,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}};
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -338,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.
|
||||
*/
|
||||
@@ -355,17 +400,20 @@ function parse(args: string[]): IConfigLine
|
||||
.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')
|
||||
.option('-t, --tag <s>', 'The subgroup.', 'CrunchyRoll')
|
||||
.option('-r, --resolution <s>', 'The video resolution. (valid: 360, 480, 720, 1080)', '1080')
|
||||
.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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
312
src/episode.ts
312
src/episode.ts
@@ -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,73 @@ 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;
|
||||
if (seasonNumber === '0') { seasonNumber = '1'; }
|
||||
|
||||
done(null, {
|
||||
episode: '0',
|
||||
id: epId,
|
||||
series: seasonTitle,
|
||||
season: seasonTitle,
|
||||
title: episodeTitle,
|
||||
swf: swf[1],
|
||||
volume: '0',
|
||||
filename: '',
|
||||
});
|
||||
seasonTitle = pageMetadata.partOfSeason.name;
|
||||
}
|
||||
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 +464,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;
|
||||
}
|
||||
|
||||
5
src/interface/IConfig.d.ts
vendored
5
src/interface/IConfig.d.ts
vendored
@@ -8,6 +8,7 @@ interface IConfig {
|
||||
episodes?: string;
|
||||
// Settings
|
||||
crlang?: string;
|
||||
sublang?: any;
|
||||
format?: string;
|
||||
output?: string;
|
||||
series?: string;
|
||||
@@ -23,6 +24,7 @@ interface IConfig {
|
||||
debug?: boolean;
|
||||
unlog?: boolean;
|
||||
retry?: number;
|
||||
sleepTime?: number;
|
||||
// Login options
|
||||
userAgent?: string;
|
||||
logUsingApi?: boolean;
|
||||
@@ -33,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;
|
||||
|
||||
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
|
||||
}
|
||||
1
src/interface/IEpisodePage.d.ts
vendored
1
src/interface/IEpisodePage.d.ts
vendored
@@ -7,4 +7,5 @@ interface IEpisodePage {
|
||||
title: string;
|
||||
swf: string;
|
||||
filename: string;
|
||||
media: IVlosScript;
|
||||
}
|
||||
|
||||
14
src/interface/IVlosScript.d.ts
vendored
Normal file
14
src/interface/IVlosScript.d.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
interface IVlosScript
|
||||
{
|
||||
metadata: {
|
||||
episode_number: any;
|
||||
id: any;
|
||||
title: any;
|
||||
};
|
||||
confic: any;
|
||||
subtitles: any;
|
||||
streams: any;
|
||||
series: {
|
||||
title: any;
|
||||
};
|
||||
}
|
||||
@@ -25,9 +25,9 @@ export function localeToCC(locale: string): string
|
||||
|
||||
const dubignore_regexp: { [id: string]: RegExp; } =
|
||||
{
|
||||
en: /\(.*Dub(?:bed)?.*\)|(?:\(RU\))/i,
|
||||
fr: /\(.*Dub(?:bed)?.*\)|(?:\(RU\))|\(?Doublage.*\)?/,
|
||||
de: /\(.*isch\)|\(Dubbed\)|\(RU\)/
|
||||
en: /\(.*Dub(?:bed)?.*\)|(?:\(RU\))|\(Russian\)/i,
|
||||
fr: /\(.*Dub(?:bed)?.*\)|(?:\(RU\))|\(?Doublage.*\)|\(Russian\)?/,
|
||||
de: /\(.*isch\)|\(Dubbed\)|\(RU\)|\(Russian\)/
|
||||
};
|
||||
|
||||
export function get_diregexp(config: IConfig): RegExp
|
||||
|
||||
@@ -22,26 +22,10 @@ let isPremium = false;
|
||||
|
||||
let j: request.CookieJar;
|
||||
|
||||
const defaultHeaders =
|
||||
{
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36',
|
||||
'Connection': 'keep-alive',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3',
|
||||
'Referer': 'https://www.crunchyroll.com/login',
|
||||
'Cache-Control': 'private',
|
||||
'Accept-Language': 'en-US,en;q=0.9'
|
||||
};
|
||||
|
||||
const defaultOptions =
|
||||
{
|
||||
followAllRedirects: true,
|
||||
decodeEmails: true,
|
||||
challengesToSolve: 3,
|
||||
gzip: true,
|
||||
};
|
||||
|
||||
// tslint:disable-next-line:no-var-requires
|
||||
const cloudscraper = require('cloudscraper').defaults(defaultOptions);
|
||||
import cloudscraper = require('cloudscraper');
|
||||
let currentOptions: any;
|
||||
let optionsSet = false;
|
||||
|
||||
function AuthError(msg: string): IAuthError
|
||||
{
|
||||
@@ -98,19 +82,14 @@ function APIlogin(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 url = 'http://www.crunchyroll.com/';
|
||||
|
||||
cloudscraper.get({gzip: true, uri: url}, (err: Error, rep: string, body: string) =>
|
||||
cloudscraper.get(url, getOptions(config, null), (err: any, rep: Response, body: string) =>
|
||||
{
|
||||
if (err)
|
||||
{
|
||||
@@ -204,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|any, 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)
|
||||
@@ -229,7 +198,7 @@ export function get(config: IConfig, options: string|any, done: (err: any, resul
|
||||
return done(err);
|
||||
}
|
||||
|
||||
cloudscraper.get(modify(options), (error: any, response: any, body: any) =>
|
||||
cloudscraper.get(url, getOptions(config, null), (error: any, response: any, body: any) =>
|
||||
{
|
||||
if (error) return done(error);
|
||||
|
||||
@@ -241,18 +210,8 @@ export function get(config: IConfig, options: string|any, done: (err: any, resul
|
||||
/**
|
||||
* 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)
|
||||
@@ -260,7 +219,7 @@ export function post(config: IConfig, options: request.Options, done: (err: Erro
|
||||
return done(err);
|
||||
}
|
||||
|
||||
cloudscraper.post(modify(options), (error: Error, response: any, body: any) =>
|
||||
cloudscraper.post(url, getOptions(config, form), (error: Error, response: any, body: any) =>
|
||||
{
|
||||
if (error)
|
||||
{
|
||||
@@ -271,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)
|
||||
{
|
||||
@@ -289,145 +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.logUsingApi && !config.logUsingCookie) && (!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 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));
|
||||
});
|
||||
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 =
|
||||
{
|
||||
// jar: j,
|
||||
uri: 'https://www.crunchyroll.com/login'
|
||||
};
|
||||
|
||||
cloudscraper.get(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 =
|
||||
{
|
||||
form:
|
||||
{
|
||||
'login_form[name]': config.user,
|
||||
'login_form[password]': config.pass,
|
||||
'login_form[redirect_url]': '/',
|
||||
'login_form[_token]': token
|
||||
},
|
||||
// jar: j,
|
||||
url: 'https://www.crunchyroll.com/login'
|
||||
};
|
||||
|
||||
cloudscraper.post(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|any): any
|
||||
function getOptions(config: IConfig, form: any)
|
||||
{
|
||||
if (typeof options !== 'string')
|
||||
if (!optionsSet)
|
||||
{
|
||||
options.jar = j;
|
||||
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,
|
||||
url: options.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
currentOptions.form = {};
|
||||
|
||||
if (form !== null)
|
||||
{
|
||||
currentOptions.form = form;
|
||||
}
|
||||
|
||||
|
||||
return currentOptions;
|
||||
}
|
||||
@@ -106,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
|
||||
{
|
||||
@@ -121,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;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -144,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);
|
||||
}
|
||||
|
||||
@@ -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" ' +
|
||||
|
||||
70
src/vlos.ts
Normal file
70
src/vlos.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user