episode.ts 5.9 KB

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