episode.ts 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. 'use strict';
  2. import cheerio = require('cheerio');
  3. import fs = require('fs');
  4. import mkdirp = require('mkdirp');
  5. import request = require('./request');
  6. import path = require('path');
  7. import subtitle from './subtitle/index';
  8. import video from './video/index';
  9. import xml2js = require('xml2js');
  10. /**
  11. * Streams the episode to disk.
  12. */
  13. export default function(config: IConfig, address: string, done: (err: Error) => void) {
  14. scrapePage(config, address, (err, page) => {
  15. if (err) return done(err);
  16. scrapePlayer(config, address, page.id, (err, player) => {
  17. if (err) return done(err);
  18. download(config, page, player, done);
  19. });
  20. });
  21. }
  22. /**
  23. * Completes a download and writes the message with an elapsed time.
  24. */
  25. function complete(message: string, begin: number, done: (err: Error) => void) {
  26. var timeInMs = Date.now() - begin;
  27. var seconds = prefix(Math.floor(timeInMs / 1000) % 60, 2);
  28. var minutes = prefix(Math.floor(timeInMs / 1000 / 60) % 60, 2);
  29. var hours = prefix(Math.floor(timeInMs / 1000 / 60 / 60), 2);
  30. console.log(message + ' (' + hours + ':' + minutes + ':' + seconds + ')');
  31. done(null);
  32. }
  33. /**
  34. * Downloads the subtitle and video.
  35. */
  36. function download(config: IConfig, page: IEpisodePage, player: IEpisodePlayer, done: (err: Error) => void) {
  37. var series = config.series || page.series;
  38. series = series.replace("/","_").replace("'","_");
  39. var fileName = name(config, page, series).replace("/","_").replace("'","_");
  40. var filePath = path.join(config.output || process.cwd(), series, fileName);
  41. mkdirp(path.dirname(filePath), (err: Error) => {
  42. if (err) return done(err);
  43. downloadSubtitle(config, player, filePath, err => {
  44. if (err) return done(err);
  45. var now = Date.now();
  46. console.log('Fetching ' + fileName);
  47. downloadVideo(config, page, player, filePath, err => {
  48. if (err) return done(err);
  49. if (config.merge) return complete('Finished ' + fileName, now, done);
  50. var isSubtited = Boolean(player.subtitle);
  51. video.merge(config, isSubtited, player.video.file, filePath, player.video.mode, err => {
  52. if (err) return done(err);
  53. complete('Finished ' + fileName, now, done);
  54. });
  55. });
  56. });
  57. });
  58. }
  59. /**
  60. * Saves the subtitles to disk.
  61. */
  62. function downloadSubtitle(config: IConfig, player: IEpisodePlayer, filePath: string, done: (err?: Error) => void) {
  63. var enc = player.subtitle;
  64. if (!enc) return done();
  65. subtitle.decode(enc.id, enc.iv, enc.data, (err, data) => {
  66. if (err) return done(err);
  67. var formats = subtitle.formats;
  68. var format = formats[config.format] ? config.format : 'ass';
  69. formats[format](data, (err: Error, decodedSubtitle: string) => {
  70. if (err) return done(err);
  71. fs.writeFile(filePath + '.' + format, '\ufeff' + decodedSubtitle, done);
  72. });
  73. });
  74. }
  75. /**
  76. * Streams the video to disk.
  77. */
  78. function downloadVideo(config: IConfig,
  79. page: IEpisodePage,
  80. player: IEpisodePlayer,
  81. filePath: string,
  82. done: (err: Error) => void) {
  83. video.stream(
  84. player.video.host,
  85. player.video.file,
  86. page.swf,
  87. filePath, path.extname(player.video.file),
  88. player.video.mode,
  89. done);
  90. }
  91. /**
  92. * Names the file based on the config, page, series and tag.
  93. */
  94. function name(config: IConfig, page: IEpisodePage, series: string) {
  95. var episodeNum = parseInt(page.episode, 10);
  96. var volumeNum = parseInt(page.volume, 10);
  97. var episode = (episodeNum < 10 ? '0' : '') + page.episode;
  98. var volume = (volumeNum < 10 ? '0' : '') + page.volume;
  99. var tag = config.tag || 'CrunchyRoll';
  100. return series + ' ' + volume + 'x' + episode + ' [' + tag + ']';
  101. }
  102. /**
  103. * Prefixes a value.
  104. */
  105. function prefix(value: number|string, length: number) {
  106. var valueString = typeof value !== 'string' ? String(value) : value;
  107. while (valueString.length < length) valueString = '0' + valueString;
  108. return valueString;
  109. }
  110. /**
  111. * Requests the page data and scrapes the id, episode, series and swf.
  112. */
  113. function scrapePage(config: IConfig, address: string, done: (err: Error, page?: IEpisodePage) => void) {
  114. var id = parseInt((address.match(/[0-9]+$/) || ['0'])[0], 10);
  115. if (!id) return done(new Error('Invalid address.'));
  116. request.get(config, address, (err, result) => {
  117. if (err) return done(err);
  118. var $ = cheerio.load(result);
  119. var swf = /^([^?]+)/.exec($('link[rel=video_src]').attr('href'));
  120. var regexp = /\s*([^\n\r\t\f]+)\n?\s*[^0-9]*([0-9][0-9.]*)?,?\n?\s\s*[^0-9]*((PV )?[S0-9][P0-9.]*[a-fA-F]?)/;
  121. var look = $('#showmedia_about_media').text();
  122. var seasonTitle = $('span[itemprop="title"]').text();
  123. var data = regexp.exec(look);
  124. if (!swf || !data)
  125. {
  126. console.info('Something wrong in the page at '+address+' (data are: '+look+')');
  127. console.info('Setting Season to 0 and episode to \’0\’...');
  128. done(null, {
  129. id: id,
  130. episode: "0",
  131. series: seasonTitle,
  132. swf: swf[1],
  133. volume: "0"
  134. });
  135. }
  136. done(null, {
  137. id: id,
  138. episode: data[3],
  139. series: data[1],
  140. swf: swf[1],
  141. volume: data[2] || "1"
  142. });
  143. });
  144. }
  145. /**
  146. * Requests the player data and scrapes the subtitle and video data.
  147. */
  148. function scrapePlayer(config: IConfig, address: string, id: number, done: (err: Error, player?: IEpisodePlayer) => void) {
  149. var url = address.match(/^(https?:\/\/[^\/]+)/);
  150. if (!url) return done(new Error('Invalid address.'));
  151. request.post(config, {
  152. form: {current_page: address},
  153. url: url[1] + '/xml/?req=RpcApiVideoPlayer_GetStandardConfig&media_id=' + id
  154. }, (err, result) => {
  155. if (err) return done(err);
  156. xml2js.parseString(result, {
  157. explicitArray: false,
  158. explicitRoot: false
  159. }, (err: Error, player: IEpisodePlayerConfig) => {
  160. if (err) return done(err);
  161. try {
  162. var isSubtitled = Boolean(player['default:preload'].subtitle);
  163. var streamMode="RTMP";
  164. if (player['default:preload'].stream_info.host == "")
  165. {
  166. streamMode="HLS";
  167. }
  168. done(null, {
  169. subtitle: isSubtitled ? {
  170. id: parseInt(player['default:preload'].subtitle.$.id, 10),
  171. iv: player['default:preload'].subtitle.iv,
  172. data: player['default:preload'].subtitle.data
  173. } : null,
  174. video: {
  175. mode: streamMode,
  176. file: player['default:preload'].stream_info.file,
  177. host: player['default:preload'].stream_info.host
  178. }
  179. });
  180. } catch (parseError) {
  181. done(parseError);
  182. }
  183. });
  184. });
  185. }