episode.ts 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. 'use strict';
  2. import cheerio = require('cheerio');
  3. import fs = require('fs');
  4. import mkdirp = require('mkdirp');
  5. import my_request = require('./my_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, (errS, player) =>
  23. {
  24. if (errS)
  25. {
  26. return done(errS, 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. }
  54. catch (e)
  55. {
  56. return false;
  57. }
  58. }
  59. function sanitiseFileName(str: string)
  60. {
  61. const sanitized = str.replace(/[\/':\?\*"<>\\\.\|]/g, '_');
  62. return sanitized.replace(/{DIR_SEPARATOR}/g, '/');
  63. }
  64. /**
  65. * Downloads the subtitle and video.
  66. */
  67. function download(config: IConfig, page: IEpisodePage, player: IEpisodePlayer, done: (err: Error | string, ign: boolean) => void)
  68. {
  69. const serieFolder = sanitiseFileName(config.series || page.series);
  70. let fileName = sanitiseFileName(generateName(config, page));
  71. let filePath = path.join(config.output || process.cwd(), serieFolder, fileName);
  72. if (fileExist(filePath + '.mkv'))
  73. {
  74. let count = 0;
  75. if (config.rebuildcrp)
  76. {
  77. log.warn('Adding \'' + fileName + '\' to the DB...');
  78. return done(null, false);
  79. }
  80. log.warn('File \'' + fileName + '\' already exist...');
  81. do
  82. {
  83. count = count + 1;
  84. fileName = sanitiseFileName(generateName(config, page, '-' + count));
  85. filePath = path.join(config.output || process.cwd(), serieFolder, fileName);
  86. } while (fileExist(filePath + '.mkv'));
  87. log.warn('Renaming to \'' + fileName + '\'...');
  88. page.filename = fileName;
  89. }
  90. if (config.rebuildcrp)
  91. {
  92. log.warn('Ignoring \'' + fileName + '\' as it does not exist...');
  93. return done(null, true);
  94. }
  95. const ret = mkdirp(path.dirname(filePath));
  96. if (ret)
  97. {
  98. log.dispEpisode(fileName, 'Fetching...', false);
  99. downloadSubtitle(config, player, filePath, (errDS) =>
  100. {
  101. if (errDS)
  102. {
  103. log.dispEpisode(fileName, 'Error...', true);
  104. return done(errDS, false);
  105. }
  106. const now = Date.now();
  107. if (player.video.file !== undefined)
  108. {
  109. log.dispEpisode(fileName, 'Fetching video...', false);
  110. downloadVideo(config, page, player, filePath, (errDV) =>
  111. {
  112. if (errDV)
  113. {
  114. log.dispEpisode(fileName, 'Error...', true);
  115. return done(errDV, false);
  116. }
  117. if (config.merge)
  118. {
  119. return complete(fileName, 'Finished!', now, done);
  120. }
  121. const isSubtited = Boolean(player.subtitle);
  122. log.dispEpisode(fileName, 'Merging...', false);
  123. video.merge(config, isSubtited, player.video.file, filePath, player.video.mode, config.verbose, (errVM) =>
  124. {
  125. if (errVM)
  126. {
  127. log.dispEpisode(fileName, 'Error...', true);
  128. return done(errVM, false);
  129. }
  130. complete(fileName, 'Finished!', now, done);
  131. });
  132. });
  133. }
  134. else
  135. {
  136. log.dispEpisode(fileName, 'Ignoring: not released yet', true);
  137. done(null, true);
  138. }
  139. });
  140. }
  141. else
  142. {
  143. log.dispEpisode(fileName, 'Error creating folder \'" + filePath + "\'...', true);
  144. return done('Cannot create folder', false);
  145. }
  146. }
  147. /**
  148. * Saves the subtitles to disk.
  149. */
  150. function downloadSubtitle(config: IConfig, player: IEpisodePlayer, filePath: string, done: (err?: Error | string) => void)
  151. {
  152. const enc = player.subtitle;
  153. if (!enc)
  154. {
  155. return done();
  156. }
  157. subtitle.decode(enc.id, enc.iv, enc.data, (errSD, data) =>
  158. {
  159. if (errSD)
  160. {
  161. return done(errSD);
  162. }
  163. if (config.debug)
  164. {
  165. log.dumpToDebug('SubtitlesXML', data);
  166. }
  167. const formats = subtitle.formats;
  168. const format = formats[config.format] ? config.format : 'ass';
  169. formats[format](config, data, (errF: Error, decodedSubtitle: string) =>
  170. {
  171. if (errF)
  172. {
  173. return done(errF);
  174. }
  175. fs.writeFile(filePath + '.' + format, '\ufeff' + decodedSubtitle, done);
  176. });
  177. });
  178. }
  179. /**
  180. * Streams the video to disk.
  181. */
  182. function downloadVideo(config: IConfig, page: IEpisodePage, player: IEpisodePlayer,
  183. filePath: string, done: (err: Error) => void)
  184. {
  185. video.stream(player.video.host, player.video.file, page.swf, filePath,
  186. path.extname(player.video.file), player.video.mode, config.verbose, done);
  187. }
  188. /**
  189. * Names the file based on the config, page, series and tag.
  190. */
  191. function generateName(config: IConfig, page: IEpisodePage, extra = '')
  192. {
  193. const episodeNum = parseInt(page.episode, 10);
  194. const volumeNum = parseInt(page.volume, 10);
  195. const episode = (episodeNum < 10 ? '0' : '') + page.episode;
  196. const volume = (volumeNum < 10 ? '0' : '') + page.volume;
  197. const tag = config.tag || 'CrunchyRoll';
  198. const series = config.series || page.series;
  199. return config.nametmpl
  200. .replace(/{EPISODE_ID}/g, page.id.toString())
  201. .replace(/{EPISODE_NUMBER}/g, episode)
  202. .replace(/{SEASON_NUMBER}/g, volume)
  203. .replace(/{VOLUME_NUMBER}/g, volume)
  204. .replace(/{SEASON_TITLE}/g, page.season)
  205. .replace(/{VOLUME_TITLE}/g, page.season)
  206. .replace(/{SERIES_TITLE}/g, series)
  207. .replace(/{EPISODE_TITLE}/g, page.title)
  208. .replace(/{TAG}/g, tag) + extra;
  209. }
  210. /**
  211. * Prefixes a value.
  212. */
  213. function prefix(value: number|string, length: number)
  214. {
  215. let valueString = (typeof value !== 'string') ? String(value) : value;
  216. while (valueString.length < length)
  217. {
  218. valueString = '0' + valueString;
  219. }
  220. return valueString;
  221. }
  222. /**
  223. * Requests the page data and scrapes the id, episode, series and swf.
  224. */
  225. function scrapePage(config: IConfig, address: string, done: (err: Error, page?: IEpisodePage) => void)
  226. {
  227. const epId = parseInt((address.match(/[0-9]+$/) || ['0'])[0], 10);
  228. if (!epId)
  229. {
  230. return done(new Error('Invalid address.'));
  231. }
  232. my_request.get(config, address, (err, result) =>
  233. {
  234. if (err)
  235. {
  236. return done(err);
  237. }
  238. const $ = cheerio.load(result);
  239. const swf = /^([^?]+)/.exec($('link[rel=video_src]').attr('href'));
  240. 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]?)/;
  241. const look = $('#showmedia_about_media').text();
  242. const seasonTitle = $('span[itemprop="title"]').text();
  243. const episodeTitle = $('#showmedia_about_name').text().replace(/[“”]/g, '');
  244. const data = regexp.exec(look);
  245. if (config.debug)
  246. {
  247. log.dumpToDebug('episode page', $.html());
  248. }
  249. if (!swf || !data)
  250. {
  251. log.warn('Somethig unexpected in the page at ' + address + ' (data are: ' + look + ')');
  252. log.warn('Setting Season to ’0’ and episode to ’0’...');
  253. if (config.debug)
  254. {
  255. log.dumpToDebug('episode unexpected', look);
  256. }
  257. done(null, {
  258. episode: '0',
  259. id: epId,
  260. series: seasonTitle,
  261. season: seasonTitle,
  262. title: episodeTitle,
  263. swf: swf[1],
  264. volume: '0',
  265. filename: '',
  266. });
  267. }
  268. else
  269. {
  270. done(null, {
  271. episode: data[3],
  272. id: epId,
  273. series: data[1],
  274. season: seasonTitle,
  275. title: episodeTitle,
  276. swf: swf[1],
  277. volume: data[2] || '1',
  278. filename: '',
  279. });
  280. }
  281. });
  282. }
  283. /**
  284. * Requests the player data and scrapes the subtitle and video data.
  285. */
  286. function scrapePlayer(config: IConfig, address: string, id: number, done: (err: Error, player?: IEpisodePlayer) => void)
  287. {
  288. const url = address.match(/^(https?:\/\/[^\/]+)/);
  289. if (!url)
  290. {
  291. return done(new Error('Invalid address.'));
  292. }
  293. const postForm = {
  294. current_page: address,
  295. video_format: config.video_format,
  296. video_quality: config.video_quality,
  297. media_id: id
  298. };
  299. my_request.post(config, url[1] + '/xml/?req=RpcApiVideoPlayer_GetStandardConfig&media_id=' + id, postForm,
  300. (err, result) =>
  301. {
  302. if (err)
  303. {
  304. return done(err);
  305. }
  306. xml2js.parseString(result, {
  307. explicitArray: false,
  308. explicitRoot: false,
  309. }, (errPS: Error, player: IEpisodePlayerConfig) =>
  310. {
  311. if (errPS)
  312. {
  313. return done(errPS);
  314. }
  315. try
  316. {
  317. const isSubtitled = Boolean(player['default:preload'].subtitle);
  318. let streamMode = 'RTMP';
  319. if (player['default:preload'].stream_info.host === '')
  320. {
  321. streamMode = 'HLS';
  322. }
  323. done(null, {
  324. subtitle: isSubtitled ? {
  325. data: player['default:preload'].subtitle.data,
  326. id: parseInt(player['default:preload'].subtitle.$.id, 10),
  327. iv: player['default:preload'].subtitle.iv,
  328. } : null,
  329. video: {
  330. file: player['default:preload'].stream_info.file,
  331. host: player['default:preload'].stream_info.host,
  332. mode: streamMode,
  333. },
  334. });
  335. } catch (parseError)
  336. {
  337. if (config.debug)
  338. {
  339. log.dumpToDebug('player scrape', parseError);
  340. }
  341. done(parseError);
  342. }
  343. });
  344. });
  345. }