diff --git a/README.md b/README.md index 80aeb63..1bcff3b 100644 --- a/README.md +++ b/README.md @@ -34,10 +34,10 @@ prior to using this application. * Video streaming. * Episode page scraping with subtitle saving and video streaming. * Add ASS support. +* Add muxing (MP4+ASS=MKV). ### Pending Implementation -* Add muxing (MP4+ASS=MKV). * Add series API to save an entire series rather than per-episode. * Add batch-mode to queue a bunch of series and do incremental saves. * Add authentication to the entire stack to support premium content. diff --git a/app.js b/app.js index 3db8b42..e272e9f 100644 --- a/app.js +++ b/app.js @@ -1,17 +1,9 @@ 'use strict'; - -// TODO: Improve SRT support for , and . -// 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 + format: 'ass', // defaults to srt + merge: true, // defaults to false + path: 'F:\\Anime', // defaults to process.cwd() + tag: undefined, // defaults to CrunchyRoll }; var episode = require('./src/episode'); diff --git a/bin/mkvmerge.exe b/bin/mkvmerge.exe new file mode 100644 index 0000000..7136c60 Binary files /dev/null and b/bin/mkvmerge.exe differ diff --git a/src/episode.js b/src/episode.js index 21122e0..9f67b09 100644 --- a/src/episode.js +++ b/src/episode.js @@ -23,6 +23,36 @@ module.exports = function (config, address, done) { }); }; +/** + * Affixes zero-padding to the value. + * @private + * @param {(number|string)} value + * @param {number} length + * @returns {string} + */ +function _affix(value, length) { + if (typeof value !== 'string') value = String(value); + var suffix = value.indexOf('.') !== -1; + var add = length - (suffix ? value.indexOf('.') : value.length); + while ((add -= 1) >= 0) value = '0' + value; + return value; +} + +/** + * Completes a download and writes the message with a time indication. + * @param {string} message + * @param {number} begin + * @param {function(Error)} done + */ +function _complete(message, begin, done) { + var timeInMs = Date.now() - begin; + var seconds = _affix(Math.floor(timeInMs / 1000) % 60, 2); + var minutes = _affix(Math.floor(timeInMs / 1000 / 60) % 60, 2); + var hours = _affix(Math.floor(timeInMs / 1000 / 60 / 60), 2); + console.log(message + ' (' + hours + ':' + minutes + ':' + seconds + ')'); + done(undefined); +} + /** * Downloads the subtitle and video. * @private @@ -36,9 +66,18 @@ function _download(config, page, player, done) { 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); + _subtitle(config, player, filePath, function(err, exists) { + if (err || exists) return done(err || undefined); + var begin = Date.now(); + console.log('Fetching ' + fileName); + _video(config, page, player, filePath, function(err, exists) { + if (err || exists) return done(err || undefined); + if (!config.merge) return _complete('Finished ' + fileName, begin, done); + video.merge(config, player.video.file, filePath, function(err) { + if (err) return done(err); + _complete('Finished ' + fileName, begin, done); + }); + }); }); } @@ -111,16 +150,22 @@ function _player(address, id, done) { * @param {Object} config * @param {Object} player * @param {string} filePath - * @param {function(Error)} done + * @param {function(Error, boolean=)} 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) { + var format = subtitle.formats[config.format] ? config.format : 'srt'; + fs.exists(filePath + (config.merge ? '.mkv' : format), function(exists) { + if (exists) return done(undefined, true); + var enc = player.subtitle; + subtitle.decode(enc.id, enc.iv, enc.data, function(err, data) { if (err) return done(err); - fs.writeFile(filePath + '.' + format, decodedSubtitle, done); + subtitle.formats[format](data, function(err, decodedSubtitle) { + if (err) return done(err); + fs.writeFile(filePath + '.' + format, decodedSubtitle, function(err) { + if (err) return done(err); + done(undefined, false); + }); + }); }); }); } @@ -132,13 +177,20 @@ function _subtitle(config, player, filePath, done) { * @param {Object} page * @param {Object} player * @param {string} filePath -* @param {function(Error)} done +* @param {function(Error, boolean=)} 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); + var extension = path.extname(player.video.file); + fs.exists(filePath + (config.merge ? '.mkv' : extension), function(exists) { + if (exists) return done(undefined, true); + video.stream( + player.video.host, + player.video.file, + page.swf, + filePath + extension, + function(err) { + if (err) return done(err); + done(undefined, false); + }); + }); } diff --git a/src/video/index.js b/src/video/index.js index 82621e6..23ab138 100644 --- a/src/video/index.js +++ b/src/video/index.js @@ -1,3 +1,4 @@ module.exports = { + merge: require('./merge'), stream: require('./stream') }; diff --git a/src/video/merge.js b/src/video/merge.js new file mode 100644 index 0000000..52259c1 --- /dev/null +++ b/src/video/merge.js @@ -0,0 +1,39 @@ +'use strict'; +var childProcess = require('child_process'); +var fs = require('fs'); +var path = require('path'); +var os = require('os'); + +/** + * 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 subtitlePath = filePath + '.' + config.format; + var videoPath = filePath + path.extname(rtmpInputPath); + childProcess.exec(_command() + ' ' + + '-o "' + filePath + '.mkv" ' + + '"' + videoPath + '" ' + + '"' + subtitlePath + '"', { + maxBuffer: Infinity + }, function(err) { + if (err) return done(err); + fs.unlink(videoPath, function(err) { + if (err) return done(err); + fs.unlink(subtitlePath, 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'); +} diff --git a/src/video/stream.js b/src/video/stream.js index 0e15fb4..c872850 100644 --- a/src/video/stream.js +++ b/src/video/stream.js @@ -23,6 +23,7 @@ module.exports = function(rtmpUrl, rtmpInputPath, swfUrl, filePath, done) { /** * Determines the command for the operating system. + * @private * @returns {string} */ function _command() {