Compare commits
91 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b248405437 | ||
|
|
bf941819a8 | ||
|
|
fcae53baae | ||
|
|
05ead50c0d | ||
|
|
0a80f80f91 | ||
|
|
3bf5fea735 | ||
|
|
3a95994cc2 | ||
|
|
a29870691b | ||
|
|
547fdc4aa0 | ||
|
|
c78552795f | ||
|
|
090c7e4789 | ||
|
|
bf8e1fe80f | ||
|
|
7344ce3d61 | ||
|
|
c642e76cce | ||
|
|
8ef27066f6 | ||
|
|
621df26b58 | ||
|
|
8060b1b73b | ||
|
|
11f6b3feff | ||
|
|
537639f2a8 | ||
|
|
813f8a997d | ||
|
|
48544020a1 | ||
|
|
cc68d21107 | ||
|
|
acd91e2679 | ||
|
|
53f0a9462a | ||
|
|
10d71944d9 | ||
|
|
b5bbde7cdd | ||
|
|
c406bc70ee | ||
|
|
1dea620295 | ||
|
|
2019c104b6 | ||
|
|
9f1ead1368 | ||
|
|
41f67798d6 | ||
|
|
2c2ed2c136 | ||
|
|
4dc90aeb00 | ||
|
|
361c6cf54c | ||
|
|
b691b953d4 | ||
|
|
3d067979e9 | ||
|
|
58247f53e4 | ||
|
|
6189e31e6b | ||
|
|
3df650a0a6 | ||
|
|
c785c0f7c3 | ||
|
|
a01f3cd09c | ||
|
|
ed4f398062 | ||
|
|
e9cf0c353b | ||
|
|
6bc39083b9 | ||
|
|
67d06246d4 | ||
|
|
2ab1daf2b3 | ||
|
|
065d3b4c66 | ||
|
|
cfe73f5ca8 | ||
|
|
2fea379484 | ||
|
|
bee3f33e20 | ||
|
|
5d9c25491d | ||
|
|
58f4dc61ff | ||
|
|
b96efacbd2 | ||
|
|
a346ab8854 | ||
|
|
499530141e | ||
|
|
d1457bb893 | ||
|
|
8dfd1b447c | ||
|
|
ce63ae9a16 | ||
|
|
70d80ccd17 | ||
|
|
7833fbe292 | ||
|
|
fa6aa74442 | ||
|
|
fe2ed9fb76 | ||
|
|
cc655b9e00 | ||
|
|
e1d2a55a01 | ||
|
|
a31de0ef9d | ||
|
|
2853334d7f | ||
|
|
69dd28d31b | ||
|
|
56afce02ea | ||
|
|
bc4697061e | ||
|
|
55ffe85f77 | ||
|
|
ec8c2c7716 | ||
|
|
714a528f8b | ||
|
|
8314d91bd7 | ||
|
|
5bd31f9e0b | ||
|
|
95a93930f3 | ||
|
|
4a9e1d0410 | ||
|
|
1eacd0a5ca | ||
|
|
3c32726745 | ||
|
|
42ae0ae1fb | ||
|
|
e4b3871919 | ||
|
|
58e4a557e2 | ||
|
|
8371d68113 | ||
|
|
b7d496fc9d | ||
|
|
14260d04b3 | ||
|
|
3d46b65d67 | ||
|
|
62a08e14bb | ||
|
|
422d0827f9 | ||
|
|
546a849aa5 | ||
|
|
e06ff53210 | ||
|
|
18375d3d22 | ||
|
|
5fdee94b38 |
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
|
||||
1
LICENSE
1
LICENSE
@@ -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
|
||||
|
||||
69
README.md
69
README.md
@@ -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.
|
||||
[](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
|
||||
|
||||
@@ -10,6 +12,8 @@
|
||||
|
||||
This application is not endorsed or affliated with *CrunchyRoll*. The usage of this application enables episodes to be downloaded for offline convenience which may be forbidden by law in your country. Usage of this application may also cause a violation of the agreed *Terms of Service* between you and the stream provider. A tool is not responsible for your actions; please make an informed decision prior to using this application.
|
||||
|
||||
**PLEASE _ONLY_ USE THIS TOOL IF YOU HAVE A _PREMIUM ACCOUNT_**
|
||||
|
||||
## 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.
|
||||
@@ -17,7 +21,7 @@ It is recommended to enable authentication (`-p` and `-u`) so your account permi
|
||||
|
||||
## Prerequisites
|
||||
|
||||
* NodeJS >= 0.12.x (http://nodejs.org/)
|
||||
* NodeJS >= 5.x (http://nodejs.org/)
|
||||
* NPM >= 2.5.x (https://www.npmjs.org/)
|
||||
|
||||
## Installation
|
||||
@@ -26,30 +30,30 @@ Use the applicable instructions to install. Is your operating system not listed?
|
||||
|
||||
### 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`
|
||||
|
||||
### 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`
|
||||
|
||||
### 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`
|
||||
|
||||
## 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:
|
||||
|
||||
@@ -74,15 +78,15 @@ When no sequence of series addresses is provided, the batch-mode source file wil
|
||||
|
||||
Download in batch-mode:
|
||||
|
||||
crunchyroll
|
||||
crunchy
|
||||
|
||||
Download *Fairy Tail* to the current work directory:
|
||||
|
||||
crunchyroll http://www.crunchyroll.com/fairy-tail
|
||||
crunchy http://www.crunchyroll.com/fairy-tail
|
||||
|
||||
Download *Fairy Tail* to `C:\Anime`:
|
||||
|
||||
crunchyroll --output C:\Anime http://www.crunchyroll.com/fairy-tail
|
||||
crunchy --output C:\Anime http://www.crunchyroll.com/fairy-tail
|
||||
|
||||
#### Switches
|
||||
|
||||
@@ -91,6 +95,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.
|
||||
@@ -108,6 +114,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:
|
||||
@@ -115,3 +151,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/crunchyroll → bin/crunchy
Normal file → Executable file
0
bin/crunchyroll → bin/crunchy
Normal file → Executable file
10
bin/crunchy.sh
Executable file
10
bin/crunchy.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
PARAMS=$*
|
||||
for i in {1..20}; do
|
||||
crunchy ${PARAMS}
|
||||
if [ $? == 0 ]; then
|
||||
break
|
||||
fi
|
||||
echo "Going to retry..."
|
||||
sleep 3
|
||||
done
|
||||
BIN
bin/ffmpeg.exe
Executable file
BIN
bin/ffmpeg.exe
Executable file
Binary file not shown.
1173
package-lock.json
generated
Normal file
1173
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
47
package.json
47
package.json
@@ -1,36 +1,51 @@
|
||||
{
|
||||
"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.2.2",
|
||||
"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.27",
|
||||
"cheerio": "^0.22.0",
|
||||
"cloudscraper": "^1.5.0",
|
||||
"commander": "^2.15.1",
|
||||
"fs-extra": "^5.0.0",
|
||||
"mkdirp": "^0.5.0",
|
||||
"request": "^2.53.0",
|
||||
"request": "^2.85.0",
|
||||
"xml2js": "^0.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsd": "^0.5.7",
|
||||
"tslint": "^2.1.1",
|
||||
"typescript": "^1.4.1"
|
||||
"@types/cheerio": "^0.22.7",
|
||||
"@types/mkdirp": "^0.5.2",
|
||||
"@types/request": "^2.47.0",
|
||||
"@types/xml2js": "^0.4.2",
|
||||
"tsconfig-lint": "^0.12.0",
|
||||
"tslint": "^5.9.1",
|
||||
"typescript": "^2.8.1"
|
||||
},
|
||||
"scripts": {
|
||||
"prepublish": "npm run tsd && node ts",
|
||||
"test": "node ts --only-test",
|
||||
"tsd": "tsd reinstall --overwrite"
|
||||
"prepublishOnly": "npm run build",
|
||||
"build": "tsc",
|
||||
"test": "tslint -c ./tslint.json --project ./tsconfig.json ./src/**/*.ts",
|
||||
"start": "node ./bin/crunchy"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/Godzil/Crunchy/issues"
|
||||
}
|
||||
}
|
||||
|
||||
168
src/batch.ts
168
src/batch.ts
@@ -1,24 +1,70 @@
|
||||
'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 series from './series';
|
||||
import log = require('./log');
|
||||
|
||||
/* correspondances between resolution and value CR excpect */
|
||||
let 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);
|
||||
export default function(args: string[], done: (err?: Error) => void)
|
||||
{
|
||||
const config = parse(args);
|
||||
const batchPath = path.join(config.output || process.cwd(), 'CrunchyRoll.txt');
|
||||
|
||||
// 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, tasks) =>
|
||||
{
|
||||
if (err)
|
||||
{
|
||||
return done(err);
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
|
||||
(function next()
|
||||
{
|
||||
if (i >= tasks.length)
|
||||
{
|
||||
return done();
|
||||
}
|
||||
|
||||
series(tasks[i].config, tasks[i].address, (errin) =>
|
||||
{
|
||||
if (errin)
|
||||
{
|
||||
return done(errin);
|
||||
}
|
||||
i += 1;
|
||||
next();
|
||||
});
|
||||
@@ -29,42 +75,85 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
const configIn = config;
|
||||
|
||||
return done(null, config.args.map((addressIn) =>
|
||||
{
|
||||
return {address: addressIn, config: configIn};
|
||||
}));
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
map.push({address: addressIn, config: lineConfig});
|
||||
});
|
||||
});
|
||||
done(null, map);
|
||||
@@ -75,7 +164,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.')
|
||||
@@ -90,6 +180,8 @@ function parse(args: string[]): typings.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('-n, --filename <s>', 'The name override.')
|
||||
.option('-t, --tag <s>', 'The subgroup. (Default: CrunchyRoll)')
|
||||
.option('-r, --resolution <s>', 'The video resolution. (Default: 1080 (360, 480, 720, 1080))')
|
||||
.parse(args);
|
||||
}
|
||||
|
||||
13
src/cli.ts
13
src/cli.ts
@@ -1,6 +1,13 @@
|
||||
'use strict';
|
||||
import batch = require('./batch');
|
||||
import batch from './batch';
|
||||
|
||||
batch(process.argv, (err: any) => {
|
||||
if (err) console.error(err.stack || err);
|
||||
batch(process.argv, (err: any) =>
|
||||
{
|
||||
if (err)
|
||||
{
|
||||
console.error(err.stack || err);
|
||||
process.exit(-1)
|
||||
}
|
||||
console.info("Done!")
|
||||
process.exit(0)
|
||||
});
|
||||
|
||||
379
src/episode.ts
379
src/episode.ts
@@ -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,114 @@ 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)
|
||||
{
|
||||
let series = config.series || page.series;
|
||||
|
||||
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;
|
||||
log.warn('File \'' + fileName + '\' already exist...');
|
||||
|
||||
do
|
||||
{
|
||||
count = count + 1;
|
||||
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 + '\'...');
|
||||
}
|
||||
|
||||
mkdirp(path.dirname(filePath), (errM: Error) =>
|
||||
{
|
||||
if (errM)
|
||||
{
|
||||
return done(errM, false);
|
||||
}
|
||||
|
||||
log.dispEpisode(fileName, 'Fetching...', false);
|
||||
downloadSubtitle(config, player, filePath, (errDS) =>
|
||||
{
|
||||
if (errDS)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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, (errVM) =>
|
||||
{
|
||||
if (errVM)
|
||||
{
|
||||
return done(errVM, false);
|
||||
}
|
||||
|
||||
complete(fileName, 'Finished!', now, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
log.dispEpisode(fileName, 'Ignoring: not released yet', true);
|
||||
done(null, true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -64,15 +151,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](data, (errF: Error, decodedSubtitle: string) =>
|
||||
{
|
||||
if (errF)
|
||||
{
|
||||
return done(errF);
|
||||
}
|
||||
|
||||
fs.writeFile(filePath + '.' + format, '\ufeff' + decodedSubtitle, done);
|
||||
});
|
||||
});
|
||||
@@ -81,91 +185,172 @@ 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(ignored/*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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 name(config: IConfig, page: IEpisodePage, series: string, extra: string)
|
||||
{
|
||||
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';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
let episodeTitle = $('#showmedia_about_name').text().replace(/[“”]/g, '');
|
||||
const data = regexp.exec(look);
|
||||
|
||||
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’...');
|
||||
done(null, {
|
||||
episode: '0',
|
||||
id: epId,
|
||||
series: seasonTitle,
|
||||
season: seasonTitle,
|
||||
title: episodeTitle,
|
||||
swf: swf[1],
|
||||
volume: '0',
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
done(null, {
|
||||
episode: data[3],
|
||||
id: epId,
|
||||
series: data[1],
|
||||
season: seasonTitle,
|
||||
title: episodeTitle,
|
||||
swf: swf[1],
|
||||
volume: data[2] || '1',
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
done(parseError);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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};
|
||||
|
||||
20
src/interface/IConfig.d.ts
vendored
Normal file
20
src/interface/IConfig.d.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
interface IConfig {
|
||||
// Authentication
|
||||
pass?: string;
|
||||
user?: string;
|
||||
// Disables
|
||||
cache?: boolean;
|
||||
merge?: boolean;
|
||||
// Filters
|
||||
episode?: number;
|
||||
volume?: number;
|
||||
// Settings
|
||||
format?: string;
|
||||
output?: string;
|
||||
series?: string;
|
||||
filename?: string;
|
||||
tag?: string;
|
||||
resolution?: string;
|
||||
video_format?: string;
|
||||
video_quality?: string;
|
||||
}
|
||||
3
src/interface/IConfigLine.d.ts
vendored
Normal file
3
src/interface/IConfigLine.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
interface IConfigLine extends IConfig {
|
||||
args: string[];
|
||||
}
|
||||
4
src/interface/IConfigTask.d.ts
vendored
Normal file
4
src/interface/IConfigTask.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
interface IConfigTask {
|
||||
address: string;
|
||||
config: IConfigLine;
|
||||
}
|
||||
9
src/interface/IEpisodePage.d.ts
vendored
Normal file
9
src/interface/IEpisodePage.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
interface IEpisodePage {
|
||||
id: number;
|
||||
episode: string;
|
||||
series: string;
|
||||
volume: string;
|
||||
season: string;
|
||||
title: string;
|
||||
swf: string;
|
||||
}
|
||||
12
src/interface/IEpisodePlayer.d.ts
vendored
Normal file
12
src/interface/IEpisodePlayer.d.ts
vendored
Normal 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
15
src/interface/IEpisodePlayerConfig.d.ts
vendored
Normal 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
3
src/interface/IFormatterTable.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
interface IFormatterTable {
|
||||
[key: string]: (input: string|Buffer, done: (err: Error, subtitle?: string) => void) => void;
|
||||
}
|
||||
4
src/interface/IResolData.d.ts
vendored
Normal file
4
src/interface/IResolData.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
interface IResolData {
|
||||
quality: string;
|
||||
format: string;
|
||||
}
|
||||
4
src/interface/ISeries.d.ts
vendored
Normal file
4
src/interface/ISeries.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
interface ISeries {
|
||||
episodes: ISeriesEpisode[];
|
||||
series: string;
|
||||
}
|
||||
5
src/interface/ISeriesEpisode.d.ts
vendored
Normal file
5
src/interface/ISeriesEpisode.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
interface ISeriesEpisode {
|
||||
address: string;
|
||||
episode: string;
|
||||
volume: number;
|
||||
}
|
||||
13
src/interface/ISubtitle.d.ts
vendored
Normal file
13
src/interface/ISubtitle.d.ts
vendored
Normal 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
15
src/interface/ISubtitleEvent.d.ts
vendored
Normal 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
29
src/interface/ISubtitleStyle.d.ts
vendored
Normal 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;
|
||||
};
|
||||
}[];
|
||||
}
|
||||
37
src/log.ts
Normal file
37
src/log.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
'use strict';
|
||||
import os = require('os');
|
||||
|
||||
export function error(str: string)
|
||||
{
|
||||
/* 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('');
|
||||
}
|
||||
}
|
||||
192
src/my_request.ts
Normal file
192
src/my_request.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
'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; x64; rv:58.0) Gecko/20100101 Firefox/58.0',
|
||||
// Mozilla/5.0 (Windows NT 6.2; WOW64; rv:34.0) Gecko/20100101 Firefox/34.0',
|
||||
'Connection': 'keep-alive',
|
||||
'Referer': 'https://www.crunchyroll.com/login'
|
||||
};
|
||||
|
||||
/**
|
||||
* 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'), (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[name]': config.user,
|
||||
'login_form[password]': config.pass,
|
||||
'login_form[redirect_url]': '/',
|
||||
'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 };
|
||||
}
|
||||
@@ -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()};
|
||||
}
|
||||
224
src/series.ts
224
src/series.ts
@@ -1,34 +1,83 @@
|
||||
'use strict';
|
||||
export = main;
|
||||
import cheerio = require('cheerio');
|
||||
import episode = require('./episode');
|
||||
import episode from './episode';
|
||||
import fs = require('fs');
|
||||
import request = require('./request');
|
||||
const fse = 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, address: string, done: (err: Error) => void)
|
||||
{
|
||||
const persistentPath = path.join(config.output || process.cwd(), persistent);
|
||||
|
||||
/* Make a backup of the persistent file in case of */
|
||||
if (fileExist(persistentPath))
|
||||
{
|
||||
fse.copySync(persistentPath, persistentPath + '.backup');
|
||||
}
|
||||
|
||||
fs.readFile(persistentPath, 'utf8', (err: Error, contents: string) =>
|
||||
{
|
||||
const cache = config.cache ? {} : JSON.parse(contents || '{}');
|
||||
|
||||
page(config, address, (errP, page) =>
|
||||
{
|
||||
if (errP)
|
||||
{
|
||||
return done(errP);
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
(function next()
|
||||
{
|
||||
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);
|
||||
download(cache, config, address, page.episodes[i], (errD, ignored) =>
|
||||
{
|
||||
if (errD)
|
||||
{
|
||||
return done(errD);
|
||||
}
|
||||
|
||||
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 +87,117 @@ 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,
|
||||
baseAddress: string, item: ISeriesEpisode,
|
||||
done: (err: Error, ign: boolean) => void)
|
||||
{
|
||||
if (!filter(config, item))
|
||||
{
|
||||
return done(null, false);
|
||||
}
|
||||
|
||||
const address = url.resolve(baseAddress, 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) {
|
||||
function filter(config: IConfig, item: ISeriesEpisode)
|
||||
{
|
||||
// Filter on chapter.
|
||||
var episodeFilter = config.episode;
|
||||
if (episodeFilter > 0 && item.episode <= episodeFilter) return false;
|
||||
if (episodeFilter < 0 && item.episode >= -episodeFilter) return false;
|
||||
|
||||
const episodeFilter = config.episode;
|
||||
// Filter on volume.
|
||||
var volumeFilter = config.volume;
|
||||
if (volumeFilter > 0 && item.volume <= volumeFilter) return false;
|
||||
if (volumeFilter < 0 && item.volume >= -volumeFilter) return false;
|
||||
const volumeFilter = config.volume;
|
||||
|
||||
const currentEpisode = parseInt(item.episode, 10);
|
||||
const currentVolume = item.volume;
|
||||
|
||||
if ( ( (episodeFilter > 0) && (currentEpisode <= episodeFilter) ) ||
|
||||
( (episodeFilter < 0) && (currentEpisode >= -episodeFilter) ) ||
|
||||
( (volumeFilter > 0) && (currentVolume <= volumeFilter ) ) ||
|
||||
( (volumeFilter < 0) && (currentVolume >= -volumeFilter ) ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the page and scrapes the episodes and series.
|
||||
*/
|
||||
function page(config: 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 page(config: IConfig, address: string, done: (err: Error, result?: ISeries) => void)
|
||||
{
|
||||
if (address[0] === '@')
|
||||
{
|
||||
log.info('Trying to fetch from ' + address.substr(1));
|
||||
const episodes: ISeriesEpisode[] = [];
|
||||
episodes.push({
|
||||
address: address.substr(1),
|
||||
episode: '',
|
||||
volume: 0,
|
||||
});
|
||||
done(null, {episodes: episodes.reverse(), series: title});
|
||||
});
|
||||
done(null, {episodes: episodes.reverse(), series: ""});
|
||||
}
|
||||
else
|
||||
{
|
||||
let episodeCount = 0;
|
||||
my_request.get(config, address, (err, result) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
const $ = cheerio.load(result);
|
||||
const title = $('span[itemprop=name]').text();
|
||||
|
||||
if (!title) {
|
||||
return done(new Error('Invalid page.(' + 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,
|
||||
});
|
||||
});
|
||||
if (episodeCount === 0)
|
||||
{
|
||||
log.warn("No episodes found for " + title + ". Could it be a movie?");
|
||||
}
|
||||
done(null, {episodes: episodes.reverse(), series: title});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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' ? new Buffer(iv, 'base64') : iv;
|
||||
const dataBuffer = typeof data === 'string' ? new Buffer(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 = new Buffer(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;
|
||||
}
|
||||
|
||||
@@ -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(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 {
|
||||
}, (err: Error, xml: ISubtitle) =>
|
||||
{
|
||||
if (err)
|
||||
{
|
||||
return done(err);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
done(null, script(xml) + '\n' +
|
||||
style(xml.styles) + '\n' +
|
||||
event(xml.events));
|
||||
} catch (err) {
|
||||
style(xml.styles) + '\n' +
|
||||
event(xml.events));
|
||||
} catch (err)
|
||||
{
|
||||
done(err);
|
||||
}
|
||||
});
|
||||
@@ -25,69 +31,73 @@ function main(input: string|Buffer, done: (err: Error, subtitle?: string) => voi
|
||||
/**
|
||||
* Converts the event block.
|
||||
*/
|
||||
function event(block: typings.ISubtitleEvent): string {
|
||||
function event(block: ISubtitleEvent): string
|
||||
{
|
||||
var 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(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';
|
||||
'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 {
|
||||
function style(block: 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';
|
||||
'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';
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
export default <IFormatterTable> {
|
||||
ass: ass,
|
||||
srt: srt
|
||||
};
|
||||
|
||||
@@ -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(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;
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
136
src/typings.ts
136
src/typings.ts
@@ -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;
|
||||
};
|
||||
}[];
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
@@ -1,37 +1,63 @@
|
||||
'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);
|
||||
export default function(config: IConfig, isSubtitled: boolean, rtmpInputPath: string, filePath: string,
|
||||
streamMode: string, done: (err: Error) => void)
|
||||
{
|
||||
const subtitlePath = filePath + '.' + (subtitle.formats[config.format] ? config.format : 'ass');
|
||||
let videoPath = filePath;
|
||||
|
||||
if (streamMode === 'RTMP')
|
||||
{
|
||||
videoPath += path.extname(rtmpInputPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
videoPath += '.mp4';
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
'-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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 +65,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 +81,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);
|
||||
}
|
||||
|
||||
@@ -1,26 +1,51 @@
|
||||
'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, 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
11
ts.js
@@ -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() {
|
||||
|
||||
@@ -1,41 +1,11 @@
|
||||
{
|
||||
"version": "1.4.1",
|
||||
"target": "es6",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"noImplicitAny": true,
|
||||
"removeComments": false,
|
||||
"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"
|
||||
]
|
||||
}
|
||||
"sourceMap": true
|
||||
}
|
||||
}
|
||||
|
||||
33
tsd.json
33
tsd.json
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"version": "v4",
|
||||
"repo": "borisyankov/DefinitelyTyped",
|
||||
"ref": "master",
|
||||
"path": "typings",
|
||||
"bundle": "typings/tsd.d.ts",
|
||||
"installed": {
|
||||
"node/node.d.ts": {
|
||||
"commit": "3882d337bb0808cde9fe4c08012508a48c135482"
|
||||
},
|
||||
"commander/commander.d.ts": {
|
||||
"commit": "3882d337bb0808cde9fe4c08012508a48c135482"
|
||||
},
|
||||
"xml2js/xml2js.d.ts": {
|
||||
"commit": "3882d337bb0808cde9fe4c08012508a48c135482"
|
||||
},
|
||||
"cheerio/cheerio.d.ts": {
|
||||
"commit": "3882d337bb0808cde9fe4c08012508a48c135482"
|
||||
},
|
||||
"mkdirp/mkdirp.d.ts": {
|
||||
"commit": "3882d337bb0808cde9fe4c08012508a48c135482"
|
||||
},
|
||||
"request/request.d.ts": {
|
||||
"commit": "3882d337bb0808cde9fe4c08012508a48c135482"
|
||||
},
|
||||
"big-integer/big-integer.d.ts": {
|
||||
"commit": "3882d337bb0808cde9fe4c08012508a48c135482"
|
||||
},
|
||||
"form-data/form-data.d.ts": {
|
||||
"commit": "3882d337bb0808cde9fe4c08012508a48c135482"
|
||||
}
|
||||
}
|
||||
}
|
||||
11
tslint.json
11
tslint.json
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"extends": "tslint:latest",
|
||||
"rules": {
|
||||
"ban": false,
|
||||
"class-name": true,
|
||||
@@ -12,13 +13,13 @@
|
||||
"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"
|
||||
],
|
||||
"array-type": [true, "array"],
|
||||
"no-any": false,
|
||||
"no-arg": true,
|
||||
"no-bitwise": true,
|
||||
@@ -30,25 +31,18 @@
|
||||
"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 +60,6 @@
|
||||
"property-declaration": "nospace",
|
||||
"variable-declaration": "nospace"
|
||||
}],
|
||||
"use-strict": false,
|
||||
"variable-name": false,
|
||||
"whitespace": [true,
|
||||
"check-branch",
|
||||
|
||||
Reference in New Issue
Block a user