episode.js 5.9 KB

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