Current state of development (~2 days in)
This commit is contained in:
parent
db3211cf8d
commit
5394596317
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
node_modules/
|
||||||
60
.jscsrc
Normal file
60
.jscsrc
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
79
.jshintrc
Normal file
79
.jshintrc
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
{
|
||||||
|
"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" : true,
|
||||||
|
"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" : []
|
||||||
|
}
|
||||||
19
LICENSE
Normal file
19
LICENSE
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) 2015 Roel van Uden
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to
|
||||||
|
deal in the Software without restriction, including without limitation the
|
||||||
|
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||||
|
sell copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||||
|
IN THE SOFTWARE.
|
||||||
46
README.md
46
README.md
@ -1,2 +1,44 @@
|
|||||||
# crunchyroll.js
|
# CrunchyRoll.js
|
||||||
CrunchyRoll.js is capable of downloading anime from the popular CrunchyRoll streaming site.
|
|
||||||
|
CrunchyRoll.js is capable of downloading anime from the popular CrunchyRoll
|
||||||
|
streaming site. Each episode is stored with the original video encoding
|
||||||
|
(usually H.264, MP4) and the configured subtitle format (ASS or SRT). The two
|
||||||
|
output files can optionally be muxed into a single MKV file.
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
I **love** *CrunchyRoll*. They have been working hard to provide an amazing service to
|
||||||
|
*anime* fans and provide the *absolute best* way to watch *anime* series in a legal
|
||||||
|
way. As they offer a streaming service, they do not offer files to be downloaded
|
||||||
|
and enjoyed while offline (or travelling). This is understandable from a
|
||||||
|
business perspective, but extremely annoying for the end-user. I put this
|
||||||
|
application together to enable the files to be stored and enjoyed while offline.
|
||||||
|
Please do not download as much as you can and stop your premium subscription;
|
||||||
|
without our financial support, they cannot give us such as great service!
|
||||||
|
**Do not share downloaded files and delete them if you ever stop your
|
||||||
|
premium subscription!**
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
### Implemented
|
||||||
|
|
||||||
|
* Subtitle decoding.
|
||||||
|
* Subtitle converter for SRT subtitle output.
|
||||||
|
* Video streaming.
|
||||||
|
* Episode page scraping with subtitle saving and video streaming.
|
||||||
|
|
||||||
|
### Pending Implementation
|
||||||
|
|
||||||
|
* Detect and write the appropriate video extension (instead of hard-code mp4).
|
||||||
|
* Improve SRT support for i, b and u.
|
||||||
|
* Add ASS support.
|
||||||
|
* Add muxing (MP4+ASS=MKV).
|
||||||
|
* Add series API to download an entire series rather than per-episode.
|
||||||
|
* Add batch-mode to queue a bunch of series and do incremental downloads.
|
||||||
|
* Add authentication to the entire stack to support premium content.
|
||||||
|
* Add CLI interface with all the options.
|
||||||
|
* Enjoy beautiful anime series from disk when internet is down.
|
||||||
|
|
||||||
|
## Work In Progress
|
||||||
|
|
||||||
|
Open an issue or e-mail me directly. I'd be happy to answer your questions.
|
||||||
|
|||||||
24
app.js
Normal file
24
app.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// TODO: Improve SRT support for <i>, <b> and <u>.
|
||||||
|
// TODO: Add ASS support.
|
||||||
|
// TODO: Add muxing (MP4+ASS=MKV).
|
||||||
|
// TODO: Add series API to download an entire series rather than per-episode.
|
||||||
|
// TODO: Add batch-mode to queue a bunch of series and do incremental downloads.
|
||||||
|
// TODO: Add authentication to the entire stack to support premium content.
|
||||||
|
// TODO: Add CLI interface with all the options.
|
||||||
|
|
||||||
|
var config = {
|
||||||
|
format: undefined, // defaults to srt
|
||||||
|
path: undefined, // defaults to process.cwd()
|
||||||
|
tag: undefined, // defaults to CrunchyRoll
|
||||||
|
};
|
||||||
|
|
||||||
|
var episode = require('./src/episode');
|
||||||
|
episode(
|
||||||
|
config,
|
||||||
|
'http://www.crunchyroll.com/fairy-tail/episode-1-the-dragon-king-652167',
|
||||||
|
function(err) {
|
||||||
|
if (err) return console.log(err);
|
||||||
|
console.log('All done!');
|
||||||
|
});
|
||||||
BIN
bin/rtmpdump.exe
Normal file
BIN
bin/rtmpdump.exe
Normal file
Binary file not shown.
17
package.json
Normal file
17
package.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "crunchyroll",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "app.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"big-integer": "^1.4.1",
|
||||||
|
"cheerio": "^0.18.0",
|
||||||
|
"request": "^2.51.0",
|
||||||
|
"xml2js": "^0.4.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
137
src/episode.js
Normal file
137
src/episode.js
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
'use strict';
|
||||||
|
var cheerio = require('cheerio');
|
||||||
|
var fs = require('fs');
|
||||||
|
var request = require('request');
|
||||||
|
var path = require('path');
|
||||||
|
var subtitle = require('./subtitle');
|
||||||
|
var video = require('./video');
|
||||||
|
var xml2js = require('xml2js');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Streams the episode video and subtitle to disk.
|
||||||
|
* @param {Object} config
|
||||||
|
* @param {string} address
|
||||||
|
* @param {function(Error)} done
|
||||||
|
*/
|
||||||
|
module.exports = function (config, address, done) {
|
||||||
|
_page(address, function(err, page) {
|
||||||
|
if (err) return done(err);
|
||||||
|
_player(address, page.id, function(err, player) {
|
||||||
|
if (err) return done(err);
|
||||||
|
_download(config, page, player, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads the subtitle and video.
|
||||||
|
* @param {Object} config
|
||||||
|
* @param {Object} page
|
||||||
|
* @param {Object} player
|
||||||
|
* @param {function(Error)} done
|
||||||
|
*/
|
||||||
|
function _download(config, page, player, done) {
|
||||||
|
var tag = config.tag || 'CrunchyRoll';
|
||||||
|
var episode = (page.episode < 10 ? '0' : '') + page.episode;
|
||||||
|
var fileName = page.series + ' - ' + episode + ' [' + tag + ']';
|
||||||
|
var filePath = path.join(config.path || process.cwd(), fileName);
|
||||||
|
_subtitle(config, player, filePath, function(err) {
|
||||||
|
if (err) return done(err);
|
||||||
|
_video(config, page, player, filePath, done);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests the page data and scrapes the id, episode, series and swf.
|
||||||
|
* @private
|
||||||
|
* @param {string} address
|
||||||
|
* @param {function(Error, Object=)} done
|
||||||
|
*/
|
||||||
|
function _page(address, done) {
|
||||||
|
var id = parseInt((address.match(/[0-9]+$/) || [0])[0], 10);
|
||||||
|
if (!id) return done(new Error('Invalid address.'));
|
||||||
|
request.get(address, function(err, res, body) {
|
||||||
|
if (err) return done(err);
|
||||||
|
var $ = cheerio.load(body);
|
||||||
|
var swf = /^([^?]+)/.exec($('link[rel=video_src]').attr('href'));
|
||||||
|
var title = /Watch ([\w\W]+) Episode ([0-9]+)/.exec($('title').text());
|
||||||
|
if (!swf || !title) return done(new Error('Invalid page.'));
|
||||||
|
done(undefined, {
|
||||||
|
id: id,
|
||||||
|
episode: parseInt(title[2], 10),
|
||||||
|
series: title[1],
|
||||||
|
swf: swf[1]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests the player data and scrapes the subtitle and video data.
|
||||||
|
* @private
|
||||||
|
* @param {string} address
|
||||||
|
* @param {number} id
|
||||||
|
* @param {function(Error, Object=)} done
|
||||||
|
*/
|
||||||
|
function _player(address, id, done) {
|
||||||
|
var url = address.match(/^(https?:\/\/[^\/]+)/);
|
||||||
|
if (!url) return done(new Error('Invalid address.'));
|
||||||
|
request.post({
|
||||||
|
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);
|
||||||
|
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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the subtitles to disk.
|
||||||
|
* @param {Object} config
|
||||||
|
* @param {Object} player
|
||||||
|
* @param {string} filePath
|
||||||
|
* @param {function(Error)} done
|
||||||
|
*/
|
||||||
|
function _subtitle(config, player, filePath, done) {
|
||||||
|
var contents = player.subtitle;
|
||||||
|
subtitle.decode(contents.id, contents.iv, contents.data, function(err, data) {
|
||||||
|
if (err) return done(err);
|
||||||
|
var format = subtitle.formats[config.format] ? config.format : 'srt';
|
||||||
|
subtitle.formats[format](data, function(err, decodedSubtitle) {
|
||||||
|
if (err) return done(err);
|
||||||
|
fs.writeFile(filePath + '.' + format, decodedSubtitle, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Streams the video to disk.
|
||||||
|
* @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 + '.mp4',
|
||||||
|
done);
|
||||||
|
}
|
||||||
98
src/subtitle/decode.js
Normal file
98
src/subtitle/decode.js
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
'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;
|
||||||
|
}
|
||||||
3
src/subtitle/formats/index.js
Normal file
3
src/subtitle/formats/index.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
srt: require('./srt')
|
||||||
|
};
|
||||||
81
src/subtitle/formats/srt.js
Normal file
81
src/subtitle/formats/srt.js
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
'use strict';
|
||||||
|
var xml2js = require('xml2js');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an input buffer to a SRT 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(_convert).join('\n'));
|
||||||
|
} catch(err) {
|
||||||
|
done(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an event.
|
||||||
|
* @param {Object} event
|
||||||
|
* @param {number} index
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function _convert(event, index) {
|
||||||
|
var attributes = event.$;
|
||||||
|
return (index + 1) + '\n' +
|
||||||
|
_time(attributes.start) + ' --> ' + _time(attributes.end) + '\n' +
|
||||||
|
_text(attributes.text) + '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefixes a value.
|
||||||
|
* @param {string} value
|
||||||
|
* @param {number} length
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function _prefix(value, length) {
|
||||||
|
while (value.length < length) value = '0' + value;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Suffixes a value.
|
||||||
|
* @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.
|
||||||
|
* @param {string} text
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function _text(text) {
|
||||||
|
return text.replace(/{[^}]+}/g, '').replace(/\\n/ig, '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a time stamp.
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
4
src/subtitle/index.js
Normal file
4
src/subtitle/index.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
module.exports = {
|
||||||
|
decode: require('./decode'),
|
||||||
|
formats: require('./formats')
|
||||||
|
};
|
||||||
3
src/video/index.js
Normal file
3
src/video/index.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
stream: require('./stream')
|
||||||
|
};
|
||||||
31
src/video/stream.js
Normal file
31
src/video/stream.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
'use strict';
|
||||||
|
var childProcess = require('child_process');
|
||||||
|
var path = require('path');
|
||||||
|
var os = require('os');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Streams the video to disk using rtmpdump.
|
||||||
|
* @param {string} rtmpUrl
|
||||||
|
* @param {string} inputPath
|
||||||
|
* @param {string} swfUrl
|
||||||
|
* @param {string} outputPath
|
||||||
|
* @param {function(Error)} done
|
||||||
|
*/
|
||||||
|
module.exports = function(rtmpUrl, inputPath, swfUrl, outputPath, done) {
|
||||||
|
childProcess.exec(_command() + ' ' +
|
||||||
|
'-r "' + rtmpUrl + '" ' +
|
||||||
|
'-y "' + inputPath + '" ' +
|
||||||
|
'-W "' + swfUrl + '" ' +
|
||||||
|
'-o "' + outputPath + '"', {
|
||||||
|
maxBuffer: Infinity
|
||||||
|
}, done);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the command for the operating system.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function _command() {
|
||||||
|
if (os.platform() !== 'win32') return 'rtmpdump';
|
||||||
|
return path.join(__dirname, '../../bin/rtmpdump.exe');
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user