diff --git a/.gitignore b/.gitignore
index c2658d7..5adc8c1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,8 @@
+dist/
node_modules/
+obj/
+typings/
+*.dat
+*.dll
+*.suo
+*.tmp
\ No newline at end of file
diff --git a/.jscsrc b/.jscsrc
deleted file mode 100644
index c29b41b..0000000
--- a/.jscsrc
+++ /dev/null
@@ -1,60 +0,0 @@
-{
- "disallowEmptyBlocks": true,
- "disallowImplicitTypeConversion": ["binary", "boolean", "numeric", "string"],
- "disallowKeywords": ["delete", "with"],
- "disallowKeywordsOnNewLine": ["catch", "else", "finally"],
- "disallowMixedSpacesAndTabs": true,
- "disallowMultipleLineBreaks": true,
- "disallowMultipleLineStrings": true,
- "disallowMultipleVarDecl": true,
- "disallowNewlineBeforeBlockStatements": true,
- "disallowPaddingNewlinesInBlocks": true,
- "disallowQuotedKeysInObjects": "allButReserved",
- "disallowSpaceAfterObjectKeys": true,
- "disallowSpaceAfterPrefixUnaryOperators": true,
- "disallowSpaceBeforePostfixUnaryOperators": true,
- "disallowSpacesInCallExpression": true,
- "disallowSpacesInFunction": {"beforeOpeningRoundBrace": true},
- "disallowSpacesInsideArrayBrackets": true,
- "disallowSpacesInsideObjectBrackets": true,
- "disallowSpacesInsideParentheses": true,
- "disallowTrailingComma": true,
- "disallowTrailingWhitespace": true,
- "disallowYodaConditions": true,
- "jsDoc": {
- "checkAnnotations": "closurecompiler",
- "checkParamNames": true,
- "checkRedundantAccess": true,
- "checkRedundantParams": true,
- "checkReturnTypes": true,
- "checkTypes": "strictNativeCase",
- "enforceExistence": true,
- "leadingUnderscoreAccess": true,
- "requireParamTypes": true,
- "requireReturnTypes": true
- },
- "maximumLineLength": 80,
- "plugins": ["jscs-jsdoc"],
- "requireBlocksOnNewline": true,
- "requireCamelCaseOrUpperCaseIdentifiers": true,
- "requireCapitalizedConstructors": true,
- "requireCommaBeforeLineBreak": true,
- "requireDotNotation": true,
- "requireFunctionDeclarations": true,
- "requireLineFeedAtFileEnd": true,
- "requireOperatorBeforeLineBreak": true,
- "requireParenthesesAroundIIFE": true,
- "requireSpaceAfterBinaryOperators": true,
- "requireSpaceAfterKeywords": ["case", "catch", "do", "else", "for", "if", "return", "switch", "try", "typeof", "void", "while", "with"],
- "requireSpaceAfterLineComment": true,
- "requireSpaceBeforeBinaryOperators": true,
- "requireSpaceBeforeBlockStatements": true,
- "requireSpaceBeforeObjectValues": true,
- "requireSpacesInConditionalExpression": true,
- "requireSpacesInFunction": {"beforeOpeningCurlyBrace": true},
- "safeContextKeyword": ["that"],
- "validateIndentation": 2,
- "validateParameterSeparator": ", ",
- "validateQuoteMarks": "'",
- "validateLineBreaks": "LF"
-}
diff --git a/.jshintrc b/.jshintrc
deleted file mode 100644
index 0d4a772..0000000
--- a/.jshintrc
+++ /dev/null
@@ -1,79 +0,0 @@
-{
- "bitwise" : false,
- "camelcase" : false,
- "curly" : false,
- "eqeqeq" : true,
- "es3" : false,
- "forin" : true,
- "freeze" : true,
- "immed" : true,
- "indent" : 4,
- "latedef" : "nofunc",
- "newcap" : true,
- "noarg" : true,
- "noempty" : true,
- "nonbsp" : true,
- "nonew" : true,
- "plusplus" : true,
- "quotmark" : "single",
- "undef" : true,
- "unused" : true,
- "singleGroups" : false,
- "strict" : true,
- "maxparams" : 5,
- "maxdepth" : 5,
- "maxstatements": 25,
- "maxcomplexity": 5,
- "maxlen" : 80,
-
- "asi" : false,
- "boss" : false,
- "debug" : false,
- "eqnull" : false,
- "evil" : false,
- "expr" : false,
- "esnext" : false,
- "funcscope" : false,
- "globalstrict" : false,
- "iterator" : false,
- "lastsemic" : false,
- "laxbreak" : false,
- "laxcomma" : false,
- "loopfunc" : false,
- "maxerr" : 50,
- "moz" : false,
- "multistr" : false,
- "notypeof" : false,
- "noyield" : false,
- "proto" : false,
- "scripturl" : false,
- "scope" : false,
- "shadow" : false,
- "sub" : false,
- "supernew" : false,
- "validthis" : false,
- "withstmt" : false,
-
- "browser" : false,
- "browserify" : false,
- "couch" : false,
- "devel" : false,
- "dojo" : false,
- "jasmine" : false,
- "jquery" : false,
- "mootools" : false,
- "mocha" : false,
- "node" : true,
- "nonstandard" : false,
- "phantom" : false,
- "prototypejs" : false,
- "qunit" : false,
- "rhino" : false,
- "shelljs" : false,
- "typed" : false,
- "worker" : false,
- "wsh" : false,
- "yui" : false,
-
- "globals" : []
-}
diff --git a/.npmignore b/.npmignore
index d12a3f1..0d7a2b0 100644
--- a/.npmignore
+++ b/.npmignore
@@ -1 +1,11 @@
-extras/
+node_modules/
+obj/
+src/
+typings/
+*.dat
+*.dll
+*.map
+*.njsproj
+*.sln
+*.suo
+*.tmp
\ No newline at end of file
diff --git a/README.md b/README.md
index c57816e..178ff40 100644
--- a/README.md
+++ b/README.md
@@ -45,6 +45,7 @@ decision prior to using this application.
* Binary runner for `npm`
* Windows examples with a .bat for ease of use.
* Publish to `npm` with a fixed package.json.
+* Conversion to beautiful TypeScript 1.4 code.
### Pending Implementation
diff --git a/bin/crunchyroll b/bin/crunchyroll
index 52c02f5..160eb1b 100644
--- a/bin/crunchyroll
+++ b/bin/crunchyroll
@@ -1,4 +1,4 @@
#!/usr/bin/env node
var path = require('path');
var fs = require('fs');
-require(path.join(path.dirname(fs.realpathSync(__filename)), '..'));
+require(path.join(path.dirname(fs.realpathSync(__filename)), '../dist/cli'));
\ No newline at end of file
diff --git a/crunchyroll.js.njsproj b/crunchyroll.js.njsproj
new file mode 100644
index 0000000..d91f19a
--- /dev/null
+++ b/crunchyroll.js.njsproj
@@ -0,0 +1,102 @@
+
+
+
+ Debug
+ True
+ .
+ {c5cff68a-d733-4347-83e7-6e5fe58eb0e3}
+
+ {3AF33F2E-1136-4D97-BBB7-1795711AC8B8};{349c5851-65df-11da-9384-00065b846f21};{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}
+ ShowAllFiles
+ 2.0
+ cli.ts
+ CommonJS
+ True
+ dist
+ 11.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+ .
+
+
+ True
+
+
+ True
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ True
+ http://localhost:1337
+ 0
+ /
+ http://localhost:48022/
+ False
+ False
+ True
+ False
+
+
+
+
+ False
+ True
+ False
+
+ False
+ False
+ False
+ CurrentPage
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/crunchyroll.js.sln b/crunchyroll.js.sln
new file mode 100644
index 0000000..475427a
--- /dev/null
+++ b/crunchyroll.js.sln
@@ -0,0 +1,22 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 2013
+VisualStudioVersion = 12.0.31101.0
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}") = "crunchyroll.js", "crunchyroll.js.njsproj", "{C5CFF68A-D733-4347-83E7-6E5FE58EB0E3}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {C5CFF68A-D733-4347-83E7-6E5FE58EB0E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C5CFF68A-D733-4347-83E7-6E5FE58EB0E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C5CFF68A-D733-4347-83E7-6E5FE58EB0E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C5CFF68A-D733-4347-83E7-6E5FE58EB0E3}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal
diff --git a/crunchyroll.js.sln.DotSettings b/crunchyroll.js.sln.DotSettings
new file mode 100644
index 0000000..dd8e72f
--- /dev/null
+++ b/crunchyroll.js.sln.DotSettings
@@ -0,0 +1,57 @@
+
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+
+ DO_NOT_SHOW
+ TypeScript14
+ <?xml version="1.0" encoding="utf-16"?><Profile name="TypeScript"><FormatAttributeQuoteDescriptor>True</FormatAttributeQuoteDescriptor><JsReformatCode>True</JsReformatCode><JsFormatDocComments>True</JsFormatDocComments><JsInsertSemicolon>True</JsInsertSemicolon><RemoveRedundantQualifiersTs>True</RemoveRedundantQualifiersTs><OptimizeImportsTs>True</OptimizeImportsTs><OptimizeReferenceCommentsTs>True</OptimizeReferenceCommentsTs><PublicModifierStyleTs>True</PublicModifierStyleTs><RelativePathStyleTs>True</RelativePathStyleTs><TypeAnnotationStyleTs>True</TypeAnnotationStyleTs></Profile>
+ TypeScript
+ ONLY_FOR_MULTILINE
+ 1
+ 1
+ SingleQuoted
+
+
+ False
+ False
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="I" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="T" Suffix="" Style="AaBb" />
+ True
+ RelativeDotSlash
+ True
+
+ False
+ True
+ C:\Dropbox\Github\crunchyroll.js\crunchyroll.js.sln.DotSettings
+ True
+ 1
\ No newline at end of file
diff --git a/index.js b/index.js
deleted file mode 100644
index ffc4a88..0000000
--- a/index.js
+++ /dev/null
@@ -1,6 +0,0 @@
-'use strict';
-var src = require('./src');
-
-src.batch(process.argv, function(err) {
- if (err) return console.error(err.stack || err);
-});
diff --git a/package.json b/package.json
index b03bf9d..f62e08a 100644
--- a/package.json
+++ b/package.json
@@ -8,19 +8,25 @@
],
"name": "crunchyroll",
"repository": "git://github.com/Deathspike/crunchyroll.js.git",
- "version": "1.0.6",
+ "version": "1.1.0",
"bin": {
"crunchyroll": "./bin/crunchyroll"
},
"dependencies": {
- "big-integer": "^1.4.1",
+ "big-integer": "^1.4.3",
"cheerio": "^0.18.0",
"commander": "^2.6.0",
"mkdirp": "^0.5.0",
- "request": "^2.51.0",
+ "request": "^2.53.0",
"xml2js": "^0.4.4"
},
"scripts": {
- "test": "echo \"Error: no test specified\" && exit 1"
+ "prepublish": "npm run tsd && npm run tsc",
+ "tsc": "./node_modules/.bin/tsc --declaration --module CommonJS --noImplicitAny --outDir dist typings/tsd.d.ts src/cli.ts",
+ "tsd": "./node_modules/.bin/tsd reinstall --overwrite --save"
+ },
+ "devDependencies": {
+ "tsd": "^0.5.7",
+ "typescript": "^1.4.1"
}
}
diff --git a/src/batch.js b/src/batch.ts
similarity index 57%
rename from src/batch.js
rename to src/batch.ts
index 0b1ec18..90c7c31 100644
--- a/src/batch.js
+++ b/src/batch.ts
@@ -1,45 +1,41 @@
'use strict';
-var Command = require('commander').Command;
-var fs = require('fs');
-var path = require('path');
-var series = require('./series');
+export = main;
+import commander = require('commander');
+import fs = require('fs');
+import path = require('path');
+import series = require('./series');
+import typings = require('./typings');
/**
* Streams the batch of series to disk.
- * @param {Array.} args
- * @param {function(Error)} done
*/
-module.exports = function(args, done) {
- var config = _parse(args);
+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, function(err, tasks) {
+ 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, function(err) {
+ series(tasks[i].config, tasks[i].address, err => {
if (err) return done(err);
i += 1;
next();
});
})();
});
-};
+}
/**
* Splits the value into arguments.
- * @private
- * @param {string} value
- * @returns {Array.}
*/
-function _split(value) {
+function split(value: string): string[] {
var inQuote = false;
- var pieces = [];
+ var i: number;
+ var pieces: string[] = [];
var previous = 0;
- for (var i = 0; i < value.length; i += 1) {
- if (value.charAt(i) === '"') {
- inQuote = !inQuote;
- }
+ 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;
@@ -51,43 +47,36 @@ function _split(value) {
/**
* Parses the configuration or reads the batch-mode file for tasks.
- * @private
- * @param {Object} config
- * @param {string} batchPath
- * @param {function(Error, Object=} done
*/
-function _tasks(config, batchPath, done) {
+function tasks(config: typings.IConfigLine, batchPath: string, done: (err: Error, tasks?: typings.IConfigTask[]) => void) {
if (config.args.length) {
- return done(undefined, config.args.map(function(address) {
+ return done(null, config.args.map(address => {
return {address: address, config: config};
}));
}
- fs.exists(batchPath, function(exists) {
- if (!exists) return done(undefined, []);
- fs.readFile(batchPath, 'utf8', function(err, data) {
+ fs.exists(batchPath, exists => {
+ if (!exists) return done(null, []);
+ fs.readFile(batchPath, 'utf8', (err, data) => {
if (err) return done(err);
- var map = [];
- data.split(/\r?\n/).forEach(function(line) {
+ 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(function(address) {
+ var lineConfig = parse(process.argv.concat(split(line)));
+ lineConfig.args.forEach(address => {
if (!address) return;
map.push({address: address, config: lineConfig});
});
});
- done(undefined, map);
+ done(null, map);
});
});
}
/**
* Parses the arguments and returns a configuration.
- * @private
- * @param {Array.} args
- * @returns {Object}
*/
-function _parse(args) {
- return new Command().version(require('../package').version)
+function parse(args: string[]): typings.IConfigLine {
+ return new commander.Command().version(require('../package').version)
// Authentication
.option('-p, --pass ', 'The password.')
.option('-u, --user ', 'The e-mail address or username.')
@@ -103,4 +92,4 @@ function _parse(args) {
.option('-s, --series ', 'The series override.')
.option('-t, --tag ', 'The subgroup. (Default: CrunchyRoll)')
.parse(args);
-}
+}
\ No newline at end of file
diff --git a/src/cli.ts b/src/cli.ts
new file mode 100644
index 0000000..33ca998
--- /dev/null
+++ b/src/cli.ts
@@ -0,0 +1,6 @@
+'use strict';
+import batch = require('./batch');
+
+batch(process.argv, err => {
+ if (err) console.error(err);
+});
\ No newline at end of file
diff --git a/src/episode.js b/src/episode.js
deleted file mode 100644
index 14c147a..0000000
--- a/src/episode.js
+++ /dev/null
@@ -1,204 +0,0 @@
-'use strict';
-var cheerio = require('cheerio');
-var fs = require('fs');
-var mkdirp = require('mkdirp');
-var request = require('./request');
-var path = require('path');
-var subtitle = require('./subtitle');
-var video = require('./video');
-var xml2js = require('xml2js');
-
-/**
- * Streams the episode to disk.
- * @param {Object} config
- * @param {string} address
- * @param {function(Error)} done
- */
-module.exports = function (config, address, done) {
- _page(config, address, function(err, page) {
- if (err) return done(err);
- _player(config, address, page.id, function(err, player) {
- if (err) return done(err);
- _download(config, page, player, done);
- });
- });
-};
-
-/**
- * Completes a download and writes the message with an elapsed time.
- * @private
- * @param {string} message
- * @param {number} begin
- * @param {function(Error)} done
- */
-function _complete(message, begin, done) {
- 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();
-}
-
-/**
- * Downloads the subtitle and video.
- * @private
- * @param {Object} config
- * @param {Object} page
- * @param {Object} player
- * @param {function(Error)} done
- */
-function _download(config, page, player, done) {
- 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), function(err) {
- if (err) return done(err);
- _subtitle(config, player, filePath, function(err) {
- if (err) return done(err);
- var now = Date.now();
- console.log('Fetching ' + fileName);
- _video(config, page, player, filePath, function(err) {
- if (err) return done(err);
- if (config.merge) return _complete('Finished ' + fileName, now, done);
- video.merge(config, player.video.file, filePath, function(err) {
- if (err) return done(err);
- _complete('Finished ' + fileName, now, done);
- });
- });
- });
- });
-}
-
-/**
- * Names the file based on the config, page, series and tag.
- * @private
- * @param {Object} config
- * @param {Object} page
- * @param {string} series
- * @returns {string}
- */
-function _name(config, page, series) {
- 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 + ']';
-}
-
-/**
- * Requests the page data and scrapes the id, episode, series and swf.
- * @private
- * @param {Object} config
- * @param {string} address
- * @param {function(Error, Object=)} done
- */
-function _page(config, address, done) {
- var id = parseInt((address.match(/[0-9]+$/) || [0])[0], 10);
- if (!id) return done(new Error('Invalid address.'));
- request.get(config, address, function(err, res, body) {
- if (err) return done(err);
- var $ = cheerio.load(body);
- var swf = /^([^?]+)/.exec($('link[rel=video_src]').attr('href'));
- var regexp = /Watch\s+(.+?)(?:\s+Season\s+([0-9]+))?\s+Episode\s+([0-9]+)/;
- var data = regexp.exec($('title').text());
- if (!swf || !data) return done(new Error('Invalid page.'));
- done(undefined, {
- id: id,
- episode: parseInt(data[3], 10),
- series: data[1],
- swf: swf[1],
- volume: parseInt(data[2], 10) || 1
- });
- });
-}
-
-/**
- * Prefixes a value.
- * @private
- * @param {(number|string)} value
- * @param {number} length
- * @returns {string}
- */
-function _prefix(value, length) {
- if (typeof value !== 'string') value = String(value);
- while (value.length < length) value = '0' + value;
- return value;
-}
-
-/**
- * Requests the player data and scrapes the subtitle and video data.
- * @private
- * @param {Object} config
- * @param {string} address
- * @param {number} id
- * @param {function(Error, Object=)} done
- */
-function _player(config, address, id, done) {
- 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
- }, function(err, res, xml) {
- if (err) return done(err);
- xml2js.parseString(xml, {
- explicitArray: false,
- explicitRoot: false
- }, function(err, player) {
- if (err) return done(err);
- try {
- done(undefined, {
- subtitle: {
- id: player['default:preload'].subtitle.$.id,
- iv: player['default:preload'].subtitle.iv,
- data: player['default:preload'].subtitle.data
- },
- video: {
- file: player['default:preload'].stream_info.file,
- host: player['default:preload'].stream_info.host
- }
- });
- } catch(err) {
- done(err);
- }
- });
- });
-}
-
-/**
- * Saves the subtitles to disk.
- * @private
- * @param {Object} config
- * @param {Object} player
- * @param {string} filePath
- * @param {function(Error)} done
- */
-function _subtitle(config, player, filePath, done) {
- var enc = player.subtitle;
- subtitle.decode(enc.id, enc.iv, enc.data, function(err, data) {
- if (err) return done(err);
- var format = subtitle.formats[config.format] ? config.format : 'ass';
- subtitle.formats[format](data, function(err, decodedSubtitle) {
- if (err) return done(err);
- fs.writeFile(filePath + '.' + format, '\ufeff' + decodedSubtitle, done);
- });
- });
-}
-
-/**
- * Streams the video to disk.
- * @private
- * @param {Object} config
- * @param {Object} page
- * @param {Object} player
- * @param {string} filePath
- * @param {function(Error)} done
- */
-function _video(config, page, player, filePath, done) {
- video.stream(
- player.video.host,
- player.video.file,
- page.swf,
- filePath + path.extname(player.video.file),
- done);
-}
diff --git a/src/episode.ts b/src/episode.ts
new file mode 100644
index 0000000..d4d8a30
--- /dev/null
+++ b/src/episode.ts
@@ -0,0 +1,166 @@
+'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 xml2js = require('xml2js');
+
+/**
+ * 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);
+ download(config, page, player, done);
+ });
+ });
+}
+
+/**
+ * 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);
+}
+
+/**
+ * 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);
+ video.merge(config, player.video.file, filePath, err => {
+ if (err) return done(err);
+ complete('Finished ' + fileName, now, done);
+ });
+ });
+ });
+ });
+}
+
+/**
+ * Saves the subtitles to disk.
+ */
+function downloadSubtitle(config: typings.IConfig, player: typings.IEpisodePlayer, filePath: string, done: (err: Error) => void) {
+ var enc = player.subtitle;
+ 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);
+ fs.writeFile(filePath + '.' + format, '\ufeff' + decodedSubtitle, done);
+ });
+ });
+}
+
+/**
+ * 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);
+}
+
+/**
+ * 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 + ']';
+}
+
+/**
+ * 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;
+ 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 = /Watch\s+(.+?)(?:\s+Season\s+([0-9]+))?\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
+ });
+ });
+}
+
+/**
+ * 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);
+ xml2js.parseString(result, {
+ explicitArray: false,
+ explicitRoot: false
+ }, (err: Error, player: typings.IEpisodePlayerConfig) => {
+ if (err) return done(err);
+ try {
+ done(null, {
+ subtitle: {
+ id: parseInt(player['default:preload'].subtitle.$.id, 10),
+ iv: player['default:preload'].subtitle.iv,
+ data: player['default:preload'].subtitle.data
+ },
+ video: {
+ file: player['default:preload'].stream_info.file,
+ host: player['default:preload'].stream_info.host
+ }
+ });
+ } catch (parseError) {
+ done(parseError);
+ }
+ });
+ });
+}
\ No newline at end of file
diff --git a/src/index.js b/src/index.js
deleted file mode 100644
index b0d833d..0000000
--- a/src/index.js
+++ /dev/null
@@ -1,5 +0,0 @@
-module.exports = {
- batch: require('./batch'),
- episode: require('./episode'),
- series: require('./series')
-};
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..eae512a
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,4 @@
+'use strict';
+export import batch = require('./batch');
+export import episode = require('./episode');
+export import series = require('./series');
\ No newline at end of file
diff --git a/src/request.js b/src/request.js
deleted file mode 100644
index 1a241f6..0000000
--- a/src/request.js
+++ /dev/null
@@ -1,66 +0,0 @@
-'use strict';
-var isAuthenticated = false;
-var request = require('request');
-
-/**
- * Performs a GET request for the resource.
- * @param {Object} config
- * @param {(string|Object)} options
- * @param {function(Error, Object, string)} done
- */
-module.exports.get = function(config, options, done) {
- _authenticate(config, function(err) {
- if (err) return done(err);
- request.get(_modify(options), done);
- });
-};
-
-/**
-* Performs a POST request for the resource.
-* @private
-* @param {Object} config
-* @param {(string|Object)} options
-* @param {function(Error, Object, string)} done
-*/
-module.exports.post = function(config, options, done) {
- _authenticate(config, function(err) {
- if (err) return done(err);
- request.post(_modify(options), done);
- });
-};
-
-/**
- * Authenticates using the configured pass and user.
- * @private
- * @param {Object} config
- * @param {function(Error)} done
- */
-function _authenticate(config, done) {
- if (isAuthenticated || !config.pass || !config.user) return done();
- request.post({
- 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'
- }, function(err) {
- if (err) return done(err);
- isAuthenticated = true;
- done();
- });
-}
-
-/**
- * Modifies the options to use the authenticated cookie jar.
- * @private
- * @param {(string|Object)} options
- * @returns {Object}
- */
-function _modify(options) {
- if (typeof options === 'string') options = {url: options};
- options.jar = true;
- return options;
-}
diff --git a/src/request.ts b/src/request.ts
new file mode 100644
index 0000000..2e15185
--- /dev/null
+++ b/src/request.ts
@@ -0,0 +1,63 @@
+'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()};
+}
\ No newline at end of file
diff --git a/src/series.js b/src/series.ts
similarity index 52%
rename from src/series.js
rename to src/series.ts
index bd1d1e2..0946763 100644
--- a/src/series.js
+++ b/src/series.ts
@@ -1,31 +1,30 @@
'use strict';
-var cheerio = require('cheerio');
-var episode = require('./episode');
+export = main;
+import cheerio = require('cheerio');
+import episode = require('./episode');
+import fs = require('fs');
+import request = require('./request');
+import path = require('path');
+import typings = require('./typings');
+import url = require('url');
var persistent = '.crpersistent';
-var fs = require('fs');
-var request = require('./request');
-var path = require('path');
-var url = require('url');
/**
* Streams the series to disk.
- * @param {Object} config
- * @param {string} address
- * @param {function(Error)} done
*/
-module.exports = function(config, address, done) {
+function main(config: typings.IConfig, address: string, done: (err: Error) => void) {
var persistentPath = path.join(config.output || process.cwd(), persistent);
- fs.readFile(persistentPath, 'utf8', function(err, contents) {
+ fs.readFile(persistentPath, 'utf8', (err, contents) => {
var cache = config.cache ? {} : JSON.parse(contents || '{}');
- _page(config, address, function(err, page) {
+ page(config, address, (err, page) => {
if (err) return done(err);
var i = 0;
(function next() {
- if (i >= page.episodes.length) return done();
- _download(cache, config, address, page.episodes[i], function(err) {
+ 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, function(err) {
+ fs.writeFile(persistentPath, newCache, err => {
if (err) return done(err);
i += 1;
next();
@@ -34,42 +33,33 @@ module.exports = function(config, address, done) {
})();
});
});
-};
+}
/**
* Downloads the episode.
- * @private
- * @param {Object.} cache
- * @param {Object} config
- * @param {string} baseAddress
- * @param {Object} item
- * @param {function(Error)} done
*/
-function _download(cache, config, baseAddress, item, done) {
- if (!_filter(config, item)) return done();
+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();
- episode(config, address, function(err) {
+ if (cache[address]) return done(null);
+ episode(config, address, err => {
if (err) return done(err);
cache[address] = Date.now();
- done();
+ done(null);
});
}
/**
-* Filters the item based on the configuration.
-* @param {Object} config
-* @param {Object} item
-* @returns {boolean}
-*/
-function _filter(config, item) {
+ * Filters the item based on the configuration.
+ */
+function filter(config: typings.IConfig, item: typings.ISeriesEpisode) {
// Filter on chapter.
- var episodeFilter = parseInt(config.episode, 10);
+ var episodeFilter = config.episode;
if (episodeFilter > 0 && item.episode <= episodeFilter) return false;
if (episodeFilter < 0 && item.episode >= -episodeFilter) return false;
// Filter on volume.
- var volumeFilter = parseInt(config.volume, 10);
+ var volumeFilter = config.volume;
if (volumeFilter > 0 && item.volume <= volumeFilter) return false;
if (volumeFilter < 0 && item.volume >= -volumeFilter) return false;
return true;
@@ -77,19 +67,15 @@ function _filter(config, item) {
/**
* Requests the page and scrapes the episodes and series.
- * @private
- * @param {Object} config
- * @param {string} address
- * @param {function(Error, Object=)} done
*/
-function _page(config, address, done) {
- request.get(config, address, function(err, res, body) {
+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(body);
+ var $ = cheerio.load(result);
var title = $('span[itemprop=name]').text();
if (!title) return done(new Error('Invalid page.'));
- var episodes = [];
- $('.episode').each(function(i, el) {
+ 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;
@@ -102,6 +88,6 @@ function _page(config, address, done) {
volume: volume ? parseInt(volume[0], 10) : 1
});
});
- done(undefined, {episodes: episodes.reverse(), series: title});
+ done(null, {episodes: episodes.reverse(), series: title});
});
-}
+}
\ No newline at end of file
diff --git a/src/subtitle/decode.js b/src/subtitle/decode.js
deleted file mode 100644
index e3728b7..0000000
--- a/src/subtitle/decode.js
+++ /dev/null
@@ -1,98 +0,0 @@
-'use strict';
-var crypto = require('crypto');
-var bigInt = require('big-integer');
-var zlib = require('zlib');
-
-/**
- * Decodes the data.
- * @param {number} id
- * @param {(Buffer|string)} iv
- * @param {(Buffer|string)} data
- * @param {function(Error, Buffer=)} done
- */
-module.exports = function(id, iv, data, done) {
- try {
- _decompress(_decrypt(id, iv, data), done);
- } catch(e) {
- done(e);
- }
-};
-
-/**
- * Decrypts the data.
- * @private
- * @param {number} id
- * @param {(Buffer|string)} iv
- * @param {(Buffer|string)} data
- * @return {Buffer}
- */
-function _decrypt(id, iv, data) {
- if (typeof iv === 'string') iv = new Buffer(iv, 'base64');
- if (typeof data === 'string') data = new Buffer(data, 'base64');
- var decipher = crypto.createDecipheriv('aes-256-cbc', _key(id), iv);
- decipher.setAutoPadding(false);
- return Buffer.concat([decipher.update(data), decipher.final()]);
-}
-
-/**
- * Decompresses the data.
- * @private
- * @param {Buffer} data
- * @param {function(Error, Buffer=)} done
- */
-function _decompress(data, done) {
- try {
- zlib.inflate(data, done);
- } catch(e) {
- done(undefined, data);
- }
-}
-
-/**
- * Generates a key.
- * @private
- * @param {number} subtitleId
- * @return {Buffer}
- */
-function _key(subtitleId) {
- var hash = _secret(20, 97, 1, 2) + _magic(subtitleId);
- var result = new Buffer(32);
- result.fill(0);
- crypto.createHash('sha1').update(hash).digest().copy(result);
- return result;
-}
-
-/**
- * Generates a magic number.
- * @private
- * @param {number} subtitleId
- * @return {number}
- */
-function _magic(subtitleId) {
- var base = Math.floor(Math.sqrt(6.9) * Math.pow(2, 25));
- var hash = bigInt(base).xor(subtitleId);
- var multipliedHash = bigInt(hash).multiply(32);
- return bigInt(hash).xor(hash >> 3).xor(multipliedHash).toJSNumber();
-}
-
-/**
- * Generates a secret string based on a Fibonacci sequence.
- * @private
- * @param {number} size
- * @param {number} modulo
- * @param {number} firstSeed
- * @param {number} secondSeed
- * @return {string}
- */
-function _secret(size, modulo, firstSeed, secondSeed) {
- var currentValue = firstSeed + secondSeed;
- var previousValue = secondSeed;
- var result = '';
- for (var i = 0; i < size; i += 1) {
- var oldValue = currentValue;
- result += String.fromCharCode(currentValue % modulo + 33);
- currentValue += previousValue;
- previousValue = oldValue;
- }
- return result;
-}
diff --git a/src/subtitle/decode.ts b/src/subtitle/decode.ts
new file mode 100644
index 0000000..ec90651
--- /dev/null
+++ b/src/subtitle/decode.ts
@@ -0,0 +1,75 @@
+'use strict';
+export = main;
+import crypto = require('crypto');
+import bigInt = require('big-integer');
+import zlib = require('zlib');
+
+/**
+ * Decodes the data.
+ */
+function main(id: number, iv: Buffer|string, data: Buffer|string, done: (err?: Error, result?: Buffer) => void) {
+ try {
+ decompress(decrypt(id, iv, data), done);
+ } catch (e) {
+ done(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);
+ 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 {
+ zlib.inflate(data, done);
+ } catch (e) {
+ done(null, data);
+ }
+}
+
+/**
+ * Generates a key.
+ */
+function key(subtitleId: number): Buffer {
+ var hash = secret(20, 97, 1, 2) + magic(subtitleId);
+ var 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();
+ 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;
+ result += String.fromCharCode(currentValue % modulo + 33);
+ currentValue += previousValue;
+ previousValue = oldValue;
+ }
+ return result;
+}
\ No newline at end of file
diff --git a/src/subtitle/formats/ass.js b/src/subtitle/formats/ass.js
deleted file mode 100644
index 01628d8..0000000
--- a/src/subtitle/formats/ass.js
+++ /dev/null
@@ -1,92 +0,0 @@
-'use strict';
-var xml2js = require('xml2js');
-
-/**
- * Converts an input buffer to a SubStation Alpha subtitle.
- * @param {Buffer|string} input
- * @param {function(Error, string=)} done
- */
-module.exports = function(input, done) {
- if (typeof buffer !== 'string') input = input.toString();
- xml2js.parseString(input, {
- explicitArray: false,
- explicitRoot: false
- }, function(err, xml) {
- if (err) return done(err);
- try {
- done(undefined, _script(xml) + '\n' +
- _style(xml.styles) + '\n' +
- _event(xml.events));
- } catch(err) {
- done(err);
- }
- });
-};
-
-/**
- * Converts the event block.
- * @param {Object} events
- * @returns {string}
- */
-function _event(events) {
- var format = 'Layer,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text';
- var items = [].concat(events.event).map(function(style) {
- return _values(style.$, 'Dialogue: 0,{start},{end},{style},{name},' +
- '{margin_l},{margin_r},{margin_v},{effect},{text}');
- });
- return '[Events]\n' +
- 'Format: ' + format + '\n' +
- items.join('\n') + '\n';
-}
-
-/**
- * Converts the script block.
- * @param {Object} script
- * @returns {string}
- */
-function _script(script) {
- return _values(script.$,
- '[Script Info]\n' +
- 'Title: {title}\n' +
- 'ScriptType: v4.00+\n' +
- 'WrapStyle: {wrap_style}\n' +
- 'PlayResX: {play_res_x}}\n' +
- 'PlayResY: {play_res_y}\n' +
- 'Subtitle ID: {id}\n' +
- 'Language: {lang_string}\n' +
- 'Created: {created}\n');
-}
-
-/**
- * Converts the style block.
- * @param {Object} styles
- * @returns {string}
- */
-function _style(styles) {
- 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';
- var items = [].concat(styles.style).map(function(style) {
- return _values(style.$, 'Style: {name},{font_name},{font_size}, ' +
- '{primary_colour},{secondary_colour},{outline_colour}, ' +
- '{back_colour},{bold},{italic},{underline},{strikeout},{scale_x}, ' +
- '{scale_y},{spacing},{angle},{border_style},{outline},{shadow},' +
- '{alignment},{margin_l},{margin_r},{margin_v},{encoding}');
- });
- return '[V4+ Styles]\n' +
- 'Format: ' + format + '\n' +
- items.join('\n') + '\n';
-}
-
-/**
- * Fills a predetermined format with the values from the attributes.
- * @param {Object.} attributes
- * @param {string} value
- * @returns {string}
- */
-function _values(attributes, format) {
- return format.replace(/{([^}]+)}/g, function(match, key) {
- return attributes[key] || '';
- });
-}
diff --git a/src/subtitle/formats/ass.ts b/src/subtitle/formats/ass.ts
new file mode 100644
index 0000000..8693032
--- /dev/null
+++ b/src/subtitle/formats/ass.ts
@@ -0,0 +1,93 @@
+'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) {
+ xml2js.parseString(input.toString(), {
+ explicitArray: false,
+ explicitRoot: false
+ }, (err: Error, xml: typings.ISubtitle) => {
+ if (err) return done(err);
+ try {
+ done(null, script(xml) + '\n' +
+ style(xml.styles) + '\n' +
+ event(xml.events));
+ } catch (err) {
+ done(err);
+ }
+ });
+}
+
+/**
+ * Converts the event block.
+ */
+function event(block: typings.ISubtitleEvent): string {
+ var format = 'Layer,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text';
+ return '[Events]\n' +
+ 'Format: ' + format + '\n' +
+ 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');
+}
+
+/**
+ * Converts the script block.
+ */
+function script(block: typings.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;
+}
+
+/**
+ * Converts the style block.
+ */
+function style(block: typings.ISubtitleStyle): string {
+ var format = 'Name,Fontname,Fontsize,PrimaryColour,SecondaryColour,' +
+ 'OutlineColour,BackColour,Bold,Italic,Underline,StrikeOut,ScaleX,' +
+ 'ScaleY,Spacing,Angle,BorderStyle,Outline,Shadow,Alignment,' +
+ 'MarginL,MarginR,MarginV,Encoding';
+ return '[V4+ Styles]\n' +
+ 'Format: ' + format + '\n' +
+ 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');
+}
\ No newline at end of file
diff --git a/src/subtitle/formats/index.js b/src/subtitle/formats/index.js
deleted file mode 100644
index 57a7798..0000000
--- a/src/subtitle/formats/index.js
+++ /dev/null
@@ -1,4 +0,0 @@
-module.exports = {
- ass: require('./ass'),
- srt: require('./srt')
-};
diff --git a/src/subtitle/formats/index.ts b/src/subtitle/formats/index.ts
new file mode 100644
index 0000000..507da2a
--- /dev/null
+++ b/src/subtitle/formats/index.ts
@@ -0,0 +1,10 @@
+'use strict';
+export = main;
+import ass = require('./ass');
+import srt = require('./srt');
+import typings = require('../../typings');
+
+var main: typings.IFormatterTable = {
+ ass: ass,
+ srt: srt
+};
\ No newline at end of file
diff --git a/src/subtitle/formats/srt.js b/src/subtitle/formats/srt.js
deleted file mode 100644
index 6962e05..0000000
--- a/src/subtitle/formats/srt.js
+++ /dev/null
@@ -1,92 +0,0 @@
-'use strict';
-var xml2js = require('xml2js');
-
-/**
- * Converts an input buffer to a SubRip subtitle.
- * @param {Buffer|string} input
- * @param {function(Error, string=)} done
- */
-module.exports = function(input, done) {
- if (typeof buffer !== 'string') input = input.toString();
- xml2js.parseString(input, {
- explicitArray: false,
- explicitRoot: false
- }, function(err, xml) {
- try {
- if (err) return done(err);
- done(undefined, xml.events.event.map(_event).join('\n'));
- } catch(err) {
- done(err);
- }
- });
-};
-
-/**
- * Converts an event.
- * @private
- * @param {Object} event
- * @param {number} index
- * @returns {string}
- */
-function _event(event, index) {
- var attributes = event.$;
- return (index + 1) + '\n' +
- _time(attributes.start) + ' --> ' + _time(attributes.end) + '\n' +
- _text(attributes.text) + '\n';
-}
-
-/**
- * Prefixes a value.
- * @private
- * @param {string} value
- * @param {number} length
- * @returns {string}
- */
-function _prefix(value, length) {
- while (value.length < length) value = '0' + value;
- return value;
-}
-
-/**
- * Suffixes a value.
- * @private
- * @param {string} value
- * @param {number} length
- * @returns {string}
- */
-function _suffix(value, length) {
- while (value.length < length) value = value + '0';
- return value;
-}
-
-/**
- * Formats a text value.
- * @private
- * @param {string} text
- * @returns {string}
- */
-function _text(text) {
- return text
- .replace(/{\\i1}/g, '').replace(/{\\i0}/g, '')
- .replace(/{\\b1}/g, '').replace(/{\\b0}/g, '')
- .replace(/{\\u1}/g, '').replace(/{\\u0}/g, '')
- .replace(/{[^}]+}/g, '')
- .replace(/(\s+)?\\n(\s+)?/ig, '\n')
- .trim();
-}
-
-/**
- * Formats a time stamp.
- * @private
- * @param {string} time
- * @returns {string}
- */
-function _time(time) {
- var all = time.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);
- return hours + ':' + minutes + ':' + seconds + ',' + milliseconds;
-}
diff --git a/src/subtitle/formats/srt.ts b/src/subtitle/formats/srt.ts
new file mode 100644
index 0000000..ce7ffea
--- /dev/null
+++ b/src/subtitle/formats/srt.ts
@@ -0,0 +1,66 @@
+'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.$;
+ return (index + 1) + '\n' +
+ time(attributes.start) + ' --> ' + time(attributes.end) + '\n' +
+ text(attributes.text) + '\n';
+ }).join('\n'));
+ } catch (err) {
+ done(err);
+ }
+ });
+}
+
+/**
+ * Prefixes a 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';
+ return value;
+}
+
+/**
+ * Formats a text value.
+ */
+function text(value: string): string {
+ return value
+ .replace(/{\\i1}/g, '').replace(/{\\i0}/g, '')
+ .replace(/{\\b1}/g, '').replace(/{\\b0}/g, '')
+ .replace(/{\\u1}/g, '').replace(/{\\u0}/g, '')
+ .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);
+ return hours + ':' + minutes + ':' + seconds + ',' + milliseconds;
+}
\ No newline at end of file
diff --git a/src/subtitle/index.js b/src/subtitle/index.js
deleted file mode 100644
index 8b29f16..0000000
--- a/src/subtitle/index.js
+++ /dev/null
@@ -1,4 +0,0 @@
-module.exports = {
- decode: require('./decode'),
- formats: require('./formats')
-};
diff --git a/src/subtitle/index.ts b/src/subtitle/index.ts
new file mode 100644
index 0000000..272b460
--- /dev/null
+++ b/src/subtitle/index.ts
@@ -0,0 +1,3 @@
+'use strict';
+export import decode = require('./decode');
+export import formats = require('./formats/index');
\ No newline at end of file
diff --git a/src/typings.ts b/src/typings.ts
new file mode 100644
index 0000000..cabbb47
--- /dev/null
+++ b/src/typings.ts
@@ -0,0 +1,136 @@
+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;
+ };
+ }[];
+}
\ No newline at end of file
diff --git a/src/video/index.js b/src/video/index.js
deleted file mode 100644
index 23ab138..0000000
--- a/src/video/index.js
+++ /dev/null
@@ -1,4 +0,0 @@
-module.exports = {
- merge: require('./merge'),
- stream: require('./stream')
-};
diff --git a/src/video/index.ts b/src/video/index.ts
new file mode 100644
index 0000000..f1d4e50
--- /dev/null
+++ b/src/video/index.ts
@@ -0,0 +1,3 @@
+'use strict';
+export import merge = require('./merge');
+export import stream = require('./stream');
\ No newline at end of file
diff --git a/src/video/merge.js b/src/video/merge.js
deleted file mode 100644
index 3b12911..0000000
--- a/src/video/merge.js
+++ /dev/null
@@ -1,71 +0,0 @@
-'use strict';
-var childProcess = require('child_process');
-var fs = require('fs');
-var path = require('path');
-var os = require('os');
-var subtitle = require('../subtitle');
-
-/**
- * Merges the subtitle and video files into a Matroska Multimedia Container.
- * @param {Object} config
- * @param {string} rtmpInputPath
- * @param {string} filePath
- * @param {function(Error)} done
- */
-module.exports = function(config, rtmpInputPath, filePath, done) {
- var format = subtitle.formats[config.format] ? config.format : 'ass';
- var subtitlePath = filePath + '.' + format;
- var videoPath = filePath + path.extname(rtmpInputPath);
- childProcess.exec(_command() + ' ' +
- '-o "' + filePath + '.mkv" ' +
- '"' + videoPath + '" ' +
- '"' + subtitlePath + '"', {
- maxBuffer: Infinity
- }, function(err) {
- if (err) return done(err);
- _unlink(videoPath, subtitlePath, function(err) {
- if (err) _unlinkTimeout(videoPath, subtitlePath, 5000);
- done();
- });
- });
-};
-
-/**
- * Determines the command for the operating system.
- * @private
- * @returns {string}
- */
-function _command() {
- if (os.platform() !== 'win32') return 'mkvmerge';
- return path.join(__dirname, '../../bin/mkvmerge.exe');
-}
-
-/**
- * Unlinks the video and subtitle.
- * @private
- * @param {string} videoPath
- * @param {string} subtitlePath
- * @param {function(Error)} done
- */
-function _unlink(videoPath, subtitlePath, done) {
- fs.unlink(videoPath, function(err) {
- if (err) return done(err);
- fs.unlink(subtitlePath, done);
- });
-}
-
-/**
- * Attempts to unlink the video and subtitle with a timeout between each try.
- * @private
- * @param {string} videoPath
- * @param {string} subtitlePath
- * @param {function(Error)} done
- */
-function _unlinkTimeout(videoPath, subtitlePath, timeout) {
- console.log('Trying to unlink...' + Date.now());
- setTimeout(function() {
- _unlink(videoPath, subtitlePath, function(err) {
- if (err) _unlinkTimeout(videoPath, subtitlePath, timeout);
- });
- }, timeout);
-}
diff --git a/src/video/merge.ts b/src/video/merge.ts
new file mode 100644
index 0000000..2e68fbf
--- /dev/null
+++ b/src/video/merge.ts
@@ -0,0 +1,58 @@
+'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');
+
+/**
+ * Merges the subtitle and video files into a Matroska Multimedia Container.
+ */
+function main(config: typings.IConfig, rtmpInputPath: string, filePath: string, done: (err: Error) => void) {
+ var subtitlePath = filePath + '.' + (subtitle.formats[config.format] ? config.format : 'ass');
+ var videoPath = filePath + path.extname(rtmpInputPath);
+ childProcess.exec(command() + ' ' +
+ '-o "' + filePath + '.mkv" ' +
+ '"' + videoPath + '" ' +
+ '"' + subtitlePath + '"', {
+ maxBuffer: Infinity
+ }, err => {
+ if (err) return done(err);
+ unlink(videoPath, subtitlePath, err => {
+ if (err) unlinkTimeout(videoPath, subtitlePath, 5000);
+ done(null);
+ });
+ });
+}
+
+/**
+ * Determines the command for the operating system.
+ */
+function command(): string {
+ if (os.platform() !== 'win32') return 'mkvmerge';
+ return path.join(__dirname, '../../bin/mkvmerge.exe');
+}
+
+/**
+ * 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);
+ fs.unlink(subtitlePath, done);
+ });
+}
+
+/**
+ * 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);
+ });
+ }, timeout);
+}
\ No newline at end of file
diff --git a/src/video/stream.js b/src/video/stream.js
deleted file mode 100644
index c872850..0000000
--- a/src/video/stream.js
+++ /dev/null
@@ -1,32 +0,0 @@
-'use strict';
-var childProcess = require('child_process');
-var path = require('path');
-var os = require('os');
-
-/**
- * Streams the video to disk.
- * @param {string} rtmpUrl
- * @param {string} rtmpInputPath
- * @param {string} swfUrl
- * @param {string} filePath
- * @param {function(Error)} done
- */
-module.exports = function(rtmpUrl, rtmpInputPath, swfUrl, filePath, done) {
- childProcess.exec(_command() + ' ' +
- '-r "' + rtmpUrl + '" ' +
- '-y "' + rtmpInputPath + '" ' +
- '-W "' + swfUrl + '" ' +
- '-o "' + filePath + '"', {
- maxBuffer: Infinity
- }, done);
-};
-
-/**
- * Determines the command for the operating system.
- * @private
- * @returns {string}
- */
-function _command() {
- if (os.platform() !== 'win32') return 'rtmpdump';
- return path.join(__dirname, '../../bin/rtmpdump.exe');
-}
diff --git a/src/video/stream.ts b/src/video/stream.ts
new file mode 100644
index 0000000..c4130bd
--- /dev/null
+++ b/src/video/stream.ts
@@ -0,0 +1,26 @@
+'use strict';
+export = main;
+import childProcess = require('child_process');
+import path = require('path');
+import os = require('os');
+
+/**
+ * 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);
+}
+
+/**
+ * Determines the command for the operating system.
+ */
+function command(): string {
+ if (os.platform() !== 'win32') return 'rtmpdump';
+ return path.join(__dirname, '../../bin/rtmpdump.exe');
+}
\ No newline at end of file
diff --git a/tsd.json b/tsd.json
new file mode 100644
index 0000000..ba0e305
--- /dev/null
+++ b/tsd.json
@@ -0,0 +1,33 @@
+{
+ "version": "v4",
+ "repo": "borisyankov/DefinitelyTyped",
+ "ref": "master",
+ "path": "typings",
+ "bundle": "typings/tsd.d.ts",
+ "installed": {
+ "node/node.d.ts": {
+ "commit": "42c8a3b74c05f6887ce21dd63c6234e424f9f8fe"
+ },
+ "commander/commander.d.ts": {
+ "commit": "42c8a3b74c05f6887ce21dd63c6234e424f9f8fe"
+ },
+ "xml2js/xml2js.d.ts": {
+ "commit": "42c8a3b74c05f6887ce21dd63c6234e424f9f8fe"
+ },
+ "cheerio/cheerio.d.ts": {
+ "commit": "42c8a3b74c05f6887ce21dd63c6234e424f9f8fe"
+ },
+ "mkdirp/mkdirp.d.ts": {
+ "commit": "42c8a3b74c05f6887ce21dd63c6234e424f9f8fe"
+ },
+ "request/request.d.ts": {
+ "commit": "42c8a3b74c05f6887ce21dd63c6234e424f9f8fe"
+ },
+ "big-integer/big-integer.d.ts": {
+ "commit": "42c8a3b74c05f6887ce21dd63c6234e424f9f8fe"
+ },
+ "form-data/form-data.d.ts": {
+ "commit": "42c8a3b74c05f6887ce21dd63c6234e424f9f8fe"
+ }
+ }
+}