Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdf7f223db | ||
|
|
bb70161652 | ||
|
|
7f2f983f55 | ||
|
|
8dab83b3ef | ||
|
|
cbafa5bc90 | ||
|
|
67735fb52a | ||
|
|
7d6f762f59 | ||
|
|
f3a0d0129d | ||
|
|
65c9032839 | ||
|
|
978a3282a4 | ||
|
|
9f0195bebc | ||
|
|
ea20108222 | ||
|
|
4ee814864c | ||
|
|
4cbfd691c3 | ||
|
|
7c04fb7282 | ||
|
|
849c7612aa | ||
|
|
6ad4cbed0a | ||
|
|
9e2f5401d0 | ||
|
|
b064b97f2d | ||
|
|
b248405437 | ||
|
|
bf941819a8 | ||
|
|
fcae53baae | ||
|
|
05ead50c0d | ||
|
|
0a80f80f91 | ||
|
|
3bf5fea735 | ||
|
|
3a95994cc2 | ||
|
|
a29870691b | ||
|
|
547fdc4aa0 | ||
|
|
c78552795f | ||
|
|
090c7e4789 | ||
|
|
bf8e1fe80f | ||
|
|
7344ce3d61 | ||
|
|
c642e76cce | ||
|
|
8ef27066f6 | ||
|
|
621df26b58 | ||
|
|
8060b1b73b | ||
|
|
11f6b3feff | ||
|
|
537639f2a8 | ||
|
|
813f8a997d | ||
|
|
48544020a1 | ||
|
|
cc68d21107 | ||
|
|
acd91e2679 | ||
|
|
53f0a9462a | ||
|
|
10d71944d9 | ||
|
|
b5bbde7cdd | ||
|
|
c406bc70ee | ||
|
|
1dea620295 | ||
|
|
2019c104b6 | ||
|
|
9f1ead1368 | ||
|
|
41f67798d6 | ||
|
|
2c2ed2c136 | ||
|
|
4dc90aeb00 | ||
|
|
361c6cf54c | ||
|
|
b691b953d4 | ||
|
|
3d067979e9 | ||
|
|
58247f53e4 | ||
|
|
6189e31e6b | ||
|
|
3df650a0a6 | ||
|
|
c785c0f7c3 | ||
|
|
a01f3cd09c | ||
|
|
ed4f398062 |
10
.travis.yml
Normal file
10
.travis.yml
Normal 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
|
||||
54
README.md
54
README.md
@@ -1,5 +1,7 @@
|
||||
# Crunchy: a fork of Deathspike/CrunchyRoll.js
|
||||
|
||||
[](http://issuestats.com/github/Godzil/Crunchy) [](https://travis-ci.org/Godzil/Crunchy) [](https://codeclimate.com/github/Godzil/Crunchy/maintainability)
|
||||
|
||||
*Crunchy* is capable of downloading *anime* episodes from the popular *CrunchyRoll* streaming service. An episode is stored in the original video format (often H.264 in a MP4 container) and the configured subtitle format (ASS or SRT).The two output files are then merged into a single MKV file.
|
||||
|
||||
## Motivation
|
||||
@@ -19,30 +21,39 @@ It is recommended to enable authentication (`-p` and `-u`) so your account permi
|
||||
|
||||
## 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 ffmpeg`
|
||||
2. Run in *Terminal*: `sudo ln -s /usr/bin/nodejs /usr/bin/node`
|
||||
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 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 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).
|
||||
@@ -93,6 +104,8 @@ Download *Fairy Tail* to `C:\Anime`:
|
||||
* `-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.
|
||||
@@ -110,6 +123,36 @@ Download *Fairy Tail* to `C:\Anime`:
|
||||
* `-s or --series <s>` sets the series override.
|
||||
* `-t or --tag <s>` sets The subgroup. (Default: CrunchyRoll)
|
||||
|
||||
## 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 often solved the 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, they will are used as a metric to know how many people are annoyed by that issue
|
||||
If you find one which correspond and 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 can create a new one.
|
||||
|
||||
### What to put in a bug report
|
||||
It is really important for me to know:
|
||||
- on which Operating System you are running Crunchy,
|
||||
- which anime you want to fetch if it is related to a specific one,
|
||||
- The command line you use to run Crunchy
|
||||
- What message Crunchy is giving you if any
|
||||
|
||||
**Please be careful to remove your real account login and password if they appear!**
|
||||
|
||||
Also don't hesitate to add labels you feel apropriate on your report.
|
||||
|
||||
_Note: You can also use a bug report for a feature requests._
|
||||
|
||||
## Developers
|
||||
|
||||
More information will be added at a later point. For now the recommendations are:
|
||||
@@ -117,3 +160,8 @@ 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`.
|
||||
|
||||
#### A note about pull requests:
|
||||
If you want to help working on this project, Pull request are welcome, but please explain the goal of your changes, and do a pull request per change: you want to add support for _X_, _Y_ and _Z_, make a pull request for X, one for Y and one for Z. I'm not saying a pull request per commit that would be idiotic.
|
||||
The idea is if your pull request changes lots of thing at the same time, if just a single part can't be accepted, if will delay the acceptation of the whole pull request where some of the feature could be integrated quicker is they were requested alone.
|
||||
Also if for example the change _Y_ depends on _X_, you can wait for _X_ to be accepted before requesting for _Y_, if they are independant you can send a pull request for each at the same time.
|
||||
|
||||
0
bin/crunchy
Normal file → Executable file
0
bin/crunchy
Normal file → Executable file
10
bin/crunchy.sh
Executable file
10
bin/crunchy.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
PARAMS=$*
|
||||
for i in {1..20}; do
|
||||
crunchy ${PARAMS}
|
||||
if [ $? == 0 ]; then
|
||||
break
|
||||
fi
|
||||
echo "Going to retry..."
|
||||
sleep 3
|
||||
done
|
||||
1180
package-lock.json
generated
Normal file
1180
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
40
package.json
40
package.json
@@ -12,32 +12,44 @@
|
||||
"type": "git",
|
||||
"url": "git://github.com/Godzil/crunchyroll.js.git"
|
||||
},
|
||||
"version": "1.1.17",
|
||||
"engines": {
|
||||
"node": ">=5.0"
|
||||
},
|
||||
"version": "1.3.1",
|
||||
"bin": {
|
||||
"crunchy": "./bin/crunchy"
|
||||
"crunchy": "./bin/crunchy",
|
||||
"crunchy.sh": "./bin/crunchy.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"big-integer": "^1.4.4",
|
||||
"big-integer": "^1.6.30",
|
||||
"bluebird": "^3.5.1",
|
||||
"cheerio": "^0.22.0",
|
||||
"cloudscraper": "^1.4.1",
|
||||
"commander": "^2.6.0",
|
||||
"fs-extra": "^2.0.0",
|
||||
"cloudscraper": "^1.5.0",
|
||||
"commander": "^2.15.1",
|
||||
"fs-extra": "^6.0.1",
|
||||
"mkdirp": "^0.5.0",
|
||||
"request": "^2.74.0",
|
||||
"pjson": "^1.0.9",
|
||||
"request": "^2.87.0",
|
||||
"request-promise": "^4.2.2",
|
||||
"xml2js": "^0.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bluebird": "^3.5.20",
|
||||
"@types/cheerio": "^0.22.7",
|
||||
"@types/fs-extra": "^5.0.2",
|
||||
"@types/mkdirp": "^0.5.2",
|
||||
"@types/request": "^2.47.0",
|
||||
"@types/request-promise": "^4.1.41",
|
||||
"@types/xml2js": "^0.4.2",
|
||||
"tsconfig-lint": "^0.12.0",
|
||||
"tslint": "^4.4.2",
|
||||
"typescript": "^2.2.0",
|
||||
"typings": "^2.1.0"
|
||||
"tslint": "^5.10.0",
|
||||
"typescript": "^2.9.1"
|
||||
},
|
||||
"scripts": {
|
||||
"prepublish": "npm run types && tsc",
|
||||
"prepublishOnly": "npm run build",
|
||||
"compile": "tsc",
|
||||
"test": "tslint -c ./tslint.json --project ./tsconfig.json ./src/**/*.ts",
|
||||
"types": "typings install",
|
||||
"reinstall": "tsd reinstall; npm run types",
|
||||
"build": "tsc",
|
||||
"test": "tslint --project .",
|
||||
"start": "node ./bin/crunchy"
|
||||
},
|
||||
"bugs": {
|
||||
|
||||
75
src/batch.ts
75
src/batch.ts
@@ -2,39 +2,87 @@
|
||||
import commander = require('commander');
|
||||
import fs = require('fs');
|
||||
import path = require('path');
|
||||
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.
|
||||
*/
|
||||
export default function(args: string[], done: (err?: Error) => void)
|
||||
{
|
||||
const config = parse(args);
|
||||
const batchPath = path.join(config.output || process.cwd(), 'CrunchyRoll.txt');
|
||||
const batchPath = path.join(config.output || process.cwd(), config.batch);
|
||||
|
||||
tasks(config, batchPath, (err, tasks) =>
|
||||
// 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;
|
||||
}
|
||||
|
||||
tasks(config, batchPath, (err, tasksArr) =>
|
||||
{
|
||||
if (err)
|
||||
{
|
||||
return done(err);
|
||||
return done(err);
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
|
||||
(function next()
|
||||
{
|
||||
if (i >= tasks.length)
|
||||
if (i >= tasksArr.length)
|
||||
{
|
||||
return done();
|
||||
}
|
||||
|
||||
series(tasks[i].config, tasks[i].address, (errin) =>
|
||||
series(tasksArr[i].config, tasksArr[i].address, (errin) =>
|
||||
{
|
||||
if (errin)
|
||||
{
|
||||
return done(errin);
|
||||
if (tasksArr[i].retry <= 0)
|
||||
{
|
||||
console.error(err.stack || err);
|
||||
log.error('Cannot get episodes from "' + tasksArr[i].address + '", please rerun later');
|
||||
}
|
||||
else
|
||||
{
|
||||
if (config.verbose)
|
||||
{
|
||||
console.error(err.stack || err);
|
||||
}
|
||||
log.warn('Retrying to fetch episodes ' + tasksArr[i].retry + ' / ' + config.retry);
|
||||
tasksArr[i].retry -= 1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
i += 1;
|
||||
}
|
||||
i += 1;
|
||||
next();
|
||||
});
|
||||
})();
|
||||
@@ -86,7 +134,7 @@ function tasks(config: IConfigLine, batchPath: string, done: (err: Error, tasks?
|
||||
|
||||
return done(null, config.args.map((addressIn) =>
|
||||
{
|
||||
return {address: addressIn, config: configIn};
|
||||
return {address: addressIn, config: configIn, retry: config.retry};
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -122,7 +170,7 @@ function tasks(config: IConfigLine, batchPath: string, done: (err: Error, tasks?
|
||||
return;
|
||||
}
|
||||
|
||||
map.push({address: addressIn, config: lineConfig});
|
||||
map.push({address: addressIn, config: lineConfig, retry: config.retry});
|
||||
});
|
||||
});
|
||||
done(null, map);
|
||||
@@ -149,6 +197,13 @@ function parse(args: string[]): IConfigLine
|
||||
.option('-f, --format <s>', 'The subtitle format. (Default: ass)')
|
||||
.option('-o, --output <s>', 'The output path.')
|
||||
.option('-s, --series <s>', 'The series override.')
|
||||
.option('-t, --tag <s>', 'The subgroup. (Default: CrunchyRoll)')
|
||||
.option('-n, --filename <s>', 'The name override.')
|
||||
.option('-t, --tag <s>', 'The subgroup. (Default: CrunchyRoll)', 'CrunchyRoll')
|
||||
.option('-r, --resolution <s>', 'The video resolution. (Default: 1080 (360, 480, 720, 1080))',
|
||||
'1080')
|
||||
.option('-g, --rebuildcrp', 'Rebuild the crpersistant file.')
|
||||
.option('-b, --batch <s>', 'Batch file', 'CrunchyRoll.txt')
|
||||
.option('--verbose', 'Make tool verbose')
|
||||
.option('--retry <i>', 'Number or time to retry fetching an episode. Default: 5', 5)
|
||||
.parse(args);
|
||||
}
|
||||
|
||||
24
src/cli.ts
24
src/cli.ts
@@ -1,10 +1,34 @@
|
||||
'use strict';
|
||||
import batch from './batch';
|
||||
import request = require('request');
|
||||
import log = require('./log');
|
||||
import pjson = require('pjson');
|
||||
|
||||
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)
|
||||
{
|
||||
console.error(err.stack || err);
|
||||
process.exit(-1);
|
||||
}
|
||||
console.info('Done!');
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
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 from './subtitle/index';
|
||||
import video from './video/index';
|
||||
@@ -57,12 +57,18 @@ function fileExist(path: string)
|
||||
{
|
||||
fs.statSync(path);
|
||||
return true;
|
||||
} catch (e)
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function sanitiseFileName(str: string)
|
||||
{
|
||||
return str.replace(/[\/':\?\*"<>\.]/g, '_');
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the subtitle and video.
|
||||
*/
|
||||
@@ -70,25 +76,38 @@ function download(config: IConfig, page: IEpisodePage, player: IEpisodePlayer, d
|
||||
{
|
||||
let series = config.series || page.series;
|
||||
|
||||
series = series.replace('/', '_').replace('\'', '_').replace(':', '_');
|
||||
let fileName = name(config, page, series, '').replace('/', '_').replace('\'', '_').replace(':', '_');
|
||||
series = sanitiseFileName(series);
|
||||
let fileName = sanitiseFileName(name(config, page, series, ''));
|
||||
let filePath = path.join(config.output || process.cwd(), series, 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 = name(config, page, series, '-' + count).replace('/', '_').replace('\'', '_').replace(':', '_');
|
||||
fileName = sanitiseFileName(name(config, page, series, '-' + count));
|
||||
filePath = path.join(config.output || process.cwd(), series, fileName);
|
||||
} while (fileExist(filePath + '.mkv'));
|
||||
|
||||
log.warn('Renaming to \'' + 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)
|
||||
@@ -96,6 +115,7 @@ function download(config: IConfig, page: IEpisodePage, player: IEpisodePlayer, d
|
||||
return done(errM, false);
|
||||
}
|
||||
|
||||
log.dispEpisode(fileName, 'Fetching...', false);
|
||||
downloadSubtitle(config, player, filePath, (errDS) =>
|
||||
{
|
||||
if (errDS)
|
||||
@@ -106,7 +126,7 @@ function download(config: IConfig, page: IEpisodePage, player: IEpisodePlayer, d
|
||||
const now = Date.now();
|
||||
if (player.video.file !== undefined)
|
||||
{
|
||||
log.dispEpisode(fileName, 'Fetching...', false);
|
||||
log.dispEpisode(fileName, 'Fetching video...', false);
|
||||
downloadVideo(config, page, player, filePath, (errDV) =>
|
||||
{
|
||||
if (errDV)
|
||||
@@ -121,7 +141,8 @@ function download(config: IConfig, page: IEpisodePage, player: IEpisodePlayer, d
|
||||
|
||||
const isSubtited = Boolean(player.subtitle);
|
||||
|
||||
video.merge(config, isSubtited, player.video.file, filePath, player.video.mode, (errVM) =>
|
||||
log.dispEpisode(fileName, 'Merging...', false);
|
||||
video.merge(config, isSubtited, player.video.file, filePath, player.video.mode, config.verbose, (errVM) =>
|
||||
{
|
||||
if (errVM)
|
||||
{
|
||||
@@ -178,11 +199,11 @@ function downloadSubtitle(config: IConfig, player: IEpisodePlayer, filePath: str
|
||||
/**
|
||||
* Streams the video to disk.
|
||||
*/
|
||||
function downloadVideo(ignored/*config*/: IConfig, page: IEpisodePage, player: IEpisodePlayer,
|
||||
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, done);
|
||||
path.extname(player.video.file), player.video.mode, config.verbose, done);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -196,7 +217,20 @@ function name(config: IConfig, page: IEpisodePage, series: string, extra: string
|
||||
const volume = (volumeNum < 10 ? '0' : '') + page.volume;
|
||||
const tag = config.tag || 'CrunchyRoll';
|
||||
|
||||
return series + ' - s' + volume + 'e' + episode + ' - [' + tag + ']' + extra;
|
||||
if (!config.filename) {
|
||||
return page.series + ' - s' + volume + 'e' + episode + ' - [' + tag + ']' + extra;
|
||||
}
|
||||
|
||||
return config.filename
|
||||
.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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -226,7 +260,7 @@ function scrapePage(config: IConfig, address: string, done: (err: Error, page?:
|
||||
return done(new Error('Invalid address.'));
|
||||
}
|
||||
|
||||
request.get(config, address, (err, result) =>
|
||||
my_request.get(config, address, (err, result) =>
|
||||
{
|
||||
if (err)
|
||||
{
|
||||
@@ -238,6 +272,7 @@ function scrapePage(config: IConfig, address: string, done: (err: Error, page?:
|
||||
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 (!swf || !data)
|
||||
@@ -248,6 +283,8 @@ function scrapePage(config: IConfig, address: string, done: (err: Error, page?:
|
||||
episode: '0',
|
||||
id: epId,
|
||||
series: seasonTitle,
|
||||
season: seasonTitle,
|
||||
title: episodeTitle,
|
||||
swf: swf[1],
|
||||
volume: '0',
|
||||
});
|
||||
@@ -258,6 +295,8 @@ function scrapePage(config: IConfig, address: string, done: (err: Error, page?:
|
||||
episode: data[3],
|
||||
id: epId,
|
||||
series: data[1],
|
||||
season: seasonTitle,
|
||||
title: episodeTitle,
|
||||
swf: swf[1],
|
||||
volume: data[2] || '1',
|
||||
});
|
||||
@@ -277,8 +316,13 @@ function scrapePlayer(config: IConfig, address: string, id: number, done: (err:
|
||||
return done(new Error('Invalid address.'));
|
||||
}
|
||||
|
||||
request.post(config, {
|
||||
form: {current_page: 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) =>
|
||||
{
|
||||
@@ -303,9 +347,9 @@ function scrapePlayer(config: IConfig, address: string, id: number, done: (err:
|
||||
let streamMode = 'RTMP';
|
||||
|
||||
if (player['default:preload'].stream_info.host === '')
|
||||
{
|
||||
streamMode = 'HLS';
|
||||
}
|
||||
{
|
||||
streamMode = 'HLS';
|
||||
}
|
||||
|
||||
done(null, {
|
||||
subtitle: isSubtitled ? {
|
||||
|
||||
8
src/interface/IConfig.d.ts
vendored
8
src/interface/IConfig.d.ts
vendored
@@ -12,5 +12,13 @@ interface IConfig {
|
||||
format?: string;
|
||||
output?: string;
|
||||
series?: string;
|
||||
filename?: string;
|
||||
tag?: string;
|
||||
resolution?: string;
|
||||
video_format?: string;
|
||||
video_quality?: string;
|
||||
rebuildcrp?: boolean;
|
||||
batch?: string;
|
||||
verbose?: boolean;
|
||||
retry?: number;
|
||||
}
|
||||
|
||||
1
src/interface/IConfigTask.d.ts
vendored
1
src/interface/IConfigTask.d.ts
vendored
@@ -1,4 +1,5 @@
|
||||
interface IConfigTask {
|
||||
address: string;
|
||||
config: IConfigLine;
|
||||
retry: number;
|
||||
}
|
||||
|
||||
2
src/interface/IEpisodePage.d.ts
vendored
2
src/interface/IEpisodePage.d.ts
vendored
@@ -3,5 +3,7 @@ interface IEpisodePage {
|
||||
episode: string;
|
||||
series: string;
|
||||
volume: string;
|
||||
season: string;
|
||||
title: string;
|
||||
swf: string;
|
||||
}
|
||||
|
||||
4
src/interface/IResolData.d.ts
vendored
Normal file
4
src/interface/IResolData.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
interface IResolData {
|
||||
quality: string;
|
||||
format: string;
|
||||
}
|
||||
1
src/interface/ISeriesEpisode.d.ts
vendored
1
src/interface/ISeriesEpisode.d.ts
vendored
@@ -2,4 +2,5 @@ interface ISeriesEpisode {
|
||||
address: string;
|
||||
episode: string;
|
||||
volume: number;
|
||||
retry: number;
|
||||
}
|
||||
|
||||
@@ -28,10 +28,10 @@ export function warn(str: string)
|
||||
export function dispEpisode(name: string, status: string, addNL: boolean)
|
||||
{
|
||||
/* Do fancy output */
|
||||
process.stdout.write(' \x1B[1;33m> \x1B[37m' + name + '\x1B[0m : \x1B[33m' + status + '\x1B[0m\x1B[0G');
|
||||
process.stdout.write('\x1B[K \x1B[1;33m> \x1B[37m' + name + '\x1B[0m : \x1B[33m' + status + '\x1B[0m\x1B[0G');
|
||||
|
||||
if (addNL)
|
||||
{
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
219
src/my_request.ts
Normal file
219
src/my_request.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
'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: Error, result?: string) => void)
|
||||
{
|
||||
authenticate(config, (err) =>
|
||||
{
|
||||
if (err)
|
||||
{
|
||||
return done(err);
|
||||
}
|
||||
|
||||
cloudscraper.request(modify(options, 'GET'), (error: Error, 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 || !config.pass || !config.user)
|
||||
{
|
||||
return done(null);
|
||||
}
|
||||
|
||||
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();
|
||||
return done(new Error('Authentication failed: ' + error));
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
190
src/request.ts
190
src/request.ts
@@ -1,190 +0,0 @@
|
||||
'use strict';
|
||||
import request = require('request');
|
||||
import cheerio = require('cheerio');
|
||||
import log = require('./log');
|
||||
const cloudscraper = require('cloudscraper');
|
||||
|
||||
let isAuthenticated = false;
|
||||
let isPremium = false;
|
||||
|
||||
const defaultHeaders: request.Headers =
|
||||
{
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 6.2; WOW64; rv:34.0) Gecko/20100101 Firefox/34.0',
|
||||
'Connection': 'keep-alive'
|
||||
};
|
||||
|
||||
/**
|
||||
* Performs a GET request for the resource.
|
||||
*/
|
||||
export function get(config: IConfig, options: request.Options, done: (err: Error, result?: string) => void)
|
||||
{
|
||||
authenticate(config, err =>
|
||||
{
|
||||
if (err)
|
||||
{
|
||||
return done(err);
|
||||
}
|
||||
|
||||
cloudscraper.request(modify(options, 'GET'), (err: Error, response: any, body: any) =>
|
||||
{
|
||||
if (err)
|
||||
{
|
||||
return done(err);
|
||||
}
|
||||
|
||||
done(null, typeof body === 'string' ? body : String(body));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a POST request for the resource.
|
||||
*/
|
||||
export function post(config: IConfig, options: request.Options, done: (err: Error, result?: string) => void)
|
||||
{
|
||||
authenticate(config, err =>
|
||||
{
|
||||
if (err)
|
||||
{
|
||||
return done(err);
|
||||
}
|
||||
|
||||
cloudscraper.request(modify(options, 'POST'), (err: Error, response: any, body: any) =>
|
||||
{
|
||||
if (err)
|
||||
{
|
||||
return done(err);
|
||||
}
|
||||
|
||||
done(null, typeof body === 'string' ? body : String(body));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates using the configured pass and user.
|
||||
*/
|
||||
function authenticate(config: IConfig, done: (err: Error) => void)
|
||||
{
|
||||
if (isAuthenticated || !config.pass || !config.user)
|
||||
{
|
||||
return done(null);
|
||||
}
|
||||
|
||||
/* Bypass the login page and send a login request directly */
|
||||
let options =
|
||||
{
|
||||
headers: defaultHeaders,
|
||||
jar: true,
|
||||
gzip: false,
|
||||
method: 'GET',
|
||||
url: 'https://www.crunchyroll.com/login'
|
||||
};
|
||||
|
||||
cloudscraper.request(options, (err: Error, rep: string, body: string) =>
|
||||
{
|
||||
if (err) return done(err);
|
||||
|
||||
const $ = cheerio.load(body);
|
||||
|
||||
/* Get the token from the login page */
|
||||
const token = $('input[name="login_form[_token]"]').attr('value');
|
||||
if (token === '')
|
||||
{
|
||||
return done(new Error('Can`t find token!'));
|
||||
}
|
||||
|
||||
let options =
|
||||
{
|
||||
headers: defaultHeaders,
|
||||
form:
|
||||
{
|
||||
'login_form[redirect_url]': '/',
|
||||
'login_form[name]': config.user,
|
||||
'login_form[password]': config.pass,
|
||||
'login_form[_token]': token
|
||||
},
|
||||
jar: true,
|
||||
gzip: false,
|
||||
method: 'POST',
|
||||
url: 'https://www.crunchyroll.com/login'
|
||||
};
|
||||
|
||||
cloudscraper.request(options, (err: Error, rep: string, body: string) =>
|
||||
{
|
||||
if (err)
|
||||
{
|
||||
return done(err);
|
||||
}
|
||||
|
||||
/* 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.
|
||||
*/
|
||||
let 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);
|
||||
}
|
||||
|
||||
let $ = cheerio.load(body);
|
||||
|
||||
/* Check if auth worked */
|
||||
const regexps = /ga\('set', 'dimension[5-8]', '([^']*)'\);/g;
|
||||
const dims = regexps.exec($('script').text());
|
||||
|
||||
for (let i = 1; i < 5; i++)
|
||||
{
|
||||
if ((dims[i] !== undefined) && (dims[i] !== '') && (dims[i] !== 'not-registered'))
|
||||
{
|
||||
isAuthenticated = true;
|
||||
}
|
||||
|
||||
if ((dims[i] === 'premium') || (dims[i] === 'premiumplus'))
|
||||
{
|
||||
isPremium = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isAuthenticated === false)
|
||||
{
|
||||
const error = $('ul.message, li.error').text();
|
||||
return done(new Error('Authentication failed: ' + error));
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 };
|
||||
}
|
||||
@@ -2,13 +2,28 @@
|
||||
import cheerio = require('cheerio');
|
||||
import episode from './episode';
|
||||
import fs = require('fs');
|
||||
const fse = require('fs-extra');
|
||||
import request = require('./request');
|
||||
import fse = require('fs-extra');
|
||||
import my_request = require('./my_request');
|
||||
import path = require('path');
|
||||
import url = require('url');
|
||||
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.
|
||||
*/
|
||||
@@ -17,7 +32,10 @@ export default function(config: IConfig, address: string, done: (err: Error) =>
|
||||
const persistentPath = path.join(config.output || process.cwd(), persistent);
|
||||
|
||||
/* Make a backup of the persistent file in case of */
|
||||
fse.copySync(persistentPath, persistentPath + '.backup');
|
||||
if (fileExist(persistentPath))
|
||||
{
|
||||
fse.copySync(persistentPath, persistentPath + '.backup');
|
||||
}
|
||||
|
||||
fs.readFile(persistentPath, 'utf8', (err: Error, contents: string) =>
|
||||
{
|
||||
@@ -38,27 +56,45 @@ export default function(config: IConfig, address: string, done: (err: Error) =>
|
||||
{
|
||||
if (errD)
|
||||
{
|
||||
return done(errD);
|
||||
}
|
||||
|
||||
if ((ignored === false) || (ignored === undefined))
|
||||
{
|
||||
const newCache = JSON.stringify(cache, null, ' ');
|
||||
fs.writeFile(persistentPath, newCache, (errW: Error) =>
|
||||
if (page.episodes[i].retry <= 0)
|
||||
{
|
||||
if (errW)
|
||||
console.error(err.stack || err);
|
||||
log.error('Cannot fetch episode "s' + page.episodes[i].volume + 'e' + page.episodes[i].episode +
|
||||
'", please rerun later');
|
||||
}
|
||||
else
|
||||
{
|
||||
if (config.verbose)
|
||||
{
|
||||
return done(errW);
|
||||
console.error(errD.stack || errD);
|
||||
}
|
||||
|
||||
i += 1;
|
||||
next();
|
||||
});
|
||||
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
|
||||
{
|
||||
i += 1;
|
||||
next();
|
||||
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();
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -134,13 +170,14 @@ function page(config: IConfig, address: string, done: (err: Error, result?: ISer
|
||||
address: address.substr(1),
|
||||
episode: '',
|
||||
volume: 0,
|
||||
retry: config.retry,
|
||||
});
|
||||
done(null, {episodes: episodes.reverse(), series: ""});
|
||||
done(null, {episodes: episodes.reverse(), series: ''});
|
||||
}
|
||||
else
|
||||
{
|
||||
let episodeCount = 0;
|
||||
request.get(config, address, (err, result) => {
|
||||
my_request.get(config, address, (err, result) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
@@ -173,11 +210,12 @@ function page(config: IConfig, address: string, done: (err: Error, result?: ISer
|
||||
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?");
|
||||
log.warn('No episodes found for ' + title + '. Could it be a movie?');
|
||||
}
|
||||
done(null, {episodes: episodes.reverse(), series: title});
|
||||
});
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
/* tslint:disable:no-bitwise false */
|
||||
'use strict';
|
||||
import crypto = require('crypto');
|
||||
import bigInt = require('big-integer');
|
||||
import crypto = require('crypto');
|
||||
import zlib = require('zlib');
|
||||
|
||||
/**
|
||||
* Decodes the data.
|
||||
*/
|
||||
export default function(id: number, iv: Buffer|string, data: Buffer|string,
|
||||
done: (err?: Error, result?: Buffer) => void)
|
||||
export default function(id: number, iv: Buffer|string, data: Buffer|string,
|
||||
done: (err?: Error, result?: Buffer) => void)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
@@ -33,10 +33,10 @@ export default function(input: string|Buffer, done: (err: Error, subtitle?: stri
|
||||
*/
|
||||
function event(block: ISubtitleEvent): string
|
||||
{
|
||||
var format = 'Layer,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text';
|
||||
const format = 'Layer,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text';
|
||||
|
||||
return '[Events]\n' +
|
||||
'Format: ' + format + '\n' + [].concat(block.event).map(style => ('Dialogue: 0,' +
|
||||
'Format: ' + format + '\n' + [].concat(block.event).map((style) => ('Dialogue: 0,' +
|
||||
style.$.start + ',' +
|
||||
style.$.end + ',' +
|
||||
style.$.style + ',' +
|
||||
@@ -70,13 +70,13 @@ function script(block: ISubtitle): string
|
||||
*/
|
||||
function style(block: ISubtitleStyle): string
|
||||
{
|
||||
var format = 'Name,Fontname,Fontsize,PrimaryColour,SecondaryColour,' +
|
||||
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: ' +
|
||||
'Format: ' + format + '\n' + [].concat(block.style).map((style) => 'Style: ' +
|
||||
style.$.name + ',' +
|
||||
style.$.font_name + ',' +
|
||||
style.$.font_size + ',' +
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import ass from './ass';
|
||||
import srt from './srt';
|
||||
|
||||
export default <IFormatterTable> {
|
||||
ass: ass,
|
||||
srt: srt
|
||||
};
|
||||
export default {
|
||||
ass,
|
||||
srt
|
||||
} as IFormatterTable;
|
||||
|
||||
@@ -1,29 +1,31 @@
|
||||
'use strict';
|
||||
import childProcess = require('child_process');
|
||||
import fs = require('fs');
|
||||
import path = require('path');
|
||||
import os = require('os');
|
||||
import path = require('path');
|
||||
|
||||
import subtitle from '../subtitle/index';
|
||||
|
||||
/**
|
||||
* Merges the subtitle and video files into a Matroska Multimedia Container.
|
||||
*/
|
||||
export default function(config: IConfig, isSubtitled: boolean, rtmpInputPath: string, filePath: string,
|
||||
streamMode: string, done: (err: Error) => void)
|
||||
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);
|
||||
videoPath += path.extname(rtmpInputPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
videoPath += '.mp4';
|
||||
}
|
||||
|
||||
childProcess.exec(command() + ' ' +
|
||||
cp = childProcess.exec(command() + ' ' +
|
||||
'-o "' + filePath + '.mkv" ' +
|
||||
'"' + videoPath + '" ' +
|
||||
(isSubtitled ? '"' + subtitlePath + '"' : ''), {
|
||||
@@ -45,6 +47,13 @@ import subtitle from '../subtitle/index';
|
||||
done(null);
|
||||
});
|
||||
});
|
||||
|
||||
if (verbose === true)
|
||||
{
|
||||
cp.stdin.pipe(process.stdin);
|
||||
cp.stdout.pipe(process.stdout);
|
||||
cp.stderr.pipe(process.stderr);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,39 +1,50 @@
|
||||
'use strict';
|
||||
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.
|
||||
*/
|
||||
export default function(rtmpUrl: string, rtmpInputPath: string, swfUrl: string, filePath: string,
|
||||
fileExt: string, mode: string, done: (err: Error) => void)
|
||||
fileExt: string, mode: string, verbose: boolean, done: (err: Error) => void)
|
||||
{
|
||||
if (mode === 'RTMP')
|
||||
{
|
||||
childProcess.exec(command('rtmpdump') + ' ' +
|
||||
'-r "' + rtmpUrl + '" ' +
|
||||
'-y "' + rtmpInputPath + '" ' +
|
||||
'-W "' + swfUrl + '" ' +
|
||||
'-o "' + filePath + fileExt + '"', {
|
||||
maxBuffer: Infinity,
|
||||
}, done);
|
||||
}
|
||||
else if (mode === 'HLS')
|
||||
{
|
||||
const cmd = command('ffmpeg') + ' ' +
|
||||
'-i "' + rtmpInputPath + '" ' +
|
||||
'-c copy -bsf:a aac_adtstoasc ' +
|
||||
'"' + filePath + '.mp4"';
|
||||
childProcess.exec(cmd, {
|
||||
maxBuffer: Infinity,
|
||||
}, done);
|
||||
}
|
||||
else
|
||||
{
|
||||
log.error('No such mode: ' + mode);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,7 +54,7 @@ function command(exe: string): string
|
||||
{
|
||||
if (os.platform() !== 'win32')
|
||||
{
|
||||
return exe;
|
||||
return exe;
|
||||
}
|
||||
|
||||
return '"' + path.join(__dirname, '../../bin/' + exe + '.exe') + '"';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.5.1-beta",
|
||||
"target": "es6",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"noImplicitAny": true,
|
||||
@@ -7,40 +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/log.ts",
|
||||
"src/interface/IConfig.d.ts",
|
||||
"src/interface/IConfigLine.d.ts",
|
||||
"src/interface/IConfigTask.d.ts",
|
||||
"src/interface/IEpisodePage.d.ts",
|
||||
"src/interface/IEpisodePlayer.d.ts",
|
||||
"src/interface/IEpisodePlayerConfig.d.ts",
|
||||
"src/interface/IFormatterTable.d.ts",
|
||||
"src/interface/ISeries.d.ts",
|
||||
"src/interface/ISeriesEpisode.d.ts",
|
||||
"src/interface/ISubtitle.d.ts",
|
||||
"src/interface/ISubtitleEvent.d.ts",
|
||||
"src/interface/ISubtitleStyle.d.ts",
|
||||
"src/request.ts",
|
||||
"src/series.ts",
|
||||
"src/subtitle/decode.ts",
|
||||
"src/subtitle/formats/ass.ts",
|
||||
"src/subtitle/formats/index.ts",
|
||||
"src/subtitle/formats/srt.ts",
|
||||
"src/subtitle/index.ts",
|
||||
"src/video/index.ts",
|
||||
"src/video/merge.ts",
|
||||
"src/video/stream.ts",
|
||||
"typings/index.d.ts"
|
||||
]
|
||||
"lib": [
|
||||
"es2015"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
18
tslint.json
18
tslint.json
@@ -9,23 +9,22 @@
|
||||
"curly": false,
|
||||
"eofline": false,
|
||||
"forin": true,
|
||||
"indent": [true, 2],
|
||||
"indent": [true, "spaces", 2],
|
||||
"interface-name": true,
|
||||
"jsdoc-format": true,
|
||||
"label-position": 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"
|
||||
@@ -42,7 +41,6 @@
|
||||
"no-use-before-declare": false,
|
||||
"no-var-requires": true,
|
||||
"one-line": [true,
|
||||
"check-catch",
|
||||
"check-whitespace"
|
||||
],
|
||||
"quotemark": [true, "single"],
|
||||
@@ -67,6 +65,8 @@
|
||||
"check-operator",
|
||||
"check-separator",
|
||||
"check-type"
|
||||
]
|
||||
],
|
||||
"object-literal-sort-keys": false,
|
||||
"ordered-imports": false
|
||||
}
|
||||
}
|
||||
13
typings.json
13
typings.json
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"name": "crunchy",
|
||||
"globalDependencies": {
|
||||
"node": "github:DefinitelyTyped/DefinitelyTyped/node/node.d.ts#3882d337bb0808cde9fe4c08012508a48c135482",
|
||||
"commander": "github:DefinitelyTyped/DefinitelyTyped/commander/commander.d.ts#3882d337bb0808cde9fe4c08012508a48c135482",
|
||||
"xml2js": "github:DefinitelyTyped/DefinitelyTyped/xml2js/xml2js.d.ts#3882d337bb0808cde9fe4c08012508a48c135482",
|
||||
"cheerio": "github:DefinitelyTyped/DefinitelyTyped/cheerio/cheerio.d.ts#3882d337bb0808cde9fe4c08012508a48c135482",
|
||||
"mkdirp": "github:DefinitelyTyped/DefinitelyTyped/mkdirp/mkdirp.d.ts#3882d337bb0808cde9fe4c08012508a48c135482",
|
||||
"request": "github:DefinitelyTyped/DefinitelyTyped/request/request.d.ts#3882d337bb0808cde9fe4c08012508a48c135482",
|
||||
"big-integer": "github:DefinitelyTyped/DefinitelyTyped/big-integer/big-integer.d.ts#3882d337bb0808cde9fe4c08012508a48c135482",
|
||||
"form-data": "github:DefinitelyTyped/DefinitelyTyped/form-data/form-data.d.ts#3882d337bb0808cde9fe4c08012508a48c135482"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user