Added MKV muxing support
This commit is contained in:
parent
ed8f7c90f5
commit
256f9cd887
@ -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.
|
||||
|
||||
14
app.js
14
app.js
@ -1,16 +1,8 @@
|
||||
'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()
|
||||
format: 'ass', // defaults to srt
|
||||
merge: true, // defaults to false
|
||||
path: 'F:\\Anime', // defaults to process.cwd()
|
||||
tag: undefined, // defaults to CrunchyRoll
|
||||
};
|
||||
|
||||
|
||||
BIN
bin/mkvmerge.exe
Normal file
BIN
bin/mkvmerge.exe
Normal file
Binary file not shown.
@ -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) {
|
||||
_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);
|
||||
_video(config, page, player, filePath, done);
|
||||
_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';
|
||||
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);
|
||||
subtitle.formats[format](data, function(err, decodedSubtitle) {
|
||||
if (err) return done(err);
|
||||
fs.writeFile(filePath + '.' + format, decodedSubtitle, done);
|
||||
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) {
|
||||
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 + path.extname(player.video.file),
|
||||
done);
|
||||
filePath + extension,
|
||||
function(err) {
|
||||
if (err) return done(err);
|
||||
done(undefined, false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
module.exports = {
|
||||
merge: require('./merge'),
|
||||
stream: require('./stream')
|
||||
};
|
||||
|
||||
39
src/video/merge.js
Normal file
39
src/video/merge.js
Normal file
@ -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');
|
||||
}
|
||||
@ -23,6 +23,7 @@ module.exports = function(rtmpUrl, rtmpInputPath, swfUrl, filePath, done) {
|
||||
|
||||
/**
|
||||
* Determines the command for the operating system.
|
||||
* @private
|
||||
* @returns {string}
|
||||
*/
|
||||
function _command() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user