Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7da4289097 | ||
|
|
ce5038cf08 | ||
|
|
1b0f53a88c | ||
|
|
d19992f0d3 | ||
|
|
a44d1ae668 | ||
|
|
14fd18479e | ||
|
|
1106a28288 | ||
|
|
f1a5d1b6a8 | ||
|
|
4193643306 | ||
|
|
ebe671ff5b | ||
|
|
fd447f2cc6 | ||
|
|
7dcd932ee5 | ||
|
|
ed233de565 | ||
|
|
a679573bf3 | ||
|
|
24d6892261 | ||
|
|
25dabd4955 | ||
|
|
ce65324c57 | ||
|
|
a0f10252a1 | ||
|
|
6e638488dc | ||
|
|
2e8de8c5c2 | ||
|
|
9c3aaf220a | ||
|
|
ab35bb4439 | ||
|
|
b48877b786 | ||
|
|
9fb85d4376 | ||
|
|
a582b15103 | ||
|
|
da3a51991c | ||
|
|
22f70c86f5 | ||
|
|
0daf4d895f | ||
|
|
80165a76e0 | ||
|
|
a6b025bdbf | ||
|
|
02a9ed1eb8 | ||
|
|
6f192b1712 | ||
|
|
b947a110e2 | ||
|
|
68885db538 | ||
|
|
0b54549c64 | ||
|
|
141bdccf02 | ||
|
|
4990effa1c | ||
|
|
2459f342c5 | ||
|
|
d68a2b7bce | ||
|
|
69d5ceac36 | ||
|
|
cf7039400c | ||
|
|
02a9d763cd | ||
|
|
d549d46979 | ||
|
|
3f5b4b2585 | ||
|
|
1d596b02f7 | ||
|
|
cee53fb113 | ||
|
|
1e56cab73f | ||
|
|
0dc3c1e8e2 | ||
|
|
0124e38a89 | ||
|
|
6765b517ec | ||
|
|
8c1e0f2e0c | ||
|
|
817843c40c | ||
|
|
04b22fdce5 | ||
|
|
eb15d7d854 | ||
|
|
66670547b9 | ||
|
|
987e424324 | ||
|
|
523c780b18 | ||
|
|
6c2100fbff | ||
|
|
f10bead0dc |
7
.github/ISSUE_TEMPLATE/bug_report.md
vendored
7
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -14,10 +14,12 @@ A clear and concise description of what you expected to happen.
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Please fill theses informations:**
|
||||
(Add a X between brackets to make them ticked)
|
||||
(Add a X between brackets to make them ticked if relevant)
|
||||
- OS: [e.g:. Windows 10, Mac OS X 10.13, ...]
|
||||
- [ ] I'm using the latest version of Crunchy
|
||||
- [ ] I have a premium accrount on CR
|
||||
- [ ] I am using a VPN
|
||||
- My region in the world (country or continent):
|
||||
- Serie you get a problem with (and specify which episode if it is specific to one):
|
||||
- The command line you are running Crunchy with:
|
||||
- The message Crunchy is giving you, if any:
|
||||
@@ -29,4 +31,5 @@ If applicable, add screenshots to help explain your problem.
|
||||
Add any other context about the problem here.
|
||||
|
||||
|
||||
_Also don't hesitate to add labels you feel apropriate on your report._
|
||||
_Also don't hesitate to add labels you feel apropriate on your report._
|
||||
_Please don't edit logs if you are adding them, apart from removing sensitive informations like login/password_
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,2 @@
|
||||
dist/
|
||||
node_modules/
|
||||
typings/
|
||||
|
||||
147
README.md
147
README.md
@@ -12,11 +12,11 @@
|
||||
|
||||
This application is not endorsed or affliated with *CrunchyRoll*. The usage of this application enables episodes to be downloaded for offline convenience which may be forbidden by law in your country. Usage of this application may also cause a violation of the agreed *Terms of Service* between you and the stream provider. A tool is not responsible for your actions; please make an informed decision prior to using this application.
|
||||
|
||||
**PLEASE _ONLY_ USE THIS TOOL IF YOU HAVE A _PREMIUM ACCOUNT_**
|
||||
**_ONLY_ USE THIS TOOL IF YOU HAVE A _PREMIUM ACCOUNT_**
|
||||
|
||||
## Configuration
|
||||
|
||||
It is recommended to enable authentication (`-p` and `-u`) so your account permissions and settings are available for use. It is not possible to download non-free material without an account and premium subscription. Furthermore, the default account settings are used when downloading. If you want the highest quality videos, configure these preferences at https://www.crunchyroll.com/acct/?action=video.
|
||||
You need to authentication (`-p` and `-u`) to use Crunchy so you need to have an account on *CrunchyRool*. It is not possible to download non-free material without an account and premium subscription.
|
||||
|
||||
|
||||
## Prerequisites
|
||||
@@ -66,42 +66,122 @@ The [command-line interface](http://en.wikipedia.org/wiki/Command-line_interface
|
||||
|
||||
Options:
|
||||
|
||||
-V, --version output the version number
|
||||
-p, --pass <s> The password.
|
||||
-u, --user <s> The e-mail address or username.
|
||||
-c, --cache Disables the cache.
|
||||
-m, --merge Disables merging subtitles and videos.
|
||||
-e, --episode <i> The episode filter.
|
||||
-v, --volume <i> The volume filter.
|
||||
-f, --format <s> The subtitle format. (Default: ass)
|
||||
-o, --output <s> The output path.
|
||||
-s, --series <s> The series override.
|
||||
-n, --filename <s> The name override.
|
||||
-t, --tag <s> The subgroup. (Default: CrunchyRoll) (default: CrunchyRoll)
|
||||
-r, --resolution <s> The video resolution. (Default: 1080 (360, 480, 720, 1080)) (default: 1080)
|
||||
-g, --rebuildcrp Rebuild the crpersistant file.
|
||||
-b, --batch <s> Batch file (default: CrunchyRoll.txt)
|
||||
--verbose Make tool verbose
|
||||
--retry <i> Number or time to retry fetching an episode. Default: 5 (default: 5)
|
||||
-h, --help output usage information
|
||||
-V, --version output the version number
|
||||
-p, --pass <s> The password.
|
||||
-u, --user <s> The e-mail address or username.
|
||||
-d, --unlog Unlog
|
||||
-c, --cache Disables the cache.
|
||||
-m, --merge Disables merging subtitles and videos.
|
||||
-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)
|
||||
-r, --resolution <s> The video resolution. (valid: 360, 480, 720, 1080) (default: 1080)
|
||||
-b, --batch <s> Batch file (default: CrunchyRoll.txt)
|
||||
--verbose Make tool verbose
|
||||
--rebuildcrp Rebuild the crpersistant file.
|
||||
--retry <i> Number or time to retry fetching an episode. (default: 5)
|
||||
-h, --help output usage information
|
||||
|
||||
#### Batch-mode
|
||||
|
||||
When no sequence of series addresses is provided, the batch-mode source file will be read (which is *CrunchyRoll.txt* in the current work directory. Each line in this file is processed as a seperate command-line statement. This makes it ideal to manage a large sequence of series addresses with variating command-line options or incremental episode updates.
|
||||
When no sequence of series addresses is provided, the batch-mode source file will be read (which is *CrunchyRoll.txt* in the current work directory. Each line in this file is processed contain the URL of a series and can support some of the command line parameter (like `-e`). This makes it ideal to manage a large sequence of series addresses.
|
||||
|
||||
#### Configuration file
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
There are some parameter that the config file can accept which are not created by default, and some of them are cannont be set form the command line parameter.
|
||||
|
||||
Don't mess with them if you don't know what you are doing.
|
||||
|
||||
Here are the list of valid parameter in the config file:
|
||||
|
||||
- Output options
|
||||
* `merge` see `--merge`
|
||||
* `format` see `--format`
|
||||
* `output` see `--output`
|
||||
* `nametmpl` see `--nametmpl`
|
||||
* `tag` see `--tag`
|
||||
* `resolution` see `--resolution`
|
||||
|
||||
- Login related options:
|
||||
* `pass` see `--user`
|
||||
* `user` see `--pass`
|
||||
* `userAgent` set the user agent reported by Crunchy while crawling pages
|
||||
* `logUsingApi`
|
||||
* `logUsingCookie`
|
||||
* `crSessionUrl`
|
||||
* `crDeviceType`
|
||||
* `crAPIVersion`
|
||||
* `crLocale`
|
||||
* `crSessionKey`
|
||||
* `crLoginUrl`
|
||||
* `crUserId`
|
||||
* `crUserKey`
|
||||
|
||||
- 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.
|
||||
|
||||
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.
|
||||
|
||||
#### Examples
|
||||
|
||||
Download in batch-mode:
|
||||
|
||||
crunchy
|
||||
You will need to create the batch file (default name is `CrunchyRoll.txt`):
|
||||
|
||||
Download *Fairy Tail* to the current work directory:
|
||||
http://www.cr.com/tail-fairy
|
||||
http://www.cr.com/gin-mama
|
||||
http://www.cr.com/two-parts
|
||||
// Just download episodes 3 to 42
|
||||
http://www.cr.com/defense-of-dwarfs -e 3-42
|
||||
|
||||
crunchy http://www.crunchyroll.com/fairy-tail
|
||||
Then launch crunchy:
|
||||
|
||||
crunchy -u login -p password http://www.cr.com/tail-fairy
|
||||
|
||||
Download *Tail Fairy* to the current work directory:
|
||||
|
||||
crunchy -u login -p password http://www.cr.com/tail-fairy
|
||||
|
||||
Download *Tail Fairy* to `C:\Anime`:
|
||||
|
||||
crunchy -u login -p password --output C:\Anime http://www.cr.com/tail-fairy
|
||||
|
||||
Download episode 42 of *Tail Fairy* to `C:\Anime`:
|
||||
|
||||
crunchy -u login -p password --output C:\Anime @http://www.cr.com/tail-fairy/episode-42-the-episode-which-dont-exist-665544
|
||||
|
||||
*Notice the '@' in front of the URL, it is there to tell Crunchy that the URL is an episode URL and not a series URL.*
|
||||
|
||||
or
|
||||
|
||||
crunchy -u login -p password --output C:\Anime http://www.cr.com/tail-fairy -e 42
|
||||
|
||||
Download episode 10 to 42 (both included) of *Tail Fairy*:
|
||||
|
||||
crunchy -u login -p password http://www.cr.com/tail-fairy -e 10-42
|
||||
|
||||
Download episode up to 42 (included) of *Tail Fairy*:
|
||||
|
||||
crunchy -u login -p password http://www.cr.com/tail-fairy -e -42
|
||||
|
||||
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 *Fairy Tail* to `C:\Anime`:
|
||||
|
||||
crunchy --output C:\Anime http://www.crunchyroll.com/fairy-tail
|
||||
|
||||
#### Command line parameters
|
||||
|
||||
@@ -109,29 +189,28 @@ Download *Fairy Tail* to `C:\Anime`:
|
||||
|
||||
* `-p or --pass <s>` sets the password.
|
||||
* `-u or --user <s>` sets the e-mail address or username.
|
||||
* `-d or --unlog` unlog
|
||||
|
||||
_Please remember that login has to be done for each call of Crunchy, as none of the credentials are stored_
|
||||
_New in 1.4.0_: Crunchy remember between run about login information and other, so you need to passe the login and password only once
|
||||
I recommend to unlog if you see some problems during the run.
|
||||
|
||||
*When you unlog, the cookie file is deleted as for some parameter in the config file (like username and password).*
|
||||
|
||||
##### Disables
|
||||
|
||||
* `-c or --cache` disables the cache in batch mode.
|
||||
* `-m or --merge` disables merging subtitles and videos.
|
||||
|
||||
##### Filters
|
||||
|
||||
* `-e or --episode <i>` filters episodes (positive is greater than, negative is smaller than).
|
||||
* `-v or --volume <i>` filters volumes (positive is greater than, negative is smaller than).
|
||||
|
||||
_These parameters are probably extremely buggy at the moment..._
|
||||
|
||||
##### Settings
|
||||
|
||||
* `-e or --episodes <s>` set an episode
|
||||
* `-f or --format <s>` sets the subtitle format. (Default: ass)
|
||||
* `-o or --output <s>` sets the output path.
|
||||
* `-s or --series <s>` sets the series override.
|
||||
* `-t or --tag <s>` sets The subgroup. (Default: CrunchyRoll)
|
||||
* `-r or --resolution <s>` sets the resolutoin you want to download (360, 480, 720, 1080)
|
||||
* `--retry <i>` set the number of try Crunchy will use if downloading a serie or episode fail
|
||||
* `--ignoredub` It is an experimental features that will ignore all season where the name ends with 'dub)'. The idea is to try to ignore dubbed season.
|
||||
|
||||
##### Others
|
||||
|
||||
|
||||
1930
package-lock.json
generated
1930
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
24
package.json
24
package.json
@@ -15,35 +15,39 @@
|
||||
"engines": {
|
||||
"node": ">=5.0"
|
||||
},
|
||||
"version": "1.3.3",
|
||||
"version": "1.4.2-0",
|
||||
"bin": {
|
||||
"crunchy": "./bin/crunchy",
|
||||
"crunchy.sh": "./bin/crunchy.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "^10.3.3",
|
||||
"big-integer": "^1.6.31",
|
||||
"big-integer": "^1.6.32",
|
||||
"bluebird": "^3.5.1",
|
||||
"cheerio": "^0.22.0",
|
||||
"cloudscraper": "^1.5.0",
|
||||
"commander": "^2.15.1",
|
||||
"fs-extra": "^6.0.1",
|
||||
"commander": "^2.16.0",
|
||||
"fs-extra": "^7.0.0",
|
||||
"mkdirp": "^0.5.0",
|
||||
"pjson": "^1.0.9",
|
||||
"request": "^2.87.0",
|
||||
"request-promise": "^4.2.2",
|
||||
"tough-cookie-file-store": "^1.2.0",
|
||||
"uuid": "^3.3.2",
|
||||
"xml2js": "^0.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bluebird": "^3.5.20",
|
||||
"@types/cheerio": "^0.22.7",
|
||||
"@types/fs-extra": "^5.0.3",
|
||||
"@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.41",
|
||||
"@types/request-promise": "^4.1.42",
|
||||
"@types/uuid": "^3.4.3",
|
||||
"@types/xml2js": "^0.4.3",
|
||||
"npm-check": "^5.7.1",
|
||||
"tsconfig-lint": "^0.12.0",
|
||||
"tslint": "^5.10.0",
|
||||
"tslint": "^5.11.0",
|
||||
"typescript": "^2.9.2"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
196
src/batch.ts
196
src/batch.ts
@@ -3,6 +3,8 @@ import commander = require('commander');
|
||||
import fs = require('fs');
|
||||
import path = require('path');
|
||||
import log = require('./log');
|
||||
import my_request = require('./my_request');
|
||||
import cfg = require('./config');
|
||||
import series from './series';
|
||||
|
||||
/* correspondances between resolution and value CR excpect */
|
||||
@@ -19,8 +21,32 @@ const resol_table: { [id: string]: IResolData; } =
|
||||
*/
|
||||
export default function(args: string[], done: (err?: Error) => void)
|
||||
{
|
||||
const config = parse(args);
|
||||
const batchPath = path.join(config.output || process.cwd(), config.batch);
|
||||
const config = Object.assign(cfg.load(), parse(args));
|
||||
let batchPath;
|
||||
|
||||
if (path.isAbsolute(config.batch))
|
||||
{
|
||||
batchPath = path.normalize(config.batch);
|
||||
}
|
||||
else
|
||||
{
|
||||
batchPath = path.normalize(path.join(process.cwd(), config.batch));
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// set resolution
|
||||
if (config.resolution)
|
||||
@@ -44,6 +70,17 @@ export default function(args: string[], done: (err?: Error) => void)
|
||||
config.video_quality = resol_table['1080'].quality;
|
||||
}
|
||||
|
||||
if (config.debug)
|
||||
{
|
||||
/* Ugly but meh */
|
||||
const tmp = JSON.parse(JSON.stringify(config));
|
||||
tmp.pass = 'obfuscated';
|
||||
tmp.user = 'obfustated';
|
||||
tmp.rawArgs = undefined;
|
||||
tmp.options = undefined;
|
||||
log.dumpToDebug('Config', JSON.stringify(tmp), true);
|
||||
}
|
||||
|
||||
tasks(config, batchPath, (err, tasksArr) =>
|
||||
{
|
||||
if (err)
|
||||
@@ -51,22 +88,51 @@ export default function(args: string[], done: (err?: Error) => void)
|
||||
return done(err);
|
||||
}
|
||||
|
||||
if (tasksArr[0].address === '')
|
||||
{
|
||||
return done();
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
|
||||
(function next()
|
||||
{
|
||||
if (i >= tasksArr.length)
|
||||
{
|
||||
// Save configuration before leaving (should store info like session & other)
|
||||
cfg.save(config);
|
||||
|
||||
return done();
|
||||
}
|
||||
|
||||
series(tasksArr[i].config, tasksArr[i].address, (errin) =>
|
||||
if (config.debug)
|
||||
{
|
||||
log.dumpToDebug('Task ' + i, JSON.stringify(tasksArr[i]));
|
||||
}
|
||||
|
||||
series(config, tasksArr[i], (errin) =>
|
||||
{
|
||||
if (errin)
|
||||
{
|
||||
if (tasksArr[i].retry <= 0)
|
||||
if (errin.error)
|
||||
{
|
||||
console.error(errin);
|
||||
/* Error from the request, so ignore it */
|
||||
tasksArr[i].retry = 0;
|
||||
}
|
||||
|
||||
if (errin.authError)
|
||||
{
|
||||
/* Force a graceful exit */
|
||||
log.error(errin.message);
|
||||
i = tasksArr.length;
|
||||
}
|
||||
else if (tasksArr[i].retry <= 0)
|
||||
{
|
||||
log.error(JSON.stringify(errin));
|
||||
if (config.debug)
|
||||
{
|
||||
log.dumpToDebug('BatchGiveUp', JSON.stringify(errin));
|
||||
}
|
||||
log.error('Cannot get episodes from "' + tasksArr[i].address + '", please rerun later');
|
||||
/* Go to the next on the list */
|
||||
i += 1;
|
||||
@@ -75,7 +141,11 @@ export default function(args: string[], done: (err?: Error) => void)
|
||||
{
|
||||
if (config.verbose)
|
||||
{
|
||||
console.error(errin);
|
||||
log.error(JSON.stringify(errin));
|
||||
}
|
||||
if (config.debug)
|
||||
{
|
||||
log.dumpToDebug('BatchRetry', JSON.stringify(errin));
|
||||
}
|
||||
log.warn('Retrying to fetch episodes list from' + tasksArr[i].retry + ' / ' + config.retry);
|
||||
tasksArr[i].retry -= 1;
|
||||
@@ -125,6 +195,81 @@ function split(value: string): string[]
|
||||
return pieces;
|
||||
}
|
||||
|
||||
function get_min_filter(filter: string): number
|
||||
{
|
||||
if (filter !== undefined)
|
||||
{
|
||||
const tok = filter.split('-');
|
||||
|
||||
if (tok.length > 2)
|
||||
{
|
||||
log.error('Invalid episode filter \'' + filter + '\'');
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
if (tok[0] !== '')
|
||||
{
|
||||
return parseInt(tok[0], 10);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function get_max_filter(filter: string): number
|
||||
{
|
||||
if (filter !== undefined)
|
||||
{
|
||||
const tok = filter.split('-');
|
||||
|
||||
if (tok.length > 2)
|
||||
{
|
||||
log.error('Invalid episode filter \'' + filter + '\'');
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
if ((tok.length > 1) && (tok[1] !== ''))
|
||||
{
|
||||
/* We have a max value */
|
||||
return parseInt(tok[1], 10);
|
||||
}
|
||||
else if ((tok.length === 1) && (tok[0] !== ''))
|
||||
{
|
||||
/* A single episode has been requested */
|
||||
return parseInt(tok[0], 10);
|
||||
}
|
||||
}
|
||||
return +Infinity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that URL start with http:// or https://
|
||||
* As for some reason request just return an error but a useless one when that happen so check it
|
||||
* soon enough.
|
||||
*/
|
||||
function checkURL(address: string): boolean
|
||||
{
|
||||
if (address.startsWith('http:\/\/'))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (address.startsWith('https:\/\/'))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (address.startsWith('@http:\/\/'))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (address.startsWith('@https:\/\/'))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
log.error('URL ' + address + ' miss \'http:\/\/\' or \'https:\/\/\' => will be ignored');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the configuration or reads the batch-mode file for tasks.
|
||||
*/
|
||||
@@ -132,11 +277,15 @@ function tasks(config: IConfigLine, batchPath: string, done: (err: Error, tasks?
|
||||
{
|
||||
if (config.args.length)
|
||||
{
|
||||
const configIn = config;
|
||||
|
||||
return done(null, config.args.map((addressIn) =>
|
||||
{
|
||||
return {address: addressIn, config: configIn, retry: config.retry};
|
||||
if (checkURL(addressIn))
|
||||
{
|
||||
return {address: addressIn, retry: config.retry,
|
||||
episode_min: get_min_filter(config.episodes), episode_max: get_max_filter(config.episodes)};
|
||||
}
|
||||
|
||||
return {address: '', retry: 0, episode_min: 0, episode_max: 0};
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -172,7 +321,11 @@ function tasks(config: IConfigLine, batchPath: string, done: (err: Error, tasks?
|
||||
return;
|
||||
}
|
||||
|
||||
map.push({address: addressIn, config: lineConfig, retry: config.retry});
|
||||
if (checkURL(addressIn))
|
||||
{
|
||||
map.push({address: addressIn, retry: lineConfig.retry,
|
||||
episode_min: get_min_filter(lineConfig.episodes), episode_max: get_max_filter(lineConfig.episodes)});
|
||||
}
|
||||
});
|
||||
});
|
||||
done(null, map);
|
||||
@@ -189,23 +342,24 @@ function parse(args: string[]): IConfigLine
|
||||
// Authentication
|
||||
.option('-p, --pass <s>', 'The password.')
|
||||
.option('-u, --user <s>', 'The e-mail address or username.')
|
||||
.option('-d, --unlog', 'Unlog')
|
||||
// Disables
|
||||
.option('-c, --cache', 'Disables the cache.')
|
||||
.option('-m, --merge', 'Disables merging subtitles and videos.')
|
||||
// Filters
|
||||
.option('-e, --episode <i>', 'The episode filter.')
|
||||
.option('-v, --volume <i>', 'The volume filter.')
|
||||
// Episode filter
|
||||
.option('-e, --episodes <s>', 'Episode list. Read documentation on how to use')
|
||||
// Settings
|
||||
.option('-f, --format <s>', 'The subtitle format. (Default: ass)')
|
||||
.option('-f, --format <s>', 'The subtitle format.', 'ass')
|
||||
.option('-o, --output <s>', 'The output path.')
|
||||
.option('-s, --series <s>', 'The series override.')
|
||||
.option('-n, --filename <s>', 'The name override.')
|
||||
.option('-t, --tag <s>', 'The subgroup. (Default: CrunchyRoll)', 'CrunchyRoll')
|
||||
.option('-r, --resolution <s>', 'The video resolution. (Default: 1080 (360, 480, 720, 1080))',
|
||||
'1080')
|
||||
.option('-g, --rebuildcrp', 'Rebuild the crpersistant file.')
|
||||
.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('-b, --batch <s>', 'Batch file', 'CrunchyRoll.txt')
|
||||
.option('--verbose', 'Make tool verbose')
|
||||
.option('--retry <i>', 'Number or time to retry fetching an episode. Default: 5', 5)
|
||||
.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)
|
||||
.parse(args);
|
||||
}
|
||||
|
||||
17
src/cli.ts
17
src/cli.ts
@@ -8,17 +8,20 @@ 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 (onlinepkg.status === 'ok')
|
||||
{
|
||||
log.warn('There is a newer version of crunchy (v' + onlinepkg.version + '), you should update!');
|
||||
let tmp = current_version.split('.');
|
||||
const cur = (Number(tmp[0]) * 10000) + (Number(tmp[1]) * 100) + Number(tmp[2]);
|
||||
tmp = onlinepkg.version.split('.');
|
||||
const dist = (Number(tmp[0]) * 10000) + (Number(tmp[1]) * 100) + Number(tmp[2]);
|
||||
if (dist > cur)
|
||||
{
|
||||
log.warnMore('There is a newer version of crunchy (v' + onlinepkg.version + '), you should update!');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
63
src/config.ts
Normal file
63
src/config.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
'use strict';
|
||||
import os = require('os');
|
||||
import fs = require('fs-extra');
|
||||
import path = require('path');
|
||||
|
||||
const configFile = path.join(process.cwd(), 'config.json');
|
||||
|
||||
function fileExist(path: string)
|
||||
{
|
||||
try
|
||||
{
|
||||
fs.statSync(path);
|
||||
return true;
|
||||
} catch (e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function load(): IConfigLine
|
||||
{
|
||||
if (fileExist(configFile))
|
||||
{
|
||||
const data = fs.readFileSync(configFile, 'utf8');
|
||||
return JSON.parse(data);
|
||||
}
|
||||
|
||||
return {args: undefined};
|
||||
}
|
||||
|
||||
export function save(config: IConfig)
|
||||
{
|
||||
const tmp = JSON.parse(JSON.stringify(config));
|
||||
|
||||
// Things added by the command line parser
|
||||
tmp.rawArgs = undefined;
|
||||
tmp.options = undefined;
|
||||
tmp._execs = undefined;
|
||||
tmp._args = undefined;
|
||||
tmp._name = undefined;
|
||||
tmp._version = undefined;
|
||||
tmp._versionOptionName = undefined;
|
||||
tmp._events = undefined;
|
||||
tmp._eventsCount = undefined;
|
||||
tmp.args = undefined;
|
||||
tmp.commands = undefined;
|
||||
tmp._allowUnknownOption = 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;
|
||||
tmp.debug = undefined;
|
||||
tmp.unlog = undefined;
|
||||
tmp.ignoredub = undefined;
|
||||
|
||||
fs.writeFileSync(configFile, JSON.stringify(tmp, null, ' '));
|
||||
}
|
||||
@@ -66,7 +66,7 @@ function fileExist(path: string)
|
||||
|
||||
function sanitiseFileName(str: string)
|
||||
{
|
||||
return str.replace(/[\/':\?\*"<>\.]/g, '_');
|
||||
return str.replace(/[\/':\?\*"<>\\\.]/g, '_');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,11 +74,10 @@ function sanitiseFileName(str: string)
|
||||
*/
|
||||
function download(config: IConfig, page: IEpisodePage, player: IEpisodePlayer, done: (err: Error, ign: boolean) => void)
|
||||
{
|
||||
let series = config.series || page.series;
|
||||
const serieFolder = sanitiseFileName(config.series || page.series);
|
||||
|
||||
series = sanitiseFileName(series);
|
||||
let fileName = sanitiseFileName(name(config, page, series, ''));
|
||||
let filePath = path.join(config.output || process.cwd(), series, fileName);
|
||||
let fileName = sanitiseFileName(generateName(config, page));
|
||||
let filePath = path.join(config.output || process.cwd(), serieFolder, fileName);
|
||||
|
||||
if (fileExist(filePath + '.mkv'))
|
||||
{
|
||||
@@ -95,13 +94,13 @@ function download(config: IConfig, page: IEpisodePage, player: IEpisodePlayer, d
|
||||
do
|
||||
{
|
||||
count = count + 1;
|
||||
fileName = sanitiseFileName(name(config, page, series, '-' + count));
|
||||
filePath = path.join(config.output || process.cwd(), series, fileName);
|
||||
fileName = sanitiseFileName(generateName(config, page, '-' + count));
|
||||
filePath = path.join(config.output || process.cwd(), serieFolder, fileName);
|
||||
} while (fileExist(filePath + '.mkv'));
|
||||
|
||||
log.warn('Renaming to \'' + fileName + '\'...');
|
||||
|
||||
config.filename = fileName;
|
||||
page.filename = fileName;
|
||||
}
|
||||
|
||||
if (config.rebuildcrp)
|
||||
@@ -114,6 +113,7 @@ function download(config: IConfig, page: IEpisodePage, player: IEpisodePlayer, d
|
||||
{
|
||||
if (errM)
|
||||
{
|
||||
log.dispEpisode(fileName, 'Error...', true);
|
||||
return done(errM, false);
|
||||
}
|
||||
|
||||
@@ -122,6 +122,7 @@ function download(config: IConfig, page: IEpisodePage, player: IEpisodePlayer, d
|
||||
{
|
||||
if (errDS)
|
||||
{
|
||||
log.dispEpisode(fileName, 'Error...', true);
|
||||
return done(errDS, false);
|
||||
}
|
||||
|
||||
@@ -133,6 +134,7 @@ function download(config: IConfig, page: IEpisodePage, player: IEpisodePlayer, d
|
||||
{
|
||||
if (errDV)
|
||||
{
|
||||
log.dispEpisode(fileName, 'Error...', true);
|
||||
return done(errDV, false);
|
||||
}
|
||||
|
||||
@@ -148,6 +150,7 @@ function download(config: IConfig, page: IEpisodePage, player: IEpisodePlayer, d
|
||||
{
|
||||
if (errVM)
|
||||
{
|
||||
log.dispEpisode(fileName, 'Error...', true);
|
||||
return done(errVM, false);
|
||||
}
|
||||
|
||||
@@ -183,10 +186,15 @@ function downloadSubtitle(config: IConfig, player: IEpisodePlayer, filePath: str
|
||||
return done(errSD);
|
||||
}
|
||||
|
||||
if (config.debug)
|
||||
{
|
||||
log.dumpToDebug('SubtitlesXML', data);
|
||||
}
|
||||
|
||||
const formats = subtitle.formats;
|
||||
const format = formats[config.format] ? config.format : 'ass';
|
||||
|
||||
formats[format](data, (errF: Error, decodedSubtitle: string) =>
|
||||
formats[format](config, data, (errF: Error, decodedSubtitle: string) =>
|
||||
{
|
||||
if (errF)
|
||||
{
|
||||
@@ -211,19 +219,16 @@ function downloadVideo(config: IConfig, page: IEpisodePage, player: IEpisodePla
|
||||
/**
|
||||
* Names the file based on the config, page, series and tag.
|
||||
*/
|
||||
function name(config: IConfig, page: IEpisodePage, series: string, extra: string)
|
||||
function generateName(config: IConfig, page: IEpisodePage, extra = '')
|
||||
{
|
||||
const episodeNum = parseInt(page.episode, 10);
|
||||
const volumeNum = parseInt(page.volume, 10);
|
||||
const episode = (episodeNum < 10 ? '0' : '') + page.episode;
|
||||
const volume = (volumeNum < 10 ? '0' : '') + page.volume;
|
||||
const tag = config.tag || 'CrunchyRoll';
|
||||
const series = config.series || page.series;
|
||||
|
||||
if (!config.filename) {
|
||||
return page.series + ' - s' + volume + 'e' + episode + ' - [' + tag + ']' + extra;
|
||||
}
|
||||
|
||||
return config.filename
|
||||
return config.nametmpl
|
||||
.replace(/{EPISODE_ID}/g, page.id.toString())
|
||||
.replace(/{EPISODE_NUMBER}/g, episode)
|
||||
.replace(/{SEASON_NUMBER}/g, volume)
|
||||
@@ -277,10 +282,21 @@ function scrapePage(config: IConfig, address: string, done: (err: Error, page?:
|
||||
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,
|
||||
@@ -289,6 +305,7 @@ function scrapePage(config: IConfig, address: string, done: (err: Error, page?:
|
||||
title: episodeTitle,
|
||||
swf: swf[1],
|
||||
volume: '0',
|
||||
filename: '',
|
||||
});
|
||||
}
|
||||
else
|
||||
@@ -301,6 +318,7 @@ function scrapePage(config: IConfig, address: string, done: (err: Error, page?:
|
||||
title: episodeTitle,
|
||||
swf: swf[1],
|
||||
volume: data[2] || '1',
|
||||
filename: '',
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -367,6 +385,11 @@ function scrapePlayer(config: IConfig, address: string, id: number, done: (err:
|
||||
});
|
||||
} catch (parseError)
|
||||
{
|
||||
if (config.debug)
|
||||
{
|
||||
log.dumpToDebug('player scrape', parseError);
|
||||
}
|
||||
|
||||
done(parseError);
|
||||
}
|
||||
});
|
||||
|
||||
3
src/interface/AuthError.d.ts
vendored
Normal file
3
src/interface/AuthError.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
interface IAuthError extends Error {
|
||||
authError: boolean;
|
||||
}
|
||||
25
src/interface/IConfig.d.ts
vendored
25
src/interface/IConfig.d.ts
vendored
@@ -5,20 +5,37 @@ interface IConfig {
|
||||
// Disables
|
||||
cache?: boolean;
|
||||
merge?: boolean;
|
||||
// Filters
|
||||
episode?: number;
|
||||
volume?: number;
|
||||
episodes?: string;
|
||||
// Settings
|
||||
format?: string;
|
||||
output?: string;
|
||||
series?: string;
|
||||
filename?: string;
|
||||
nametmpl?: string;
|
||||
tag?: string;
|
||||
ignoredub?: boolean;
|
||||
resolution?: string;
|
||||
video_format?: string;
|
||||
video_quality?: string;
|
||||
rebuildcrp?: boolean;
|
||||
batch?: string;
|
||||
verbose?: boolean;
|
||||
debug?: boolean;
|
||||
unlog?: boolean;
|
||||
retry?: number;
|
||||
// Login options
|
||||
userAgent?: string;
|
||||
logUsingApi?: boolean;
|
||||
logUsingCookie?: boolean;
|
||||
crSessionUrl?: string;
|
||||
crDeviceType?: string;
|
||||
crAPIVersion?: string;
|
||||
crLocale?: string;
|
||||
crSessionKey?: string;
|
||||
crLoginUrl?: string;
|
||||
// Third method, injecting data from cookies
|
||||
crUserId?: string;
|
||||
crUserKey?: string;
|
||||
// Generated values
|
||||
crDeviceId?: string;
|
||||
crSessionId?: string;
|
||||
}
|
||||
|
||||
3
src/interface/IConfigTask.d.ts
vendored
3
src/interface/IConfigTask.d.ts
vendored
@@ -1,5 +1,6 @@
|
||||
interface IConfigTask {
|
||||
address: string;
|
||||
config: IConfigLine;
|
||||
retry: number;
|
||||
episode_min: number;
|
||||
episode_max: number;
|
||||
}
|
||||
|
||||
1
src/interface/IEpisodePage.d.ts
vendored
1
src/interface/IEpisodePage.d.ts
vendored
@@ -6,4 +6,5 @@ interface IEpisodePage {
|
||||
season: string;
|
||||
title: string;
|
||||
swf: string;
|
||||
filename: string;
|
||||
}
|
||||
|
||||
2
src/interface/IFormatterTable.d.ts
vendored
2
src/interface/IFormatterTable.d.ts
vendored
@@ -1,3 +1,3 @@
|
||||
interface IFormatterTable {
|
||||
[key: string]: (input: string|Buffer, done: (err: Error, subtitle?: string) => void) => void;
|
||||
[key: string]: (config: IConfig, input: string|Buffer, done: (err: Error, subtitle?: string) => void) => void;
|
||||
}
|
||||
|
||||
1
src/interface/ISeriesEpisode.d.ts
vendored
1
src/interface/ISeriesEpisode.d.ts
vendored
@@ -1,6 +1,7 @@
|
||||
interface ISeriesEpisode {
|
||||
address: string;
|
||||
episode: string;
|
||||
seasonName: string;
|
||||
volume: number;
|
||||
retry: number;
|
||||
}
|
||||
|
||||
20
src/log.ts
20
src/log.ts
@@ -1,7 +1,8 @@
|
||||
'use strict';
|
||||
import os = require('os');
|
||||
import fs = require('fs-extra');
|
||||
|
||||
export function error(str: string)
|
||||
export function error(str: string|Error)
|
||||
{
|
||||
/* Do fancy output */
|
||||
console.error(' \x1B[1;31m* ERROR\x1B[0m: ' + str);
|
||||
@@ -25,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 */
|
||||
@@ -35,3 +42,14 @@ export function dispEpisode(name: string, status: string, addNL: boolean)
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
|
||||
export function dumpToDebug(what: string, data: any, create = false)
|
||||
{
|
||||
if (create)
|
||||
{
|
||||
fs.writeFileSync('debug.txt', '>>>>>>>> ' + what + ':\n' + data + '\n<<<<<<<<\n');
|
||||
return;
|
||||
}
|
||||
|
||||
fs.appendFileSync('debug.txt', '>>>>>>>> ' + what + ':\n' + data + '\n<<<<<<<<\n');
|
||||
}
|
||||
|
||||
@@ -3,72 +3,80 @@ import cheerio = require('cheerio');
|
||||
import request = require('request');
|
||||
import rp = require('request-promise');
|
||||
import Promise = require('bluebird');
|
||||
import uuid = require('uuid');
|
||||
import path = require('path');
|
||||
import fs = require('fs-extra');
|
||||
import log = require('./log');
|
||||
|
||||
import { RequestPromise } from 'request-promise';
|
||||
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';
|
||||
|
||||
let isAuthenticated = false;
|
||||
let isPremium = false;
|
||||
|
||||
let j: request.CookieJar;
|
||||
|
||||
const defaultHeaders: request.Headers =
|
||||
{
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 6.2; WOW64; x64; rv:58.0) Gecko/20100101 Firefox/58.0',
|
||||
'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',
|
||||
};
|
||||
|
||||
function generateDeviceId(): string
|
||||
function AuthError(msg: string): IAuthError
|
||||
{
|
||||
let id = '';
|
||||
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
|
||||
for (let i = 0; i < 32; i++)
|
||||
{
|
||||
id += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
|
||||
return id;
|
||||
return { name: 'AuthError', message: msg, authError: true };
|
||||
}
|
||||
|
||||
function startSession(): Promise<string>
|
||||
function startSession(config: IConfig): Promise<any>
|
||||
{
|
||||
return rp(
|
||||
{
|
||||
method: 'GET',
|
||||
url: 'CR_SESSION_URL',
|
||||
url: config.crSessionUrl,
|
||||
qs:
|
||||
{
|
||||
device_id: generateDeviceId(),
|
||||
device_type: 'CR_DEVICE_TYPE',
|
||||
access_token: 'CR_SESSION_KEY',
|
||||
version: 'CR_API_VERSION',
|
||||
locale: 'CR_LOCALE',
|
||||
device_id: config.crDeviceId,
|
||||
device_type: config.crDeviceType,
|
||||
access_token: config.crSessionKey,
|
||||
version: config.crAPIVersion,
|
||||
locale: config.crLocale,
|
||||
},
|
||||
json: true,
|
||||
})
|
||||
.then((response: any) =>
|
||||
{
|
||||
if ((response.data === undefined) || (response.data.session_id === undefined))
|
||||
{
|
||||
throw new Error('Getting session failed: ' + JSON.stringify(response));
|
||||
}
|
||||
|
||||
return response.data.session_id;
|
||||
});
|
||||
}
|
||||
|
||||
function login(sessionId: string, user: string, pass: string): Promise<any>
|
||||
function login(config: IConfig, sessionId: string, user: string, pass: string): Promise<any>
|
||||
{
|
||||
return rp(
|
||||
{
|
||||
method: 'POST',
|
||||
url: 'CR_LOGIN_URL',
|
||||
url: config.crLoginUrl,
|
||||
form:
|
||||
{
|
||||
account: user,
|
||||
password: pass,
|
||||
session_id: sessionId,
|
||||
version: 'CR_API_VERSION',
|
||||
version: config.crAPIVersion,
|
||||
},
|
||||
json: true,
|
||||
jar: j,
|
||||
})
|
||||
.then((response) =>
|
||||
{
|
||||
@@ -77,13 +85,114 @@ function login(sessionId: string, user: string, pass: string): Promise<any>
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: logout
|
||||
function checkIfUserIsAuth(config: IConfig, done: (err: Error) => 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) =>
|
||||
{
|
||||
if (err)
|
||||
{
|
||||
return done(err);
|
||||
}
|
||||
|
||||
const $ = cheerio.load(body);
|
||||
|
||||
/* Check if auth worked */
|
||||
const regexps = /ga\('set', 'dimension[5-8]', '([^']*)'\);/g;
|
||||
const dims = regexps.exec($('script').text());
|
||||
|
||||
for (let i = 1; i < 5; i++)
|
||||
{
|
||||
if ((dims[i] !== undefined) && (dims[i] !== '') && (dims[i] !== 'not-registered'))
|
||||
{
|
||||
isAuthenticated = true;
|
||||
}
|
||||
|
||||
if ((dims[i] === 'premium') || (dims[i] === 'premiumplus'))
|
||||
{
|
||||
isPremium = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isAuthenticated === false)
|
||||
{
|
||||
const error = $('ul.message, li.error').text();
|
||||
return done(AuthError('Authentication failed: ' + error));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isPremium === false)
|
||||
{
|
||||
log.warn('Do not use this app without a premium account.');
|
||||
}
|
||||
else
|
||||
{
|
||||
log.info('You have a premium account! Good!');
|
||||
}
|
||||
}
|
||||
|
||||
done(null);
|
||||
});
|
||||
}
|
||||
|
||||
function loadCookies(config: IConfig)
|
||||
{
|
||||
const cookiePath = path.join(config.output || process.cwd(), '.cookies.json');
|
||||
|
||||
if (!fs.existsSync(cookiePath))
|
||||
{
|
||||
fs.closeSync(fs.openSync(cookiePath, 'w'));
|
||||
}
|
||||
|
||||
j = request.jar(new cookieStore(cookiePath));
|
||||
}
|
||||
|
||||
export function eatCookies(config: IConfig)
|
||||
{
|
||||
const cookiePath = path.join(config.output || process.cwd(), '.cookies.json');
|
||||
|
||||
if (fs.existsSync(cookiePath))
|
||||
{
|
||||
fs.removeSync(cookiePath);
|
||||
}
|
||||
|
||||
j = undefined;
|
||||
}
|
||||
|
||||
export function getUserAgent(): string
|
||||
{
|
||||
return defaultHeaders['User-Agent'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a GET request for the resource.
|
||||
*/
|
||||
export function get(config: IConfig, options: string|request.Options, done: (err: Error, result?: string) => void)
|
||||
export function get(config: IConfig, options: string|request.Options, done: (err: any, result?: string) => void)
|
||||
{
|
||||
if (j === undefined)
|
||||
{
|
||||
loadCookies(config);
|
||||
}
|
||||
|
||||
if (config.userAgent)
|
||||
{
|
||||
defaultHeaders['User-Agent'] = config.userAgent;
|
||||
}
|
||||
|
||||
authenticate(config, (err) =>
|
||||
{
|
||||
if (err)
|
||||
@@ -91,9 +200,10 @@ export function get(config: IConfig, options: string|request.Options, done: (err
|
||||
return done(err);
|
||||
}
|
||||
|
||||
cloudscraper.request(modify(options, 'GET'), (error: Error, response: any, body: any) =>
|
||||
cloudscraper.request(modify(options, 'GET'), (error: any, response: any, body: any) =>
|
||||
{
|
||||
if (error) return done(error);
|
||||
|
||||
done(null, typeof body === 'string' ? body : String(body));
|
||||
});
|
||||
});
|
||||
@@ -104,18 +214,28 @@ export function get(config: IConfig, options: string|request.Options, done: (err
|
||||
*/
|
||||
export function post(config: IConfig, options: request.Options, done: (err: Error, result?: string) => void)
|
||||
{
|
||||
if (j === undefined)
|
||||
{
|
||||
loadCookies(config);
|
||||
}
|
||||
|
||||
if (config.userAgent)
|
||||
{
|
||||
defaultHeaders['User-Agent'] = config.userAgent;
|
||||
}
|
||||
|
||||
authenticate(config, (err) =>
|
||||
{
|
||||
if (err)
|
||||
{
|
||||
return done(err);
|
||||
return done(err);
|
||||
}
|
||||
|
||||
cloudscraper.request(modify(options, 'POST'), (error: Error, response: any, body: any) =>
|
||||
{
|
||||
if (error)
|
||||
{
|
||||
return done(error);
|
||||
return done(error);
|
||||
}
|
||||
done(null, typeof body === 'string' ? body : String(body));
|
||||
});
|
||||
@@ -127,75 +247,150 @@ export function post(config: IConfig, options: request.Options, done: (err: Erro
|
||||
*/
|
||||
function authenticate(config: IConfig, done: (err: Error) => void)
|
||||
{
|
||||
if (isAuthenticated || !config.pass || !config.user)
|
||||
if (isAuthenticated)
|
||||
{
|
||||
return done(null);
|
||||
return done(null);
|
||||
}
|
||||
|
||||
startSession()
|
||||
.then((sessionId: string) =>
|
||||
/* First of all, check if the user is not already logged via the cookies */
|
||||
checkIfUserIsAuth(config, (errCheckAuth) =>
|
||||
{
|
||||
defaultHeaders.Cookie = `sess_id=${sessionId}; c_locale=enUS`;
|
||||
return login(sessionId, config.user, config.pass);
|
||||
})
|
||||
.then((userData) =>
|
||||
{
|
||||
/**
|
||||
* The page return with a meta based redirection, as we wan't to check that everything is fine, reload
|
||||
* the main page. A bit convoluted, but more sure.
|
||||
*/
|
||||
const options =
|
||||
if (isAuthenticated)
|
||||
{
|
||||
headers: defaultHeaders,
|
||||
jar: true,
|
||||
url: 'http://www.crunchyroll.com/',
|
||||
method: 'GET',
|
||||
};
|
||||
return done(null);
|
||||
}
|
||||
|
||||
cloudscraper.request(options, (err: Error, rep: string, body: string) =>
|
||||
/* So if we are here now, that mean we are not authenticated so do as usual */
|
||||
if (!config.pass || !config.user)
|
||||
{
|
||||
if (err)
|
||||
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)
|
||||
{
|
||||
return done(err);
|
||||
config.crDeviceId = uuid.v4();
|
||||
}
|
||||
|
||||
const $ = cheerio.load(body);
|
||||
|
||||
/* Check if auth worked */
|
||||
const regexps = /ga\('set', 'dimension[5-8]', '([^']*)'\);/g;
|
||||
const dims = regexps.exec($('script').text());
|
||||
|
||||
for (let i = 1; i < 5; i++)
|
||||
if (!config.crSessionUrl || !config.crDeviceType || !config.crAPIVersion ||
|
||||
!config.crLocale || !config.crLoginUrl)
|
||||
{
|
||||
if ((dims[i] !== undefined) && (dims[i] !== '') && (dims[i] !== 'not-registered'))
|
||||
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) =>
|
||||
{
|
||||
isAuthenticated = true;
|
||||
if (isAuthenticated)
|
||||
{
|
||||
return done(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
return done(errCheckAuth2);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((errInChk) =>
|
||||
{
|
||||
return done(AuthError(errInChk.message));
|
||||
});
|
||||
}
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
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!'));
|
||||
}
|
||||
|
||||
if ((dims[i] === 'premium') || (dims[i] === 'premiumplus'))
|
||||
/* Now call the page again with the token and credentials */
|
||||
const options =
|
||||
{
|
||||
isPremium = true;
|
||||
}
|
||||
}
|
||||
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'
|
||||
};
|
||||
|
||||
if (isAuthenticated === false)
|
||||
{
|
||||
const error = $('ul.message, li.error').text();
|
||||
return done(new Error('Authentication failed: ' + error));
|
||||
}
|
||||
cloudscraper.request(options, (err: Error, rep: string, body: string) =>
|
||||
{
|
||||
if (err)
|
||||
{
|
||||
return done(err);
|
||||
}
|
||||
|
||||
if (isPremium === false)
|
||||
{
|
||||
log.warn('Do not use this app without a premium account.');
|
||||
}
|
||||
else
|
||||
{
|
||||
log.info('You have a premium account! Good!');
|
||||
}
|
||||
done(null);
|
||||
});
|
||||
})
|
||||
.catch(done);
|
||||
/* Now let's check if we are authentificated */
|
||||
checkIfUserIsAuth(config, (errCheckAuth2) =>
|
||||
{
|
||||
if (isAuthenticated)
|
||||
{
|
||||
return done(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
return done(errCheckAuth2);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -205,15 +400,15 @@ function modify(options: string|request.Options, reqMethod: string): request.Opt
|
||||
{
|
||||
if (typeof options !== 'string')
|
||||
{
|
||||
options.jar = true;
|
||||
options.jar = j;
|
||||
options.headers = defaultHeaders;
|
||||
options.method = reqMethod;
|
||||
return options;
|
||||
}
|
||||
return {
|
||||
jar: true,
|
||||
jar: j,
|
||||
headers: defaultHeaders,
|
||||
url: options.toString(),
|
||||
method: reqMethod
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
128
src/series.ts
128
src/series.ts
@@ -27,7 +27,7 @@ function fileExist(path: string)
|
||||
/**
|
||||
* Streams the series to disk.
|
||||
*/
|
||||
export default function(config: IConfig, address: string, done: (err: Error) => void)
|
||||
export default function(config: IConfig, task: IConfigTask, done: (err: any) => void)
|
||||
{
|
||||
const persistentPath = path.join(config.output || process.cwd(), persistent);
|
||||
|
||||
@@ -41,25 +41,49 @@ export default function(config: IConfig, address: string, done: (err: Error) =>
|
||||
{
|
||||
const cache = config.cache ? {} : JSON.parse(contents || '{}');
|
||||
|
||||
page(config, address, (errP, page) =>
|
||||
pageScrape(config, task, (errP, page) =>
|
||||
{
|
||||
if (errP)
|
||||
{
|
||||
const reqErr = errP.error;
|
||||
if ((reqErr !== undefined) && (reqErr.syscall))
|
||||
{
|
||||
if ((reqErr.syscall === 'getaddrinfo') && (reqErr.errno === 'ENOTFOUND'))
|
||||
{
|
||||
log.error('The URL \'' + task.address + '\' is invalid, please check => I\'m ignoring it.');
|
||||
}
|
||||
}
|
||||
|
||||
return done(errP);
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
(function next()
|
||||
{
|
||||
if (config.debug)
|
||||
{
|
||||
log.dumpToDebug('Episode ' + i, JSON.stringify(page.episodes[i]));
|
||||
}
|
||||
|
||||
if (i >= page.episodes.length) return done(null);
|
||||
download(cache, config, address, page.episodes[i], (errD, ignored) =>
|
||||
download(cache, config, task, page.episodes[i], (errD, ignored) =>
|
||||
{
|
||||
if (errD)
|
||||
{
|
||||
/* Check if domain is valid */
|
||||
const reqErr = errD.error;
|
||||
if ((reqErr !== undefined) && (reqErr.syscall))
|
||||
{
|
||||
if ((reqErr.syscall === 'getaddrinfo') && (reqErr.errno === 'ENOTFOUND'))
|
||||
{
|
||||
page.episodes[i].retry = 0;
|
||||
log.error('The URL \'' + task.address + '\' is invalid, please check => I\'m ignoring it.');
|
||||
}
|
||||
}
|
||||
|
||||
if (page.episodes[i].retry <= 0)
|
||||
{
|
||||
log.dispEpisode(config.filename, 'Error...', true);
|
||||
console.error(errD);
|
||||
log.error(JSON.stringify(errD));
|
||||
log.error('Cannot fetch episode "s' + page.episodes[i].volume + 'e' + page.episodes[i].episode +
|
||||
'", please rerun later');
|
||||
/* Go to the next on the list */
|
||||
@@ -67,10 +91,15 @@ export default function(config: IConfig, address: string, done: (err: Error) =>
|
||||
}
|
||||
else
|
||||
{
|
||||
log.dispEpisode(config.filename, 'Error...', true);
|
||||
if (config.verbose)
|
||||
if ((config.verbose) || (config.debug))
|
||||
{
|
||||
console.error(errD);
|
||||
if (config.debug)
|
||||
{
|
||||
log.dumpToDebug('series address', task.address);
|
||||
log.dumpToDebug('series error', JSON.stringify(errD));
|
||||
log.dumpToDebug('series data', JSON.stringify(page));
|
||||
}
|
||||
log.error(errD);
|
||||
}
|
||||
log.warn('Retrying to fetch episode "s' + page.episodes[i].volume + 'e' + page.episodes[i].episode +
|
||||
'" - Retry ' + page.episodes[i].retry + ' / ' + config.retry);
|
||||
@@ -110,15 +139,18 @@ export default function(config: IConfig, address: string, done: (err: Error) =>
|
||||
* Downloads the episode.
|
||||
*/
|
||||
function download(cache: {[address: string]: number}, config: IConfig,
|
||||
baseAddress: string, item: ISeriesEpisode,
|
||||
done: (err: Error, ign: boolean) => void)
|
||||
task: IConfigTask, item: ISeriesEpisode,
|
||||
done: (err: any, ign: boolean) => void)
|
||||
{
|
||||
if (!filter(config, item))
|
||||
const episodeNumber = parseInt(item.episode, 10);
|
||||
|
||||
if ( (episodeNumber < task.episode_min) ||
|
||||
(episodeNumber > task.episode_max) )
|
||||
{
|
||||
return done(null, false);
|
||||
}
|
||||
|
||||
const address = url.resolve(baseAddress, item.address);
|
||||
const address = url.resolve(task.address, item.address);
|
||||
|
||||
if (cache[address])
|
||||
{
|
||||
@@ -137,42 +169,19 @@ function download(cache: {[address: string]: number}, config: IConfig,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the item based on the configuration.
|
||||
*/
|
||||
function filter(config: IConfig, item: ISeriesEpisode)
|
||||
{
|
||||
// Filter on chapter.
|
||||
const episodeFilter = config.episode;
|
||||
// Filter on volume.
|
||||
const volumeFilter = config.volume;
|
||||
|
||||
const currentEpisode = parseInt(item.episode, 10);
|
||||
const currentVolume = item.volume;
|
||||
|
||||
if ( ( (episodeFilter > 0) && (currentEpisode <= episodeFilter) ) ||
|
||||
( (episodeFilter < 0) && (currentEpisode >= -episodeFilter) ) ||
|
||||
( (volumeFilter > 0) && (currentVolume <= volumeFilter ) ) ||
|
||||
( (volumeFilter < 0) && (currentVolume >= -volumeFilter ) ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the page and scrapes the episodes and series.
|
||||
*/
|
||||
function page(config: IConfig, address: string, done: (err: Error, result?: ISeries) => void)
|
||||
function pageScrape(config: IConfig, task: IConfigTask, done: (err: any, result?: ISeries) => void)
|
||||
{
|
||||
if (address[0] === '@')
|
||||
if (task.address[0] === '@')
|
||||
{
|
||||
log.info('Trying to fetch from ' + address.substr(1));
|
||||
log.info('Trying to fetch from ' + task.address.substr(1));
|
||||
const episodes: ISeriesEpisode[] = [];
|
||||
episodes.push({
|
||||
address: address.substr(1),
|
||||
address: task.address.substr(1),
|
||||
episode: '',
|
||||
seasonName: '',
|
||||
volume: 0,
|
||||
retry: config.retry,
|
||||
});
|
||||
@@ -181,42 +190,73 @@ function page(config: IConfig, address: string, done: (err: Error, result?: ISer
|
||||
else
|
||||
{
|
||||
let episodeCount = 0;
|
||||
my_request.get(config, address, (err, result) => {
|
||||
if (err) {
|
||||
my_request.get(config, task.address, (err, result) => {
|
||||
if (err)
|
||||
{
|
||||
return done(err);
|
||||
}
|
||||
|
||||
const $ = cheerio.load(result);
|
||||
const title = $('span[itemprop=name]').text();
|
||||
|
||||
if (config.debug)
|
||||
{
|
||||
log.dumpToDebug('serie page', $.html());
|
||||
}
|
||||
|
||||
if (!title) {
|
||||
return done(new Error('Invalid page.(' + address + ')'));
|
||||
if (config.debug)
|
||||
{
|
||||
log.dumpToDebug('missing title', task.address);
|
||||
}
|
||||
return done(new Error('Invalid page.(' + task.address + ')'));
|
||||
}
|
||||
|
||||
log.info('Checking availability for ' + title);
|
||||
const episodes: ISeriesEpisode[] = [];
|
||||
|
||||
if ($('.availability-notes-low').length)
|
||||
{
|
||||
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) => {
|
||||
if ($(el).children('img[src*=coming_soon]').length) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 episode = regexp.exec($(el).children('.series-title').text());
|
||||
const url = $(el).attr('href');
|
||||
|
||||
if ((!url) || (!episode)) {
|
||||
if (config.ignoredub && (season_name.endsWith('Dub)') || season_name.endsWith('dub)')))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if ((!url) || (!episode))
|
||||
{
|
||||
return;
|
||||
}
|
||||
episodeCount += 1;
|
||||
episodes.push({
|
||||
address: url,
|
||||
episode: episode[1],
|
||||
seasonName: season_name,
|
||||
volume: volume ? parseInt(volume[0], 10) : 1,
|
||||
retry: config.retry,
|
||||
});
|
||||
});
|
||||
|
||||
if (episodeCount === 0)
|
||||
{
|
||||
log.warn('No episodes found for ' + title + '. Could it be a movie?');
|
||||
|
||||
@@ -4,7 +4,7 @@ import xml2js = require('xml2js');
|
||||
/**
|
||||
* Converts an input buffer to a SubStation Alpha subtitle.
|
||||
*/
|
||||
export default function(input: string|Buffer, done: (err: Error, subtitle?: string) => void)
|
||||
export default function(config: IConfig, input: string|Buffer, done: (err: Error, subtitle?: string) => void)
|
||||
{
|
||||
xml2js.parseString(input.toString(), {
|
||||
explicitArray: false,
|
||||
@@ -18,9 +18,9 @@ export default function(input: string|Buffer, done: (err: Error, subtitle?: stri
|
||||
|
||||
try
|
||||
{
|
||||
done(null, script(xml) + '\n' +
|
||||
done(null, script(config, xml) + '\n' +
|
||||
style(xml.styles) + '\n' +
|
||||
event(xml.events));
|
||||
event(config, xml.events));
|
||||
} catch (err)
|
||||
{
|
||||
done(err);
|
||||
@@ -31,7 +31,7 @@ export default function(input: string|Buffer, done: (err: Error, subtitle?: stri
|
||||
/**
|
||||
* Converts the event block.
|
||||
*/
|
||||
function event(block: ISubtitleEvent): string
|
||||
function event(config: IConfig, block: ISubtitleEvent): string
|
||||
{
|
||||
const format = 'Layer,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text';
|
||||
|
||||
@@ -51,10 +51,11 @@ function event(block: ISubtitleEvent): string
|
||||
/**
|
||||
* Converts the script block.
|
||||
*/
|
||||
function script(block: ISubtitle): string
|
||||
function script(config: IConfig, block: ISubtitle): string
|
||||
{
|
||||
|
||||
return '[Script Info]\n' +
|
||||
'Origin: Downloaded from Crunchyroll.com by ' + config.user + '\n' +
|
||||
'Title: ' + block.$.title + '\n' +
|
||||
'ScriptType: v4.00+\n' +
|
||||
'WrapStyle: ' + block.$.wrap_style + '\n' +
|
||||
|
||||
@@ -4,7 +4,7 @@ import xml2js = require('xml2js');
|
||||
/**
|
||||
* Converts an input buffer to a SubRip subtitle.
|
||||
*/
|
||||
export default function(input: Buffer|string, done: (err: Error, subtitle?: string) => void)
|
||||
export default function(config: IConfig, input: Buffer|string, done: (err: Error, subtitle?: string) => void)
|
||||
{
|
||||
const options = {explicitArray: false, explicitRoot: false};
|
||||
|
||||
|
||||
@@ -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"';
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"no-any": false,
|
||||
"no-arg": true,
|
||||
"no-bitwise": true,
|
||||
"prefer-conditional-expression": false,
|
||||
"space-within-parens": false,
|
||||
"no-object-literal-type-assertion": false,
|
||||
"no-console": [true,
|
||||
|
||||
Reference in New Issue
Block a user