Compare commits
245 Commits
ffmpeg_pul
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa0e8568b4 | ||
|
|
53c1940143 | ||
|
|
1a1753ee5b | ||
|
|
99b5ec246b | ||
|
|
49e6f07237 | ||
|
|
62606005b5 | ||
|
|
eb9fdc9177 | ||
|
|
546f1eeeb4 | ||
|
|
d92fbace08 | ||
|
|
43c91eea08 | ||
|
|
9542e451f2 | ||
|
|
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 | ||
|
|
cc0bf4dfb1 | ||
|
|
00857ba46f | ||
|
|
b77a35e0e9 | ||
|
|
ca59e3b2fd | ||
|
|
95c0c4d6d3 | ||
|
|
0d2d36251a | ||
|
|
48a58ffca6 | ||
|
|
505e6c67ce | ||
|
|
83e8a5e08c | ||
|
|
c82319a2c6 | ||
|
|
1fe7c697c5 | ||
|
|
239d1c60a3 | ||
|
|
bdfc96d56e | ||
|
|
8f7babd809 | ||
|
|
c708df574b | ||
|
|
401a511668 | ||
|
|
969879921e | ||
|
|
546ba9b45a | ||
|
|
27bdf54782 | ||
|
|
beed932e93 | ||
|
|
e5c4c08e66 | ||
|
|
2b201b0785 | ||
|
|
fdf5805911 | ||
|
|
9191075f48 | ||
|
|
9f73e4f865 | ||
|
|
1f20e028e1 | ||
|
|
da0fb17015 | ||
|
|
2aa71832b3 | ||
|
|
876def4392 | ||
|
|
0ba51b7270 | ||
|
|
7da4289097 | ||
|
|
ce5038cf08 | ||
|
|
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 | ||
|
|
6448f4ec97 | ||
|
|
829bb080ee | ||
|
|
5edd7cf05a | ||
|
|
8cf70e57cf | ||
|
|
2545c36241 | ||
|
|
e2a4ba738f | ||
|
|
e4e0fc3ea7 | ||
|
|
c56998312c | ||
|
|
3ac1f4ee9e | ||
|
|
c2e9449630 | ||
|
|
fb14020a7f | ||
|
|
5a51d888b8 | ||
|
|
301fa1c860 | ||
|
|
7e32028195 | ||
|
|
dea2c38dc4 | ||
|
|
cdf7f223db | ||
|
|
bb70161652 | ||
|
|
7f2f983f55 | ||
|
|
8dab83b3ef | ||
|
|
cbafa5bc90 | ||
|
|
67735fb52a | ||
|
|
7d6f762f59 | ||
|
|
f3a0d0129d | ||
|
|
65c9032839 | ||
|
|
978a3282a4 | ||
|
|
9f0195bebc | ||
|
|
ea20108222 | ||
|
|
4ee814864c | ||
|
|
4cbfd691c3 | ||
|
|
7c04fb7282 | ||
|
|
849c7612aa | ||
|
|
6ad4cbed0a | ||
|
|
9e2f5401d0 | ||
|
|
b064b97f2d | ||
|
|
b248405437 | ||
|
|
bf941819a8 | ||
|
|
fcae53baae | ||
|
|
05ead50c0d | ||
|
|
0a80f80f91 | ||
|
|
3bf5fea735 | ||
|
|
3a95994cc2 | ||
|
|
a29870691b | ||
|
|
547fdc4aa0 | ||
|
|
c78552795f | ||
|
|
090c7e4789 | ||
|
|
bf8e1fe80f | ||
|
|
7344ce3d61 | ||
|
|
c642e76cce | ||
|
|
8ef27066f6 | ||
|
|
621df26b58 | ||
|
|
8060b1b73b | ||
|
|
11f6b3feff | ||
|
|
537639f2a8 | ||
|
|
813f8a997d | ||
|
|
48544020a1 | ||
|
|
cc68d21107 | ||
|
|
acd91e2679 | ||
|
|
53f0a9462a | ||
|
|
10d71944d9 | ||
|
|
b5bbde7cdd | ||
|
|
c406bc70ee | ||
|
|
1dea620295 | ||
|
|
2019c104b6 | ||
|
|
9f1ead1368 | ||
|
|
41f67798d6 | ||
|
|
2c2ed2c136 | ||
|
|
4dc90aeb00 | ||
|
|
361c6cf54c | ||
|
|
b691b953d4 | ||
|
|
3d067979e9 | ||
|
|
58247f53e4 | ||
|
|
6189e31e6b | ||
|
|
3df650a0a6 | ||
|
|
c785c0f7c3 | ||
|
|
a01f3cd09c | ||
|
|
ed4f398062 | ||
|
|
e9cf0c353b | ||
|
|
6bc39083b9 | ||
|
|
67d06246d4 | ||
|
|
2ab1daf2b3 | ||
|
|
065d3b4c66 | ||
|
|
cfe73f5ca8 | ||
|
|
2fea379484 | ||
|
|
bee3f33e20 | ||
|
|
5d9c25491d | ||
|
|
58f4dc61ff | ||
|
|
b96efacbd2 | ||
|
|
a346ab8854 | ||
|
|
499530141e | ||
|
|
d1457bb893 | ||
|
|
8dfd1b447c | ||
|
|
ce63ae9a16 | ||
|
|
70d80ccd17 | ||
|
|
7833fbe292 | ||
|
|
fa6aa74442 | ||
|
|
fe2ed9fb76 | ||
|
|
cc655b9e00 | ||
|
|
e1d2a55a01 | ||
|
|
a31de0ef9d | ||
|
|
2853334d7f | ||
|
|
69dd28d31b | ||
|
|
56afce02ea | ||
|
|
bc4697061e | ||
|
|
55ffe85f77 | ||
|
|
ec8c2c7716 | ||
|
|
714a528f8b | ||
|
|
8314d91bd7 | ||
|
|
5bd31f9e0b | ||
|
|
95a93930f3 | ||
|
|
4a9e1d0410 | ||
|
|
1eacd0a5ca | ||
|
|
3c32726745 | ||
|
|
42ae0ae1fb | ||
|
|
e4b3871919 | ||
|
|
58e4a557e2 | ||
|
|
8371d68113 | ||
|
|
b7d496fc9d | ||
|
|
14260d04b3 | ||
|
|
3d46b65d67 |
0
.github/CONTRIBUTING.md
vendored
Normal file
0
.github/CONTRIBUTING.md
vendored
Normal file
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Please fill theses informations:**
|
||||||
|
(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:
|
||||||
|
|
||||||
|
**Please be careful to remove your real account login and password if they appear!**
|
||||||
|
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
|
|
||||||
|
|
||||||
|
_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_
|
||||||
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
11
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
11
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
*Goal for this pull request*
|
||||||
|
A clear and concise description of pull request
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Checklist
|
||||||
|
[ ] I've run `npm run compile` and it produce no error
|
||||||
|
[ ] I've run `npm run test` and it produce no error
|
||||||
|
[ ] I've not pushing more than one feature in that pull request
|
||||||
|
[ ] My branch is updated with the latest from main when I make that pull request
|
||||||
|
[ ] I've tested as much as I can my changes
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,3 +1,5 @@
|
|||||||
dist/
|
dist/
|
||||||
node_modules/
|
node_modules/
|
||||||
typings/
|
.idea/
|
||||||
|
test/
|
||||||
|
config.json
|
||||||
|
|||||||
@ -1,8 +1,12 @@
|
|||||||
extras/
|
extras/
|
||||||
node_modules/
|
node_modules/
|
||||||
src/
|
src/
|
||||||
typings/
|
|
||||||
ts.js
|
ts.js
|
||||||
tsconfig.json
|
tsconfig.json
|
||||||
tsd.json
|
tsd.json
|
||||||
tslint.json
|
tslint.json
|
||||||
|
.idea/
|
||||||
|
.github/
|
||||||
|
test/
|
||||||
|
config.json
|
||||||
|
.travis.yml
|
||||||
|
|||||||
15
.travis.yml
Normal file
15
.travis.yml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
language: node_js
|
||||||
|
sudo: false
|
||||||
|
node_js:
|
||||||
|
- 8
|
||||||
|
- 9
|
||||||
|
- 10
|
||||||
|
- 11
|
||||||
|
- 12
|
||||||
|
# - 13
|
||||||
|
# - 14
|
||||||
|
before_install:
|
||||||
|
- npm install --only=dev
|
||||||
|
script:
|
||||||
|
- npm run build
|
||||||
|
- npm test
|
||||||
1
LICENSE
1
LICENSE
@ -1,4 +1,5 @@
|
|||||||
Copyright (c) 2015 Roel van Uden
|
Copyright (c) 2015 Roel van Uden
|
||||||
|
Copyright (c) 2016 Manoel <Godzil> Trapier
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to
|
of this software and associated documentation files (the "Software"), to
|
||||||
|
|||||||
229
README.md
229
README.md
@ -1,6 +1,12 @@
|
|||||||
# CrunchyRoll.js
|
# Crunchy: a fork of Deathspike/CrunchyRoll.js
|
||||||
|
|
||||||
*CrunchyRoll.js* is capable of downloading *anime* episodes from the popular *CrunchyRoll* streaming service. An episode is stored in the original video format (often H.264 in a MP4 container) and the configured subtitle format (ASS or SRT).The two output files are then merged into a single MKV file.
|
[](https://travis-ci.org/Godzil/Crunchy)
|
||||||
|
[](https://codeclimate.com/github/Godzil/Crunchy/maintainability)
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
*Crunchy* is capable of downloading *anime* episodes from the popular *CrunchyRoll* streaming service. An episode is stored in the original video format (often H.264 in a MP4 container) and the configured subtitle format (ASS or SRT).The two output files are then merged into a single MKV file.
|
||||||
|
|
||||||
## Motivation
|
## Motivation
|
||||||
|
|
||||||
@ -10,103 +16,249 @@
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
**_ONLY_ USE THIS TOOL IF YOU HAVE A _PREMIUM ACCOUNT_**
|
||||||
|
|
||||||
## Configuration
|
## 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
|
## Prerequisites
|
||||||
|
|
||||||
* NodeJS >= 0.12.x (http://nodejs.org/)
|
* NodeJS >= 8.1 (http://nodejs.org/)
|
||||||
* NPM >= 2.5.x (https://www.npmjs.org/)
|
* NPM >= 5.8 (https://www.npmjs.org/)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Use the applicable instructions to install. Is your operating system not listed? Please ask or contribute!
|
Use the applicable instructions to install. Is your operating system not listed? Please ask or contribute!
|
||||||
|
|
||||||
### Debian (Mint, Ubuntu, etc)
|
### Linux (Debian, Mint, Ubuntu, etc)
|
||||||
|
|
||||||
1. Run in *Terminal*: `sudo apt-get install nodejs npm mkvtoolnix rtmpdump ffmpeg`
|
1. Run in *Terminal*: `sudo apt-get install nodejs npm mkvtoolnix rtmpdump ffmpeg`
|
||||||
2. Run in *Terminal*: `sudo ln -s /usr/bin/nodejs /usr/bin/node`
|
2. Run in *Terminal*: `sudo ln -s /usr/bin/nodejs /usr/bin/node`
|
||||||
3. Run in *Terminal*: `sudo npm install -g crunchyroll`
|
3. Run in *Terminal*: `sudo npm install -g crunchy`
|
||||||
|
|
||||||
|
#### Updating:
|
||||||
|
1. Run in *Terminal*: `sudo npm update -g crunchy`
|
||||||
|
|
||||||
### Mac OS X
|
### Mac OS X
|
||||||
|
|
||||||
1. Install *Homebrew* following the instructions at http://brew.sh/
|
1. Install *Homebrew* following the instructions at http://brew.sh/
|
||||||
2. Run in *Terminal*: `brew install node mkvtoolnix rtmpdump ffmpeg`
|
2. Run in *Terminal*: `brew install node mkvtoolnix rtmpdump ffmpeg`
|
||||||
3. Run in *Terminal*: `npm install -g crunchyroll`
|
3. Run in *Terminal*: `npm install -g crunchy`
|
||||||
|
|
||||||
|
#### Updating:
|
||||||
|
1. Run in *Terminal*: `sudo npm update -g crunchy`
|
||||||
|
|
||||||
### Windows
|
### Windows
|
||||||
|
|
||||||
1. Install *NodeJS* following the instructions at http://nodejs.org/
|
1. Install *NodeJS* following the instructions at http://nodejs.org/
|
||||||
3. Run in *Command Prompt*: `npm install -g crunchyroll`
|
3. Run in *Command Prompt*: `npm install -g crunchy`
|
||||||
|
|
||||||
|
#### Updating:
|
||||||
|
1. Run in *Command Prompt*: `npm update -g crunchy`
|
||||||
|
|
||||||
## Instructions
|
## Instructions
|
||||||
|
|
||||||
Use the applicable instructions for the interface of your choice (currently limited to command-line).
|
Use the applicable instructions for the interface of your choice (currently limited to command-line).
|
||||||
|
|
||||||
### Command-line Interface (`crunchyroll`)
|
### Command-line Interface (`crunchy`)
|
||||||
|
|
||||||
The [command-line interface](http://en.wikipedia.org/wiki/Command-line_interface) does not have a graphical component and is ideal for automation purposes and headless machines. The interface can run using a sequence of series addresses (the site address containing the episode listing), or with a batch-mode source file. The `crunchyroll --help` command will produce the following output:
|
The [command-line interface](http://en.wikipedia.org/wiki/Command-line_interface) does not have a graphical component and is ideal for automation purposes and headless machines. The interface can run using a sequence of series addresses (the site address containing the episode listing), or with a batch-mode source file. The `crunchy --help` command will produce the following output:
|
||||||
|
|
||||||
Usage: crunchyroll [options]
|
Usage: crunchy [options]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
|
|
||||||
-h, --help output usage information
|
-V, --version output the version number
|
||||||
-V, --version output the version number
|
-p, --pass <s> The password.
|
||||||
-p, --pass <s> The password.
|
-u, --user <s> The e-mail address or username.
|
||||||
-u, --user <s> The e-mail address or username.
|
-d, --unlog Unlog
|
||||||
-c, --cache Disables the cache.
|
-c, --cache Disables the cache.
|
||||||
-m, --merge Disables merging subtitles and videos.
|
-m, --merge Disables merging subtitles and videos.
|
||||||
-e, --episode <i> The episode filter.
|
-e, --episodes <s> Episode list. Read documentation on how to use
|
||||||
-v, --volume <i> The volume filter.
|
-f, --format <s> The subtitle format. (default: ass)
|
||||||
-f, --format <s> The subtitle format. (Default: ass)
|
-o, --output <s> The output path.
|
||||||
-o, --output <s> The output path.
|
--ignoredub Experimental: Ignore all seasons where the title end with 'Dub)'
|
||||||
-s, --series <s> The series override.
|
-n, --nametmpl <s> Output name template (default: {SERIES_TITLE} - s{SEASON_NUMBER}e{EPISODE_NUMBER} - [{TAG}])
|
||||||
-t, --tag <s> The subgroup. (Default: CrunchyRoll)
|
-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)
|
||||||
|
-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
|
#### 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 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.
|
||||||
|
|
||||||
|
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`
|
||||||
|
* `sublang` see `--sublang`
|
||||||
|
|
||||||
|
- 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`
|
||||||
|
|
||||||
|
- Other options:
|
||||||
|
* `sleepTime`: minimum delay (in ms) between each page load
|
||||||
|
|
||||||
|
- Generated values: don't touch them:
|
||||||
|
* `crDeviceId`
|
||||||
|
* `crSessionId`
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
#### Examples
|
#### Examples
|
||||||
|
|
||||||
Download in batch-mode:
|
Download in batch-mode:
|
||||||
|
|
||||||
crunchyroll
|
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
|
||||||
|
|
||||||
crunchyroll http://www.crunchyroll.com/fairy-tail
|
Then launch crunchy:
|
||||||
|
|
||||||
Download *Fairy Tail* to `C:\Anime`:
|
crunchy -u login -p password http://www.cr.com/tail-fairy
|
||||||
|
|
||||||
crunchyroll --output C:\Anime http://www.crunchyroll.com/fairy-tail
|
Download *Tail Fairy* to the current work directory:
|
||||||
|
|
||||||
#### Switches
|
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 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
|
||||||
|
|
||||||
##### Authentication
|
##### Authentication
|
||||||
|
|
||||||
* `-p or --pass <s>` sets the password.
|
* `-p or --pass <s>` sets the password.
|
||||||
* `-u or --user <s>` sets the e-mail address or username.
|
* `-u or --user <s>` sets the e-mail address or username.
|
||||||
|
* `-d or --unlog` unlog
|
||||||
|
|
||||||
|
_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
|
##### Disables
|
||||||
|
|
||||||
* `-c or --cache` disables the cache.
|
* `-c or --cache` disables the cache in batch mode.
|
||||||
* `-m or --merge` disables merging subtitles and videos.
|
* `-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).
|
|
||||||
|
|
||||||
##### Settings
|
##### Settings
|
||||||
|
|
||||||
|
* `-e or --episodes <s>` set an episode
|
||||||
* `-f or --format <s>` sets the subtitle format. (Default: ass)
|
* `-f or --format <s>` sets the subtitle format. (Default: ass)
|
||||||
* `-o or --output <s>` sets the output path.
|
* `-o or --output <s>` sets the output path.
|
||||||
* `-s or --series <s>` sets the series override.
|
* `-s or --series <s>` sets the series override.
|
||||||
* `-t or --tag <s>` sets The subgroup. (Default: CrunchyRoll)
|
* `-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
|
||||||
|
|
||||||
|
* `-b or --batch <s>` specify the batch file to use. Default to "CrunchyRoll.txt"
|
||||||
|
* `--verbose` make Crunchy really verbose. You should use it only for bug reporting or to try to see why it does not work
|
||||||
|
* `-g or --rebuildcrp` use that parameter only if the .crpersistent file has been corrupted and Crunchy try to redownload everything. It will try to rebuild the cache file from the file if find. If you renamed of move any file they will be ignored and not added to the cache file.
|
||||||
|
|
||||||
|
|
||||||
|
## When things goes wrong
|
||||||
|
|
||||||
|
First, make sure you have the latest version of Crunchy installed, if you run an older version, the issue you face may have been solved.
|
||||||
|
|
||||||
|
Second thing to check, you have to give your credentials (-u and -p parameters) each time you run Crunchy. It does not actually store the token it receive when login and need to relog each time it is called. This may change in the future.
|
||||||
|
|
||||||
|
Third, is it a recently released episode? If yes, sometimes CR have issues were the requested format is not available, and Crunchy is not able to get it. When in doubt, try to watch CR website, if it does not work there, Crunchy will not either. This is valid in all cases even on non recently released.
|
||||||
|
|
||||||
|
Fourth, sometimes, CR website does weird things, and there are some transient errors, wait a couple of minutes (or hours) and try again. It has really often solved lots of weird issue on my side (yes I know that's not really a way of fixing, but if the error is on CR side, Crunchy can't do anything)
|
||||||
|
|
||||||
|
If really nothing works or you find a problem with Crunchy, then you can go and fill an Issue, first read the already open and closed one to make sure you are not reporting an existing problem. If your problem has been already reported, what you can do is to either:
|
||||||
|
- Add a comment saying you also have the same issue
|
||||||
|
- Add a Thumbs Up reaction to the original entry in the issue, I use them as a metric to know how many people are annoyed by that issue
|
||||||
|
If you find one which correspond and it is close, don't hesitate to add a comment, the issue may have not be fully solved.
|
||||||
|
|
||||||
|
If there is no comparable opened or close issue, you are welcome to create a new one.
|
||||||
|
|
||||||
## Developers
|
## Developers
|
||||||
|
|
||||||
@ -114,4 +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).
|
* Atom with `atom-typescript` and `linter-tslint` (and dependencies).
|
||||||
|
|
||||||
Since this project uses TypeScript, compile with `node ts` or `npm install`.
|
Since this project uses TypeScript, compile with `node run build` to build the tool and `npm run test` to run the linter.
|
||||||
|
|
||||||
|
|||||||
0
bin/crunchyroll → bin/crunchy
Normal file → Executable file
0
bin/crunchyroll → bin/crunchy
Normal file → Executable file
10
bin/crunchy.sh
Executable file
10
bin/crunchy.sh
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
PARAMS=$*
|
||||||
|
for i in {1..20}; do
|
||||||
|
crunchy ${PARAMS}
|
||||||
|
if [ $? == 0 ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
echo "Going to retry..."
|
||||||
|
sleep 3
|
||||||
|
done
|
||||||
9152
package-lock.json
generated
Normal file
9152
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
63
package.json
63
package.json
@ -1,36 +1,63 @@
|
|||||||
{
|
{
|
||||||
"author": "Roel van Uden",
|
"author": "Godzil",
|
||||||
"description": "CrunchyRoll.js is capable of downloading anime episodes from the popular CrunchyRoll streaming service.",
|
"description": "Crunchy is a fork of Crunchyroll.js, capable of downloading anime episodes from the popular CrunchyRoll streaming service.",
|
||||||
|
"license": "MIT",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"anime",
|
"anime",
|
||||||
"download",
|
"download",
|
||||||
"crunchyroll"
|
"crunchyroll"
|
||||||
],
|
],
|
||||||
"name": "crunchyroll",
|
"name": "crunchy",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git://github.com/Deathspike/crunchyroll.js.git"
|
"url": "git://github.com/Godzil/crunchyroll.js.git"
|
||||||
},
|
},
|
||||||
"version": "1.1.5",
|
"engines": {
|
||||||
|
"node": ">=5.0"
|
||||||
|
},
|
||||||
|
"version": "1.6.1",
|
||||||
"bin": {
|
"bin": {
|
||||||
"crunchyroll": "./bin/crunchyroll"
|
"crunchy": "./bin/crunchy",
|
||||||
|
"crunchy.sh": "./bin/crunchy.sh"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"big-integer": "1.4.4",
|
"big-integer": ">=1.6.51",
|
||||||
"cheerio": "0.18.0",
|
"bluebird": ">=3.7.2",
|
||||||
"commander": "2.6.0",
|
"brotli": ">=1.3.3",
|
||||||
"mkdirp": "0.5.0",
|
"cloudscraper": ">=4.6.0",
|
||||||
"request": "2.53.0",
|
"commander": ">=11.0.0",
|
||||||
"xml2js": "0.4.5"
|
"fs-extra": ">=11.1.1",
|
||||||
|
"mkdirp": ">=3.0.1",
|
||||||
|
"pjson": ">=1.0.9",
|
||||||
|
"request": ">=2.88.2",
|
||||||
|
"request-promise": ">=4.2.6",
|
||||||
|
"tough-cookie-file-store": ">=2.0.3",
|
||||||
|
"uuid": ">=9.0.0",
|
||||||
|
"xml2js": ">=0.6.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"tsd": "0.5.7",
|
"@types/bluebird": ">=3.5.38",
|
||||||
"tslint": "2.3.0-beta",
|
"@types/cheerio": ">=0.22.31",
|
||||||
"typescript": "1.5.0-beta"
|
"@types/fs-extra": ">=11.0.1",
|
||||||
|
"@types/mkdirp": ">=2.0.0",
|
||||||
|
"@types/node": ">=20.3.2",
|
||||||
|
"@types/request": ">=2.48.8",
|
||||||
|
"@types/request-promise": ">=4.1.48",
|
||||||
|
"@types/uuid": ">=9.0.2",
|
||||||
|
"@types/xml2js": ">=0.4.11",
|
||||||
|
"cheerio": "^1.0.0-rc.12",
|
||||||
|
"npm-check": "^6.0.1",
|
||||||
|
"tslint": "^6.1.3",
|
||||||
|
"typescript": ">=5.1.3"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepublish": "npm run tsd && tsc",
|
"prepublishOnly": "npm run build",
|
||||||
"test": "node ts --only-test",
|
"compile": "tsc",
|
||||||
"tsd": "tsd reinstall -o -s"
|
"build": "tsc",
|
||||||
|
"test": "tslint --project .",
|
||||||
|
"start": "node ./bin/crunchy"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/Godzil/Crunchy/issues"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
413
src/batch.ts
413
src/batch.ts
@ -2,23 +2,178 @@
|
|||||||
import commander = require('commander');
|
import commander = require('commander');
|
||||||
import fs = require('fs');
|
import fs = require('fs');
|
||||||
import path = require('path');
|
import path = require('path');
|
||||||
|
import log = require('./log');
|
||||||
|
import my_request = require('./my_request');
|
||||||
|
import cfg = require('./config');
|
||||||
import series from './series';
|
import series from './series';
|
||||||
|
|
||||||
|
/* correspondances between resolution and value CR excpect */
|
||||||
|
const resol_table: { [id: string]: IResolData; } =
|
||||||
|
{
|
||||||
|
360: {quality: '60', format: '106'},
|
||||||
|
480: {quality: '61', format: '106'},
|
||||||
|
720: {quality: '62', format: '106'},
|
||||||
|
1080: {quality: '80', format: '108'},
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Streams the batch of series to disk.
|
* Streams the batch of series to disk.
|
||||||
*/
|
*/
|
||||||
export default function(args: string[], done: (err?: Error) => void) {
|
export default function(args: string[], done: (err?: Error) => void)
|
||||||
var config = parse(args);
|
{
|
||||||
var batchPath = path.join(config.output || process.cwd(), 'CrunchyRoll.txt');
|
const config = Object.assign(cfg.load(), parse(args));
|
||||||
tasks(config, batchPath, (err, tasks) => {
|
let batchPath;
|
||||||
if (err) return done(err);
|
|
||||||
var i = 0;
|
if (path.isAbsolute(config.batch))
|
||||||
(function next() {
|
{
|
||||||
if (i >= tasks.length) return done();
|
batchPath = path.normalize(config.batch);
|
||||||
series(tasks[i].config, tasks[i].address, err => {
|
}
|
||||||
if (err) return done(err);
|
else
|
||||||
i += 1;
|
{
|
||||||
next();
|
batchPath = path.normalize(path.join(process.cwd(), config.batch));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.nametmpl === undefined)
|
||||||
|
{
|
||||||
|
config.nametmpl = '{SERIES_TITLE} - s{SEASON_NUMBER}e{EPISODE_NUMBER} - {EPISODE_TITLE} - [{TAG}]';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.tag === undefined)
|
||||||
|
{
|
||||||
|
config.tag = 'CrunchyRoll';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.sublang === undefined)
|
||||||
|
{
|
||||||
|
config.sublang = [ 'enUS' ];
|
||||||
|
}
|
||||||
|
|
||||||
|
// set resolution
|
||||||
|
if (config.resolution)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
config.video_format = resol_table[config.resolution].format;
|
||||||
|
config.video_quality = resol_table[config.resolution].quality;
|
||||||
|
}
|
||||||
|
catch (e)
|
||||||
|
{
|
||||||
|
log.warn('Invalid resolution ' + config.resolution + 'p. Setting to 1080p');
|
||||||
|
config.video_format = resol_table['1080'].format;
|
||||||
|
config.video_quality = resol_table['1080'].quality;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* 1080 by default */
|
||||||
|
config.video_format = resol_table['1080'].format;
|
||||||
|
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 */
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tasksArr || !tasksArr[0] || (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();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.debug)
|
||||||
|
{
|
||||||
|
log.dumpToDebug('Task ' + i, JSON.stringify(tasksArr[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
series(config, tasksArr[i], (errin) =>
|
||||||
|
{
|
||||||
|
if (errin)
|
||||||
|
{
|
||||||
|
if (errin.error)
|
||||||
|
{
|
||||||
|
/* 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)
|
||||||
|
{
|
||||||
|
if (config.verbose)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (config.verbose)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
setTimeout(next, config.sleepTime);
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
});
|
});
|
||||||
@ -27,43 +182,196 @@ export default function(args: string[], done: (err?: Error) => void) {
|
|||||||
/**
|
/**
|
||||||
* Splits the value into arguments.
|
* Splits the value into arguments.
|
||||||
*/
|
*/
|
||||||
function split(value: string): string[] {
|
function split(value: string): string[]
|
||||||
var inQuote = false;
|
{
|
||||||
var i: number;
|
let inQuote = false;
|
||||||
var pieces: string[] = [];
|
let i: number;
|
||||||
var previous = 0;
|
const pieces: string[] = [];
|
||||||
for (i = 0; i < value.length; i += 1) {
|
let previous = 0;
|
||||||
if (value.charAt(i) === '"') inQuote = !inQuote;
|
|
||||||
if (!inQuote && value.charAt(i) === ' ') {
|
for (i = 0; i < value.length; i += 1)
|
||||||
|
{
|
||||||
|
if (value.charAt(i) === '"')
|
||||||
|
{
|
||||||
|
inQuote = !inQuote;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inQuote && value.charAt(i) === ' ')
|
||||||
|
{
|
||||||
pieces.push(value.substring(previous, i).match(/^"?(.+?)"?$/)[1]);
|
pieces.push(value.substring(previous, i).match(/^"?(.+?)"?$/)[1]);
|
||||||
previous = i + 1;
|
previous = i + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var lastPiece = value.substring(previous, i).match(/^"?(.+?)"?$/);
|
|
||||||
if (lastPiece) pieces.push(lastPiece[1]);
|
const lastPiece = value.substring(previous, i).match(/^"?(.+?)"?$/);
|
||||||
|
|
||||||
|
if (lastPiece)
|
||||||
|
{
|
||||||
|
pieces.push(lastPiece[1]);
|
||||||
|
}
|
||||||
|
|
||||||
return pieces;
|
return pieces;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function get_min_filter(filter: string): IEpisodeNumber
|
||||||
|
{
|
||||||
|
if (filter !== undefined)
|
||||||
|
{
|
||||||
|
const tok = filter.split('-');
|
||||||
|
|
||||||
|
if (tok.length > 2)
|
||||||
|
{
|
||||||
|
log.error('Invalid episode filter \'' + filter + '\'');
|
||||||
|
process.exit(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tok[0] !== '')
|
||||||
|
{
|
||||||
|
/* 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)};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* First item is empty, ie '-20' */
|
||||||
|
return {season: 0, episode: 0};
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_max_filter(filter: string): IEpisodeNumber
|
||||||
|
{
|
||||||
|
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 {season: +Infinity, episode: parseInt(tok[1], 10)};
|
||||||
|
}
|
||||||
|
else if ((tok.length === 1) && (tok[0] !== ''))
|
||||||
|
{
|
||||||
|
/* A single episode has been requested */
|
||||||
|
return {season: +Infinity, episode: parseInt(tok[0], 10) + 1};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {season: +Infinity, episode: +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.
|
* Parses the configuration or reads the batch-mode file for tasks.
|
||||||
*/
|
*/
|
||||||
function tasks(config: IConfigLine, batchPath: string, done: (err: Error, tasks?: IConfigTask[]) => void) {
|
function tasks(config: IConfigLine, batchPath: string, done: (err: Error, tasks?: IConfigTask[]) => void)
|
||||||
if (config.args.length) {
|
{
|
||||||
return done(null, config.args.map(address => {
|
if (config.args.length)
|
||||||
return {address: address, config: config};
|
{
|
||||||
|
return done(null, config.args.map((addressIn) =>
|
||||||
|
{
|
||||||
|
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: {season: 0, episode: 0}, episode_max: {season: 0, episode: 0}};
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
fs.exists(batchPath, exists => {
|
|
||||||
if (!exists) return done(null, []);
|
fs.exists(batchPath, (exists) =>
|
||||||
fs.readFile(batchPath, 'utf8', (err, data) => {
|
{
|
||||||
if (err) return done(err);
|
if (!exists)
|
||||||
var map: IConfigTask[] = [];
|
{
|
||||||
data.split(/\r?\n/).forEach(line => {
|
return done(null, []);
|
||||||
if (/^(\/\/|#)/.test(line)) return;
|
}
|
||||||
var lineConfig = parse(process.argv.concat(split(line)));
|
|
||||||
lineConfig.args.forEach(address => {
|
fs.readFile(batchPath, 'utf8', (err, data) =>
|
||||||
if (!address) return;
|
{
|
||||||
map.push({address: address, config: lineConfig});
|
if (err)
|
||||||
|
{
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const map: IConfigTask[] = [];
|
||||||
|
|
||||||
|
data.split(/\r?\n/).forEach((line) =>
|
||||||
|
{
|
||||||
|
if (/^(\/\/|#)/.test(line))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lineConfig = parse(process.argv.concat(split(line)));
|
||||||
|
|
||||||
|
lineConfig.args.forEach((addressIn) =>
|
||||||
|
{
|
||||||
|
if (!addressIn)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
done(null, map);
|
||||||
@ -71,24 +379,41 @@ 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.
|
* Parses the arguments and returns a configuration.
|
||||||
*/
|
*/
|
||||||
function parse(args: string[]): IConfigLine {
|
function parse(args: string[]): IConfigLine
|
||||||
|
{
|
||||||
return new commander.Command().version(require('../package').version)
|
return new commander.Command().version(require('../package').version)
|
||||||
// Authentication
|
// Authentication
|
||||||
.option('-p, --pass <s>', 'The password.')
|
.option('-p, --pass <s>', 'The password.')
|
||||||
.option('-u, --user <s>', 'The e-mail address or username.')
|
.option('-u, --user <s>', 'The e-mail address or username.')
|
||||||
|
.option('-d, --unlog', 'Unlog')
|
||||||
// Disables
|
// Disables
|
||||||
.option('-c, --cache', 'Disables the cache.')
|
.option('-c, --cache', 'Disables the cache.')
|
||||||
.option('-m, --merge', 'Disables merging subtitles and videos.')
|
.option('-m, --merge', 'Disables merging subtitles and videos.')
|
||||||
// Filters
|
// Episode filter
|
||||||
.option('-e, --episode <i>', 'The episode filter.')
|
.option('-e, --episodes <s>', 'Episode list. Read documentation on how to use')
|
||||||
.option('-v, --volume <i>', 'The volume filter.')
|
|
||||||
// Settings
|
// Settings
|
||||||
.option('-f, --format <s>', 'The subtitle format. (Default: ass)')
|
.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('-o, --output <s>', 'The output path.')
|
||||||
.option('-s, --series <s>', 'The series override.')
|
.option('-s, --series <s>', 'The series name override.')
|
||||||
.option('-t, --tag <s>', 'The subgroup. (Default: CrunchyRoll)')
|
.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.')
|
||||||
|
.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('--sleepTime <i>', 'Minimum wait time between each http requests.')
|
||||||
.parse(args);
|
.parse(args);
|
||||||
}
|
}
|
||||||
|
|||||||
49
src/cli.ts
49
src/cli.ts
@ -1,6 +1,51 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
import batch from './batch';
|
import batch from './batch';
|
||||||
|
import request = require('request');
|
||||||
|
import log = require('./log');
|
||||||
|
import pjson = require('pjson');
|
||||||
|
|
||||||
batch(process.argv, (err: any) => {
|
const current_version = pjson.version;
|
||||||
if (err) console.error(err.stack || err);
|
|
||||||
|
/* Check if the current version is the latest */
|
||||||
|
log.info('Crunchy version ' + current_version);
|
||||||
|
request.get({ uri: 'https://box.godzil.net/getVersion.php?tool=crunchy&v=' + current_version },
|
||||||
|
(error: Error, response: any, body: any) =>
|
||||||
|
{
|
||||||
|
if (response && (response.statusCode === 200))
|
||||||
|
{
|
||||||
|
const onlinepkg = JSON.parse(body);
|
||||||
|
if (onlinepkg.status === 'ok')
|
||||||
|
{
|
||||||
|
let tmp = current_version.split('.');
|
||||||
|
const cur = (Number(tmp[0]) * 10000) + (Number(tmp[1]) * 100) + Number(tmp[2]);
|
||||||
|
tmp = onlinepkg.version.split('.');
|
||||||
|
const dist = (Number(tmp[0]) * 10000) + (Number(tmp[1]) * 100) + Number(tmp[2]);
|
||||||
|
if (dist > cur)
|
||||||
|
{
|
||||||
|
log.warnMore('There is a newer version of crunchy (v' + onlinepkg.version + '), you should update!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.info('Error while checking for the current version.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
batch(process.argv, (err: any) =>
|
||||||
|
{
|
||||||
|
if (err)
|
||||||
|
{
|
||||||
|
if (err.stack)
|
||||||
|
{
|
||||||
|
console.error(err.stack || err);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
process.exit(-1);
|
||||||
|
}
|
||||||
|
console.info('Done!');
|
||||||
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|||||||
82
src/config.ts
Normal file
82
src/config.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
'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;
|
||||||
|
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.rebuildcrp = undefined;
|
||||||
|
tmp.batch = undefined;
|
||||||
|
tmp.verbose = undefined;
|
||||||
|
tmp.debug = undefined;
|
||||||
|
tmp.unlog = undefined;
|
||||||
|
tmp.ignoredub = undefined;
|
||||||
|
|
||||||
|
fs.writeFileSync(configFile, JSON.stringify(tmp, null, ' '));
|
||||||
|
}
|
||||||
557
src/episode.ts
557
src/episode.ts
@ -2,175 +2,522 @@
|
|||||||
import cheerio = require('cheerio');
|
import cheerio = require('cheerio');
|
||||||
import fs = require('fs');
|
import fs = require('fs');
|
||||||
import mkdirp = require('mkdirp');
|
import mkdirp = require('mkdirp');
|
||||||
import request = require('./request');
|
import my_request = require('./my_request');
|
||||||
import path = require('path');
|
import path = require('path');
|
||||||
import subtitle from './subtitle/index';
|
import subtitle from './subtitle/index';
|
||||||
|
import vlos from './vlos';
|
||||||
import video from './video/index';
|
import video from './video/index';
|
||||||
import xml2js = require('xml2js');
|
import xml2js = require('xml2js');
|
||||||
|
import log = require('./log');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Streams the episode to disk.
|
* Streams the episode to disk.
|
||||||
*/
|
*/
|
||||||
export default function(config: IConfig, address: string, done: (err: Error) => void) {
|
export default function(config: IConfig, address: string, done: (err: Error, ign: boolean) => void)
|
||||||
scrapePage(config, address, (err, page) => {
|
{
|
||||||
if (err) return done(err);
|
scrapePage(config, address, (err, page) =>
|
||||||
scrapePlayer(config, address, page.id, (err, player) => {
|
{
|
||||||
if (err) return done(err);
|
if (err)
|
||||||
download(config, page, player, done);
|
{
|
||||||
});
|
return done(err, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page.media != null)
|
||||||
|
{
|
||||||
|
/* No player to scrape */
|
||||||
|
download(config, page, null, done);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* The old way */
|
||||||
|
scrapePlayer(config, address, page.id, (errS, player) =>
|
||||||
|
{
|
||||||
|
if (errS)
|
||||||
|
{
|
||||||
|
return done(errS, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
download(config, page, player, done);
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Completes a download and writes the message with an elapsed time.
|
* Completes a download and writes the message with an elapsed time.
|
||||||
*/
|
*/
|
||||||
function complete(message: string, begin: number, done: (err: Error) => void) {
|
function complete(epName: string, message: string, begin: number, done: (err: Error, ign: boolean) => void)
|
||||||
var timeInMs = Date.now() - begin;
|
{
|
||||||
var seconds = prefix(Math.floor(timeInMs / 1000) % 60, 2);
|
const timeInMs = Date.now() - begin;
|
||||||
var minutes = prefix(Math.floor(timeInMs / 1000 / 60) % 60, 2);
|
const seconds = prefix(Math.floor(timeInMs / 1000) % 60, 2);
|
||||||
var hours = prefix(Math.floor(timeInMs / 1000 / 60 / 60), 2);
|
const minutes = prefix(Math.floor(timeInMs / 1000 / 60) % 60, 2);
|
||||||
console.log(message + ' (' + hours + ':' + minutes + ':' + seconds + ')');
|
const hours = prefix(Math.floor(timeInMs / 1000 / 60 / 60), 2);
|
||||||
done(null);
|
|
||||||
|
log.dispEpisode(epName, message + ' (' + hours + ':' + minutes + ':' + seconds + ')', true);
|
||||||
|
|
||||||
|
done(null, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a file exist..
|
||||||
|
*/
|
||||||
|
function fileExist(path: string)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fs.statSync(path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (e)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitiseFileName(str: string)
|
||||||
|
{
|
||||||
|
const sanitized = str.replace(/[\/':\?\*"<>\\\.\|]/g, '_');
|
||||||
|
|
||||||
|
return sanitized.replace(/{DIR_SEPARATOR}/g, '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads the subtitle and video.
|
* Downloads the subtitle and video.
|
||||||
*/
|
*/
|
||||||
function download(config: IConfig, page: IEpisodePage, player: IEpisodePlayer, done: (err: Error) => void) {
|
function download(config: IConfig, page: IEpisodePage, player: IEpisodePlayer, done: (err: Error | string, ign: boolean) => void)
|
||||||
var series = config.series || page.series;
|
{
|
||||||
var fileName = name(config, page, series);
|
const serieFolder = sanitiseFileName(config.series || page.series);
|
||||||
var filePath = path.join(config.output || process.cwd(), series, fileName);
|
|
||||||
mkdirp(path.dirname(filePath), (err: Error) => {
|
let fileName = sanitiseFileName(generateName(config, page));
|
||||||
if (err) return done(err);
|
let filePath = path.join(config.output || process.cwd(), serieFolder, fileName);
|
||||||
downloadSubtitle(config, player, filePath, err => {
|
|
||||||
if (err) return done(err);
|
if (fileExist(filePath + '.mkv'))
|
||||||
var now = Date.now();
|
{
|
||||||
console.log('Fetching ' + fileName);
|
let count = 0;
|
||||||
downloadVideo(config, page, player, filePath, err => {
|
|
||||||
if (err) return done(err);
|
if (config.rebuildcrp)
|
||||||
if (config.merge) return complete('Finished ' + fileName, now, done);
|
{
|
||||||
var isSubtited = Boolean(player.subtitle);
|
log.warn('Adding \'' + fileName + '\' to the DB...');
|
||||||
video.merge(config, isSubtited, player.video.file, filePath, player.mode, err => {
|
return done(null, false);
|
||||||
if (err) return done(err);
|
}
|
||||||
complete('Finished ' + fileName, now, done);
|
|
||||||
|
log.warn('File \'' + fileName + '\' already exist...');
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
count = count + 1;
|
||||||
|
fileName = sanitiseFileName(generateName(config, page, '-' + count));
|
||||||
|
filePath = path.join(config.output || process.cwd(), serieFolder, fileName);
|
||||||
|
} while (fileExist(filePath + '.mkv'));
|
||||||
|
|
||||||
|
log.warn('Renaming to \'' + fileName + '\'...');
|
||||||
|
|
||||||
|
page.filename = fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.rebuildcrp)
|
||||||
|
{
|
||||||
|
log.warn('Ignoring \'' + fileName + '\' as it does not exist...');
|
||||||
|
return done(null, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ret = mkdirp(path.dirname(filePath));
|
||||||
|
if (ret)
|
||||||
|
{
|
||||||
|
log.dispEpisode(fileName, 'Fetching...', false);
|
||||||
|
downloadSubtitle(config, page, player, filePath, (errDS) =>
|
||||||
|
{
|
||||||
|
if (errDS)
|
||||||
|
{
|
||||||
|
log.dispEpisode(fileName, 'Error...', true);
|
||||||
|
return done(errDS, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
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) =>
|
||||||
|
{
|
||||||
|
if (errDV)
|
||||||
|
{
|
||||||
|
log.dispEpisode(fileName, 'Error...', true);
|
||||||
|
return done(errDV, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.merge)
|
||||||
|
{
|
||||||
|
return complete(fileName, 'Finished!', now, done);
|
||||||
|
}
|
||||||
|
|
||||||
|
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, isSubtitled, videoExt, filePath, config.verbose, (errVM) =>
|
||||||
|
{
|
||||||
|
if (errVM)
|
||||||
|
{
|
||||||
|
log.dispEpisode(fileName, 'Error...', true);
|
||||||
|
return done(errVM, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
complete(fileName, 'Finished!', now, done);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.dispEpisode(fileName, 'Ignoring: not released yet', true);
|
||||||
|
done(null, true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.dispEpisode(fileName, 'Error creating folder \'' + filePath + '\'...', true);
|
||||||
|
return done('Cannot create folder', false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves the subtitles to disk.
|
* 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,
|
||||||
var enc = player.subtitle;
|
filePath: string, done: (err?: Error | string) => void)
|
||||||
if (!enc) return done();
|
{
|
||||||
subtitle.decode(enc.id, enc.iv, enc.data, (err, data) => {
|
if (page.media !== null)
|
||||||
if (err) return done(err);
|
{
|
||||||
var formats = subtitle.formats;
|
const subs = page.media.subtitles;
|
||||||
var format = formats[config.format] ? config.format : 'ass';
|
if (subs.length === 0)
|
||||||
formats[format](data, (err: Error, decodedSubtitle: string) => {
|
{
|
||||||
if (err) return done(err);
|
/* No downloadable subtitles */
|
||||||
fs.writeFile(filePath + '.' + format, '\ufeff' + decodedSubtitle, done);
|
console.warn('Can\'t find subtitle ?!');
|
||||||
|
return done();
|
||||||
|
}
|
||||||
|
|
||||||
|
let i;
|
||||||
|
let j;
|
||||||
|
|
||||||
|
/* Find a proper subtitles */
|
||||||
|
for (j = 0; j < config.sublang.length; j++)
|
||||||
|
{
|
||||||
|
const reqSubLang = config.sublang[j];
|
||||||
|
for (i = 0; i < subs.length; i++)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
* Streams the video to disk.
|
||||||
*/
|
*/
|
||||||
function downloadVideo(config: IConfig,
|
function downloadVideo(config: IConfig, page: IEpisodePage, player: IEpisodePlayer,
|
||||||
page: IEpisodePage,
|
filePath: string, done: (err: any) => void)
|
||||||
player: IEpisodePlayer,
|
{
|
||||||
filePath: string,
|
if (player == null)
|
||||||
done: (err: Error) => void) {
|
{
|
||||||
video.stream(
|
/* new way */
|
||||||
player.video.host,
|
|
||||||
player.video.file,
|
const streams = page.media.streams;
|
||||||
page.swf,
|
let i;
|
||||||
filePath, path.extname(player.video.file),
|
/* Find a proper subtitles */
|
||||||
player.video.mode,
|
for (i = 0; i < streams.length; i++)
|
||||||
done);
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Names the file based on the config, page, series and tag.
|
* Names the file based on the config, page, series and tag.
|
||||||
*/
|
*/
|
||||||
function name(config: IConfig, page: IEpisodePage, series: string) {
|
function generateName(config: IConfig, page: IEpisodePage, extra = '')
|
||||||
var episode = (page.episode < 10 ? '0' : '') + page.episode;
|
{
|
||||||
var volume = (page.volume < 10 ? '0' : '') + page.volume;
|
const episodeNum = parseInt(page.episode, 10);
|
||||||
var tag = config.tag || 'CrunchyRoll';
|
const volumeNum = parseInt(page.volume, 10);
|
||||||
return series + ' ' + volume + 'x' + episode + ' [' + tag + ']';
|
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;
|
||||||
|
|
||||||
|
return config.nametmpl
|
||||||
|
.replace(/{EPISODE_ID}/g, page.id.toString())
|
||||||
|
.replace(/{EPISODE_NUMBER}/g, episode)
|
||||||
|
.replace(/{SEASON_NUMBER}/g, volume)
|
||||||
|
.replace(/{VOLUME_NUMBER}/g, volume)
|
||||||
|
.replace(/{SEASON_TITLE}/g, page.season)
|
||||||
|
.replace(/{VOLUME_TITLE}/g, page.season)
|
||||||
|
.replace(/{SERIES_TITLE}/g, series)
|
||||||
|
.replace(/{EPISODE_TITLE}/g, page.title)
|
||||||
|
.replace(/{TAG}/g, tag) + extra;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prefixes a value.
|
* Prefixes a value.
|
||||||
*/
|
*/
|
||||||
function prefix(value: number|string, length: number) {
|
function prefix(value: number|string, length: number)
|
||||||
var valueString = typeof value !== 'string' ? String(value) : value;
|
{
|
||||||
while (valueString.length < length) valueString = '0' + valueString;
|
let valueString = (typeof value !== 'string') ? String(value) : value;
|
||||||
|
|
||||||
|
while (valueString.length < length)
|
||||||
|
{
|
||||||
|
valueString = '0' + valueString;
|
||||||
|
}
|
||||||
|
|
||||||
return valueString;
|
return valueString;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Requests the page data and scrapes the id, episode, series and swf.
|
* Requests the page data and scrapes the id, episode, series and swf.
|
||||||
*/
|
*/
|
||||||
function scrapePage(config: IConfig, address: string, done: (err: Error, page?: IEpisodePage) => void) {
|
function scrapePage(config: IConfig, address: string, done: (err: Error, page?: IEpisodePage) => void)
|
||||||
var id = parseInt((address.match(/[0-9]+$/) || ['0'])[0], 10);
|
{
|
||||||
if (!id) return done(new Error('Invalid address.'));
|
const epId = parseInt((address.match(/[0-9]+$/) || ['0'])[0], 10);
|
||||||
request.get(config, address, (err, result) => {
|
|
||||||
if (err) return done(err);
|
if (!epId)
|
||||||
var $ = cheerio.load(result);
|
{
|
||||||
var swf = /^([^?]+)/.exec($('link[rel=video_src]').attr('href'));
|
return done(new Error('Invalid address.'));
|
||||||
var regexp = /-\s+(?:Watch\s+)?(.+?)(?:\s+Season\s+([0-9]+))?(?:\s+-)?\s+Episode\s+([0-9]+)/;
|
}
|
||||||
var data = regexp.exec($('title').text());
|
|
||||||
if (!swf || !data) return done(new Error('Invalid page.'));
|
my_request.get(config, address, (err, result) =>
|
||||||
done(null, {
|
{
|
||||||
id: id,
|
if (err)
|
||||||
episode: parseInt(data[3], 10),
|
{
|
||||||
series: data[1],
|
return done(err);
|
||||||
swf: swf[1],
|
}
|
||||||
volume: parseInt(data[2], 10) || 1
|
|
||||||
});
|
const $ = cheerio.load(result);
|
||||||
|
/* First check if we have the new player */
|
||||||
|
const vlosScript = $('#vilos-iframe-container');
|
||||||
|
|
||||||
|
if (vlosScript)
|
||||||
|
{
|
||||||
|
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 (pageMetadata.partOfSeason)
|
||||||
|
{
|
||||||
|
seasonNumber = pageMetadata.partOfSeason.seasonNumber;
|
||||||
|
if (seasonNumber === '0') { seasonNumber = '1'; }
|
||||||
|
|
||||||
|
seasonTitle = pageMetadata.partOfSeason.name;
|
||||||
|
}
|
||||||
|
done(null, vlos.getMedia(script, seasonTitle, seasonNumber));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Requests the player data and scrapes the subtitle and video data.
|
* Requests the player data and scrapes the subtitle and video data.
|
||||||
*/
|
*/
|
||||||
function scrapePlayer(config: IConfig, address: string, id: number, done: (err: Error, player?: IEpisodePlayer) => void) {
|
function scrapePlayer(config: IConfig, address: string, id: number, done: (err: Error, player?: IEpisodePlayer) => void)
|
||||||
var url = address.match(/^(https?:\/\/[^\/]+)/);
|
{
|
||||||
if (!url) return done(new Error('Invalid address.'));
|
const url = address.match(/^(https?:\/\/[^\/]+)/);
|
||||||
request.post(config, {
|
|
||||||
form: {current_page: address},
|
if (!url)
|
||||||
url: url[1] + '/xml/?req=RpcApiVideoPlayer_GetStandardConfig&media_id=' + id
|
{
|
||||||
}, (err, result) => {
|
return done(new Error('Invalid address.'));
|
||||||
if (err) return done(err);
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
|
||||||
xml2js.parseString(result, {
|
xml2js.parseString(result, {
|
||||||
explicitArray: false,
|
explicitArray: false,
|
||||||
explicitRoot: false
|
explicitRoot: false,
|
||||||
}, (err: Error, player: IEpisodePlayerConfig) => {
|
}, (errPS: Error, player: IEpisodePlayerConfig) =>
|
||||||
if (err) return done(err);
|
{
|
||||||
try {
|
if (errPS)
|
||||||
var isSubtitled = Boolean(player['default:preload'].subtitle);
|
{
|
||||||
var streamMode="RTMP";
|
return done(errPS);
|
||||||
if (player['default:preload'].stream_info.host == "")
|
}
|
||||||
{
|
|
||||||
streamMode="HLS";
|
try
|
||||||
}
|
{
|
||||||
|
const isSubtitled = Boolean(player['default:preload'].subtitle);
|
||||||
|
let streamMode = 'RTMP';
|
||||||
|
|
||||||
|
if (player['default:preload'].stream_info.host === '')
|
||||||
|
{
|
||||||
|
streamMode = 'HLS';
|
||||||
|
}
|
||||||
|
|
||||||
done(null, {
|
done(null, {
|
||||||
subtitle: isSubtitled ? {
|
subtitle: isSubtitled ? {
|
||||||
|
data: player['default:preload'].subtitle.data,
|
||||||
id: parseInt(player['default:preload'].subtitle.$.id, 10),
|
id: parseInt(player['default:preload'].subtitle.$.id, 10),
|
||||||
iv: player['default:preload'].subtitle.iv,
|
iv: player['default:preload'].subtitle.iv,
|
||||||
data: player['default:preload'].subtitle.data
|
|
||||||
} : null,
|
} : null,
|
||||||
video: {
|
video: {
|
||||||
mode: streamMode;
|
|
||||||
file: player['default:preload'].stream_info.file,
|
file: player['default:preload'].stream_info.file,
|
||||||
host: player['default:preload'].stream_info.host
|
host: player['default:preload'].stream_info.host,
|
||||||
}
|
mode: streamMode,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
} catch (parseError) {
|
} catch (parseError)
|
||||||
|
{
|
||||||
|
if (config.debug)
|
||||||
|
{
|
||||||
|
log.dumpToDebug('player scrape', parseError);
|
||||||
|
}
|
||||||
|
|
||||||
done(parseError);
|
done(parseError);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
5
src/interface/AuthError.d.ts
vendored
Normal file
5
src/interface/AuthError.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
interface IAuthError extends Error {
|
||||||
|
name: string;
|
||||||
|
message: string;
|
||||||
|
authError: boolean;
|
||||||
|
}
|
||||||
31
src/interface/IConfig.d.ts
vendored
31
src/interface/IConfig.d.ts
vendored
@ -5,12 +5,37 @@ interface IConfig {
|
|||||||
// Disables
|
// Disables
|
||||||
cache?: boolean;
|
cache?: boolean;
|
||||||
merge?: boolean;
|
merge?: boolean;
|
||||||
// Filters
|
episodes?: string;
|
||||||
episode?: number;
|
|
||||||
volume?: number;
|
|
||||||
// Settings
|
// Settings
|
||||||
|
crlang?: string;
|
||||||
|
sublang?: any;
|
||||||
format?: string;
|
format?: string;
|
||||||
output?: string;
|
output?: string;
|
||||||
series?: string;
|
series?: string;
|
||||||
|
nametmpl?: string;
|
||||||
tag?: 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;
|
||||||
|
sleepTime?: number;
|
||||||
|
// Login options
|
||||||
|
userAgent?: string;
|
||||||
|
logUsingApi?: boolean;
|
||||||
|
logUsingCookie?: boolean;
|
||||||
|
crSessionUrl?: string;
|
||||||
|
crDeviceType?: string;
|
||||||
|
crAPIVersion?: string;
|
||||||
|
crLocale?: string;
|
||||||
|
crSessionKey?: string;
|
||||||
|
crLoginUrl?: string;
|
||||||
|
// Generated values
|
||||||
|
crDeviceId?: string;
|
||||||
|
crSessionId?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
4
src/interface/IConfigTask.d.ts
vendored
4
src/interface/IConfigTask.d.ts
vendored
@ -1,4 +1,6 @@
|
|||||||
interface IConfigTask {
|
interface IConfigTask {
|
||||||
address: string;
|
address: string;
|
||||||
config: IConfigLine;
|
retry: 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
|
||||||
|
}
|
||||||
8
src/interface/IEpisodePage.d.ts
vendored
8
src/interface/IEpisodePage.d.ts
vendored
@ -1,7 +1,11 @@
|
|||||||
interface IEpisodePage {
|
interface IEpisodePage {
|
||||||
id: number;
|
id: number;
|
||||||
episode: number;
|
episode: string;
|
||||||
series: string;
|
series: string;
|
||||||
volume: number;
|
volume: string;
|
||||||
|
season: string;
|
||||||
|
title: string;
|
||||||
swf: string;
|
swf: string;
|
||||||
|
filename: string;
|
||||||
|
media: IVlosScript;
|
||||||
}
|
}
|
||||||
|
|||||||
1
src/interface/IEpisodePlayer.d.ts
vendored
1
src/interface/IEpisodePlayer.d.ts
vendored
@ -5,6 +5,7 @@ interface IEpisodePlayer {
|
|||||||
data: string;
|
data: string;
|
||||||
};
|
};
|
||||||
video: {
|
video: {
|
||||||
|
mode: string;
|
||||||
file: string;
|
file: string;
|
||||||
host: string;
|
host: string;
|
||||||
};
|
};
|
||||||
|
|||||||
2
src/interface/IFormatterTable.d.ts
vendored
2
src/interface/IFormatterTable.d.ts
vendored
@ -1,3 +1,3 @@
|
|||||||
interface IFormatterTable {
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
4
src/interface/IResolData.d.ts
vendored
Normal file
4
src/interface/IResolData.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
interface IResolData {
|
||||||
|
quality: string;
|
||||||
|
format: string;
|
||||||
|
}
|
||||||
4
src/interface/ISeriesEpisode.d.ts
vendored
4
src/interface/ISeriesEpisode.d.ts
vendored
@ -1,5 +1,7 @@
|
|||||||
interface ISeriesEpisode {
|
interface ISeriesEpisode {
|
||||||
address: string;
|
address: string;
|
||||||
episode: number;
|
episode: string;
|
||||||
|
seasonName: string;
|
||||||
volume: number;
|
volume: number;
|
||||||
|
retry: number;
|
||||||
}
|
}
|
||||||
|
|||||||
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
66
src/languages.ts
Normal file
66
src/languages.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const localeCC: { [id: string]: string; } =
|
||||||
|
{
|
||||||
|
enUS: 'en', enGB: 'en',
|
||||||
|
esLA: 'es', esES: 'es',
|
||||||
|
ptPT: 'pt', ptBR: 'pt',
|
||||||
|
frFR: 'fr',
|
||||||
|
deDE: 'de',
|
||||||
|
itIT: 'it',
|
||||||
|
ruRU: 'ru',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function localeToCC(locale: string): string
|
||||||
|
{
|
||||||
|
let ret = localeCC.enGB;
|
||||||
|
|
||||||
|
if (locale in localeCC)
|
||||||
|
{
|
||||||
|
ret = localeCC[locale];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dubignore_regexp: { [id: string]: RegExp; } =
|
||||||
|
{
|
||||||
|
en: /\(.*Dub(?:bed)?.*\)|(?:\(RU\))|\(Russian\)/i,
|
||||||
|
fr: /\(.*Dub(?:bed)?.*\)|(?:\(RU\))|\(?Doublage.*\)|\(Russian\)?/,
|
||||||
|
de: /\(.*isch\)|\(Dubbed\)|\(RU\)|\(Russian\)/
|
||||||
|
};
|
||||||
|
|
||||||
|
export function get_diregexp(config: IConfig): RegExp
|
||||||
|
{
|
||||||
|
let ret = dubignore_regexp.en;
|
||||||
|
|
||||||
|
if (config.crlang in dubignore_regexp)
|
||||||
|
{
|
||||||
|
ret = dubignore_regexp[config.crlang];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
const episodes_regexp: { [id: string]: RegExp; } =
|
||||||
|
{
|
||||||
|
en: /Episode\s+((OVA)|(PV )?[S0-9][\-P0-9.]*[a-fA-F]?)\s*$/i,
|
||||||
|
fr: /Épisode\s+((OVA)|(PV )?[S0-9][\-P0-9.]*[a-fA-F]?)\s*$/i,
|
||||||
|
de: /Folge\s+((OVA)|(PV )?[S0-9][\-P0-9.]*[a-fA-F]?)\s*$/i,
|
||||||
|
es: /Episodio\s+((OVA)|(PV )?[S0-9][\-P0-9.]*[a-fA-F]?)\s*$/i,
|
||||||
|
it: /Episodio\s+((OVA)|(PV )?[S0-9][\-P0-9.]*[a-fA-F]?)\s*$/i,
|
||||||
|
pt: /Episódio\s+((OVA)|(PV )?[S0-9][\-P0-9.]*[a-fA-F]?)\s*$/i,
|
||||||
|
ru: /Серия\s+((OVA)|(PV )?[S0-9][\-P0-9.]*[a-fA-F]?)\s*$/i,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function get_epregexp(config: IConfig): RegExp
|
||||||
|
{
|
||||||
|
let ret = episodes_regexp.en;
|
||||||
|
|
||||||
|
if (config.crlang in episodes_regexp)
|
||||||
|
{
|
||||||
|
ret = episodes_regexp[config.crlang];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
55
src/log.ts
Normal file
55
src/log.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
'use strict';
|
||||||
|
import os = require('os');
|
||||||
|
import fs = require('fs-extra');
|
||||||
|
|
||||||
|
export function error(str: string|Error)
|
||||||
|
{
|
||||||
|
/* Do fancy output */
|
||||||
|
console.error(' \x1B[1;31m* ERROR\x1B[0m: ' + str);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function info(str: string)
|
||||||
|
{
|
||||||
|
/* Do fancy output */
|
||||||
|
console.log(' \x1B[1;32m* INFO \x1B[0m: ' + str);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function debug(str: string)
|
||||||
|
{
|
||||||
|
/* Do fancy output */
|
||||||
|
console.log(' \x1B[1;35m* DEBUG\x1B[0m: ' + str);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function warn(str: string)
|
||||||
|
{
|
||||||
|
/* Do fancy output */
|
||||||
|
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 */
|
||||||
|
process.stdout.write('\x1B[K \x1B[1;33m> \x1B[37m' + name + '\x1B[0m : \x1B[33m' + status + '\x1B[0m\x1B[0G');
|
||||||
|
|
||||||
|
if (addNL)
|
||||||
|
{
|
||||||
|
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');
|
||||||
|
}
|
||||||
424
src/my_request.ts
Normal file
424
src/my_request.ts
Normal file
@ -0,0 +1,424 @@
|
|||||||
|
'use strict';
|
||||||
|
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 languages = require('./languages');
|
||||||
|
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');
|
||||||
|
|
||||||
|
const CR_COOKIE_DOMAIN = 'http://crunchyroll.com';
|
||||||
|
|
||||||
|
let isAuthenticated = false;
|
||||||
|
let isPremium = false;
|
||||||
|
|
||||||
|
let j: request.CookieJar;
|
||||||
|
|
||||||
|
// tslint:disable-next-line:no-var-requires
|
||||||
|
import cloudscraper = require('cloudscraper');
|
||||||
|
let currentOptions: any;
|
||||||
|
let optionsSet = false;
|
||||||
|
|
||||||
|
function AuthError(msg: string): IAuthError
|
||||||
|
{
|
||||||
|
return { name: 'AuthError', message: msg, authError: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
function startSession(config: IConfig): Promise<any>
|
||||||
|
{
|
||||||
|
return rp(
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
url: config.crSessionUrl,
|
||||||
|
qs:
|
||||||
|
{
|
||||||
|
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 APIlogin(config: IConfig, sessionId: string, user: string, pass: string): Promise<any>
|
||||||
|
{
|
||||||
|
return rp(
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
url: config.crLoginUrl,
|
||||||
|
form:
|
||||||
|
{
|
||||||
|
account: user,
|
||||||
|
password: pass,
|
||||||
|
session_id: sessionId,
|
||||||
|
version: config.crAPIVersion,
|
||||||
|
},
|
||||||
|
json: true,
|
||||||
|
jar: j,
|
||||||
|
})
|
||||||
|
.then((response) =>
|
||||||
|
{
|
||||||
|
if (response.error) throw new Error('Login failed: ' + response.message);
|
||||||
|
return response.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkIfUserIsAuth(config: IConfig, done: (err: any) => void): void
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The main page give us some information about the user
|
||||||
|
*/
|
||||||
|
const url = 'http://www.crunchyroll.com/';
|
||||||
|
|
||||||
|
cloudscraper.get(url, getOptions(config, null), (err: any, rep: Response, body: string) =>
|
||||||
|
{
|
||||||
|
if (err)
|
||||||
|
{
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const $ = cheerio.load(body);
|
||||||
|
|
||||||
|
/* As we are here, try to detect which locale CR tell us */
|
||||||
|
const localeRE = /LOCALE = "([a-zA-Z]+)",/g;
|
||||||
|
const locale = localeRE.exec($('script').text())[1];
|
||||||
|
const countryCode = languages.localeToCC(locale);
|
||||||
|
|
||||||
|
if (config.crlang === undefined)
|
||||||
|
{
|
||||||
|
log.info('No locale set. Setting to the one reported by CR: "' + countryCode + '"');
|
||||||
|
config.crlang = countryCode;
|
||||||
|
}
|
||||||
|
else if (config.crlang !== countryCode)
|
||||||
|
{
|
||||||
|
log.warn('Crunchy is configured for locale "' + config.crlang + '" but CR report "' + countryCode + '" (LOCALE = ' + locale + ')');
|
||||||
|
log.warn('Check if it is correct or rerun (once) with "-l ' + countryCode + '" to correct.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check if auth worked */
|
||||||
|
const regexps = /ga\('set', 'dimension[5-8]', '([^']*)'\);/g;
|
||||||
|
const dims = regexps.exec($('script').text());
|
||||||
|
|
||||||
|
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();
|
||||||
|
log.warn('Authentication failed: ' + error);
|
||||||
|
|
||||||
|
log.dumpToDebug('not auth rep', rep);
|
||||||
|
log.dumpToDebug('not auth body', body);
|
||||||
|
|
||||||
|
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 currentOptions.headers['User-Agent'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a GET request for the resource.
|
||||||
|
*/
|
||||||
|
export function get(config: IConfig, url: string, done: (err: any, result?: string) => void)
|
||||||
|
{
|
||||||
|
authenticate(config, (err) =>
|
||||||
|
{
|
||||||
|
if (err)
|
||||||
|
{
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
cloudscraper.get(url, getOptions(config, null), (error: any, response: any, body: any) =>
|
||||||
|
{
|
||||||
|
if (error) return done(error);
|
||||||
|
|
||||||
|
done(null, typeof body === 'string' ? body : String(body));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a POST request for the resource.
|
||||||
|
*/
|
||||||
|
export function post(config: IConfig, url: string, form: any, done: (err: any, result?: string) => void)
|
||||||
|
{
|
||||||
|
authenticate(config, (err) =>
|
||||||
|
{
|
||||||
|
if (err)
|
||||||
|
{
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
cloudscraper.post(url, getOptions(config, form), (error: Error, response: any, body: any) =>
|
||||||
|
{
|
||||||
|
if (error)
|
||||||
|
{
|
||||||
|
return done(error);
|
||||||
|
}
|
||||||
|
done(null, typeof body === 'string' ? body : String(body));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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: any) => void)
|
||||||
|
{
|
||||||
|
if (isAuthenticated)
|
||||||
|
{
|
||||||
|
return done(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* First of all, check if the user is not already logged via the cookies */
|
||||||
|
checkIfUserIsAuth(config, (errCheckAuth) =>
|
||||||
|
{
|
||||||
|
if (isAuthenticated)
|
||||||
|
{
|
||||||
|
return done(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info('Seems we are not currently logged. Let\'s login!');
|
||||||
|
|
||||||
|
if (config.logUsingApi)
|
||||||
|
{
|
||||||
|
return authUsingApi(config, done);
|
||||||
|
}
|
||||||
|
else if (config.logUsingCookie)
|
||||||
|
{
|
||||||
|
return authUsingCookies(config, done);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return authUsingForm(config, done);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOptions(config: IConfig, form: any)
|
||||||
|
{
|
||||||
|
if (!optionsSet)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentOptions.form = {};
|
||||||
|
|
||||||
|
if (form !== null)
|
||||||
|
{
|
||||||
|
currentOptions.form = form;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return currentOptions;
|
||||||
|
}
|
||||||
@ -1,62 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
import request = require('request');
|
|
||||||
var isAuthenticated = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs a GET request for the resource.
|
|
||||||
*/
|
|
||||||
export function get(config: IConfig, options: request.Options, done: (err: Error, result?: string) => void) {
|
|
||||||
authenticate(config, err => {
|
|
||||||
if (err) return done(err);
|
|
||||||
request.get(modify(options), (err: Error, response: any, body: any) => {
|
|
||||||
if (err) return done(err);
|
|
||||||
done(null, typeof body === 'string' ? body : String(body));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs a POST request for the resource.
|
|
||||||
*/
|
|
||||||
export function post(config: IConfig, options: request.Options, done: (err: Error, result?: string) => void) {
|
|
||||||
authenticate(config, err => {
|
|
||||||
if (err) return done(err);
|
|
||||||
request.post(modify(options), (err: Error, response: any, body: any) => {
|
|
||||||
if (err) return done(err);
|
|
||||||
done(null, typeof body === 'string' ? body : String(body));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Authenticates using the configured pass and user.
|
|
||||||
*/
|
|
||||||
function authenticate(config: IConfig, done: (err: Error) => void) {
|
|
||||||
if (isAuthenticated || !config.pass || !config.user) return done(null);
|
|
||||||
var options = {
|
|
||||||
form: {
|
|
||||||
formname: 'RpcApiUser_Login',
|
|
||||||
fail_url: 'https://www.crunchyroll.com/login',
|
|
||||||
name: config.user,
|
|
||||||
password: config.pass
|
|
||||||
},
|
|
||||||
jar: true,
|
|
||||||
url: 'https://www.crunchyroll.com/?a=formhandler'
|
|
||||||
};
|
|
||||||
request.post(options, (err: Error) => {
|
|
||||||
if (err) return done(err);
|
|
||||||
isAuthenticated = true;
|
|
||||||
done(null);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Modifies the options to use the authenticated cookie jar.
|
|
||||||
*/
|
|
||||||
function modify(options: string|request.Options): request.Options {
|
|
||||||
if (typeof options !== 'string') {
|
|
||||||
options.jar = true;
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
return {jar: true, url: options.toString()};
|
|
||||||
}
|
|
||||||
313
src/series.ts
313
src/series.ts
@ -1,32 +1,138 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
import cheerio = require('cheerio');
|
import cheerio = require('cheerio');
|
||||||
import episode from './episode';
|
import episode from './episode';
|
||||||
import fs = require('fs');
|
import fs = require('fs-extra');
|
||||||
import request = require('./request');
|
import my_request = require('./my_request');
|
||||||
import path = require('path');
|
import path = require('path');
|
||||||
import url = require('url');
|
import url = require('url');
|
||||||
var persistent = '.crpersistent';
|
import log = require('./log');
|
||||||
|
import languages = require('./languages');
|
||||||
|
|
||||||
|
const persistent = '.crpersistent';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a file exist..
|
||||||
|
*/
|
||||||
|
function fileExist(path: string)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fs.statSync(path);
|
||||||
|
return true;
|
||||||
|
} catch (e)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Streams the series to disk.
|
* 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)
|
||||||
var persistentPath = path.join(config.output || process.cwd(), persistent);
|
{
|
||||||
fs.readFile(persistentPath, 'utf8', (err, contents) => {
|
const persistentPath = path.join(config.output || process.cwd(), persistent);
|
||||||
var cache = config.cache ? {} : JSON.parse(contents || '{}');
|
|
||||||
page(config, address, (err, page) => {
|
/* Make a backup of the persistent file in case of */
|
||||||
if (err) return done(err);
|
if (fileExist(persistentPath))
|
||||||
var i = 0;
|
{
|
||||||
(function next() {
|
fs.copySync(persistentPath, persistentPath + '.backup');
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.readFile(persistentPath, 'utf8', (err: Error, contents: string) =>
|
||||||
|
{
|
||||||
|
const cache = config.cache ? {} : JSON.parse(contents || '{}');
|
||||||
|
|
||||||
|
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);
|
if (i >= page.episodes.length) return done(null);
|
||||||
download(cache, config, address, page.episodes[i], err => {
|
download(cache, config, task, page.episodes[i], (errD, ignored) =>
|
||||||
if (err) return done(err);
|
{
|
||||||
var newCache = JSON.stringify(cache, null, ' ');
|
if (errD)
|
||||||
fs.writeFile(persistentPath, newCache, err => {
|
{
|
||||||
if (err) return done(err);
|
/* Check if domain is valid */
|
||||||
i += 1;
|
const reqErr = errD.error;
|
||||||
next();
|
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.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 */
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if ((config.verbose) || (config.debug))
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
page.episodes[i].retry -= 1;
|
||||||
|
}
|
||||||
|
setTimeout(next, config.sleepTime);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if ((ignored === false) || (ignored === undefined))
|
||||||
|
{
|
||||||
|
const newCache = JSON.stringify(cache, null, ' ');
|
||||||
|
fs.writeFile(persistentPath, newCache, (errW: Error) =>
|
||||||
|
{
|
||||||
|
if (errW)
|
||||||
|
{
|
||||||
|
return done(errW);
|
||||||
|
}
|
||||||
|
|
||||||
|
i += 1;
|
||||||
|
setTimeout(next, config.sleepTime);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
i += 1;
|
||||||
|
setTimeout(next, config.sleepTime);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
});
|
});
|
||||||
@ -36,60 +142,133 @@ export default function(config: IConfig, address: string, done: (err: Error) =>
|
|||||||
/**
|
/**
|
||||||
* Downloads the episode.
|
* Downloads the episode.
|
||||||
*/
|
*/
|
||||||
function download(cache: {[address: string]: number},
|
function download(cache: {[address: string]: number}, config: IConfig,
|
||||||
config: IConfig,
|
task: IConfigTask, item: ISeriesEpisode,
|
||||||
baseAddress: string,
|
done: (err: any, ign: boolean) => void)
|
||||||
item: ISeriesEpisode,
|
{
|
||||||
done: (err: Error) => void) {
|
const episodeNumber = parseInt(item.episode, 10);
|
||||||
if (!filter(config, item)) return done(null);
|
const seasonNumber = item.volume;
|
||||||
var address = url.resolve(baseAddress, item.address);
|
|
||||||
if (cache[address]) return done(null);
|
if ( (episodeNumber < task.episode_min.episode) ||
|
||||||
episode(config, address, err => {
|
(episodeNumber > task.episode_max.episode) )
|
||||||
if (err) return done(err);
|
{
|
||||||
|
return done(null, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const address = url.resolve(task.address, item.address);
|
||||||
|
|
||||||
|
if (cache[address])
|
||||||
|
{
|
||||||
|
return done(null, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
episode(config, address, (err, ignored) =>
|
||||||
|
{
|
||||||
|
if (err)
|
||||||
|
{
|
||||||
|
return done(err, false);
|
||||||
|
}
|
||||||
|
|
||||||
cache[address] = Date.now();
|
cache[address] = Date.now();
|
||||||
done(null);
|
done(null, ignored);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Filters the item based on the configuration.
|
|
||||||
*/
|
|
||||||
function filter(config: IConfig, item: ISeriesEpisode) {
|
|
||||||
// Filter on chapter.
|
|
||||||
var episodeFilter = config.episode;
|
|
||||||
if (episodeFilter > 0 && item.episode <= episodeFilter) return false;
|
|
||||||
if (episodeFilter < 0 && item.episode >= -episodeFilter) return false;
|
|
||||||
|
|
||||||
// Filter on volume.
|
|
||||||
var volumeFilter = config.volume;
|
|
||||||
if (volumeFilter > 0 && item.volume <= volumeFilter) return false;
|
|
||||||
if (volumeFilter < 0 && item.volume >= -volumeFilter) return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Requests the page and scrapes the episodes and series.
|
* 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)
|
||||||
request.get(config, address, (err, result) => {
|
{
|
||||||
if (err) return done(err);
|
if (task.address[0] === '@')
|
||||||
var $ = cheerio.load(result);
|
{
|
||||||
var title = $('span[itemprop=name]').text();
|
log.info('Trying to fetch from ' + task.address.substr(1));
|
||||||
if (!title) return done(new Error('Invalid page.'));
|
const episodes: ISeriesEpisode[] = [];
|
||||||
var episodes: ISeriesEpisode[] = [];
|
episodes.push({
|
||||||
$('.episode').each((i, el) => {
|
address: task.address.substr(1),
|
||||||
if ($(el).children('img[src*=coming_soon]').length) return;
|
episode: '',
|
||||||
var volume = /([0-9]+)\s*$/.exec($(el).closest('ul').prev('a').text());
|
seasonName: '',
|
||||||
var regexp = /Episode\s+([0-9]+)\s*$/i;
|
volume: 0,
|
||||||
var episode = regexp.exec($(el).children('.series-title').text());
|
retry: config.retry,
|
||||||
var address = $(el).attr('href');
|
|
||||||
if (!address || !episode) return;
|
|
||||||
episodes.push({
|
|
||||||
address: address,
|
|
||||||
episode: parseInt(episode[0], 10),
|
|
||||||
volume: volume ? parseInt(volume[0], 10) : 1
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
done(null, {episodes: episodes.reverse(), series: title});
|
done(null, {episodes: episodes.reverse(), series: ''});
|
||||||
});
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
let episodeCount = 0;
|
||||||
|
my_request.get(config, task.address, (err, result) => {
|
||||||
|
if (err)
|
||||||
|
{
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const $ = cheerio.load(result);
|
||||||
|
const title = $('meta[itemprop=name]').attr('content');
|
||||||
|
|
||||||
|
if (config.debug)
|
||||||
|
{
|
||||||
|
log.dumpToDebug('serie page', $.html());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!title) {
|
||||||
|
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 = languages.get_epregexp(config);
|
||||||
|
const episode = regexp.exec($(el).children('.series-title').text());
|
||||||
|
const url = $(el).attr('href');
|
||||||
|
|
||||||
|
const igndub_re = languages.get_diregexp(config);
|
||||||
|
|
||||||
|
if (config.ignoredub && (igndub_re.exec(season_name)))
|
||||||
|
{
|
||||||
|
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?');
|
||||||
|
}
|
||||||
|
done(null, {episodes: episodes.reverse(), series: title});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,20 @@
|
|||||||
/* tslint:disable:no-bitwise false */
|
/* tslint:disable:no-bitwise false */
|
||||||
'use strict';
|
'use strict';
|
||||||
import crypto = require('crypto');
|
|
||||||
import bigInt = require('big-integer');
|
import bigInt = require('big-integer');
|
||||||
|
import crypto = require('crypto');
|
||||||
import zlib = require('zlib');
|
import zlib = require('zlib');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decodes the data.
|
* Decodes the data.
|
||||||
*/
|
*/
|
||||||
export default function(id: number, iv: Buffer|string, data: Buffer|string, done: (err?: Error, result?: Buffer) => void) {
|
export default function(id: number, iv: Buffer|string, data: Buffer|string,
|
||||||
try {
|
done: (err?: Error, result?: Buffer) => void)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
decompress(decrypt(id, iv, data), done);
|
decompress(decrypt(id, iv, data), done);
|
||||||
} catch (e) {
|
} catch (e)
|
||||||
|
{
|
||||||
done(e);
|
done(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -18,21 +22,27 @@ import zlib = require('zlib');
|
|||||||
/**
|
/**
|
||||||
* Decrypts the data.
|
* Decrypts the data.
|
||||||
*/
|
*/
|
||||||
function decrypt(id: number, iv: Buffer|string, data: Buffer|string) {
|
function decrypt(id: number, iv: Buffer|string, data: Buffer|string)
|
||||||
var ivBuffer = typeof iv === 'string' ? new Buffer(iv, 'base64') : iv;
|
{
|
||||||
var dataBuffer = typeof data === 'string' ? new Buffer(data, 'base64') : data;
|
const ivBuffer = typeof iv === 'string' ? Buffer.from(iv, 'base64') : iv;
|
||||||
var decipher = crypto.createDecipheriv('aes-256-cbc', key(id), ivBuffer);
|
const dataBuffer = typeof data === 'string' ? Buffer.from(data, 'base64') : data;
|
||||||
|
const decipher = crypto.createDecipheriv('aes-256-cbc', key(id), ivBuffer);
|
||||||
|
|
||||||
decipher.setAutoPadding(false);
|
decipher.setAutoPadding(false);
|
||||||
|
|
||||||
return Buffer.concat([decipher.update(dataBuffer), decipher.final()]);
|
return Buffer.concat([decipher.update(dataBuffer), decipher.final()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decompresses the data.
|
* Decompresses the data.
|
||||||
*/
|
*/
|
||||||
function decompress(data: Buffer, done: (err: Error, result?: Buffer) => void) {
|
function decompress(data: Buffer, done: (err: Error, result?: Buffer) => void)
|
||||||
try {
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
zlib.inflate(data, done);
|
zlib.inflate(data, done);
|
||||||
} catch (e) {
|
} catch (e)
|
||||||
|
{
|
||||||
done(null, data);
|
done(null, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -40,36 +50,45 @@ function decompress(data: Buffer, done: (err: Error, result?: Buffer) => void) {
|
|||||||
/**
|
/**
|
||||||
* Generates a key.
|
* Generates a key.
|
||||||
*/
|
*/
|
||||||
function key(subtitleId: number): Buffer {
|
function key(subtitleId: number): Buffer
|
||||||
var hash = secret(20, 97, 1, 2) + magic(subtitleId);
|
{
|
||||||
var result = new Buffer(32);
|
const hash = secret(20, 97, 1, 2) + magic(subtitleId);
|
||||||
|
const result = Buffer.allocUnsafe(32);
|
||||||
|
|
||||||
result.fill(0);
|
result.fill(0);
|
||||||
crypto.createHash('sha1').update(hash).digest().copy(result);
|
crypto.createHash('sha1').update(hash).digest().copy(result);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a magic number.
|
* Generates a magic number.
|
||||||
*/
|
*/
|
||||||
function magic(subtitleId: number): number {
|
function magic(subtitleId: number): number
|
||||||
var base = Math.floor(Math.sqrt(6.9) * Math.pow(2, 25));
|
{
|
||||||
var hash = bigInt(base).xor(subtitleId).toJSNumber();
|
const base = Math.floor(Math.sqrt(6.9) * Math.pow(2, 25));
|
||||||
var multipliedHash = bigInt(hash).multiply(32).toJSNumber();
|
const hash = bigInt(base).xor(subtitleId).toJSNumber();
|
||||||
|
const multipliedHash = bigInt(hash).multiply(32).toJSNumber();
|
||||||
|
|
||||||
return bigInt(hash).xor(hash >> 3).xor(multipliedHash).toJSNumber();
|
return bigInt(hash).xor(hash >> 3).xor(multipliedHash).toJSNumber();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a secret string based on a Fibonacci sequence.
|
* Generates a secret string based on a Fibonacci sequence.
|
||||||
*/
|
*/
|
||||||
function secret(size: number, modulo: number, firstSeed: number, secondSeed: number): string {
|
function secret(size: number, modulo: number, firstSeed: number, secondSeed: number): string
|
||||||
var currentValue = firstSeed + secondSeed;
|
{
|
||||||
var previousValue = secondSeed;
|
let currentValue = firstSeed + secondSeed;
|
||||||
var result = '';
|
let previousValue = secondSeed;
|
||||||
for (var i = 0; i < size; i += 1) {
|
let result = '';
|
||||||
var oldValue = currentValue;
|
|
||||||
|
for (let i = 0; i < size; i += 1)
|
||||||
|
{
|
||||||
|
const oldValue = currentValue;
|
||||||
result += String.fromCharCode(currentValue % modulo + 33);
|
result += String.fromCharCode(currentValue % modulo + 33);
|
||||||
currentValue += previousValue;
|
currentValue += previousValue;
|
||||||
previousValue = oldValue;
|
previousValue = oldValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,17 +4,25 @@ import xml2js = require('xml2js');
|
|||||||
/**
|
/**
|
||||||
* Converts an input buffer to a SubStation Alpha subtitle.
|
* 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(), {
|
xml2js.parseString(input.toString(), {
|
||||||
explicitArray: false,
|
explicitArray: false,
|
||||||
explicitRoot: false
|
explicitRoot: false
|
||||||
}, (err: Error, xml: ISubtitle) => {
|
}, (err: Error, xml: ISubtitle) =>
|
||||||
if (err) return done(err);
|
{
|
||||||
try {
|
if (err)
|
||||||
done(null, script(xml) + '\n' +
|
{
|
||||||
style(xml.styles) + '\n' +
|
return done(err);
|
||||||
event(xml.events));
|
}
|
||||||
} catch (err) {
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
done(null, script(config, xml) + '\n' +
|
||||||
|
style(xml.styles) + '\n' +
|
||||||
|
event(config, xml.events));
|
||||||
|
} catch (err)
|
||||||
|
{
|
||||||
done(err);
|
done(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -23,69 +31,74 @@ export default function(input: string|Buffer, done: (err: Error, subtitle?: stri
|
|||||||
/**
|
/**
|
||||||
* Converts the event block.
|
* Converts the event block.
|
||||||
*/
|
*/
|
||||||
function event(block: ISubtitleEvent): string {
|
function event(config: IConfig, block: ISubtitleEvent): string
|
||||||
var format = 'Layer,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text';
|
{
|
||||||
|
const format = 'Layer,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text';
|
||||||
|
|
||||||
return '[Events]\n' +
|
return '[Events]\n' +
|
||||||
'Format: ' + format + '\n' +
|
'Format: ' + format + '\n' + [].concat(block.event).map((style) => ('Dialogue: 0,' +
|
||||||
[].concat(block.event).map(style => ('Dialogue: 0,' +
|
style.$.start + ',' +
|
||||||
style.$.start + ',' +
|
style.$.end + ',' +
|
||||||
style.$.end + ',' +
|
style.$.style + ',' +
|
||||||
style.$.style + ',' +
|
style.$.name + ',' +
|
||||||
style.$.name + ',' +
|
style.$.margin_l + ',' +
|
||||||
style.$.margin_l + ',' +
|
style.$.margin_r + ',' +
|
||||||
style.$.margin_r + ',' +
|
style.$.margin_v + ',' +
|
||||||
style.$.margin_v + ',' +
|
style.$.effect + ',' +
|
||||||
style.$.effect + ',' +
|
style.$.text)).join('\n') + '\n';
|
||||||
style.$.text)).join('\n') + '\n';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts the script block.
|
* Converts the script block.
|
||||||
*/
|
*/
|
||||||
function script(block: ISubtitle): string {
|
function script(config: IConfig, block: ISubtitle): string
|
||||||
|
{
|
||||||
|
|
||||||
return '[Script Info]\n' +
|
return '[Script Info]\n' +
|
||||||
'Title: ' + block.$.title + '\n' +
|
'Origin: Downloaded from Crunchyroll.com by ' + config.user + '\n' +
|
||||||
'ScriptType: v4.00+\n' +
|
'Title: ' + block.$.title + '\n' +
|
||||||
'WrapStyle: ' + block.$.wrap_style + '\n' +
|
'ScriptType: v4.00+\n' +
|
||||||
'PlayResX: ' + block.$.play_res_x + '\n' +
|
'WrapStyle: ' + block.$.wrap_style + '\n' +
|
||||||
'PlayResY: ' + block.$.play_res_y + '\n' +
|
'PlayResX: ' + block.$.play_res_x + '\n' +
|
||||||
'Subtitle ID: ' + block.$.id + '\n' +
|
'PlayResY: ' + block.$.play_res_y + '\n' +
|
||||||
'Language: ' + block.$.lang_string + '\n' +
|
'Subtitle ID: ' + block.$.id + '\n' +
|
||||||
'Created: ' + block.$.created + '\n';
|
'Language: ' + block.$.lang_string + '\n' +
|
||||||
|
'Created: ' + block.$.created + '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts the style block.
|
* Converts the style block.
|
||||||
*/
|
*/
|
||||||
function style(block: ISubtitleStyle): string {
|
function style(block: ISubtitleStyle): string
|
||||||
var format = 'Name,Fontname,Fontsize,PrimaryColour,SecondaryColour,' +
|
{
|
||||||
'OutlineColour,BackColour,Bold,Italic,Underline,StrikeOut,ScaleX,' +
|
const format = 'Name,Fontname,Fontsize,PrimaryColour,SecondaryColour,' +
|
||||||
'ScaleY,Spacing,Angle,BorderStyle,Outline,Shadow,Alignment,' +
|
'OutlineColour,BackColour,Bold,Italic,Underline,StrikeOut,ScaleX,' +
|
||||||
'MarginL,MarginR,MarginV,Encoding';
|
'ScaleY,Spacing,Angle,BorderStyle,Outline,Shadow,Alignment,' +
|
||||||
|
'MarginL,MarginR,MarginV,Encoding';
|
||||||
|
|
||||||
return '[V4+ Styles]\n' +
|
return '[V4+ Styles]\n' +
|
||||||
'Format: ' + format + '\n' +
|
'Format: ' + format + '\n' + [].concat(block.style).map((style) => 'Style: ' +
|
||||||
[].concat(block.style).map(style => 'Style: ' +
|
style.$.name + ',' +
|
||||||
style.$.name + ',' +
|
style.$.font_name + ',' +
|
||||||
style.$.font_name + ',' +
|
style.$.font_size + ',' +
|
||||||
style.$.font_size + ',' +
|
style.$.primary_colour + ',' +
|
||||||
style.$.primary_colour + ',' +
|
style.$.secondary_colour + ',' +
|
||||||
style.$.secondary_colour + ',' +
|
style.$.outline_colour + ',' +
|
||||||
style.$.outline_colour + ',' +
|
style.$.back_colour + ',' +
|
||||||
style.$.back_colour + ',' +
|
style.$.bold + ',' +
|
||||||
style.$.bold + ',' +
|
style.$.italic + ',' +
|
||||||
style.$.italic + ',' +
|
style.$.underline + ',' +
|
||||||
style.$.underline + ',' +
|
style.$.strikeout + ',' +
|
||||||
style.$.strikeout + ',' +
|
style.$.scale_x + ',' +
|
||||||
style.$.scale_x + ',' +
|
style.$.scale_y + ',' +
|
||||||
style.$.scale_y + ',' +
|
style.$.spacing + ',' +
|
||||||
style.$.spacing + ',' +
|
style.$.angle + ',' +
|
||||||
style.$.angle + ',' +
|
style.$.border_style + ',' +
|
||||||
style.$.border_style + ',' +
|
style.$.outline + ',' +
|
||||||
style.$.outline + ',' +
|
style.$.shadow + ',' +
|
||||||
style.$.shadow + ',' +
|
style.$.alignment + ',' +
|
||||||
style.$.alignment + ',' +
|
style.$.margin_l + ',' +
|
||||||
style.$.margin_l + ',' +
|
style.$.margin_r + ',' +
|
||||||
style.$.margin_r + ',' +
|
style.$.margin_v + ',' +
|
||||||
style.$.margin_v + ',' +
|
style.$.encoding).join('\n') + '\n';
|
||||||
style.$.encoding).join('\n') + '\n';
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
import ass from './ass';
|
import ass from './ass';
|
||||||
import srt from './srt';
|
import srt from './srt';
|
||||||
|
|
||||||
export default <IFormatterTable> {
|
export default {
|
||||||
ass: ass,
|
ass,
|
||||||
srt: srt
|
srt
|
||||||
};
|
} as IFormatterTable;
|
||||||
|
|||||||
@ -4,18 +4,30 @@ import xml2js = require('xml2js');
|
|||||||
/**
|
/**
|
||||||
* Converts an input buffer to a SubRip subtitle.
|
* 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)
|
||||||
var options = {explicitArray: false, explicitRoot: false};
|
{
|
||||||
xml2js.parseString(input.toString(), options, (err: Error, xml: ISubtitle) => {
|
const options = {explicitArray: false, explicitRoot: false};
|
||||||
try {
|
|
||||||
if (err) return done(err);
|
xml2js.parseString(input.toString(), options, (err: Error, xml: ISubtitle) =>
|
||||||
done(null, xml.events.event.map((event, index) => {
|
{
|
||||||
var attributes = event.$;
|
try
|
||||||
|
{
|
||||||
|
if (err)
|
||||||
|
{
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
done(null, xml.events.event.map((event, index) =>
|
||||||
|
{
|
||||||
|
const attributes = event.$;
|
||||||
|
|
||||||
return (index + 1) + '\n' +
|
return (index + 1) + '\n' +
|
||||||
time(attributes.start) + ' --> ' + time(attributes.end) + '\n' +
|
time(attributes.start) + ' --> ' + time(attributes.end) + '\n' +
|
||||||
text(attributes.text) + '\n';
|
text(attributes.text) + '\n';
|
||||||
}).join('\n'));
|
}).join('\n'));
|
||||||
} catch (err) {
|
|
||||||
|
} catch (err)
|
||||||
|
{
|
||||||
done(err);
|
done(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -24,41 +36,59 @@ import xml2js = require('xml2js');
|
|||||||
/**
|
/**
|
||||||
* Prefixes a value.
|
* Prefixes a value.
|
||||||
*/
|
*/
|
||||||
function prefix(value: string, length: number): string {
|
function prefix(value: string, length: number): string
|
||||||
while (value.length < length) value = '0' + value;
|
{
|
||||||
|
while (value.length < length)
|
||||||
|
{
|
||||||
|
value = '0' + value;
|
||||||
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Suffixes a value.
|
* Suffixes a value.
|
||||||
*/
|
*/
|
||||||
function suffix(value: string, length: number): string {
|
function suffix(value: string, length: number): string
|
||||||
while (value.length < length) value = value + '0';
|
{
|
||||||
|
while (value.length < length)
|
||||||
|
{
|
||||||
|
value = value + '0';
|
||||||
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats a text value.
|
* Formats a text value.
|
||||||
*/
|
*/
|
||||||
function text(value: string): string {
|
function text(value: string): string
|
||||||
|
{
|
||||||
return value
|
return value
|
||||||
.replace(/{\\i1}/g, '<i>').replace(/{\\i0}/g, '</i>')
|
.replace(/{\\i1}/g, '<i>').replace(/{\\i0}/g, '</i>')
|
||||||
.replace(/{\\b1}/g, '<b>').replace(/{\\b0}/g, '</b>')
|
.replace(/{\\b1}/g, '<b>').replace(/{\\b0}/g, '</b>')
|
||||||
.replace(/{\\u1}/g, '<u>').replace(/{\\u0}/g, '</u>')
|
.replace(/{\\u1}/g, '<u>').replace(/{\\u0}/g, '</u>')
|
||||||
.replace(/{[^}]+}/g, '')
|
.replace(/{[^}]+}/g, '')
|
||||||
.replace(/(\s+)?\\n(\s+)?/ig, '\n')
|
.replace(/(\s+)?\\n(\s+)?/ig, '\n')
|
||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats a time stamp.
|
* Formats a time stamp.
|
||||||
*/
|
*/
|
||||||
function time(value: string): string {
|
function time(value: string): string
|
||||||
var all = value.match(/^([0-9]+):([0-9]+):([0-9]+)\.([0-9]+)$/);
|
{
|
||||||
if (!all) throw new Error('Invalid time.');
|
const all = value.match(/^([0-9]+):([0-9]+):([0-9]+)\.([0-9]+)$/);
|
||||||
var hours = prefix(all[1], 2);
|
|
||||||
var minutes = prefix(all[2], 2);
|
if (!all)
|
||||||
var seconds = prefix(all[3], 2);
|
{
|
||||||
var milliseconds = suffix(all[4], 3);
|
throw new Error('Invalid time.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const hours = prefix(all[1], 2);
|
||||||
|
const minutes = prefix(all[2], 2);
|
||||||
|
const seconds = prefix(all[3], 2);
|
||||||
|
const milliseconds = suffix(all[4], 3);
|
||||||
|
|
||||||
return hours + ':' + minutes + ':' + seconds + ',' + milliseconds;
|
return hours + ':' + minutes + ':' + seconds + ',' + milliseconds;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,43 +1,64 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
import childProcess = require('child_process');
|
import childProcess = require('child_process');
|
||||||
import fs = require('fs');
|
import fs = require('fs');
|
||||||
import path = require('path');
|
|
||||||
import os = require('os');
|
import os = require('os');
|
||||||
|
import path = require('path');
|
||||||
|
|
||||||
import subtitle from '../subtitle/index';
|
import subtitle from '../subtitle/index';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Merges the subtitle and video files into a Matroska Multimedia Container.
|
* Merges the subtitle and video files into a Matroska Multimedia Container.
|
||||||
*/
|
*/
|
||||||
export default function(config: IConfig, isSubtitled: boolean, rtmpInputPath: string, filePath: string, streamMode: string, done: (err: Error) => void) {
|
export default function(config: IConfig, isSubtitled: boolean, videoFileExtention: string, filePath: string,
|
||||||
var subtitlePath = filePath + '.' + (subtitle.formats[config.format] ? config.format : 'ass');
|
verbose: boolean, done: (err: Error) => void)
|
||||||
var videoPath = filePath;
|
{
|
||||||
if (streamMode == "RTMP")
|
const subtitlePath = filePath + '.' + (subtitle.formats[config.format] ? config.format : 'ass');
|
||||||
|
let videoPath = filePath;
|
||||||
|
let cp;
|
||||||
|
|
||||||
|
videoPath += videoFileExtention;
|
||||||
|
|
||||||
|
cp = childProcess.exec(command() + ' ' +
|
||||||
|
'-o "' + filePath + '.mkv" ' +
|
||||||
|
'"' + videoPath + '" ' +
|
||||||
|
(isSubtitled ? '"' + subtitlePath + '"' : ''), {
|
||||||
|
maxBuffer: Infinity,
|
||||||
|
}, (err) =>
|
||||||
{
|
{
|
||||||
videoPath += path.extname(rtmpInputPath);
|
if (err)
|
||||||
}
|
{
|
||||||
else
|
return done(err);
|
||||||
{
|
}
|
||||||
videoPath += ".mp4";
|
|
||||||
}
|
unlink(videoPath, subtitlePath, (errin) =>
|
||||||
childProcess.exec(command() + ' ' +
|
{
|
||||||
'-o "' + filePath + '.mkv" ' +
|
if (errin)
|
||||||
'"' + videoPath + '" ' +
|
{
|
||||||
(isSubtitled ? '"' + subtitlePath + '"' : ''), {
|
unlinkTimeout(videoPath, subtitlePath, 5000);
|
||||||
maxBuffer: Infinity
|
}
|
||||||
}, err => {
|
|
||||||
if (err) return done(err);
|
done(null);
|
||||||
unlink(videoPath, subtitlePath, err => {
|
|
||||||
if (err) unlinkTimeout(videoPath, subtitlePath, 5000);
|
|
||||||
done(null);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (verbose === true)
|
||||||
|
{
|
||||||
|
cp.stdin.pipe(process.stdin);
|
||||||
|
cp.stdout.pipe(process.stdout);
|
||||||
|
cp.stderr.pipe(process.stderr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines the command for the operating system.
|
* Determines the command for the operating system.
|
||||||
*/
|
*/
|
||||||
function command(): string {
|
function command(): string
|
||||||
if (os.platform() !== 'win32') return 'mkvmerge';
|
{
|
||||||
|
if (os.platform() !== 'win32')
|
||||||
|
{
|
||||||
|
return 'mkvmerge';
|
||||||
|
}
|
||||||
|
|
||||||
return '"' + path.join(__dirname, '../../bin/mkvmerge.exe') + '"';
|
return '"' + path.join(__dirname, '../../bin/mkvmerge.exe') + '"';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,9 +66,15 @@ function command(): string {
|
|||||||
* Unlinks the video and subtitle.
|
* Unlinks the video and subtitle.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
function unlink(videoPath: string, subtitlePath: string, done: (err: Error) => void) {
|
function unlink(videoPath: string, subtitlePath: string, done: (err: Error) => void)
|
||||||
fs.unlink(videoPath, err => {
|
{
|
||||||
if (err) return done(err);
|
fs.unlink(videoPath, (err) =>
|
||||||
|
{
|
||||||
|
if (err)
|
||||||
|
{
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
|
||||||
fs.unlink(subtitlePath, done);
|
fs.unlink(subtitlePath, done);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -55,10 +82,16 @@ function unlink(videoPath: string, subtitlePath: string, done: (err: Error) => v
|
|||||||
/**
|
/**
|
||||||
* Attempts to unlink the video and subtitle with a timeout between each try.
|
* Attempts to unlink the video and subtitle with a timeout between each try.
|
||||||
*/
|
*/
|
||||||
function unlinkTimeout(videoPath: string, subtitlePath: string, timeout: number) {
|
function unlinkTimeout(videoPath: string, subtitlePath: string, timeout: number)
|
||||||
setTimeout(() => {
|
{
|
||||||
unlink(videoPath, subtitlePath, err => {
|
setTimeout(() =>
|
||||||
if (err) unlinkTimeout(videoPath, subtitlePath, timeout);
|
{
|
||||||
|
unlink(videoPath, subtitlePath, (err) =>
|
||||||
|
{
|
||||||
|
if (err)
|
||||||
|
{
|
||||||
|
unlinkTimeout(videoPath, subtitlePath, timeout);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}, timeout);
|
}, timeout);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,43 +1,63 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
import childProcess = require('child_process');
|
import childProcess = require('child_process');
|
||||||
import path = require('path');
|
|
||||||
import os = require('os');
|
import os = require('os');
|
||||||
|
import path = require('path');
|
||||||
|
|
||||||
|
import my_request = require('../my_request');
|
||||||
|
import log = require('../log');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Streams the video to disk.
|
* Streams the video to disk.
|
||||||
*/
|
*/
|
||||||
export default function(rtmpUrl: string, rtmpInputPath: string, swfUrl: string, filePath: string, fileExt: string, mode: string, done: (err: Error) => void) {
|
export default function(rtmpUrl: string, rtmpInputPath: string, swfUrl: string, filePath: string,
|
||||||
if (mode == "RTMP")
|
fileExt: string, mode: string, verbose: boolean, done: (err: Error) => void)
|
||||||
{
|
{
|
||||||
childProcess.exec(command("rtmpdump") + ' ' +
|
let cp;
|
||||||
'-r "' + rtmpUrl + '" ' +
|
let cmd;
|
||||||
'-y "' + rtmpInputPath + '" ' +
|
if (mode === 'RTMP')
|
||||||
'-W "' + swfUrl + '" ' +
|
{
|
||||||
'-o "' + filePath + fileExt + '"', {
|
cmd = command('rtmpdump') + ' ' +
|
||||||
maxBuffer: Infinity
|
'-r "' + rtmpUrl + '" ' +
|
||||||
}, done);
|
'-y "' + rtmpInputPath + '" ' +
|
||||||
}
|
'-W "' + swfUrl + '" ' +
|
||||||
else if (mode == "HLS")
|
'-o "' + filePath + fileExt + '"';
|
||||||
{
|
}
|
||||||
console.info("Experimental FFMPEG, MAY FAIL!!!");
|
else if (mode === 'HLS')
|
||||||
var cmd=command("ffmpeg") + ' ' +
|
{
|
||||||
'-i "' + rtmpInputPath + '" ' +
|
cmd = command('ffmpeg') + ' ' +
|
||||||
'-c copy -bsf:a aac_adtstoasc ' +
|
'-user_agent "' + my_request.getUserAgent() + '" ' +
|
||||||
'"' + filePath + '.mp4"';
|
'-y -xerror -discard none ' +
|
||||||
childProcess.exec(cmd, {
|
'-i "' + rtmpInputPath + '" ' +
|
||||||
maxBuffer: Infinity
|
'-c copy -bsf:a aac_adtstoasc ' +
|
||||||
}, done);
|
'"' + filePath + '.mp4"';
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
console.error("No such mode: " + mode);
|
log.error('No such mode: ' + mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cp = childProcess.exec(cmd,
|
||||||
|
{
|
||||||
|
maxBuffer: Infinity,
|
||||||
|
}, done);
|
||||||
|
|
||||||
|
if (verbose === true)
|
||||||
|
{
|
||||||
|
cp.stdin.pipe(process.stdin);
|
||||||
|
cp.stdout.pipe(process.stdout);
|
||||||
|
cp.stderr.pipe(process.stderr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines the command for the operating system.
|
* Determines the command for the operating system.
|
||||||
*/
|
*/
|
||||||
function command(exe: string): string {
|
function command(exe: string): string
|
||||||
if (os.platform() !== 'win32') return exe;
|
{
|
||||||
|
if (os.platform() !== 'win32')
|
||||||
|
{
|
||||||
|
return exe;
|
||||||
|
}
|
||||||
|
|
||||||
return '"' + path.join(__dirname, '../../bin/' + exe + '.exe') + '"';
|
return '"' + path.join(__dirname, '../../bin/' + exe + '.exe') + '"';
|
||||||
}
|
}
|
||||||
|
|||||||
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
96
ts.js
96
ts.js
@ -1,96 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
var childProcess = require('child_process');
|
|
||||||
var fs = require('fs');
|
|
||||||
var path = require('path');
|
|
||||||
var isTest = process.argv[2] === '--only-test';
|
|
||||||
|
|
||||||
// TODO: This build task should be removed upon release of TypeScript 1.5 with
|
|
||||||
// the support for `tsconfig.json`. Invoking `tsc` from `package.json` will then
|
|
||||||
// read the configuration and compile accordingly. It seems that `TSLint` will,
|
|
||||||
// eventually, support this mechanism too. That prevents the need for any kind
|
|
||||||
// of build task and will run entirely based on instructions from `npm`.
|
|
||||||
//
|
|
||||||
// Reference #1: https://github.com/Microsoft/TypeScript/issues/1667
|
|
||||||
// Reference #2: https://github.com/palantir/tslint/issues/281
|
|
||||||
|
|
||||||
read(function(err, fileNames) {
|
|
||||||
clean(fileNames, function() {
|
|
||||||
var hasLintError = false;
|
|
||||||
compile(fileNames, function(err) {
|
|
||||||
if (err) {
|
|
||||||
console.error(err);
|
|
||||||
return process.exit(1);
|
|
||||||
}
|
|
||||||
lint(fileNames, function(message) {
|
|
||||||
process.stdout.write(message);
|
|
||||||
hasLintError = true;
|
|
||||||
}, function() {
|
|
||||||
process.exit(Number(hasLintError));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clean the files.
|
|
||||||
* @param {Array.<string>} filePaths
|
|
||||||
* @param {function()} done
|
|
||||||
*/
|
|
||||||
function clean(filePaths, done) {
|
|
||||||
if (isTest) return done();
|
|
||||||
var i = -1;
|
|
||||||
(function next() {
|
|
||||||
i += 1;
|
|
||||||
if (i >= filePaths.length) return done();
|
|
||||||
var filePath = filePaths[i];
|
|
||||||
if (/\.d\.ts$/.test(filePath)) return next();
|
|
||||||
var mapName = filePath.substring(4, filePath.length - 2) + 'js.map';
|
|
||||||
var mapPath = path.join('dist', mapName);
|
|
||||||
if (fs.existsSync(mapPath)) fs.unlinkSync(mapPath);
|
|
||||||
next();
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compile the files.
|
|
||||||
* @param {Array.<string>} filePaths
|
|
||||||
* @param {function(Error)} done
|
|
||||||
*/
|
|
||||||
function compile(filePaths, done) {
|
|
||||||
if (isTest) return done(null);
|
|
||||||
var execPath = path.join(__dirname, 'node_modules/.bin/tsc');
|
|
||||||
var options = '--declaration --module CommonJS --noImplicitAny --outDir dist --target ES5';
|
|
||||||
childProcess.exec([execPath, options].concat(filePaths).join(' '), function(err, stdout) {
|
|
||||||
if (stdout) return done(new Error(stdout));
|
|
||||||
done(null);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lint the files.
|
|
||||||
* @param {Array.<string>} filePaths
|
|
||||||
* @param {function(string)} handler
|
|
||||||
* @param {function()} done
|
|
||||||
*/
|
|
||||||
function lint(filePaths, handler, done) {
|
|
||||||
var i = -1;
|
|
||||||
var execPath = path.join(__dirname, 'node_modules/.bin/tslint');
|
|
||||||
(function next() {
|
|
||||||
i += 1;
|
|
||||||
if (i >= filePaths.length) return done();
|
|
||||||
var filePath = filePaths[i];
|
|
||||||
if (/\.d\.ts$/.test(filePath)) return next();
|
|
||||||
childProcess.exec(execPath + ' -f ' + filePath, function(err, stdout) {
|
|
||||||
if (stdout) handler(stdout);
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read the files from the project file.
|
|
||||||
* @param {function(Error, Array.<string>)} done
|
|
||||||
*/
|
|
||||||
function read(done) {
|
|
||||||
done(null, JSON.parse(fs.readFileSync('tsconfig.json', 'utf8')).files);
|
|
||||||
}
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "1.5.1-beta",
|
"target": "es6",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
@ -7,46 +7,8 @@
|
|||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"target": "es5"
|
"lib": [
|
||||||
},
|
"es2015"
|
||||||
"filesGlob": [
|
]
|
||||||
"src/**/*.ts",
|
}
|
||||||
"typings/**/*.ts"
|
|
||||||
],
|
|
||||||
"files": [
|
|
||||||
"src/batch.ts",
|
|
||||||
"src/cli.ts",
|
|
||||||
"src/episode.ts",
|
|
||||||
"src/index.ts",
|
|
||||||
"src/interface/IConfig.d.ts",
|
|
||||||
"src/interface/IConfigLine.d.ts",
|
|
||||||
"src/interface/IConfigTask.d.ts",
|
|
||||||
"src/interface/IEpisodePage.d.ts",
|
|
||||||
"src/interface/IEpisodePlayer.d.ts",
|
|
||||||
"src/interface/IEpisodePlayerConfig.d.ts",
|
|
||||||
"src/interface/IFormatterTable.d.ts",
|
|
||||||
"src/interface/ISeries.d.ts",
|
|
||||||
"src/interface/ISeriesEpisode.d.ts",
|
|
||||||
"src/interface/ISubtitle.d.ts",
|
|
||||||
"src/interface/ISubtitleEvent.d.ts",
|
|
||||||
"src/interface/ISubtitleStyle.d.ts",
|
|
||||||
"src/request.ts",
|
|
||||||
"src/series.ts",
|
|
||||||
"src/subtitle/decode.ts",
|
|
||||||
"src/subtitle/formats/ass.ts",
|
|
||||||
"src/subtitle/formats/index.ts",
|
|
||||||
"src/subtitle/formats/srt.ts",
|
|
||||||
"src/subtitle/index.ts",
|
|
||||||
"src/video/index.ts",
|
|
||||||
"src/video/merge.ts",
|
|
||||||
"src/video/stream.ts",
|
|
||||||
"typings/big-integer/big-integer.d.ts",
|
|
||||||
"typings/cheerio/cheerio.d.ts",
|
|
||||||
"typings/commander/commander.d.ts",
|
|
||||||
"typings/form-data/form-data.d.ts",
|
|
||||||
"typings/mkdirp/mkdirp.d.ts",
|
|
||||||
"typings/node/node.d.ts",
|
|
||||||
"typings/request/request.d.ts",
|
|
||||||
"typings/xml2js/xml2js.d.ts"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
33
tsd.json
33
tsd.json
@ -1,33 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "v4",
|
|
||||||
"repo": "borisyankov/DefinitelyTyped",
|
|
||||||
"ref": "master",
|
|
||||||
"path": "typings",
|
|
||||||
"bundle": "typings/tsd.d.ts",
|
|
||||||
"installed": {
|
|
||||||
"node/node.d.ts": {
|
|
||||||
"commit": "3882d337bb0808cde9fe4c08012508a48c135482"
|
|
||||||
},
|
|
||||||
"commander/commander.d.ts": {
|
|
||||||
"commit": "3882d337bb0808cde9fe4c08012508a48c135482"
|
|
||||||
},
|
|
||||||
"xml2js/xml2js.d.ts": {
|
|
||||||
"commit": "3882d337bb0808cde9fe4c08012508a48c135482"
|
|
||||||
},
|
|
||||||
"cheerio/cheerio.d.ts": {
|
|
||||||
"commit": "3882d337bb0808cde9fe4c08012508a48c135482"
|
|
||||||
},
|
|
||||||
"mkdirp/mkdirp.d.ts": {
|
|
||||||
"commit": "3882d337bb0808cde9fe4c08012508a48c135482"
|
|
||||||
},
|
|
||||||
"request/request.d.ts": {
|
|
||||||
"commit": "3882d337bb0808cde9fe4c08012508a48c135482"
|
|
||||||
},
|
|
||||||
"big-integer/big-integer.d.ts": {
|
|
||||||
"commit": "3882d337bb0808cde9fe4c08012508a48c135482"
|
|
||||||
},
|
|
||||||
"form-data/form-data.d.ts": {
|
|
||||||
"commit": "3882d337bb0808cde9fe4c08012508a48c135482"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
30
tslint.json
30
tslint.json
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"extends": "tslint:latest",
|
||||||
"rules": {
|
"rules": {
|
||||||
"ban": false,
|
"ban": false,
|
||||||
"class-name": true,
|
"class-name": true,
|
||||||
@ -8,47 +9,39 @@
|
|||||||
"curly": false,
|
"curly": false,
|
||||||
"eofline": false,
|
"eofline": false,
|
||||||
"forin": true,
|
"forin": true,
|
||||||
"indent": [true, 2],
|
"indent": [true, "spaces", 2],
|
||||||
"interface-name": true,
|
"interface-name": true,
|
||||||
"jsdoc-format": true,
|
"jsdoc-format": true,
|
||||||
"label-position": true,
|
"label-position": true,
|
||||||
"label-undefined": true,
|
|
||||||
"max-line-length": [true, 140],
|
"max-line-length": [true, 140],
|
||||||
"member-ordering": [true,
|
"member-ordering": false,
|
||||||
"public-before-private",
|
"no-shadowed-variable": false,
|
||||||
"static-before-instance",
|
"array-type": [true, "array"],
|
||||||
"variables-before-functions"
|
"trailing-comma": false,
|
||||||
],
|
|
||||||
"no-any": false,
|
"no-any": false,
|
||||||
"no-arg": true,
|
"no-arg": true,
|
||||||
"no-bitwise": true,
|
"no-bitwise": true,
|
||||||
|
"prefer-conditional-expression": false,
|
||||||
|
"space-within-parens": false,
|
||||||
|
"no-object-literal-type-assertion": false,
|
||||||
"no-console": [true,
|
"no-console": [true,
|
||||||
"debug",
|
"debug",
|
||||||
"info",
|
|
||||||
"time",
|
"time",
|
||||||
"timeEnd",
|
"timeEnd",
|
||||||
"trace"
|
"trace"
|
||||||
],
|
],
|
||||||
"no-construct": true,
|
"no-construct": true,
|
||||||
"no-constructor-vars": true,
|
|
||||||
"no-debugger": true,
|
"no-debugger": true,
|
||||||
"no-duplicate-key": true,
|
|
||||||
"no-duplicate-variable": true,
|
"no-duplicate-variable": true,
|
||||||
"no-empty": true,
|
"no-empty": true,
|
||||||
"no-eval": true,
|
"no-eval": true,
|
||||||
"no-string-literal": true,
|
"no-string-literal": true,
|
||||||
"no-switch-case-fall-through": true,
|
"no-switch-case-fall-through": true,
|
||||||
"no-trailing-comma": true,
|
|
||||||
"no-trailing-whitespace": true,
|
"no-trailing-whitespace": true,
|
||||||
"no-unused-expression": true,
|
"no-unused-expression": true,
|
||||||
"no-unused-variable": true,
|
|
||||||
"no-unreachable": true,
|
|
||||||
"no-use-before-declare": false,
|
"no-use-before-declare": false,
|
||||||
"no-var-requires": true,
|
"no-var-requires": true,
|
||||||
"one-line": [true,
|
"one-line": [true,
|
||||||
"check-catch",
|
|
||||||
"check-else",
|
|
||||||
"check-open-brace",
|
|
||||||
"check-whitespace"
|
"check-whitespace"
|
||||||
],
|
],
|
||||||
"quotemark": [true, "single"],
|
"quotemark": [true, "single"],
|
||||||
@ -66,7 +59,6 @@
|
|||||||
"property-declaration": "nospace",
|
"property-declaration": "nospace",
|
||||||
"variable-declaration": "nospace"
|
"variable-declaration": "nospace"
|
||||||
}],
|
}],
|
||||||
"use-strict": false,
|
|
||||||
"variable-name": false,
|
"variable-name": false,
|
||||||
"whitespace": [true,
|
"whitespace": [true,
|
||||||
"check-branch",
|
"check-branch",
|
||||||
@ -74,6 +66,8 @@
|
|||||||
"check-operator",
|
"check-operator",
|
||||||
"check-separator",
|
"check-separator",
|
||||||
"check-type"
|
"check-type"
|
||||||
]
|
],
|
||||||
|
"object-literal-sort-keys": false,
|
||||||
|
"ordered-imports": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user