Series streaming implementation.

This commit is contained in:
Roel van Uden 2015-01-23 21:46:44 +01:00
parent 9197532ef0
commit cd17f2fd30
4 changed files with 97 additions and 10 deletions

View File

@ -35,11 +35,12 @@ prior to using this application.
* Episode page scraping with subtitle saving and video streaming. * Episode page scraping with subtitle saving and video streaming.
* Add ASS support. * Add ASS support.
* Add muxing (MP4+ASS=MKV). * Add muxing (MP4+ASS=MKV).
* Add series API to save an entire series rather than per-episode.
### Pending Implementation ### Pending Implementation
* Add series API to save an entire series rather than per-episode. * Add batch-mode to queue a bunch of series.
* Add batch-mode to queue a bunch of series and do incremental saves. * Add support for incremental saves; currently just overwriting stuff, bad.
* Add authentication to the entire stack to support premium content. * Add authentication to the entire stack to support premium content.
* Add CLI interface with all the options. * Add CLI interface with all the options.
* Enjoy beautiful anime series from disk when internet is down. * Enjoy beautiful anime series from disk when internet is down.

11
app.js
View File

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

View File

@ -8,7 +8,7 @@ var video = require('./video');
var xml2js = require('xml2js'); var xml2js = require('xml2js');
/** /**
* Streams the episode video and subtitle to disk. * Streams the episode to disk.
* @param {Object} config * @param {Object} config
* @param {string} address * @param {string} address
* @param {function(Error)} done * @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 {string} message
* @param {number} begin * @param {number} begin
* @param {function(Error)} done * @param {function(Error)} done
@ -50,7 +50,7 @@ function _complete(message, begin, done) {
var minutes = _affix(Math.floor(timeInMs / 1000 / 60) % 60, 2); var minutes = _affix(Math.floor(timeInMs / 1000 / 60) % 60, 2);
var hours = _affix(Math.floor(timeInMs / 1000 / 60 / 60), 2); var hours = _affix(Math.floor(timeInMs / 1000 / 60 / 60), 2);
console.log(message + ' (' + hours + ':' + minutes + ':' + seconds + ')'); 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'; var format = subtitle.formats[config.format] ? config.format : 'srt';
subtitle.formats[format](data, function(err, decodedSubtitle) { subtitle.formats[format](data, function(err, decodedSubtitle) {
if (err) return done(err); if (err) return done(err);
fs.writeFile(filePath + '.' + format, decodedSubtitle, function(err) { fs.writeFile(filePath + '.' + format, decodedSubtitle, done);
if (err) return done(err);
done(undefined, false);
});
}); });
}); });
} }

80
src/series.js Normal file
View File

@ -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});
});
}