Browse Source

Added MKV muxing support

Roel van Uden 9 years ago
parent
commit
256f9cd887
7 changed files with 115 additions and 30 deletions
  1. 1 1
      README.md
  2. 4 12
      app.js
  3. BIN
      bin/mkvmerge.exe
  4. 69 17
      src/episode.js
  5. 1 0
      src/video/index.js
  6. 39 0
      src/video/merge.js
  7. 1 0
      src/video/stream.js

+ 1 - 1
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.

+ 4 - 12
app.js

@@ -1,17 +1,9 @@
 '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
+	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');

BIN
bin/mkvmerge.exe


+ 69 - 17
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);
+      });
+  });
 }

+ 1 - 0
src/video/index.js

@@ -1,3 +1,4 @@
 module.exports = {
+  merge: require('./merge'),
   stream: require('./stream')
 };

+ 39 - 0
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');
+}

+ 1 - 0
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() {