episode.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. 'use strict';
  2. var cheerio = require('cheerio');
  3. var fs = require('fs');
  4. var mkdirp = require('mkdirp');
  5. var request = require('./request');
  6. var path = require('path');
  7. var subtitle = require('./subtitle');
  8. var video = require('./video');
  9. var xml2js = require('xml2js');
  10. /**
  11. * Streams the episode to disk.
  12. * @param {Object} config
  13. * @param {string} address
  14. * @param {function(Error)} done
  15. */
  16. module.exports = function (config, address, done) {
  17. _page(config, address, function(err, page) {
  18. if (err) return done(err);
  19. _player(config, address, page.id, function(err, player) {
  20. if (err) return done(err);
  21. _download(config, page, player, done);
  22. });
  23. });
  24. };
  25. /**
  26. * Completes a download and writes the message with an elapsed time.
  27. * @private
  28. * @param {string} message
  29. * @param {number} begin
  30. * @param {function(Error)} done
  31. */
  32. function _complete(message, begin, done) {
  33. var timeInMs = Date.now() - begin;
  34. var seconds = _prefix(Math.floor(timeInMs / 1000) % 60, 2);
  35. var minutes = _prefix(Math.floor(timeInMs / 1000 / 60) % 60, 2);
  36. var hours = _prefix(Math.floor(timeInMs / 1000 / 60 / 60), 2);
  37. console.log(message + ' (' + hours + ':' + minutes + ':' + seconds + ')');
  38. done();
  39. }
  40. /**
  41. * Downloads the subtitle and video.
  42. * @private
  43. * @param {Object} config
  44. * @param {Object} page
  45. * @param {Object} player
  46. * @param {function(Error)} done
  47. */
  48. function _download(config, page, player, done) {
  49. var series = config.series || page.series;
  50. var fileName = _name(config, page, series);
  51. var filePath = path.join(config.output || process.cwd(), series, fileName);
  52. mkdirp(path.dirname(filePath), function(err) {
  53. if (err) return done(err);
  54. _subtitle(config, player, filePath, function(err) {
  55. if (err) return done(err);
  56. var now = Date.now();
  57. console.log('Fetching ' + fileName);
  58. _video(config, page, player, filePath, function(err) {
  59. if (err) return done(err);
  60. if (config.merge) return _complete('Finished ' + fileName, now, done);
  61. video.merge(config, player.video.file, filePath, function(err) {
  62. if (err) return done(err);
  63. _complete('Finished ' + fileName, now, done);
  64. });
  65. });
  66. });
  67. });
  68. }
  69. /**
  70. * Names the file based on the config, page, series and tag.
  71. * @private
  72. * @param {Object} config
  73. * @param {Object} page
  74. * @param {string} series
  75. * @returns {string}
  76. */
  77. function _name(config, page, series) {
  78. var episode = (page.episode < 10 ? '0' : '') + page.episode;
  79. var volume = (page.volume < 10 ? '0' : '') + page.volume;
  80. var tag = config.tag || 'CrunchyRoll';
  81. return series + ' ' + volume + 'x' + episode + ' [' + tag + ']';
  82. }
  83. /**
  84. * Requests the page data and scrapes the id, episode, series and swf.
  85. * @private
  86. * @param {Object} config
  87. * @param {string} address
  88. * @param {function(Error, Object=)} done
  89. */
  90. function _page(config, address, done) {
  91. var id = parseInt((address.match(/[0-9]+$/) || [0])[0], 10);
  92. if (!id) return done(new Error('Invalid address.'));
  93. request.get(config, address, function(err, res, body) {
  94. if (err) return done(err);
  95. var $ = cheerio.load(body);
  96. var swf = /^([^?]+)/.exec($('link[rel=video_src]').attr('href'));
  97. var regexp = /Watch\s+(.+?)(?:\s+Season\s+([0-9]+))?\s+Episode\s+([0-9]+)/;
  98. var data = regexp.exec($('title').text());
  99. if (!swf || !data) return done(new Error('Invalid page.'));
  100. done(undefined, {
  101. id: id,
  102. episode: parseInt(data[3], 10),
  103. series: data[1],
  104. swf: swf[1],
  105. volume: parseInt(data[2], 10) || 1
  106. });
  107. });
  108. }
  109. /**
  110. * Prefixes a value.
  111. * @private
  112. * @param {(number|string)} value
  113. * @param {number} length
  114. * @returns {string}
  115. */
  116. function _prefix(value, length) {
  117. if (typeof value !== 'string') value = String(value);
  118. while (value.length < length) value = '0' + value;
  119. return value;
  120. }
  121. /**
  122. * Requests the player data and scrapes the subtitle and video data.
  123. * @private
  124. * @param {Object} config
  125. * @param {string} address
  126. * @param {number} id
  127. * @param {function(Error, Object=)} done
  128. */
  129. function _player(config, address, id, done) {
  130. var url = address.match(/^(https?:\/\/[^\/]+)/);
  131. if (!url) return done(new Error('Invalid address.'));
  132. request.post(config, {
  133. form: {current_page: address},
  134. url: url[1] + '/xml/?req=RpcApiVideoPlayer_GetStandardConfig&media_id=' + id
  135. }, function(err, res, xml) {
  136. if (err) return done(err);
  137. xml2js.parseString(xml, {
  138. explicitArray: false,
  139. explicitRoot: false
  140. }, function(err, player) {
  141. if (err) return done(err);
  142. try {
  143. done(undefined, {
  144. subtitle: {
  145. id: player['default:preload'].subtitle.$.id,
  146. iv: player['default:preload'].subtitle.iv,
  147. data: player['default:preload'].subtitle.data
  148. },
  149. video: {
  150. file: player['default:preload'].stream_info.file,
  151. host: player['default:preload'].stream_info.host
  152. }
  153. });
  154. } catch(err) {
  155. done(err);
  156. }
  157. });
  158. });
  159. }
  160. /**
  161. * Saves the subtitles to disk.
  162. * @private
  163. * @param {Object} config
  164. * @param {Object} player
  165. * @param {string} filePath
  166. * @param {function(Error)} done
  167. */
  168. function _subtitle(config, player, filePath, done) {
  169. var enc = player.subtitle;
  170. subtitle.decode(enc.id, enc.iv, enc.data, function(err, data) {
  171. if (err) return done(err);
  172. var format = subtitle.formats[config.format] ? config.format : 'ass';
  173. subtitle.formats[format](data, function(err, decodedSubtitle) {
  174. if (err) return done(err);
  175. fs.writeFile(filePath + '.' + format, '\ufeff' + decodedSubtitle, done);
  176. });
  177. });
  178. }
  179. /**
  180. * Streams the video to disk.
  181. * @private
  182. * @param {Object} config
  183. * @param {Object} page
  184. * @param {Object} player
  185. * @param {string} filePath
  186. * @param {function(Error)} done
  187. */
  188. function _video(config, page, player, filePath, done) {
  189. video.stream(
  190. player.video.host,
  191. player.video.file,
  192. page.swf,
  193. filePath + path.extname(player.video.file),
  194. done);
  195. }