episode.ts 7.9 KB

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