149 Commits

Author SHA1 Message Date
Godzil
141bdccf02 1.3.7 2018-07-30 22:47:38 +01:00
Godzil
4990effa1c Try to fix #81 and probably some other issues when the URL is not valid to properly display that the URL is not valid. Also change a bit on how error are handled 2018-07-30 22:47:38 +01:00
Godzil
2459f342c5 Force debug file to be written synchronously 2018-07-30 22:47:38 +01:00
Godzil
d68a2b7bce Update dependencies 2018-07-30 22:47:38 +01:00
Godzil
69d5ceac36 Remove useless ignore in .gitignore 2018-07-30 22:47:38 +01:00
Manoël Trapier
cf7039400c Update readme with more usefull examples 2018-07-30 22:47:38 +01:00
Godzil
02a9d763cd Add the episode title in the default file name template. 2018-07-30 22:47:38 +01:00
Godzil
d549d46979 1.3.6 2018-07-30 22:47:37 +01:00
Godzil
3f5b4b2585 Update readme 2018-07-30 22:47:37 +01:00
Godzil
1d596b02f7 Cleaning up the command line parameter to properly use default values 2018-07-30 22:47:37 +01:00
Godzil
cee53fb113 Fix for #78 (and a bit of cleanup) 2018-07-30 22:47:37 +01:00
Godzil
1e56cab73f Move error displaying when downloading an episode fail. 2018-07-30 22:47:37 +01:00
Godzil
0dc3c1e8e2 Update a bit the bug report template
(Commit #200!)
2018-07-30 22:47:37 +01:00
Godzil
0124e38a89 1.3.5 2018-07-30 22:47:36 +01:00
Godzil
6765b517ec Add a new episode filter and completely remove some dependencies on the config object. 2018-07-30 22:47:36 +01:00
Godzil
8c1e0f2e0c Stop messing with the config objet 2018-07-30 22:47:36 +01:00
Godzil
817843c40c Add more output to debug.txt 2018-07-30 22:47:36 +01:00
Godzil
04b22fdce5 Update readme file 2018-07-30 22:47:36 +01:00
Godzil
eb15d7d854 1.3.4 2018-07-30 22:47:36 +01:00
Godzil
66670547b9 Add a crude debug mechanism 2018-07-30 22:47:36 +01:00
Godzil
987e424324 Force an exit if authentication failed! 2018-07-30 22:47:35 +01:00
Godzil
523c780b18 Force to use a user account 2018-07-30 22:47:35 +01:00
Godzil
6c2100fbff Give access to the config objet to the subtitles for future changes. 2018-07-30 22:47:35 +01:00
Godzil
f10bead0dc Remove episode and volume filter, they were buggy and useless.
Use the @URL syntax do download a single episode.
2018-07-30 22:47:35 +01:00
Godzil
6448f4ec97 1.3.3 2018-07-30 22:47:35 +01:00
Godzil
829bb080ee Small update on the bug report template 2018-07-30 22:47:35 +01:00
Godzil
5edd7cf05a Fix 4 silly bugs 2018-07-30 22:47:35 +01:00
Godzil
8cf70e57cf 1.3.2 2018-07-30 22:47:34 +01:00
Godzil
2545c36241 Remove more DEP005 warning 2018-07-30 22:47:34 +01:00
Godzil
e2a4ba738f Make during series fetching error reporting a bit more clear 2018-07-30 22:47:34 +01:00
Godzil
e4e0fc3ea7 More error stack issue 2018-07-30 22:47:34 +01:00
Godzil
c56998312c Forgot to update config object with the new generated file name 2018-07-30 22:47:34 +01:00
Godzil
3ac1f4ee9e Don't display error stack when not needed, and if needed check it exist
before trying to display it.
2018-07-30 22:47:34 +01:00
Godzil
c2e9449630 Change the readme file, and add some templates for issue tracking and pull requests 2018-07-30 22:47:34 +01:00
Manoël Trapier
fb14020a7f Add issue templates 2018-07-30 22:47:34 +01:00
Godzil
5a51d888b8 Remove DEP0005 warning from use of the Buffer object 2018-07-30 22:47:34 +01:00
Godzil
301fa1c860 Remove dependencies on fs and only use fs-extra 2018-07-30 22:47:34 +01:00
Godzil
7e32028195 Update some packages 2018-07-30 22:47:34 +01:00
Godzil
dea2c38dc4 Upgrade some packages 2018-07-30 22:47:33 +01:00
Godzil
cdf7f223db 1.3.1 2018-07-30 22:47:33 +01:00
Godzil
bb70161652 Add a really simple and stupid way to check if you are running the latest version. 2018-07-30 22:47:33 +01:00
Godzil
7f2f983f55 using log instead of console is nicer 2018-07-30 22:47:33 +01:00
Godzil
8dab83b3ef Add log when not adding an episode when rebuilding the .crpresistent. 2018-07-30 22:47:33 +01:00
Godzil
cbafa5bc90 Now that retrying is there; force ffmpeg to fail in case or error (should now avoid to silently download a corrupted video file) 2018-07-30 22:47:33 +01:00
Godzil
67735fb52a Add a similar mechanism for episodes downloads.
If it can't fetch it after a couple of retry (5 by default, can be changed on the command line) it will just ignore it and go to the next episode.
2018-07-30 22:47:33 +01:00
Godzil
7d6f762f59 Add retry mechanism in case of episodes list retrieve failure instead of just failing.
If it can't after a couple of retry (5 by default, can be changed on the command line) it will just ignore it and go to the next anime.
2018-07-30 22:47:33 +01:00
Godzil
f3a0d0129d Correct a bug with ffmpeg where it will wait forever for the user to answer a question when trying to overwrite a file.
Fix #68
2018-07-30 22:47:33 +01:00
Godzil
65c9032839 Add an option to make ffmpeg, mkvmerge and rtmpdump running verbosely. 2018-07-30 22:47:33 +01:00
Godzil
978a3282a4 1.3.0 2018-07-30 22:47:33 +01:00
Manoël Trapier
9f0195bebc Update README.md 2018-07-30 22:47:33 +01:00
Manoël Trapier
ea20108222 Update README.md 2018-07-30 22:47:33 +01:00
Godzil
4ee814864c Add support for changing the batchfile on the command line 2018-07-30 22:47:33 +01:00
Godzil
4cbfd691c3 add missing package 2018-07-30 22:47:32 +01:00
Godzil
7c04fb7282 Make tslint happy! 2018-07-30 22:47:29 +01:00
Godzil
849c7612aa A bit of code reformating and add an option to regenerate the .crpersistant file in case it become corrupted and Crunchy try to redownload everything. 2018-07-30 22:46:29 +01:00
Roei Elisha
6ad4cbed0a make login work 2018-07-30 22:45:28 +01:00
Godzil
9e2f5401d0 Update tslint.json 2018-07-30 22:44:18 +01:00
Roei Elisha
b064b97f2d fix linter problems 2018-05-22 23:55:10 +03:00
Godzil
b248405437 1.2.2 2018-05-09 22:33:45 +01:00
Godzil
bf941819a8 remove unwwanted parameter 2018-05-09 22:31:50 +01:00
Godzil
fcae53baae Node 5, 6 and 7 seems to not like something. Delete them from Travis build 2018-05-08 21:50:58 +01:00
Godzil
05ead50c0d Let's try to make travis happy with older node version 2018-05-08 21:49:11 +01:00
Godzil
0a80f80f91 1.2.1 2018-05-08 21:37:43 +01:00
Godzil
3bf5fea735 Make Crunchy to properly return a return code when running fine or failing 2018-05-08 21:37:34 +01:00
Godzil
3a95994cc2 1.2.0 2018-03-29 22:33:15 +01:00
Godzil
a29870691b Update deps 2018-03-29 22:33:06 +01:00
Godzil
547fdc4aa0 Add a way to select the resolution. Use 1080p by default
Fix #58
2018-03-29 22:29:13 +01:00
Godzil
c78552795f 1.1.22 2018-03-29 20:41:45 +01:00
Godzil
090c7e4789 Trying to fix #59 by adding a referer to the header. Seems to fix it but need to be throughfully tested.. 2018-03-29 20:40:17 +01:00
Godzil
bf8e1fe80f Update cloudscraper 2018-03-29 20:38:38 +01:00
Manoël Trapier
7344ce3d61 Update README.md 2018-01-31 17:09:58 +00:00
Godzil
c642e76cce Make sure that it is rebuild before publishing 2017-12-27 05:34:22 +01:00
Godzil
8ef27066f6 1.1.21 2017-12-27 05:19:28 +01:00
Godzil
621df26b58 Try to make travis happy (again) 2017-12-27 05:16:58 +01:00
Godzil
8060b1b73b Update travis definition 2017-12-27 04:58:21 +01:00
Godzil
11f6b3feff Make tslint happy 2017-12-27 04:57:45 +01:00
Godzil
537639f2a8 Simplify tsconfig to no longer list .ts file, also simplify commands as typings is no longuer there 2017-12-27 04:57:24 +01:00
Godzil
813f8a997d Completely remote typings to use TypeScript2.0 type management, update also some deps 2017-12-27 04:56:26 +01:00
Godzil
48544020a1 1.1.20 2017-09-16 22:58:27 +01:00
Godzil
cc68d21107 correct permissions 2017-09-16 22:54:49 +01:00
Godzil
acd91e2679 Add (unless) node minimum version in packages.json 2017-09-16 22:54:27 +01:00
Godzil
53f0a9462a Better filename forbidden character handling
Logs are a bit better
2017-09-16 22:51:49 +01:00
Godzil
10d71944d9 Fix lint error 2017-08-21 16:08:58 +02:00
Manoël Trapier
b5bbde7cdd Change to make travis npm happy 2017-08-21 14:24:23 +01:00
Manoël Trapier
c406bc70ee Sanitise more characters from filenames 2017-05-17 16:17:26 +01:00
Godzil
1dea620295 1.1.19 2017-05-12 00:14:24 +01:00
Manoël Trapier
2019c104b6 Add comment on issue report 2017-05-10 16:29:58 +01:00
Manoël Trapier
9f1ead1368 Update README.md 2017-05-09 20:15:51 +01:00
Manoël Trapier
41f67798d6 Merge pull request #22 from ssttevee/master
Custom filenames
2017-05-09 19:55:45 +01:00
ssttevee
2c2ed2c136 added more custom filename variables 2017-03-29 14:10:06 -07:00
ssttevee
4dc90aeb00 added custom filenames 2017-03-29 13:59:51 -07:00
Godzil
361c6cf54c Abandon node v4 as some packages does not want to install. 2017-03-16 19:25:06 +01:00
Godzil
b691b953d4 remove a useless target and update travis 2017-03-16 18:34:54 +01:00
Godzil
3d067979e9 update node version for travis 2017-03-16 17:47:58 +01:00
Godzil
58247f53e4 Add Travis 2017-03-16 17:44:35 +01:00
Manoël Trapier
6189e31e6b Update README.md 2017-03-16 12:08:59 +00:00
Manoël Trapier
3df650a0a6 Markdown I hate you 2017-02-27 16:14:55 +00:00
Manoël Trapier
c785c0f7c3 Add comment about login 2017-02-27 16:14:16 +00:00
Godzil
a01f3cd09c 1.1.18: Fix issue #19 2017-02-16 23:05:16 +00:00
Godzil
ed4f398062 Fix #19, better to check if a file exist before trying to copy it 😂 2017-02-16 23:03:43 +00:00
Godzil
e9cf0c353b 1.1.17 2017-02-12 23:37:19 +00:00
Godzil
6bc39083b9 - Support more episode naming schemes
- Display when we are going to fetch from a single URL (@http://...)
- Display a warning when a series looks to have no episodes
- Make a backup of the .crpersistent before changing it
2017-02-12 23:10:51 +00:00
Godzil
67d06246d4 1.1.17-0 2017-02-11 19:24:59 +00:00
Godzil
2ab1daf2b3 Another lint pass on episode.ts
Correct a stupid bug where it try to download an episode two times (and led to a failure) if metadata can't be fetch as expected. Doh!
2017-02-11 19:23:48 +00:00
Godzil
065d3b4c66 1.1.16 2017-02-10 23:52:23 +00:00
Godzil
cfe73f5ca8 More lint cleaning, add a way to download a single episode by URL 2017-02-10 23:51:22 +00:00
Godzil
2fea379484 Fancy output also works under windows, so it's now enabled for all platform! 2017-02-10 23:51:04 +00:00
Godzil
bee3f33e20 Update npm packages, cleanup the code, cleanup all tslint complain 2017-02-10 17:43:52 +00:00
Manoël Trapier
5d9c25491d Typo on typings 2017-02-08 16:47:52 +00:00
Godzil
58f4dc61ff Fix login issue 2017-02-07 20:22:25 +00:00
Godzil
b96efacbd2 - Revert login using the token method
- Use the cloudscraper layer on top of request to pass through the cloudfare browser check
- switch from tsd to typings
2017-02-07 20:22:01 +00:00
Godzil
a346ab8854 1.1.14 2017-02-01 08:53:23 +00:00
Godzil
499530141e Disable debug message about ffmpeg 2017-02-01 08:53:14 +00:00
Godzil
d1457bb893 1.1.13 2017-01-28 13:38:20 +00:00
Godzil
8dfd1b447c Add a log objet to do some fancy output on the command line (not fully enabled under windows as it need some tests) 2017-01-28 13:38:14 +00:00
Godzil
ce63ae9a16 Upgrade to 1.1.12 to fix login issue 2017-01-23 21:13:13 +00:00
Godzil
70d80ccd17 Update dependency to more recent version, and correct a few warnings reported by ts 2017-01-23 21:06:34 +00:00
Manoël Trapier
7833fbe292 Merge pull request #13 from majewskim/master
Fix a login issue
2017-01-23 20:46:10 +00:00
Mateusz Majewski
fa6aa74442 Merge branch 'master' into master 2017-01-18 12:49:49 +02:00
Mateusz Majewski
fe2ed9fb76 Fixing login issue by bypassing the login form and making a request directly. 2017-01-18 11:19:41 +02:00
Mateusz Majewski
cc655b9e00 Fixing login issue by bypassing the login form and making a request directly. 2017-01-18 11:08:45 +02:00
Manoël Trapier
e1d2a55a01 Update README.md 2016-10-21 17:21:36 +01:00
Manoël Trapier
a31de0ef9d Remove .js from the name 2016-10-21 17:21:05 +01:00
Godzil
2853334d7f 1.1.11
- Update login mechanism to march CR september 2016 changes
2016-09-16 22:20:43 +01:00
Godzil
69dd28d31b Update login to match latest CR changes 2016-09-16 22:20:39 +01:00
Godzil
56afce02ea 1.1.10
- Change name format to follow Plex forvourite one.
- Remplace ":" in file name to prevent issue on Windows
2016-09-10 20:22:10 +01:00
Godzil
bc4697061e Remplace ':' in filename to make Windows happy 2016-09-10 20:17:19 +01:00
Godzil
55ffe85f77 Make the name to be more Plex friendly 2016-09-10 11:53:45 +01:00
Godzil
ec8c2c7716 1.1.9
New features:
 - Should correctly handle 10.5 or 1A episode numbers
 - Change characters in series name that are not allowed in filename (or could cause issues, like slash ( / ) or single quote ( ' )
 - Prevent overwrite of existing files (usefull when different season report the same number)
 - If an error during episode metadata scraping, it will set to Season 0, episode 0 (look at the logs!)
 - If an episode is not available yet, it will be skipped
2016-09-07 22:09:14 +01:00
Godzil
714a528f8b Prevent overwriting an existing output file 2016-09-07 21:55:14 +01:00
Godzil
8314d91bd7 Add functionality to ignore (instead of stopping) if an episode is not available yet 2016-09-07 21:51:36 +01:00
Godzil
5bd31f9e0b Add better episode numbering scheme 2016-09-07 21:34:29 +01:00
Godzil
95a93930f3 Merge branch 'master' of github.com:Godzil/crunchyroll.js 2016-08-22 13:10:02 +02:00
Godzil
4a9e1d0410 Update LICENSE 2016-08-22 12:03:05 +01:00
Godzil
1eacd0a5ca add license to npm 2016-08-22 13:03:02 +02:00
Godzil
3c32726745 1.1.8 2016-08-22 12:32:25 +02:00
Godzil
42ae0ae1fb Forget to rename main executable 2016-08-22 12:30:43 +02:00
Godzil
e4b3871919 1.1.7 2016-08-22 12:22:45 +02:00
Godzil
58e4a557e2 Update readme 2016-08-22 12:22:36 +02:00
Godzil
8371d68113 Correct errors 2016-08-22 12:21:17 +02:00
Godzil
b7d496fc9d Update README.md 2016-08-22 10:51:23 +01:00
Godzil
14260d04b3 Update README.md 2016-08-22 10:50:13 +01:00
Godzil
3d46b65d67 Update package.json 2016-08-22 10:47:18 +01:00
Godzil
62a08e14bb Add missing mode parameter to the stream object. (and correct call to it) 2016-08-13 21:24:57 +01:00
Godzil
422d0827f9 Correct file source extension for the merge pass. 2016-08-13 16:32:56 +01:00
Godzil
546a849aa5 Add ffmpeg when using HLS instead of RTMP. 2016-08-13 16:20:33 +01:00
Roel van Uden
e06ff53210 Lock down dependencies, bump version (#9) 2015-06-25 19:42:10 +02:00
Roel van Uden
18375d3d22 Update to TS1.5 2015-05-23 19:07:14 +02:00
Roel van Uden
5fdee94b38 References for future self. 2015-03-07 18:46:42 +01:00
48 changed files with 4848 additions and 729 deletions

0
.github/CONTRIBUTING.md vendored Normal file
View File

35
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View 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_

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

1
.gitignore vendored
View File

@@ -1,3 +1,2 @@
dist/
node_modules/
typings/

10
.travis.yml Normal file
View File

@@ -0,0 +1,10 @@
language: node_js
sudo: false
node_js:
- 8
- 9
before_install:
- npm install --only=dev
script:
- npm run build
- npm test

View File

@@ -1,4 +1,5 @@
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
of this software and associated documentation files (the "Software"), to

1
README Symbolic link
View File

@@ -0,0 +1 @@
README.md

157
README.md
View File

@@ -1,6 +1,8 @@
# 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.
[![Issue Stats](http://issuestats.com/github/Godzil/Crunchy/badge/issue)](http://issuestats.com/github/Godzil/Crunchy) [![Travis CI](https://travis-ci.org/Godzil/Crunchy.svg?branch=master)](https://travis-ci.org/Godzil/Crunchy) [![Maintainability](https://api.codeclimate.com/v1/badges/413c7ca11c0805b1ef3e/maintainability)](https://codeclimate.com/github/Godzil/Crunchy/maintainability)
*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
@@ -10,103 +12,177 @@
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
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
* NodeJS >= 0.12.x (http://nodejs.org/)
* NPM >= 2.5.x (https://www.npmjs.org/)
* NodeJS >= 8.1 (http://nodejs.org/)
* NPM >= 5.8 (https://www.npmjs.org/)
## Installation
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`
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`
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
1. Install *Homebrew* following the instructions at http://brew.sh/
2. Run in *Terminal*: `brew install node mkvtoolnix rtmpdump`
3. Run in *Terminal*: `npm install -g crunchyroll`
2. Run in *Terminal*: `brew install node mkvtoolnix rtmpdump ffmpeg`
3. Run in *Terminal*: `npm install -g crunchy`
#### Updating:
1. Run in *Terminal*: `sudo npm update -g crunchy`
### Windows
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
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:
-h, --help output usage information
-V, --version output the version number
-p, --pass <s> The password.
-u, --user <s> The e-mail address or username.
-c, --cache Disables the cache.
-m, --merge Disables merging subtitles and videos.
-e, --episode <i> The episode filter.
-v, --volume <i> The volume filter.
-f, --format <s> The subtitle format. (Default: ass)
-o, --output <s> The output path.
-s, --series <s> The series override.
-t, --tag <s> The subgroup. (Default: CrunchyRoll)
-V, --version output the version number
-p, --pass <s> The password.
-u, --user <s> The e-mail address or username.
-c, --cache Disables the cache.
-m, --merge Disables merging subtitles and videos.
-e, --episodes <s> Episode list. Read documentation on how to use
-f, --format <s> The subtitle format. (default: ass)
-o, --output <s> The output path.
-s, --series <s> The series name override.
-n, --nametmpl <s> Output name template (default: {SERIES_TITLE} - s{SEASON_NUMBER}e{EPISODE_NUMBER} - [{TAG}])
-t, --tag <s> The subgroup. (default: CrunchyRoll)
-r, --resolution <s> The video resolution. (valid: 360, 480, 720, 1080) (default: 1080)
-b, --batch <s> Batch file (default: CrunchyRoll.txt)
--verbose Make tool verbose
--rebuildcrp Rebuild the crpersistant file.
--retry <i> Number or time to retry fetching an episode. (default: 5)
-h, --help output usage information
#### Batch-mode
When no sequence of series addresses is provided, the batch-mode source file will be read (which is *CrunchyRoll.txt* in the current work directory. Each line in this file is processed as a seperate command-line statement. This makes it ideal to manage a large sequence of series addresses with variating command-line options or incremental episode updates.
When no sequence of series addresses is provided, the batch-mode source file will be read (which is *CrunchyRoll.txt* in the current work directory. Each line in this file is processed contain the URL of a series and can support some of the command line parameter (like `-e`). This makes it ideal to manage a large sequence of series addresses.
#### Examples
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-
#### Command line parameters
##### Authentication
* `-p or --pass <s>` sets the password.
* `-u or --user <s>` sets the e-mail address or username.
_Please remember that login has to be done for each call of Crunchy, as none of the credentials are stored_
##### Disables
* `-c or --cache` disables the cache.
* `-c or --cache` disables the cache in batch mode.
* `-m or --merge` disables merging subtitles and videos.
##### Filters
* `-e or --episode <i>` filters episodes (positive is greater than, negative is smaller than).
* `-v or --volume <i>` filters volumes (positive is greater than, negative is smaller than).
##### Settings
* `-e or --episodes <s>` set an episode
* `-f or --format <s>` sets the subtitle format. (Default: ass)
* `-o or --output <s>` sets the output path.
* `-s or --series <s>` sets the series override.
* `-t or --tag <s>` sets The subgroup. (Default: CrunchyRoll)
* `-r or --resolution <s>` sets the resolutoin you want to download (360, 480, 720, 1080)
* `--retry <i>` set the number of try Crunchy will use if downloading a serie or episode fail
##### 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
@@ -114,4 +190,5 @@ More information will be added at a later point. For now the recommendations are
* Atom with `atom-typescript` and `linter-tslint` (and dependencies).
Since this project uses TypeScript, compile with `node ts` or `npm install`.
Since this project uses TypeScript, compile with `node run compile` to build the tool and `npm run test` to run the linter.

0
bin/crunchyroll → bin/crunchy Normal file → Executable file
View File

10
bin/crunchy.sh Executable file
View 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

BIN
bin/ffmpeg.exe Executable file

Binary file not shown.

3017
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,36 +1,60 @@
{
"author": "Roel van Uden",
"description": "CrunchyRoll.js is capable of downloading anime episodes from the popular CrunchyRoll streaming service.",
"author": "Godzil",
"description": "Crunchy is a fork of Crunchyroll.js, capable of downloading anime episodes from the popular CrunchyRoll streaming service.",
"license": "MIT",
"keywords": [
"anime",
"download",
"crunchyroll"
],
"name": "crunchyroll",
"name": "crunchy",
"repository": {
"type": "git",
"url": "git://github.com/Deathspike/crunchyroll.js.git"
"url": "git://github.com/Godzil/crunchyroll.js.git"
},
"version": "1.1.3",
"engines": {
"node": ">=5.0"
},
"version": "1.3.7",
"bin": {
"crunchyroll": "./bin/crunchyroll"
"crunchy": "./bin/crunchy",
"crunchy.sh": "./bin/crunchy.sh"
},
"dependencies": {
"big-integer": "^1.4.4",
"cheerio": "^0.18.0",
"commander": "^2.6.0",
"big-integer": "^1.6.32",
"bluebird": "^3.5.1",
"cheerio": "^0.22.0",
"cloudscraper": "^1.5.0",
"commander": "^2.16.0",
"fs-extra": "^7.0.0",
"mkdirp": "^0.5.0",
"request": "^2.53.0",
"pjson": "^1.0.9",
"request": "^2.87.0",
"request-promise": "^4.2.2",
"xml2js": "^0.4.5"
},
"devDependencies": {
"tsd": "^0.5.7",
"tslint": "^2.1.1",
"typescript": "^1.4.1"
"@types/bluebird": "^3.5.23",
"@types/cheerio": "^0.22.8",
"@types/fs-extra": "^5.0.4",
"@types/mkdirp": "^0.5.2",
"@types/node": "^10.5.3",
"@types/request": "^2.47.1",
"@types/request-promise": "^4.1.42",
"@types/xml2js": "^0.4.3",
"npm-check": "^5.7.1",
"tsconfig-lint": "^0.12.0",
"tslint": "^5.11.0",
"typescript": "^2.9.2"
},
"scripts": {
"prepublish": "npm run tsd && node ts",
"test": "node ts --only-test",
"tsd": "tsd reinstall --overwrite"
"prepublishOnly": "npm run build",
"compile": "tsc",
"build": "tsc",
"test": "tslint --project .",
"start": "node ./bin/crunchy"
},
"bugs": {
"url": "https://github.com/Godzil/Crunchy/issues"
}
}

View File

@@ -1,25 +1,125 @@
'use strict';
export = main;
import commander = require('commander');
import fs = require('fs');
import path = require('path');
import series = require('./series');
import typings = require('./typings');
import log = require('./log');
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.
*/
function main(args: string[], done: (err?: Error) => void) {
var config = parse(args);
var batchPath = path.join(config.output || process.cwd(), 'CrunchyRoll.txt');
tasks(config, batchPath, (err, tasks) => {
if (err) return done(err);
var i = 0;
(function next() {
if (i >= tasks.length) return done();
series(tasks[i].config, tasks[i].address, err => {
if (err) return done(err);
i += 1;
export default function(args: string[], done: (err?: Error) => void)
{
const config = parse(args);
const batchPath = path.join(config.output || process.cwd(), config.batch);
// 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;
}
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[0].address === '')
{
return done();
}
let i = 0;
(function next()
{
if (i >= tasksArr.length)
{
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 (tasksArr[i].retry <= 0)
{
log.error(JSON.stringify(errin));
if (config.debug)
{
log.dumpToDebug('BatchGiveUp', JSON.stringify(errin));
}
log.error('Cannot get episodes from "' + tasksArr[i].address + '", please rerun later');
/* Go to the next on the list */
i += 1;
}
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;
}
next();
});
})();
@@ -29,42 +129,160 @@ function main(args: string[], done: (err?: Error) => void) {
/**
* Splits the value into arguments.
*/
function split(value: string): string[] {
var inQuote = false;
var i: number;
var pieces: string[] = [];
var previous = 0;
for (i = 0; i < value.length; i += 1) {
if (value.charAt(i) === '"') inQuote = !inQuote;
if (!inQuote && value.charAt(i) === ' ') {
function split(value: string): string[]
{
let inQuote = false;
let i: number;
const pieces: string[] = [];
let previous = 0;
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]);
previous = i + 1;
}
}
pieces.push(value.substring(previous, i).match(/^"?(.+?)"?$/)[1]);
const lastPiece = value.substring(previous, i).match(/^"?(.+?)"?$/);
if (lastPiece)
{
pieces.push(lastPiece[1]);
}
return pieces;
}
function get_min_filter(filter: string): number
{
if (filter !== undefined)
{
const tok = filter.split('-');
if (tok.length > 2)
{
log.error('Invalid episode filter \'' + filter + '\'');
process.exit(-1);
}
if (tok[0] !== '')
{
return parseInt(tok[0], 10);
}
}
return 0;
}
function get_max_filter(filter: string): number
{
if (filter !== undefined)
{
const tok = filter.split('-');
if (tok.length > 2)
{
log.error('Invalid episode filter \'' + filter + '\'');
process.exit(-1);
}
if ((tok.length > 1) && (tok[1] !== ''))
{
/* We have a max value */
return parseInt(tok[1], 10);
}
else if ((tok.length === 1) && (tok[0] !== ''))
{
/* A single episode has been requested */
return parseInt(tok[0], 10);
}
}
return +Infinity;
}
/**
* Check that URL start with http:// or https://
* As for some reason request just return an error but a useless one when that happen so check it
* soon enough.
*/
function checkURL(address: string): boolean
{
if (address.startsWith('http:\/\/'))
{
return true;
}
if (address.startsWith('http:\/\/'))
{
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.
*/
function tasks(config: typings.IConfigLine, batchPath: string, done: (err: Error, tasks?: typings.IConfigTask[]) => void) {
if (config.args.length) {
return done(null, config.args.map(address => {
return {address: address, config: config};
function tasks(config: IConfigLine, batchPath: string, done: (err: Error, tasks?: IConfigTask[]) => void)
{
if (config.args.length)
{
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: 0, episode_max: 0};
}));
}
fs.exists(batchPath, exists => {
if (!exists) return done(null, []);
fs.readFile(batchPath, 'utf8', (err, data) => {
if (err) return done(err);
var map: typings.IConfigTask[] = [];
data.split(/\r?\n/).forEach(line => {
if (/^(\/\/|#)/.test(line)) return;
var lineConfig = parse(process.argv.concat(split(line)));
lineConfig.args.forEach(address => {
if (!address) return;
map.push({address: address, config: lineConfig});
fs.exists(batchPath, (exists) =>
{
if (!exists)
{
return done(null, []);
}
fs.readFile(batchPath, 'utf8', (err, data) =>
{
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);
@@ -75,7 +293,8 @@ function tasks(config: typings.IConfigLine, batchPath: string, done: (err: Error
/**
* Parses the arguments and returns a configuration.
*/
function parse(args: string[]): typings.IConfigLine {
function parse(args: string[]): IConfigLine
{
return new commander.Command().version(require('../package').version)
// Authentication
.option('-p, --pass <s>', 'The password.')
@@ -83,13 +302,19 @@ function parse(args: string[]): typings.IConfigLine {
// Disables
.option('-c, --cache', 'Disables the cache.')
.option('-m, --merge', 'Disables merging subtitles and videos.')
// Filters
.option('-e, --episode <i>', 'The episode filter.')
.option('-v, --volume <i>', 'The volume filter.')
// Episode filter
.option('-e, --episodes <s>', 'Episode list. Read documentation on how to use')
// Settings
.option('-f, --format <s>', 'The subtitle format. (Default: ass)')
.option('-f, --format <s>', 'The subtitle format.', 'ass')
.option('-o, --output <s>', 'The output path.')
.option('-s, --series <s>', 'The series override.')
.option('-t, --tag <s>', 'The subgroup. (Default: CrunchyRoll)')
.option('-s, --series <s>', 'The series name override.')
.option('-n, --nametmpl <s>', 'Output name template', '{SERIES_TITLE} - s{SEASON_NUMBER}e{EPISODE_NUMBER} - {EPISODE_TITLE} - [{TAG}]')
.option('-t, --tag <s>', 'The subgroup.', 'CrunchyRoll')
.option('-r, --resolution <s>', 'The video resolution. (valid: 360, 480, 720, 1080)', '1080')
.option('-b, --batch <s>', 'Batch file', 'CrunchyRoll.txt')
.option('--verbose', 'Make tool verbose')
.option('--debug', 'Create a debug file. Use only if requested!')
.option('--rebuildcrp', 'Rebuild the crpersistant file.')
.option('--retry <i>', 'Number or time to retry fetching an episode.', 5)
.parse(args);
}

View File

@@ -1,6 +1,41 @@
'use strict';
import batch = require('./batch');
import batch from './batch';
import request = require('request');
import log = require('./log');
import pjson = require('pjson');
batch(process.argv, (err: any) => {
if (err) console.error(err.stack || err);
const current_version = pjson.version;
/* Check if the current version is the latest */
log.info('Crunchy version ' + current_version);
request.get({ uri: 'https://raw.githubusercontent.com/Godzil/Crunchy/master/package.json' },
(error: Error, response: any, body: any) =>
{
const onlinepkg = JSON.parse(body);
let tmp = current_version.split('.');
const cur = (Number(tmp[0]) * 10000) + (Number(tmp[1]) * 100) + Number(tmp[2]);
tmp = onlinepkg.version.split('.');
const dist = (Number(tmp[0]) * 10000) + (Number(tmp[1]) * 100) + Number(tmp[2]);
if (dist > cur)
{
log.warn('There is a newer version of crunchy (v' + onlinepkg.version + '), you should update!');
}
});
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);
});

View File

@@ -1,23 +1,33 @@
'use strict';
export = main;
import cheerio = require('cheerio');
import fs = require('fs');
import mkdirp = require('mkdirp');
import request = require('./request');
import my_request = require('./my_request');
import path = require('path');
import subtitle = require('./subtitle/index');
import typings = require('./typings');
import video = require('./video/index');
import subtitle from './subtitle/index';
import video from './video/index';
import xml2js = require('xml2js');
import log = require('./log');
/**
* Streams the episode to disk.
*/
function main(config: typings.IConfig, address: string, done: (err: Error) => void) {
scrapePage(config, address, (err, page) => {
if (err) return done(err);
scrapePlayer(config, address, page.id, (err, player) => {
if (err) return done(err);
export default function(config: IConfig, address: string, done: (err: Error, ign: boolean) => void)
{
scrapePage(config, address, (err, page) =>
{
if (err)
{
return done(err, false);
}
scrapePlayer(config, address, page.id, (errS, player) =>
{
if (errS)
{
return done(errS, false);
}
download(config, page, player, done);
});
});
@@ -26,37 +36,133 @@ function main(config: typings.IConfig, address: string, done: (err: Error) => vo
/**
* Completes a download and writes the message with an elapsed time.
*/
function complete(message: string, begin: number, done: (err: Error) => void) {
var timeInMs = Date.now() - begin;
var seconds = prefix(Math.floor(timeInMs / 1000) % 60, 2);
var minutes = prefix(Math.floor(timeInMs / 1000 / 60) % 60, 2);
var hours = prefix(Math.floor(timeInMs / 1000 / 60 / 60), 2);
console.log(message + ' (' + hours + ':' + minutes + ':' + seconds + ')');
done(null);
function complete(epName: string, message: string, begin: number, done: (err: Error, ign: boolean) => void)
{
const timeInMs = Date.now() - begin;
const seconds = prefix(Math.floor(timeInMs / 1000) % 60, 2);
const minutes = prefix(Math.floor(timeInMs / 1000 / 60) % 60, 2);
const hours = prefix(Math.floor(timeInMs / 1000 / 60 / 60), 2);
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)
{
return str.replace(/[\/':\?\*"<>\.]/g, '_');
}
/**
* Downloads the subtitle and video.
*/
function download(config: typings.IConfig, page: typings.IEpisodePage, player: typings.IEpisodePlayer, done: (err: Error) => void) {
var series = config.series || page.series;
var fileName = name(config, page, series);
var filePath = path.join(config.output || process.cwd(), series, fileName);
mkdirp(path.dirname(filePath), (err: Error) => {
if (err) return done(err);
downloadSubtitle(config, player, filePath, err => {
if (err) return done(err);
var now = Date.now();
console.log('Fetching ' + fileName);
downloadVideo(config, page, player, filePath, err => {
if (err) return done(err);
if (config.merge) return complete('Finished ' + fileName, now, done);
var isSubtited = Boolean(player.subtitle);
video.merge(config, isSubtited, player.video.file, filePath, err => {
if (err) return done(err);
complete('Finished ' + fileName, now, done);
function download(config: IConfig, page: IEpisodePage, player: IEpisodePlayer, done: (err: Error, ign: boolean) => void)
{
const serieFolder = sanitiseFileName(config.series || page.series);
let fileName = sanitiseFileName(generateName(config, page));
let filePath = path.join(config.output || process.cwd(), serieFolder, fileName);
if (fileExist(filePath + '.mkv'))
{
let count = 0;
if (config.rebuildcrp)
{
log.warn('Adding \'' + fileName + '\' to the DB...');
return done(null, false);
}
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);
}
mkdirp(path.dirname(filePath), (errM: Error) =>
{
if (errM)
{
log.dispEpisode(fileName, 'Error...', true);
return done(errM, false);
}
log.dispEpisode(fileName, 'Fetching...', false);
downloadSubtitle(config, player, filePath, (errDS) =>
{
if (errDS)
{
log.dispEpisode(fileName, 'Error...', true);
return done(errDS, false);
}
const now = Date.now();
if (player.video.file !== undefined)
{
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);
}
const isSubtited = Boolean(player.subtitle);
log.dispEpisode(fileName, 'Merging...', false);
video.merge(config, isSubtited, player.video.file, filePath, player.video.mode, 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);
}
});
});
}
@@ -64,15 +170,32 @@ function download(config: typings.IConfig, page: typings.IEpisodePage, player: t
/**
* Saves the subtitles to disk.
*/
function downloadSubtitle(config: typings.IConfig, player: typings.IEpisodePlayer, filePath: string, done: (err?: Error) => void) {
var enc = player.subtitle;
if (!enc) return done();
subtitle.decode(enc.id, enc.iv, enc.data, (err, data) => {
if (err) return done(err);
var formats = subtitle.formats;
var format = formats[config.format] ? config.format : 'ass';
formats[format](data, (err: Error, decodedSubtitle: string) => {
if (err) return done(err);
function downloadSubtitle(config: IConfig, player: IEpisodePlayer, filePath: string, done: (err?: Error) => void)
{
const enc = player.subtitle;
if (!enc)
{
return done();
}
subtitle.decode(enc.id, enc.iv, enc.data, (errSD, data) =>
{
if (errSD)
{
return done(errSD);
}
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);
});
});
@@ -81,91 +204,187 @@ function downloadSubtitle(config: typings.IConfig, player: typings.IEpisodePlaye
/**
* Streams the video to disk.
*/
function downloadVideo(config: typings.IConfig,
page: typings.IEpisodePage,
player: typings.IEpisodePlayer,
filePath: string,
done: (err: Error) => void) {
video.stream(
player.video.host,
player.video.file,
page.swf,
filePath + path.extname(player.video.file),
done);
function downloadVideo(config: IConfig, page: IEpisodePage, player: IEpisodePlayer,
filePath: string, done: (err: Error) => void)
{
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.
*/
function name(config: typings.IConfig, page: typings.IEpisodePage, series: string) {
var episode = (page.episode < 10 ? '0' : '') + page.episode;
var volume = (page.volume < 10 ? '0' : '') + page.volume;
var tag = config.tag || 'CrunchyRoll';
return series + ' ' + volume + 'x' + episode + ' [' + tag + ']';
function generateName(config: IConfig, page: IEpisodePage, extra = '')
{
const episodeNum = parseInt(page.episode, 10);
const volumeNum = parseInt(page.volume, 10);
const episode = (episodeNum < 10 ? '0' : '') + page.episode;
const volume = (volumeNum < 10 ? '0' : '') + page.volume;
const tag = config.tag || 'CrunchyRoll';
const series = config.series || page.series;
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.
*/
function prefix(value: number|string, length: number) {
var valueString = typeof value !== 'string' ? String(value) : value;
while (valueString.length < length) valueString = '0' + valueString;
function prefix(value: number|string, length: number)
{
let valueString = (typeof value !== 'string') ? String(value) : value;
while (valueString.length < length)
{
valueString = '0' + valueString;
}
return valueString;
}
/**
* Requests the page data and scrapes the id, episode, series and swf.
*/
function scrapePage(config: typings.IConfig, address: string, done: (err: Error, page?: typings.IEpisodePage) => void) {
var id = parseInt((address.match(/[0-9]+$/) || ['0'])[0], 10);
if (!id) return done(new Error('Invalid address.'));
request.get(config, address, (err, result) => {
if (err) return done(err);
var $ = cheerio.load(result);
var swf = /^([^?]+)/.exec($('link[rel=video_src]').attr('href'));
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.'));
done(null, {
id: id,
episode: parseInt(data[3], 10),
series: data[1],
swf: swf[1],
volume: parseInt(data[2], 10) || 1
});
function scrapePage(config: IConfig, address: string, done: (err: Error, page?: IEpisodePage) => void)
{
const epId = parseInt((address.match(/[0-9]+$/) || ['0'])[0], 10);
if (!epId)
{
return done(new Error('Invalid address.'));
}
my_request.get(config, address, (err, result) =>
{
if (err)
{
return done(err);
}
const $ = cheerio.load(result);
const swf = /^([^?]+)/.exec($('link[rel=video_src]').attr('href'));
const regexp = /\s*([^\n\r\t\f]+)\n?\s*[^0-9]*([0-9][\-0-9.]*)?,?\n?\s\s*[^0-9]*((PV )?[S0-9][P0-9.]*[a-fA-F]?)/;
const look = $('#showmedia_about_media').text();
const seasonTitle = $('span[itemprop="title"]').text();
const episodeTitle = $('#showmedia_about_name').text().replace(/[“”]/g, '');
const data = regexp.exec(look);
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: '',
});
}
else
{
done(null, {
episode: data[3],
id: epId,
series: data[1],
season: seasonTitle,
title: episodeTitle,
swf: swf[1],
volume: data[2] || '1',
filename: '',
});
}
});
}
/**
* Requests the player data and scrapes the subtitle and video data.
*/
function scrapePlayer(config: typings.IConfig, address: string, id: number, done: (err: Error, player?: typings.IEpisodePlayer) => void) {
var url = address.match(/^(https?:\/\/[^\/]+)/);
if (!url) return done(new Error('Invalid address.'));
request.post(config, {
form: {current_page: address},
url: url[1] + '/xml/?req=RpcApiVideoPlayer_GetStandardConfig&media_id=' + id
}, (err, result) => {
if (err) return done(err);
function scrapePlayer(config: IConfig, address: string, id: number, done: (err: Error, player?: IEpisodePlayer) => void)
{
const url = address.match(/^(https?:\/\/[^\/]+)/);
if (!url)
{
return done(new Error('Invalid address.'));
}
my_request.post(config, {
form: {
current_page: address,
video_format: config.video_format,
video_quality: config.video_quality,
media_id: id
},
url: url[1] + '/xml/?req=RpcApiVideoPlayer_GetStandardConfig&media_id=' + id,
}, (err, result) =>
{
if (err)
{
return done(err);
}
xml2js.parseString(result, {
explicitArray: false,
explicitRoot: false
}, (err: Error, player: typings.IEpisodePlayerConfig) => {
if (err) return done(err);
try {
var isSubtitled = Boolean(player['default:preload'].subtitle);
explicitRoot: false,
}, (errPS: Error, player: IEpisodePlayerConfig) =>
{
if (errPS)
{
return done(errPS);
}
try
{
const isSubtitled = Boolean(player['default:preload'].subtitle);
let streamMode = 'RTMP';
if (player['default:preload'].stream_info.host === '')
{
streamMode = 'HLS';
}
done(null, {
subtitle: isSubtitled ? {
data: player['default:preload'].subtitle.data,
id: parseInt(player['default:preload'].subtitle.$.id, 10),
iv: player['default:preload'].subtitle.iv,
data: player['default:preload'].subtitle.data
} : null,
video: {
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);
}
});

View File

@@ -1,4 +1,5 @@
'use strict';
export import batch = require('./batch');
export import episode = require('./episode');
export import series = require('./series');
import batch from './batch';
import episode from './episode';
import series from './series';
export {batch, episode, series};

23
src/interface/IConfig.d.ts vendored Normal file
View File

@@ -0,0 +1,23 @@
interface IConfig {
// Authentication
pass?: string;
user?: string;
// Disables
cache?: boolean;
merge?: boolean;
episodes?: string;
// Settings
format?: string;
output?: string;
series?: string;
nametmpl?: string;
tag?: string;
resolution?: string;
video_format?: string;
video_quality?: string;
rebuildcrp?: boolean;
batch?: string;
verbose?: boolean;
debug?: boolean;
retry?: number;
}

3
src/interface/IConfigLine.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
interface IConfigLine extends IConfig {
args: string[];
}

6
src/interface/IConfigTask.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
interface IConfigTask {
address: string;
retry: number;
episode_min: number;
episode_max: number;
}

10
src/interface/IEpisodePage.d.ts vendored Normal file
View File

@@ -0,0 +1,10 @@
interface IEpisodePage {
id: number;
episode: string;
series: string;
volume: string;
season: string;
title: string;
swf: string;
filename: string;
}

12
src/interface/IEpisodePlayer.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
interface IEpisodePlayer {
subtitle?: {
id: number;
iv: string;
data: string;
};
video: {
mode: string;
file: string;
host: string;
};
}

15
src/interface/IEpisodePlayerConfig.d.ts vendored Normal file
View File

@@ -0,0 +1,15 @@
interface IEpisodePlayerConfig {
'default:preload': {
subtitle: {
$: {
id: string;
};
iv: string;
data: string;
};
stream_info: {
file: string;
host: string;
};
};
}

3
src/interface/IFormatterTable.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
interface IFormatterTable {
[key: string]: (config: IConfig, input: string|Buffer, done: (err: Error, subtitle?: string) => void) => void;
}

4
src/interface/IResolData.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
interface IResolData {
quality: string;
format: string;
}

4
src/interface/ISeries.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
interface ISeries {
episodes: ISeriesEpisode[];
series: string;
}

6
src/interface/ISeriesEpisode.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
interface ISeriesEpisode {
address: string;
episode: string;
volume: number;
retry: number;
}

13
src/interface/ISubtitle.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
interface ISubtitle {
$: {
title: string;
wrap_style: string;
play_res_x: string;
play_res_y: string;
id: string;
lang_string: string;
created: string;
};
events: ISubtitleEvent;
styles: ISubtitleStyle;
}

15
src/interface/ISubtitleEvent.d.ts vendored Normal file
View File

@@ -0,0 +1,15 @@
interface ISubtitleEvent {
event: {
$: {
end: string;
start: string;
style: string;
name: string;
margin_l: string;
margin_r: string;
margin_v: string;
effect: string;
text: string;
};
}[];
}

29
src/interface/ISubtitleStyle.d.ts vendored Normal file
View File

@@ -0,0 +1,29 @@
interface ISubtitleStyle {
style: {
$: {
name: string;
font_name: string;
font_size: string;
primary_colour: string;
secondary_colour: string;
outline_colour: string;
back_colour: string;
bold: string;
italic: string;
underline: string;
strikeout: string;
scale_x: string;
scale_y: string;
spacing: string;
angle: string;
border_style: string;
outline: string;
shadow: string;
alignment: string;
margin_l: string;
margin_r: string;
margin_v: string;
encoding: string;
};
}[];
}

49
src/log.ts Normal file
View File

@@ -0,0 +1,49 @@
'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 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');
}

226
src/my_request.ts Normal file
View File

@@ -0,0 +1,226 @@
'use strict';
import cheerio = require('cheerio');
import request = require('request');
import rp = require('request-promise');
import Promise = require('bluebird');
import log = require('./log');
import { RequestPromise } from 'request-promise';
import { Response } from 'request';
// tslint:disable-next-line:no-var-requires
const cloudscraper = require('cloudscraper');
let isAuthenticated = false;
let isPremium = false;
const defaultHeaders: request.Headers =
{
'User-Agent': 'Mozilla/5.0 (Windows NT 6.2; WOW64; x64; rv:58.0) Gecko/20100101 Firefox/58.0',
'Connection': 'keep-alive',
'Referer': 'https://www.crunchyroll.com/login',
};
function generateDeviceId(): string
{
let id = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 32; i++)
{
id += possible.charAt(Math.floor(Math.random() * possible.length));
}
return id;
}
function startSession(): Promise<string>
{
return rp(
{
method: 'GET',
url: 'CR_SESSION_URL',
qs:
{
device_id: generateDeviceId(),
device_type: 'CR_DEVICE_TYPE',
access_token: 'CR_SESSION_KEY',
version: 'CR_API_VERSION',
locale: 'CR_LOCALE',
},
json: true,
})
.then((response: any) =>
{
return response.data.session_id;
});
}
function login(sessionId: string, user: string, pass: string): Promise<any>
{
return rp(
{
method: 'POST',
url: 'CR_LOGIN_URL',
form:
{
account: user,
password: pass,
session_id: sessionId,
version: 'CR_API_VERSION',
},
json: true,
})
.then((response) =>
{
if (response.error) throw new Error('Login failed: ' + response.message);
return response.data;
});
}
// TODO: logout
/**
* Performs a GET request for the resource.
*/
export function get(config: IConfig, options: string|request.Options, done: (err: any, result?: string) => void)
{
authenticate(config, (err) =>
{
if (err)
{
return done(err);
}
cloudscraper.request(modify(options, 'GET'), (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, options: request.Options, done: (err: Error, result?: string) => void)
{
authenticate(config, (err) =>
{
if (err)
{
return done(err);
}
cloudscraper.request(modify(options, 'POST'), (error: Error, response: any, body: any) =>
{
if (error)
{
return done(error);
}
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)
{
return done(null);
}
if (!config.pass || !config.user)
{
log.error('You need to give login/password to use Crunchy');
process.exit(-1);
}
startSession()
.then((sessionId: string) =>
{
defaultHeaders.Cookie = `sess_id=${sessionId}; c_locale=enUS`;
return login(sessionId, config.user, config.pass);
})
.then((userData) =>
{
/**
* The page return with a meta based redirection, as we wan't to check that everything is fine, reload
* the main page. A bit convoluted, but more sure.
*/
const options =
{
headers: defaultHeaders,
jar: true,
url: 'http://www.crunchyroll.com/',
method: 'GET',
};
cloudscraper.request(options, (err: Error, rep: string, body: string) =>
{
if (err)
{
return done(err);
}
const $ = cheerio.load(body);
/* Check if auth worked */
const regexps = /ga\('set', 'dimension[5-8]', '([^']*)'\);/g;
const dims = regexps.exec($('script').text());
for (let i = 1; i < 5; i++)
{
if ((dims[i] !== undefined) && (dims[i] !== '') && (dims[i] !== 'not-registered'))
{
isAuthenticated = true;
}
if ((dims[i] === 'premium') || (dims[i] === 'premiumplus'))
{
isPremium = true;
}
}
if (isAuthenticated === false)
{
const error = $('ul.message, li.error').text();
log.error('Authentication failed: ' + error);
process.exit(-1);
}
if (isPremium === false)
{
log.warn('Do not use this app without a premium account.');
}
else
{
log.info('You have a premium account! Good!');
}
done(null);
});
})
.catch(done);
}
/**
* Modifies the options to use the authenticated cookie jar.
*/
function modify(options: string|request.Options, reqMethod: string): request.Options
{
if (typeof options !== 'string')
{
options.jar = true;
options.headers = defaultHeaders;
options.method = reqMethod;
return options;
}
return {
jar: true,
headers: defaultHeaders,
url: options.toString(),
method: reqMethod
};
}

View File

@@ -1,63 +0,0 @@
'use strict';
import request = require('request');
import typings = require('./typings');
var isAuthenticated = false;
/**
* Performs a GET request for the resource.
*/
export function get(config: typings.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: typings.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: typings.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()};
}

View File

@@ -1,34 +1,128 @@
'use strict';
export = main;
import cheerio = require('cheerio');
import episode = require('./episode');
import fs = require('fs');
import request = require('./request');
import episode from './episode';
// import fs = require('fs');
import fs = require('fs-extra');
import my_request = require('./my_request');
import path = require('path');
import typings = require('./typings');
import url = require('url');
var persistent = '.crpersistent';
import log = require('./log');
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.
*/
function main(config: typings.IConfig, address: string, done: (err: Error) => void) {
var persistentPath = path.join(config.output || process.cwd(), persistent);
fs.readFile(persistentPath, 'utf8', (err, contents) => {
var cache = config.cache ? {} : JSON.parse(contents || '{}');
page(config, address, (err, page) => {
if (err) return done(err);
var i = 0;
(function next() {
export default function(config: IConfig, task: IConfigTask, done: (err: any) => void)
{
const persistentPath = path.join(config.output || process.cwd(), persistent);
/* Make a backup of the persistent file in case of */
if (fileExist(persistentPath))
{
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.syscall === 'getaddrinfo') && (reqErr.errno === 'ENOTFOUND'))
{
log.error('The URL \'' + task.address + '\' is invalid, please check => I\'m ignoring it.');
}
return done(errP);
}
let i = 0;
(function next()
{
if (config.debug)
{
log.dumpToDebug('Episode ' + i, JSON.stringify(page.episodes[i]));
}
if (i >= page.episodes.length) return done(null);
download(cache, config, address, page.episodes[i], err => {
if (err) return done(err);
var newCache = JSON.stringify(cache, null, ' ');
fs.writeFile(persistentPath, newCache, err => {
if (err) return done(err);
i += 1;
download(cache, config, task, page.episodes[i], (errD, ignored) =>
{
if (errD)
{
/* Check if domain is valid */
const reqErr = errD.error;
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;
}
next();
});
}
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;
next();
});
}
else
{
i += 1;
next();
}
}
});
})();
});
@@ -38,60 +132,108 @@ function main(config: typings.IConfig, address: string, done: (err: Error) => vo
/**
* Downloads the episode.
*/
function download(cache: {[address: string]: number},
config: typings.IConfig,
baseAddress: string,
item: typings.ISeriesEpisode,
done: (err: Error) => void) {
if (!filter(config, item)) return done(null);
var address = url.resolve(baseAddress, item.address);
if (cache[address]) return done(null);
episode(config, address, err => {
if (err) return done(err);
function download(cache: {[address: string]: number}, config: IConfig,
task: IConfigTask, item: ISeriesEpisode,
done: (err: any, ign: boolean) => void)
{
const episodeNumber = parseInt(item.episode, 10);
if ( (episodeNumber < task.episode_min) ||
(episodeNumber > task.episode_max) )
{
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();
done(null);
done(null, ignored);
});
}
/**
* Filters the item based on the configuration.
*/
function filter(config: typings.IConfig, item: typings.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.
*/
function page(config: typings.IConfig, address: string, done: (err: Error, result?: typings.ISeries) => void) {
request.get(config, address, (err, result) => {
if (err) return done(err);
var $ = cheerio.load(result);
var title = $('span[itemprop=name]').text();
if (!title) return done(new Error('Invalid page.'));
var episodes: typings.ISeriesEpisode[] = [];
$('.episode').each((i, el) => {
if ($(el).children('img[src*=coming_soon]').length) return;
var volume = /([0-9]+)\s*$/.exec($(el).closest('ul').prev('a').text());
var regexp = /Episode\s+([0-9]+)\s*$/i;
var episode = regexp.exec($(el).children('.series-title').text());
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
});
function pageScrape(config: IConfig, task: IConfigTask, done: (err: any, result?: ISeries) => void)
{
if (task.address[0] === '@')
{
log.info('Trying to fetch from ' + task.address.substr(1));
const episodes: ISeriesEpisode[] = [];
episodes.push({
address: task.address.substr(1),
episode: '',
volume: 0,
retry: config.retry,
});
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 = $('span[itemprop=name]').text();
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[] = [];
$('.episode').each((i, el) => {
if ($(el).children('img[src*=coming_soon]').length) {
return;
}
const volume = /([0-9]+)\s*$/.exec($(el).closest('ul').prev('a').text());
const regexp = /Episode\s+((PV )?[S0-9][\-P0-9.]*[a-fA-F]?)\s*$/i;
const episode = regexp.exec($(el).children('.series-title').text());
const url = $(el).attr('href');
if ((!url) || (!episode)) {
return;
}
episodeCount += 1;
episodes.push({
address: url,
episode: episode[1],
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});
});
}
}

View File

@@ -1,17 +1,20 @@
/* tslint:disable:no-bitwise false */
'use strict';
export = main;
import crypto = require('crypto');
import bigInt = require('big-integer');
import crypto = require('crypto');
import zlib = require('zlib');
/**
* Decodes the data.
*/
function main(id: number, iv: Buffer|string, data: Buffer|string, done: (err?: Error, result?: Buffer) => void) {
try {
export default function(id: number, iv: Buffer|string, data: Buffer|string,
done: (err?: Error, result?: Buffer) => void)
{
try
{
decompress(decrypt(id, iv, data), done);
} catch (e) {
} catch (e)
{
done(e);
}
}
@@ -19,21 +22,27 @@ function main(id: number, iv: Buffer|string, data: Buffer|string, done: (err?: E
/**
* Decrypts the data.
*/
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;
var decipher = crypto.createDecipheriv('aes-256-cbc', key(id), ivBuffer);
function decrypt(id: number, iv: Buffer|string, data: Buffer|string)
{
const ivBuffer = typeof iv === 'string' ? Buffer.from(iv, 'base64') : iv;
const dataBuffer = typeof data === 'string' ? Buffer.from(data, 'base64') : data;
const decipher = crypto.createDecipheriv('aes-256-cbc', key(id), ivBuffer);
decipher.setAutoPadding(false);
return Buffer.concat([decipher.update(dataBuffer), decipher.final()]);
}
/**
* Decompresses the data.
*/
function decompress(data: Buffer, done: (err: Error, result?: Buffer) => void) {
try {
function decompress(data: Buffer, done: (err: Error, result?: Buffer) => void)
{
try
{
zlib.inflate(data, done);
} catch (e) {
} catch (e)
{
done(null, data);
}
}
@@ -41,36 +50,45 @@ function decompress(data: Buffer, done: (err: Error, result?: Buffer) => void) {
/**
* Generates a key.
*/
function key(subtitleId: number): Buffer {
var hash = secret(20, 97, 1, 2) + magic(subtitleId);
var result = new Buffer(32);
function key(subtitleId: number): Buffer
{
const hash = secret(20, 97, 1, 2) + magic(subtitleId);
const result = Buffer.allocUnsafe(32);
result.fill(0);
crypto.createHash('sha1').update(hash).digest().copy(result);
return result;
}
/**
* Generates a magic 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();
var multipliedHash = bigInt(hash).multiply(32).toJSNumber();
function magic(subtitleId: number): number
{
const base = Math.floor(Math.sqrt(6.9) * Math.pow(2, 25));
const hash = bigInt(base).xor(subtitleId).toJSNumber();
const multipliedHash = bigInt(hash).multiply(32).toJSNumber();
return bigInt(hash).xor(hash >> 3).xor(multipliedHash).toJSNumber();
}
/**
* Generates a secret string based on a Fibonacci sequence.
*/
function secret(size: number, modulo: number, firstSeed: number, secondSeed: number): string {
var currentValue = firstSeed + secondSeed;
var previousValue = secondSeed;
var result = '';
for (var i = 0; i < size; i += 1) {
var oldValue = currentValue;
function secret(size: number, modulo: number, firstSeed: number, secondSeed: number): string
{
let currentValue = firstSeed + secondSeed;
let previousValue = secondSeed;
let result = '';
for (let i = 0; i < size; i += 1)
{
const oldValue = currentValue;
result += String.fromCharCode(currentValue % modulo + 33);
currentValue += previousValue;
previousValue = oldValue;
}
return result;
}

View File

@@ -1,22 +1,28 @@
'use strict';
export = main;
import xml2js = require('xml2js');
import typings = require('../../typings');
/**
* Converts an input buffer to a SubStation Alpha subtitle.
*/
function main(input: string|Buffer, done: (err: Error, subtitle?: string) => void) {
export default function(config: IConfig, input: string|Buffer, done: (err: Error, subtitle?: string) => void)
{
xml2js.parseString(input.toString(), {
explicitArray: false,
explicitRoot: false
}, (err: Error, xml: typings.ISubtitle) => {
if (err) return done(err);
try {
done(null, script(xml) + '\n' +
style(xml.styles) + '\n' +
event(xml.events));
} catch (err) {
}, (err: Error, xml: ISubtitle) =>
{
if (err)
{
return done(err);
}
try
{
done(null, script(config, xml) + '\n' +
style(xml.styles) + '\n' +
event(config, xml.events));
} catch (err)
{
done(err);
}
});
@@ -25,69 +31,74 @@ function main(input: string|Buffer, done: (err: Error, subtitle?: string) => voi
/**
* Converts the event block.
*/
function event(block: typings.ISubtitleEvent): string {
var format = 'Layer,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text';
function event(config: IConfig, block: ISubtitleEvent): string
{
const format = 'Layer,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text';
return '[Events]\n' +
'Format: ' + format + '\n' +
[].concat(block.event).map(style => ('Dialogue: 0,' +
style.$.start + ',' +
style.$.end + ',' +
style.$.style + ',' +
style.$.name + ',' +
style.$.margin_l + ',' +
style.$.margin_r + ',' +
style.$.margin_v + ',' +
style.$.effect + ',' +
style.$.text)).join('\n') + '\n';
'Format: ' + format + '\n' + [].concat(block.event).map((style) => ('Dialogue: 0,' +
style.$.start + ',' +
style.$.end + ',' +
style.$.style + ',' +
style.$.name + ',' +
style.$.margin_l + ',' +
style.$.margin_r + ',' +
style.$.margin_v + ',' +
style.$.effect + ',' +
style.$.text)).join('\n') + '\n';
}
/**
* Converts the script block.
*/
function script(block: typings.ISubtitle): string {
function script(config: IConfig, block: ISubtitle): string
{
return '[Script Info]\n' +
'Title: ' + block.$.title + '\n' +
'ScriptType: v4.00+\n' +
'WrapStyle: ' + block.$.wrap_style + '\n' +
'PlayResX: ' + block.$.play_res_x + '\n' +
'PlayResY: ' + block.$.play_res_y + '\n' +
'Subtitle ID: ' + block.$.id + '\n' +
'Language: ' + block.$.lang_string + '\n' +
'Created: ' + block.$.created + '\n';
'Origin: Downloaded from Crunchyroll.com by ' + config.user + '\n' +
'Title: ' + block.$.title + '\n' +
'ScriptType: v4.00+\n' +
'WrapStyle: ' + block.$.wrap_style + '\n' +
'PlayResX: ' + block.$.play_res_x + '\n' +
'PlayResY: ' + block.$.play_res_y + '\n' +
'Subtitle ID: ' + block.$.id + '\n' +
'Language: ' + block.$.lang_string + '\n' +
'Created: ' + block.$.created + '\n';
}
/**
* Converts the style block.
*/
function style(block: typings.ISubtitleStyle): string {
var format = 'Name,Fontname,Fontsize,PrimaryColour,SecondaryColour,' +
'OutlineColour,BackColour,Bold,Italic,Underline,StrikeOut,ScaleX,' +
'ScaleY,Spacing,Angle,BorderStyle,Outline,Shadow,Alignment,' +
'MarginL,MarginR,MarginV,Encoding';
function style(block: ISubtitleStyle): string
{
const format = 'Name,Fontname,Fontsize,PrimaryColour,SecondaryColour,' +
'OutlineColour,BackColour,Bold,Italic,Underline,StrikeOut,ScaleX,' +
'ScaleY,Spacing,Angle,BorderStyle,Outline,Shadow,Alignment,' +
'MarginL,MarginR,MarginV,Encoding';
return '[V4+ Styles]\n' +
'Format: ' + format + '\n' +
[].concat(block.style).map(style => 'Style: ' +
style.$.name + ',' +
style.$.font_name + ',' +
style.$.font_size + ',' +
style.$.primary_colour + ',' +
style.$.secondary_colour + ',' +
style.$.outline_colour + ',' +
style.$.back_colour + ',' +
style.$.bold + ',' +
style.$.italic + ',' +
style.$.underline + ',' +
style.$.strikeout + ',' +
style.$.scale_x + ',' +
style.$.scale_y + ',' +
style.$.spacing + ',' +
style.$.angle + ',' +
style.$.border_style + ',' +
style.$.outline + ',' +
style.$.shadow + ',' +
style.$.alignment + ',' +
style.$.margin_l + ',' +
style.$.margin_r + ',' +
style.$.margin_v + ',' +
style.$.encoding).join('\n') + '\n';
'Format: ' + format + '\n' + [].concat(block.style).map((style) => 'Style: ' +
style.$.name + ',' +
style.$.font_name + ',' +
style.$.font_size + ',' +
style.$.primary_colour + ',' +
style.$.secondary_colour + ',' +
style.$.outline_colour + ',' +
style.$.back_colour + ',' +
style.$.bold + ',' +
style.$.italic + ',' +
style.$.underline + ',' +
style.$.strikeout + ',' +
style.$.scale_x + ',' +
style.$.scale_y + ',' +
style.$.spacing + ',' +
style.$.angle + ',' +
style.$.border_style + ',' +
style.$.outline + ',' +
style.$.shadow + ',' +
style.$.alignment + ',' +
style.$.margin_l + ',' +
style.$.margin_r + ',' +
style.$.margin_v + ',' +
style.$.encoding).join('\n') + '\n';
}

View File

@@ -1,10 +1,8 @@
'use strict';
export = main;
import ass = require('./ass');
import srt = require('./srt');
import typings = require('../../typings');
import ass from './ass';
import srt from './srt';
var main: typings.IFormatterTable = {
ass: ass,
srt: srt
};
export default {
ass,
srt
} as IFormatterTable;

View File

@@ -1,23 +1,33 @@
'use strict';
export = srt;
import xml2js = require('xml2js');
import typings = require('../../typings');
/**
* Converts an input buffer to a SubRip subtitle.
*/
function srt(input: Buffer|string, done: (err: Error, subtitle?: string) => void) {
var options = {explicitArray: false, explicitRoot: false};
xml2js.parseString(input.toString(), options, (err: Error, xml: typings.ISubtitle) => {
try {
if (err) return done(err);
done(null, xml.events.event.map((event, index) => {
var attributes = event.$;
export default function(config: IConfig, input: Buffer|string, done: (err: Error, subtitle?: string) => void)
{
const options = {explicitArray: false, explicitRoot: false};
xml2js.parseString(input.toString(), options, (err: Error, xml: ISubtitle) =>
{
try
{
if (err)
{
return done(err);
}
done(null, xml.events.event.map((event, index) =>
{
const attributes = event.$;
return (index + 1) + '\n' +
time(attributes.start) + ' --> ' + time(attributes.end) + '\n' +
text(attributes.text) + '\n';
time(attributes.start) + ' --> ' + time(attributes.end) + '\n' +
text(attributes.text) + '\n';
}).join('\n'));
} catch (err) {
} catch (err)
{
done(err);
}
});
@@ -26,41 +36,59 @@ function srt(input: Buffer|string, done: (err: Error, subtitle?: string) => void
/**
* Prefixes a value.
*/
function prefix(value: string, length: number): string {
while (value.length < length) value = '0' + value;
function prefix(value: string, length: number): string
{
while (value.length < length)
{
value = '0' + value;
}
return value;
}
/**
* Suffixes a value.
*/
function suffix(value: string, length: number): string {
while (value.length < length) value = value + '0';
function suffix(value: string, length: number): string
{
while (value.length < length)
{
value = value + '0';
}
return value;
}
/**
* Formats a text value.
*/
function text(value: string): string {
function text(value: string): string
{
return value
.replace(/{\\i1}/g, '<i>').replace(/{\\i0}/g, '</i>')
.replace(/{\\b1}/g, '<b>').replace(/{\\b0}/g, '</b>')
.replace(/{\\u1}/g, '<u>').replace(/{\\u0}/g, '</u>')
.replace(/{[^}]+}/g, '')
.replace(/(\s+)?\\n(\s+)?/ig, '\n')
.trim();
.replace(/{\\i1}/g, '<i>').replace(/{\\i0}/g, '</i>')
.replace(/{\\b1}/g, '<b>').replace(/{\\b0}/g, '</b>')
.replace(/{\\u1}/g, '<u>').replace(/{\\u0}/g, '</u>')
.replace(/{[^}]+}/g, '')
.replace(/(\s+)?\\n(\s+)?/ig, '\n')
.trim();
}
/**
* Formats a time stamp.
*/
function time(value: string): string {
var all = value.match(/^([0-9]+):([0-9]+):([0-9]+)\.([0-9]+)$/);
if (!all) throw new Error('Invalid time.');
var hours = prefix(all[1], 2);
var minutes = prefix(all[2], 2);
var seconds = prefix(all[3], 2);
var milliseconds = suffix(all[4], 3);
function time(value: string): string
{
const all = value.match(/^([0-9]+):([0-9]+):([0-9]+)\.([0-9]+)$/);
if (!all)
{
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;
}

View File

@@ -1,3 +1,4 @@
'use strict';
export import decode = require('./decode');
export import formats = require('./formats/index');
import decode from './decode';
import formats from './formats/index';
export default {decode, formats};

View File

@@ -1,136 +0,0 @@
export interface IConfig {
// Authentication
pass?: string;
user?: string;
// Disables
cache?: boolean;
merge?: boolean;
// Filters
episode?: number;
volume?: number;
// Settings
format?: string;
output?: string;
series?: string;
tag?: string;
}
export interface IConfigLine extends IConfig {
args: string[];
}
export interface IConfigTask {
address: string;
config: IConfigLine;
}
export interface IEpisodePage {
id: number;
episode: number;
series: string;
volume: number;
swf: string;
}
export interface IEpisodePlayer {
subtitle?: {
id: number;
iv: string;
data: string;
};
video: {
file: string;
host: string;
};
}
export interface IEpisodePlayerConfig {
'default:preload': {
subtitle: {
$: {
id: string;
};
iv: string;
data: string;
};
stream_info: {
file: string;
host: string;
};
};
}
export interface IFormatterTable {
[key: string]: (input: string|Buffer, done: (err: Error, subtitle?: string) => void) => void;
}
export interface ISeries {
episodes: ISeriesEpisode[];
series: string;
}
export interface ISeriesEpisode {
address: string;
episode: number;
volume: number;
}
export interface ISubtitle {
$: {
title: string;
wrap_style: string;
play_res_x: string;
play_res_y: string;
id: string;
lang_string: string;
created: string;
};
events: ISubtitleEvent;
styles: ISubtitleStyle;
}
export interface ISubtitleEvent {
event: {
$: {
end: string;
start: string;
style: string;
name: string;
margin_l: string;
margin_r: string;
margin_v: string;
effect: string;
text: string;
};
}[];
}
export interface ISubtitleStyle {
style: {
$: {
name: string;
font_name: string;
font_size: string;
primary_colour: string;
secondary_colour: string;
outline_colour: string;
back_colour: string;
bold: string;
italic: string;
underline: string;
strikeout: string;
scale_x: string;
scale_y: string;
spacing: string;
angle: string;
border_style: string;
outline: string;
shadow: string;
alignment: string;
margin_l: string;
margin_r: string;
margin_v: string;
encoding: string;
};
}[];
}

View File

@@ -1,3 +1,4 @@
'use strict';
export import merge = require('./merge');
export import stream = require('./stream');
import merge from './merge';
import stream from './stream';
export default {merge, stream};

View File

@@ -1,37 +1,71 @@
'use strict';
export = main;
import childProcess = require('child_process');
import fs = require('fs');
import path = require('path');
import os = require('os');
import subtitle = require('../subtitle/index');
import typings = require('../typings');
import path = require('path');
import subtitle from '../subtitle/index';
/**
* Merges the subtitle and video files into a Matroska Multimedia Container.
*/
function main(config: typings.IConfig, isSubtitled: boolean, rtmpInputPath: string, filePath: string, done: (err: Error) => void) {
var subtitlePath = filePath + '.' + (subtitle.formats[config.format] ? config.format : 'ass');
var videoPath = filePath + path.extname(rtmpInputPath);
childProcess.exec(command() + ' ' +
'-o "' + filePath + '.mkv" ' +
'"' + videoPath + '" ' +
(isSubtitled ? '"' + subtitlePath + '"' : ''), {
maxBuffer: Infinity
}, err => {
if (err) return done(err);
unlink(videoPath, subtitlePath, err => {
if (err) unlinkTimeout(videoPath, subtitlePath, 5000);
done(null);
});
export default function(config: IConfig, isSubtitled: boolean, rtmpInputPath: string, filePath: string,
streamMode: string, verbose: boolean, done: (err: Error) => void)
{
const subtitlePath = filePath + '.' + (subtitle.formats[config.format] ? config.format : 'ass');
let videoPath = filePath;
let cp;
if (streamMode === 'RTMP')
{
videoPath += path.extname(rtmpInputPath);
}
else
{
videoPath += '.mp4';
}
cp = childProcess.exec(command() + ' ' +
'-o "' + filePath + '.mkv" ' +
'"' + videoPath + '" ' +
(isSubtitled ? '"' + subtitlePath + '"' : ''), {
maxBuffer: Infinity,
}, (err) =>
{
if (err)
{
return done(err);
}
unlink(videoPath, subtitlePath, (errin) =>
{
if (errin)
{
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.
*/
function command(): string {
if (os.platform() !== 'win32') return 'mkvmerge';
function command(): string
{
if (os.platform() !== 'win32')
{
return 'mkvmerge';
}
return '"' + path.join(__dirname, '../../bin/mkvmerge.exe') + '"';
}
@@ -39,9 +73,15 @@ function command(): string {
* Unlinks the video and subtitle.
* @private
*/
function unlink(videoPath: string, subtitlePath: string, done: (err: Error) => void) {
fs.unlink(videoPath, err => {
if (err) return done(err);
function unlink(videoPath: string, subtitlePath: string, done: (err: Error) => void)
{
fs.unlink(videoPath, (err) =>
{
if (err)
{
return done(err);
}
fs.unlink(subtitlePath, done);
});
}
@@ -49,10 +89,16 @@ function unlink(videoPath: string, subtitlePath: string, done: (err: Error) => v
/**
* Attempts to unlink the video and subtitle with a timeout between each try.
*/
function unlinkTimeout(videoPath: string, subtitlePath: string, timeout: number) {
setTimeout(() => {
unlink(videoPath, subtitlePath, err => {
if (err) unlinkTimeout(videoPath, subtitlePath, timeout);
function unlinkTimeout(videoPath: string, subtitlePath: string, timeout: number)
{
setTimeout(() =>
{
unlink(videoPath, subtitlePath, (err) =>
{
if (err)
{
unlinkTimeout(videoPath, subtitlePath, timeout);
}
});
}, timeout);
}

View File

@@ -1,26 +1,61 @@
'use strict';
export = main;
import childProcess = require('child_process');
import path = require('path');
import os = require('os');
import path = require('path');
import log = require('../log');
/**
* Streams the video to disk.
*/
function main(rtmpUrl: string, rtmpInputPath: string, swfUrl: string, filePath: string, done: (err: Error) => void) {
childProcess.exec(command() + ' ' +
'-r "' + rtmpUrl + '" ' +
'-y "' + rtmpInputPath + '" ' +
'-W "' + swfUrl + '" ' +
'-o "' + filePath + '"', {
maxBuffer: Infinity
}, done);
export default function(rtmpUrl: string, rtmpInputPath: string, swfUrl: string, filePath: string,
fileExt: string, mode: string, verbose: boolean, done: (err: Error) => void)
{
let cp;
let cmd;
if (mode === 'RTMP')
{
cmd = command('rtmpdump') + ' ' +
'-r "' + rtmpUrl + '" ' +
'-y "' + rtmpInputPath + '" ' +
'-W "' + swfUrl + '" ' +
'-o "' + filePath + fileExt + '"';
}
else if (mode === 'HLS')
{
cmd = command('ffmpeg') + ' ' +
'-y -xerror ' +
'-i "' + rtmpInputPath + '" ' +
'-c copy -bsf:a aac_adtstoasc ' +
'"' + filePath + '.mp4"';
}
else
{
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.
*/
function command(): string {
if (os.platform() !== 'win32') return 'rtmpdump';
return '"' + path.join(__dirname, '../../bin/rtmpdump.exe') + '"';
function command(exe: string): string
{
if (os.platform() !== 'win32')
{
return exe;
}
return '"' + path.join(__dirname, '../../bin/' + exe + '.exe') + '"';
}

11
ts.js
View File

@@ -4,9 +4,14 @@ var fs = require('fs');
var path = require('path');
var isTest = process.argv[2] === '--only-test';
// TODO: This file can use some cleaning up. We want to use the tsconfig.json
// and go from there, but then without source maps. That should give us a final
// build output. For now, this legacy build file will remain to do its job.
// 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() {

View File

@@ -1,5 +1,5 @@
{
"version": "1.4.1",
"target": "es6",
"compilerOptions": {
"declaration": true,
"noImplicitAny": true,
@@ -7,35 +7,8 @@
"module": "commonjs",
"outDir": "dist",
"sourceMap": true,
"target": "es5"
},
"filesGlob": [
"src/**/*.ts",
"typings/**/*.ts"
],
"files": [
"src/batch.ts",
"src/cli.ts",
"src/episode.ts",
"src/index.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/typings.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"
]
}
"lib": [
"es2015"
]
}
}

View File

@@ -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"
}
}
}

View File

@@ -1,4 +1,5 @@
{
"extends": "tslint:latest",
"rules": {
"ban": false,
"class-name": true,
@@ -8,47 +9,38 @@
"curly": false,
"eofline": false,
"forin": true,
"indent": [true, 2],
"indent": [true, "spaces", 2],
"interface-name": true,
"jsdoc-format": true,
"label-position": true,
"label-undefined": true,
"max-line-length": [true, 140],
"member-ordering": [true,
"public-before-private",
"static-before-instance",
"variables-before-functions"
],
"member-ordering": false,
"no-shadowed-variable": false,
"array-type": [true, "array"],
"trailing-comma": false,
"no-any": false,
"no-arg": true,
"no-bitwise": true,
"space-within-parens": false,
"no-object-literal-type-assertion": false,
"no-console": [true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-construct": true,
"no-constructor-vars": true,
"no-debugger": true,
"no-duplicate-key": true,
"no-duplicate-variable": true,
"no-empty": true,
"no-eval": true,
"no-string-literal": true,
"no-switch-case-fall-through": true,
"no-trailing-comma": true,
"no-trailing-whitespace": true,
"no-unused-expression": true,
"no-unused-variable": true,
"no-unreachable": true,
"no-use-before-declare": false,
"no-var-requires": true,
"one-line": [true,
"check-catch",
"check-else",
"check-open-brace",
"check-whitespace"
],
"quotemark": [true, "single"],
@@ -66,7 +58,6 @@
"property-declaration": "nospace",
"variable-declaration": "nospace"
}],
"use-strict": false,
"variable-name": false,
"whitespace": [true,
"check-branch",
@@ -74,6 +65,8 @@
"check-operator",
"check-separator",
"check-type"
]
],
"object-literal-sort-keys": false,
"ordered-imports": false
}
}