episode.ts 7.4 KB

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