40 Commits

Author SHA1 Message Date
Godzil
58f4dc61ff Fix login issue 2017-02-07 20:22:25 +00:00
Godzil
b96efacbd2 - Revert login using the token method
- Use the cloudscraper layer on top of request to pass through the cloudfare browser check
- switch from tsd to typings
2017-02-07 20:22:01 +00:00
Godzil
a346ab8854 1.1.14 2017-02-01 08:53:23 +00:00
Godzil
499530141e Disable debug message about ffmpeg 2017-02-01 08:53:14 +00:00
Godzil
d1457bb893 1.1.13 2017-01-28 13:38:20 +00:00
Godzil
8dfd1b447c Add a log objet to do some fancy output on the command line (not fully enabled under windows as it need some tests) 2017-01-28 13:38:14 +00:00
Godzil
ce63ae9a16 Upgrade to 1.1.12 to fix login issue 2017-01-23 21:13:13 +00:00
Godzil
70d80ccd17 Update dependency to more recent version, and correct a few warnings reported by ts 2017-01-23 21:06:34 +00:00
Manoël Trapier
7833fbe292 Merge pull request #13 from majewskim/master
Fix a login issue
2017-01-23 20:46:10 +00:00
Mateusz Majewski
fa6aa74442 Merge branch 'master' into master 2017-01-18 12:49:49 +02:00
Mateusz Majewski
fe2ed9fb76 Fixing login issue by bypassing the login form and making a request directly. 2017-01-18 11:19:41 +02:00
Mateusz Majewski
cc655b9e00 Fixing login issue by bypassing the login form and making a request directly. 2017-01-18 11:08:45 +02:00
Manoël Trapier
e1d2a55a01 Update README.md 2016-10-21 17:21:36 +01:00
Manoël Trapier
a31de0ef9d Remove .js from the name 2016-10-21 17:21:05 +01:00
Godzil
2853334d7f 1.1.11
- Update login mechanism to march CR september 2016 changes
2016-09-16 22:20:43 +01:00
Godzil
69dd28d31b Update login to match latest CR changes 2016-09-16 22:20:39 +01:00
Godzil
56afce02ea 1.1.10
- Change name format to follow Plex forvourite one.
- Remplace ":" in file name to prevent issue on Windows
2016-09-10 20:22:10 +01:00
Godzil
bc4697061e Remplace ':' in filename to make Windows happy 2016-09-10 20:17:19 +01:00
Godzil
55ffe85f77 Make the name to be more Plex friendly 2016-09-10 11:53:45 +01:00
Godzil
ec8c2c7716 1.1.9
New features:
 - Should correctly handle 10.5 or 1A episode numbers
 - Change characters in series name that are not allowed in filename (or could cause issues, like slash ( / ) or single quote ( ' )
 - Prevent overwrite of existing files (usefull when different season report the same number)
 - If an error during episode metadata scraping, it will set to Season 0, episode 0 (look at the logs!)
 - If an episode is not available yet, it will be skipped
2016-09-07 22:09:14 +01:00
Godzil
714a528f8b Prevent overwriting an existing output file 2016-09-07 21:55:14 +01:00
Godzil
8314d91bd7 Add functionality to ignore (instead of stopping) if an episode is not available yet 2016-09-07 21:51:36 +01:00
Godzil
5bd31f9e0b Add better episode numbering scheme 2016-09-07 21:34:29 +01:00
Godzil
95a93930f3 Merge branch 'master' of github.com:Godzil/crunchyroll.js 2016-08-22 13:10:02 +02:00
Godzil
4a9e1d0410 Update LICENSE 2016-08-22 12:03:05 +01:00
Godzil
1eacd0a5ca add license to npm 2016-08-22 13:03:02 +02:00
Godzil
3c32726745 1.1.8 2016-08-22 12:32:25 +02:00
Godzil
42ae0ae1fb Forget to rename main executable 2016-08-22 12:30:43 +02:00
Godzil
e4b3871919 1.1.7 2016-08-22 12:22:45 +02:00
Godzil
58e4a557e2 Update readme 2016-08-22 12:22:36 +02:00
Godzil
8371d68113 Correct errors 2016-08-22 12:21:17 +02:00
Godzil
b7d496fc9d Update README.md 2016-08-22 10:51:23 +01:00
Godzil
14260d04b3 Update README.md 2016-08-22 10:50:13 +01:00
Godzil
3d46b65d67 Update package.json 2016-08-22 10:47:18 +01:00
Godzil
62a08e14bb Add missing mode parameter to the stream object. (and correct call to it) 2016-08-13 21:24:57 +01:00
Godzil
422d0827f9 Correct file source extension for the merge pass. 2016-08-13 16:32:56 +01:00
Godzil
546a849aa5 Add ffmpeg when using HLS instead of RTMP. 2016-08-13 16:20:33 +01:00
Roel van Uden
e06ff53210 Lock down dependencies, bump version (#9) 2015-06-25 19:42:10 +02:00
Roel van Uden
18375d3d22 Update to TS1.5 2015-05-23 19:07:14 +02:00
Roel van Uden
5fdee94b38 References for future self. 2015-03-07 18:46:42 +01:00
39 changed files with 559 additions and 343 deletions

View File

@@ -1,4 +1,5 @@
Copyright (c) 2015 Roel van Uden
Copyright (c) 2016 Manoel <Godzil> Trapier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to

1
README Symbolic link
View File

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

View File

@@ -1,6 +1,6 @@
# 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.
*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 +10,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.
@@ -26,30 +28,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 +76,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

BIN
bin/ffmpeg.exe Executable file

Binary file not shown.

View File

@@ -1,36 +1,38 @@
{
"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",
"version": "1.1.15",
"bin": {
"crunchyroll": "./bin/crunchyroll"
"crunchy": "./bin/crunchy"
},
"dependencies": {
"big-integer": "^1.4.4",
"cheerio": "^0.18.0",
"commander": "^2.6.0",
"mkdirp": "^0.5.0",
"request": "^2.53.0",
"xml2js": "^0.4.5"
"big-integer": "1.4.4",
"cheerio": "0.22.0",
"cloudscraper": "1.4.1",
"commander": "2.6.0",
"mkdirp": "0.5.0",
"request": "2.74.0",
"xml2js": "0.4.5"
},
"devDependencies": {
"tsd": "^0.5.7",
"tslint": "^2.1.1",
"typescript": "^1.4.1"
"typing": "2.1.0",
"tslint": "2.3.0-beta",
"typescript": "1.5.0-beta"
},
"scripts": {
"prepublish": "npm run tsd && node ts",
"test": "node ts --only-test",
"tsd": "tsd reinstall --overwrite"
"prepublish": "npm run types && tsc",
"test": "node ts --only-test",
"types": "typings install"
}
}

View File

@@ -1,15 +1,13 @@
'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';
/**
* Streams the batch of series to disk.
*/
function main(args: string[], done: (err?: Error) => void) {
export default function(args: string[], done: (err?: Error) => void) {
var config = parse(args);
var batchPath = path.join(config.output || process.cwd(), 'CrunchyRoll.txt');
tasks(config, batchPath, (err, tasks) => {
@@ -41,14 +39,15 @@ function split(value: string): string[] {
previous = i + 1;
}
}
pieces.push(value.substring(previous, i).match(/^"?(.+?)"?$/)[1]);
var 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) {
function tasks(config: IConfigLine, batchPath: string, done: (err: Error, tasks?: IConfigTask[]) => void) {
if (config.args.length) {
return done(null, config.args.map(address => {
return {address: address, config: config};
@@ -58,7 +57,7 @@ function tasks(config: typings.IConfigLine, batchPath: string, done: (err: Error
if (!exists) return done(null, []);
fs.readFile(batchPath, 'utf8', (err, data) => {
if (err) return done(err);
var map: typings.IConfigTask[] = [];
var map: IConfigTask[] = [];
data.split(/\r?\n/).forEach(line => {
if (/^(\/\/|#)/.test(line)) return;
var lineConfig = parse(process.argv.concat(split(line)));
@@ -75,7 +74,7 @@ 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.')

View File

@@ -1,5 +1,5 @@
'use strict';
import batch = require('./batch');
import batch from './batch';
batch(process.argv, (err: any) => {
if (err) console.error(err.stack || err);

View File

@@ -1,23 +1,22 @@
'use strict';
export = main;
import cheerio = require('cheerio');
import fs = require('fs');
import mkdirp = require('mkdirp');
import request = require('./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) {
export default function(config: IConfig, address: string, done: (err: Error, ign: boolean) => void) {
scrapePage(config, address, (err, page) => {
if (err) return done(err);
if (err) return done(err, false);
scrapePlayer(config, address, page.id, (err, player) => {
if (err) return done(err);
if (err) return done(err, false);
download(config, page, player, done);
});
});
@@ -26,37 +25,72 @@ 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) {
function complete(epName: string, message: string, begin: number, done: (err: Error, ign: boolean) => void) {
var timeInMs = Date.now() - begin;
var seconds = prefix(Math.floor(timeInMs / 1000) % 60, 2);
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);
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;
}
/**
* Downloads the subtitle and video.
*/
function download(config: typings.IConfig, page: typings.IEpisodePage, player: typings.IEpisodePlayer, done: (err: Error) => void) {
function download(config: IConfig, page: IEpisodePage, player: IEpisodePlayer, done: (err: Error, ign: boolean) => void) {
var series = config.series || page.series;
var fileName = name(config, page, series);
series = series.replace("/","_").replace("'","_").replace(":","_");
var fileName = name(config, page, series, "").replace("/","_").replace("'","_").replace(":","_");
var filePath = path.join(config.output || process.cwd(), series, fileName);
if (fileExist(filePath + ".mkv"))
{
var count = 0;
log.warn("File '"+fileName+"' already exist...");
do
{
count = count + 1;
fileName = name(config, page, series, "-" + count).replace("/","_").replace("'","_").replace(":","_");
filePath = path.join(config.output || process.cwd(), series, fileName);
} while(fileExist(filePath + ".mkv"))
log.warn("Renaming to '"+fileName+"'...");
}
mkdirp(path.dirname(filePath), (err: Error) => {
if (err) return done(err);
if (err) return done(err, false);
downloadSubtitle(config, player, filePath, err => {
if (err) return done(err);
if (err) return done(err, false);
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);
if (player.video.file != undefined)
{
log.dispEpisode(fileName, 'Fetching...', false);
downloadVideo(config, page, player, filePath, err => {
if (err) return done(err, false);
if (config.merge) return complete(fileName, 'Finished!', now, done);
var isSubtited = Boolean(player.subtitle);
video.merge(config, isSubtited, player.video.file, filePath, player.video.mode, err => {
if (err) return done(err, false);
complete(fileName, 'Finished!', now, done);
});
});
});
}
else
{
log.dispEpisode(fileName, 'Ignoring: not released yet', true);
done(null, true);
}
});
});
}
@@ -64,7 +98,7 @@ 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) {
function downloadSubtitle(config: IConfig, player: 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) => {
@@ -81,27 +115,30 @@ function downloadSubtitle(config: typings.IConfig, player: typings.IEpisodePlaye
/**
* Streams the video to disk.
*/
function downloadVideo(config: typings.IConfig,
page: typings.IEpisodePage,
player: typings.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),
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;
function name(config: IConfig, page: IEpisodePage, series: string, extra: string) {
var episodeNum = parseInt(page.episode, 10);
var volumeNum = parseInt(page.volume, 10);
var episode = (episodeNum < 10 ? '0' : '') + page.episode;
var volume = (volumeNum < 10 ? '0' : '') + page.volume;
var tag = config.tag || 'CrunchyRoll';
return series + ' ' + volume + 'x' + episode + ' [' + tag + ']';
return series + ' - s' + volume + 'e' + episode +' - [' + tag + ']' + extra;
}
/**
@@ -116,22 +153,36 @@ function prefix(value: number|string, length: number) {
/**
* 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) {
function scrapePage(config: IConfig, address: string, done: (err: Error, page?: IEpisodePage) => void) {
var id = parseInt((address.match(/[0-9]+$/) || ['0'])[0], 10);
if (!id) return done(new Error('Invalid address.'));
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.'));
var 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]?)/;
var look = $('#showmedia_about_media').text();
var seasonTitle = $('span[itemprop="title"]').text();
var data = regexp.exec(look);
if (!swf || !data)
{
log.warn('Something wrong in the page at '+address+' (data are: '+look+')');
log.warn('Setting Season to 0 and episode to \0\...');
done(null, {
id: id,
episode: "0",
series: seasonTitle,
swf: swf[1],
volume: "0"
});
}
done(null, {
id: id,
episode: parseInt(data[3], 10),
episode: data[3],
series: data[1],
swf: swf[1],
volume: parseInt(data[2], 10) || 1
volume: data[2] || "1"
});
});
}
@@ -139,7 +190,7 @@ function scrapePage(config: typings.IConfig, address: string, done: (err: Error,
/**
* 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) {
function scrapePlayer(config: IConfig, address: string, id: number, done: (err: Error, player?: IEpisodePlayer) => void) {
var url = address.match(/^(https?:\/\/[^\/]+)/);
if (!url) return done(new Error('Invalid address.'));
request.post(config, {
@@ -150,10 +201,15 @@ function scrapePlayer(config: typings.IConfig, address: string, id: number, done
xml2js.parseString(result, {
explicitArray: false,
explicitRoot: false
}, (err: Error, player: typings.IEpisodePlayerConfig) => {
}, (err: Error, player: IEpisodePlayerConfig) => {
if (err) return done(err);
try {
var isSubtitled = Boolean(player['default:preload'].subtitle);
var streamMode="RTMP";
if (player['default:preload'].stream_info.host == "")
{
streamMode="HLS";
}
done(null, {
subtitle: isSubtitled ? {
id: parseInt(player['default:preload'].subtitle.$.id, 10),
@@ -161,6 +217,7 @@ function scrapePlayer(config: typings.IConfig, address: string, id: number, done
data: player['default:preload'].subtitle.data
} : null,
video: {
mode: streamMode,
file: player['default:preload'].stream_info.file,
host: player['default:preload'].stream_info.host
}

View File

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

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

@@ -0,0 +1,16 @@
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;
}

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

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

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

@@ -0,0 +1,4 @@
interface IConfigTask {
address: string;
config: IConfigLine;
}

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

@@ -0,0 +1,7 @@
interface IEpisodePage {
id: number;
episode: string;
series: string;
volume: string;
swf: string;
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

71
src/log.ts Normal file
View File

@@ -0,0 +1,71 @@
'use strict';
import os = require('os');
export function error(str: string)
{
if (os.platform() === 'win32')
{
console.log(' * ERROR: ' + str);
}
else
{
/* Do more fancy output */
console.error(' \x1B[1;31m* ERROR\x1B[0m: ' + str);
}
}
export function info(str: string)
{
if (os.platform() === 'win32')
{
console.log(' * INFO : ' + str);
}
else
{
/* Do more fancy output */
console.log(' \x1B[1;32m* INFO \x1B[0m: ' + str);
}
}
export function debug(str: string)
{
if (os.platform() === 'win32')
{
console.log(' * DEBUG: ' + str);
}
else
{
/* Do more fancy output */
console.log(' \x1B[1;35m* DEBUG\x1B[0m: ' + str);
}
}
export function warn(str: string)
{
if (os.platform() === 'win32')
{
console.log(' * WARN : ' + str);
}
else
{
/* Do more fancy output */
console.log(' \x1B[1;33m* WARN \x1B[0m: ' + str);
}
}
export function dispEpisode(name: string, status: string, addNL: boolean)
{
if (os.platform() === 'win32')
{
process.stdout.write(' > ' + name + ' : ' + status + '\x1B[0G');
}
else
{
/* Do more fancy output */
process.stdout.write(' \x1B[1;33m> \x1B[37m' + name + '\x1B[0m : \x1B[33m' + status + '\x1B[0m\x1B[0G');
}
if (addNL)
{
console.log('');
}
}

View File

@@ -1,15 +1,23 @@
'use strict';
import request = require('request');
import typings = require('./typings');
import cheerio = require('cheerio');
import log = require('./log');
var cloudscraper = require('cloudscraper');
var isAuthenticated = false;
var isPremium = false;
var 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: typings.IConfig, options: request.Options, done: (err: Error, result?: string) => void) {
export function get(config: IConfig, options: request.Options, done: (err: Error, result?: string) => void) {
authenticate(config, err => {
if (err) return done(err);
request.get(modify(options), (err: Error, response: any, body: any) => {
cloudscraper.request(modify(options, 'GET'), (err: Error, response: any, body: any) => {
if (err) return done(err);
done(null, typeof body === 'string' ? body : String(body));
});
@@ -19,10 +27,10 @@ export function get(config: typings.IConfig, options: request.Options, done: (er
/**
* Performs a POST request for the resource.
*/
export function post(config: typings.IConfig, options: request.Options, done: (err: Error, result?: string) => void) {
export function post(config: IConfig, options: request.Options, done: (err: Error, result?: string) => void) {
authenticate(config, err => {
if (err) return done(err);
request.post(modify(options), (err: Error, response: any, body: any) => {
cloudscraper.request(modify(options, 'POST'), (err: Error, response: any, body: any) => {
if (err) return done(err);
done(null, typeof body === 'string' ? body : String(body));
});
@@ -32,32 +40,94 @@ export function post(config: typings.IConfig, options: request.Options, done: (e
/**
* Authenticates using the configured pass and user.
*/
function authenticate(config: typings.IConfig, done: (err: Error) => void) {
function authenticate(config: IConfig, done: (err: Error) => void) {
if (isAuthenticated || !config.pass || !config.user) return done(null);
var options = {
form: {
formname: 'RpcApiUser_Login',
fail_url: 'https://www.crunchyroll.com/login',
name: config.user,
password: config.pass
},
/* Bypass the login page and send a login request directly */
var options =
{
headers: defaultHeaders,
jar: true,
url: 'https://www.crunchyroll.com/?a=formhandler'
gzip: false,
method: 'GET',
url: 'https://www.crunchyroll.com/login'
};
request.post(options, (err: Error) => {
// request(options, (err: Error, rep: string, body: string) =>
cloudscraper.request(options, (err: Error, rep: string, body: string) =>
{
if (err) return done(err);
isAuthenticated = true;
done(null);
var $ = cheerio.load(body);
/* Get the token from the login page */
var token = $('input[name="login_form[_token]"]').attr('value');
if (token === '') return done(new Error('Can`t find token!'));
var 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'
};
// request.post(options, (err: Error, rep: string, body: string) =>
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.
*/
var 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);
var $ = cheerio.load(body);
/* Check if auth worked */
var regexps = /ga\(\'set\', \'dimension[5-8]\', \'([^']*)\'\);/g;
var dims = regexps.exec($('script').text());
for (var 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)
{
var 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): request.Options {
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, url: options.toString()};
return { jar: true, headers: defaultHeaders, url: options.toString(), method: reqMethod};
}

View File

@@ -1,18 +1,17 @@
'use strict';
export = main;
import cheerio = require('cheerio');
import episode = require('./episode');
import episode from './episode';
import fs = require('fs');
import request = require('./request');
import path = require('path');
import typings = require('./typings');
import url = require('url');
import log = require('./log');
var persistent = '.crpersistent';
/**
* Streams the series to disk.
*/
function main(config: typings.IConfig, address: string, done: (err: Error) => void) {
export default function(config: 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 || '{}');
@@ -21,14 +20,22 @@ function main(config: typings.IConfig, address: string, done: (err: Error) => vo
var i = 0;
(function next() {
if (i >= page.episodes.length) return done(null);
download(cache, config, address, page.episodes[i], err => {
download(cache, config, address, page.episodes[i], (err, ignored) => {
if (err) return done(err);
var newCache = JSON.stringify(cache, null, ' ');
fs.writeFile(persistentPath, newCache, err => {
if (err) return done(err);
if ((ignored == false) || (ignored == undefined))
{
var newCache = JSON.stringify(cache, null, ' ');
fs.writeFile(persistentPath, newCache, err => {
if (err) return done(err);
i += 1;
next();
});
}
else
{
i += 1;
next();
});
}
});
})();
});
@@ -39,28 +46,28 @@ function main(config: typings.IConfig, address: string, done: (err: Error) => vo
* Downloads the episode.
*/
function download(cache: {[address: string]: number},
config: typings.IConfig,
config: IConfig,
baseAddress: string,
item: typings.ISeriesEpisode,
done: (err: Error) => void) {
if (!filter(config, item)) return done(null);
item: ISeriesEpisode,
done: (err: Error, ign: boolean) => void) {
if (!filter(config, item)) return done(null, false);
var address = url.resolve(baseAddress, item.address);
if (cache[address]) return done(null);
episode(config, address, err => {
if (err) return done(err);
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;
if (episodeFilter > 0 && parseInt(item.episode, 10) <= episodeFilter) return false;
if (episodeFilter < 0 && parseInt(item.episode, 10) >= -episodeFilter) return false;
// Filter on volume.
var volumeFilter = config.volume;
@@ -72,23 +79,24 @@ function filter(config: typings.IConfig, item: typings.ISeriesEpisode) {
/**
* Requests the page and scrapes the episodes and series.
*/
function page(config: typings.IConfig, address: string, done: (err: Error, result?: typings.ISeries) => void) {
function page(config: IConfig, address: string, done: (err: Error, result?: 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[] = [];
if (!title) return done(new Error('Invalid page.(' + address + ')'));
log.info("Checking availability for " + title);
var episodes: 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 regexp = /Episode\s+((PV )?[S0-9][P0-9.]*[a-fA-F]?)\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),
episode: episode[1],
volume: volume ? parseInt(volume[0], 10) : 1
});
});

View File

@@ -1,6 +1,5 @@
/* tslint:disable:no-bitwise false */
'use strict';
export = main;
import crypto = require('crypto');
import bigInt = require('big-integer');
import zlib = require('zlib');
@@ -8,7 +7,7 @@ import zlib = require('zlib');
/**
* Decodes the data.
*/
function main(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 {
decompress(decrypt(id, iv, data), done);
} catch (e) {

View File

@@ -1,16 +1,14 @@
'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) => {
}, (err: Error, xml: ISubtitle) => {
if (err) return done(err);
try {
done(null, script(xml) + '\n' +
@@ -25,7 +23,7 @@ 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' +
@@ -44,7 +42,7 @@ function event(block: typings.ISubtitleEvent): string {
/**
* 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' +
@@ -59,7 +57,7 @@ function script(block: typings.ISubtitle): string {
/**
* 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,' +

View File

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

View File

@@ -1,14 +1,12 @@
'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) {
export default function(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) => {
xml2js.parseString(input.toString(), options, (err: Error, xml: ISubtitle) => {
try {
if (err) return done(err);
done(null, xml.events.event.map((event, index) => {

View File

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

View File

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

View File

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

View File

@@ -1,18 +1,24 @@
'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 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) {
export default function(config: IConfig, isSubtitled: boolean, rtmpInputPath: string, filePath: string, streamMode: string, done: (err: Error) => void) {
var subtitlePath = filePath + '.' + (subtitle.formats[config.format] ? config.format : 'ass');
var videoPath = filePath + path.extname(rtmpInputPath);
var videoPath = filePath;
if (streamMode == "RTMP")
{
videoPath += path.extname(rtmpInputPath);
}
else
{
videoPath += ".mp4";
}
childProcess.exec(command() + ' ' +
'-o "' + filePath + '.mkv" ' +
'"' + videoPath + '" ' +

View File

@@ -1,26 +1,44 @@
'use strict';
export = main;
import childProcess = require('child_process');
import path = require('path');
import os = require('os');
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")
{
//log.debug("Experimental FFMPEG, MAY FAIL!!!");
var 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
View File

@@ -4,9 +4,14 @@ var fs = require('fs');
var path = require('path');
var isTest = process.argv[2] === '--only-test';
// TODO: This file can use some cleaning up. We want to use the tsconfig.json
// and go from there, but then without source maps. That should give us a final
// build output. For now, this legacy build file will remain to do its job.
// TODO: This build task should be removed upon release of TypeScript 1.5 with
// the support for `tsconfig.json`. Invoking `tsc` from `package.json` will then
// read the configuration and compile accordingly. It seems that `TSLint` will,
// eventually, support this mechanism too. That prevents the need for any kind
// of build task and will run entirely based on instructions from `npm`.
//
// Reference #1: https://github.com/Microsoft/TypeScript/issues/1667
// Reference #2: https://github.com/palantir/tslint/issues/281
read(function(err, fileNames) {
clean(fileNames, function() {

View File

@@ -1,5 +1,5 @@
{
"version": "1.4.1",
"version": "1.5.1-beta",
"compilerOptions": {
"declaration": true,
"noImplicitAny": true,
@@ -18,6 +18,19 @@
"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",
@@ -25,7 +38,6 @@
"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",
@@ -38,4 +50,4 @@
"typings/request/request.d.ts",
"typings/xml2js/xml2js.d.ts"
]
}
}

View File

@@ -1,33 +0,0 @@
{
"version": "v4",
"repo": "borisyankov/DefinitelyTyped",
"ref": "master",
"path": "typings",
"bundle": "typings/tsd.d.ts",
"installed": {
"node/node.d.ts": {
"commit": "3882d337bb0808cde9fe4c08012508a48c135482"
},
"commander/commander.d.ts": {
"commit": "3882d337bb0808cde9fe4c08012508a48c135482"
},
"xml2js/xml2js.d.ts": {
"commit": "3882d337bb0808cde9fe4c08012508a48c135482"
},
"cheerio/cheerio.d.ts": {
"commit": "3882d337bb0808cde9fe4c08012508a48c135482"
},
"mkdirp/mkdirp.d.ts": {
"commit": "3882d337bb0808cde9fe4c08012508a48c135482"
},
"request/request.d.ts": {
"commit": "3882d337bb0808cde9fe4c08012508a48c135482"
},
"big-integer/big-integer.d.ts": {
"commit": "3882d337bb0808cde9fe4c08012508a48c135482"
},
"form-data/form-data.d.ts": {
"commit": "3882d337bb0808cde9fe4c08012508a48c135482"
}
}
}

View File

@@ -47,8 +47,6 @@
"no-var-requires": true,
"one-line": [true,
"check-catch",
"check-else",
"check-open-brace",
"check-whitespace"
],
"quotemark": [true, "single"],

13
typings.json Normal file
View File

@@ -0,0 +1,13 @@
{
"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"
}
}