From cd17f2fd3069531c40ca2d15f81efcb15b2425ee Mon Sep 17 00:00:00 2001 From: Roel van Uden Date: Fri, 23 Jan 2015 21:46:44 +0100 Subject: [PATCH] Series streaming implementation. --- README.md | 5 ++-- app.js | 11 ++++++- src/episode.js | 11 +++---- src/series.js | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 97 insertions(+), 10 deletions(-) create mode 100644 src/series.js diff --git a/README.md b/README.md index 1bcff3b..e6c25fd 100644 --- a/README.md +++ b/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. diff --git a/app.js b/app.js index e272e9f..de9e48e 100644 --- a/app.js +++ b/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!'); }); diff --git a/src/episode.js b/src/episode.js index 1716af3..dc0affd 100644 --- a/src/episode.js +++ b/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); }); }); } diff --git a/src/series.js b/src/series.js new file mode 100644 index 0000000..2ce9075 --- /dev/null +++ b/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.} 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}); + }); +}