Explorar el Código

Series streaming implementation.

Roel van Uden hace 9 años
padre
commit
cd17f2fd30
Se han modificado 4 ficheros con 97 adiciones y 10 borrados
  1. 3 2
      README.md
  2. 10 1
      app.js
  3. 4 7
      src/episode.js
  4. 80 0
      src/series.js

+ 3 - 2
README.md

@@ -35,11 +35,12 @@ prior to using this application.
 * Episode page scraping with subtitle saving and video streaming.
 * Add ASS support.
 * Add muxing (MP4+ASS=MKV).
+* Add series API to save an entire series rather than per-episode.
 
 ### Pending Implementation
 
-* 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 batch-mode to queue a bunch of series.
+* Add support for incremental saves; currently just overwriting stuff, bad.
 * 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.

+ 10 - 1
app.js

@@ -6,11 +6,20 @@ var config = {
 	tag: undefined,  		// defaults to CrunchyRoll
 };
 
-var episode = require('./src/episode');
+/*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!');
+});*/
+
+var series = require('./src/series');
+series(
+	config,
+	'http://www.crunchyroll.com/fairy-tail',
+	function(err) {
+		if (err) return console.log(err.stack || err);
+		console.log('All done!');
 });

+ 4 - 7
src/episode.js

@@ -8,7 +8,7 @@ var video = require('./video');
 var xml2js = require('xml2js');
 
 /**
- * Streams the episode video and subtitle to disk.
+ * Streams the episode to disk.
  * @param {Object} config
  * @param {string} address
  * @param {function(Error)} done
@@ -39,7 +39,7 @@ function _affix(value, length) {
 }
 
 /**
- * Completes a download and writes the message with a time indication.
+ * Completes a download and writes the message with an elapsed time.
  * @param {string} message
  * @param {number} begin
  * @param {function(Error)} done
@@ -50,7 +50,7 @@ function _complete(message, begin, done) {
   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);
+  done();
 }
 
 /**
@@ -159,10 +159,7 @@ function _subtitle(config, player, filePath, done) {
     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, function(err) {
-        if (err) return done(err);
-        done(undefined, false);
-      });
+      fs.writeFile(filePath + '.' + format, decodedSubtitle, done);
     });
   });
 }

+ 80 - 0
src/series.js

@@ -0,0 +1,80 @@
+'use strict';
+var cheerio = require('cheerio');
+var episode = require('./episode');
+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) {
+  var persistentPath = path.join(config.path || process.cwd(), persistent);
+  fs.readFile(persistentPath, 'utf8', function(err, data) {
+    var cache = JSON.parse(data || '{}');
+    _page(address, function(err, page) {
+      if (err) return done(err);
+      var i = 0;
+      (function next() {
+        if (i >= page.episodes.length) return done();
+        var episode = page.episodes[i];
+        var episodeAddress = url.resolve(address, episode.address);
+        _download(cache, config, episodeAddress, function(err) {
+          if (err) return done(err);
+          var newCache = JSON.stringify(cache, null, '  ');
+          fs.writeFile(persistentPath, newCache, function(err) {
+            if (err) return done(err);
+            i += 1;
+            next();
+          });
+        });
+      })();
+    });
+  });
+};
+
+/**
+ * Downloads the episode.
+ * @param {Object.<string, string>} cache
+ * @param {Object} config
+ * @param {string} address
+ * @param {function(Error)} done
+ */
+function _download(cache, config, address, done) {
+  if (cache[address]) return done();
+  episode(config, address, function(err) {
+    if (err) return done(err);
+    cache[address] = Date.now();
+    done();
+  });
+}
+
+/**
+ * Requests the page data and scrapes the episodes and series.
+ * @private
+ * @param {string} address
+ * @param {function(Error, Object=)} done
+ */
+function _page(address, done) {
+  request.get(address, function(err, res, body) {
+    if (err) return done(err);
+    var $ = cheerio.load(body);
+    var title = $('.season-dropdown').text() || $('span[itemprop=name]').text();
+    if (!title) return done(new Error('Invalid page.'));
+    var episodes = [];
+    $('.episode').each(function(i, el) {
+      if ($(el).children('img[src*=coming_soon]').length) return;
+      var address = $(el).attr('href');
+      var title = ($(el).children('.series-title').text() || '').trim();
+      var match = /([0-9]+)$/.exec(title);
+      if (!address || !match) return;
+      episodes.push({address: address, episode: parseInt(match[0], 10)});
+    });
+    done(undefined, {episodes: episodes.reverse(), series: title});
+  });
+}