├── .eslintrc.js ├── .gitignore ├── .npmignore ├── README.md ├── dist └── main.bundle.js ├── package-lock.json ├── package.json ├── src ├── download_subtitle.js ├── handle_release.js ├── handle_title.js ├── handle_type.js ├── http_options.js ├── lang.js ├── main.js ├── req.js ├── save_subtitle.js └── unzip_sub_buffer.js └── webpack.config.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "google", 3 | "parserOptions": { 4 | "ecmaVersion": 7 5 | }, 6 | "rules": { 7 | "prefer-rest-params": 0 8 | }, 9 | "parser": "babel-eslint" 10 | }; 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | test.js 2 | *.srt 3 | *.zip 4 | /node_modules 5 | *.rar 6 | /backup 7 | /out 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /dev 2 | /src 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Project Name 2 | 3 | ### subscene_scraper 4 | # 5 | # 6 | [![asciicast](https://asciinema.org/a/1TwTvEdgZGUbJRZIORIGJr0ey.png)](https://asciinema.org/a/1TwTvEdgZGUbJRZIORIGJr0ey) 7 | [subd](https://github.com/jodevsa/subd) command 8 | 9 | ## changelog v1.3.5 10 | #### re-written in ES7 11 | #### fixed various bugs. 12 | ## Installation 13 | npm install subscene_scraper --save 14 | 15 | ## Usage: 16 | 17 | 18 | ### example(1) 19 | ##### download a subtitle for a movie in our current working directory 20 | #### code: 21 | 22 | var subscene_scraper=require('subscene_scraper'); 23 | 24 | // for example we will download the subtitle file at current working directory 25 | var path=process.cwd(); 26 | 27 | //all languages supported by subscene.com are now supported. 28 | 29 | subscene_scraper.passiveDownloader('interstellar','english',path) 30 | .then(function(savedFiles){ 31 | console.log('subtitle saved to ',savedFiles); 32 | }) 33 | .catch(function(err){ 34 | console.log('error:',err); 35 | }); 36 | 37 | ### example(2) 38 | ##### Interactive downloader 39 | //title subtiles have 2 steps (1) chooseTitle (2) chooseRelease 40 | // release subtitles have 1 step (1) chooseRelease 41 | // you'll have to implement chooseTitleSubtitle,chooseReleaseSubtitle functions. 42 | var subscene_scraper=require('subscene_scraper'); 43 | var interactiveDownloader=subscene_scraper.interactiveDownloader; 44 | const downloader = interactiveDownloader(movieName, language, saveLocation); 45 | downloader.on('info', async (info, choose) => { 46 | if (info.type === 'title') { 47 | // type === 'title' 48 | // chooseTitle (1) 49 | /// choose subtitle from info.result 50 | const result = choose(chooseTitleSubtitle(info.result)); 51 | choose(result); 52 | } else { 53 | /// type === 'release' 54 | // chooseRelease (1) 55 | /// choose subtitle from info.result 56 | choose(chooseReleaseSubtitle(info.result)); 57 | } 58 | }).on('title', async (list, choose) => { 59 | // chooseRelease (2) 60 | const result = chooseReleaseSubtitle(list); 61 | choose(result); 62 | }).on('done', (result, movieName) => { 63 | console.log('Downloaded Subtitle at', result) 64 | }) 65 | -------------------------------------------------------------------------------- /dist/main.bundle.js: -------------------------------------------------------------------------------- 1 | module.exports = 2 | /******/ (function(modules) { // webpackBootstrap 3 | /******/ // The module cache 4 | /******/ var installedModules = {}; 5 | /******/ 6 | /******/ // The require function 7 | /******/ function __webpack_require__(moduleId) { 8 | /******/ 9 | /******/ // Check if module is in cache 10 | /******/ if(installedModules[moduleId]) { 11 | /******/ return installedModules[moduleId].exports; 12 | /******/ } 13 | /******/ // Create a new module (and put it into the cache) 14 | /******/ var module = installedModules[moduleId] = { 15 | /******/ i: moduleId, 16 | /******/ l: false, 17 | /******/ exports: {} 18 | /******/ }; 19 | /******/ 20 | /******/ // Execute the module function 21 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 22 | /******/ 23 | /******/ // Flag the module as loaded 24 | /******/ module.l = true; 25 | /******/ 26 | /******/ // Return the exports of the module 27 | /******/ return module.exports; 28 | /******/ } 29 | /******/ 30 | /******/ 31 | /******/ // expose the modules object (__webpack_modules__) 32 | /******/ __webpack_require__.m = modules; 33 | /******/ 34 | /******/ // expose the module cache 35 | /******/ __webpack_require__.c = installedModules; 36 | /******/ 37 | /******/ // define getter function for harmony exports 38 | /******/ __webpack_require__.d = function(exports, name, getter) { 39 | /******/ if(!__webpack_require__.o(exports, name)) { 40 | /******/ Object.defineProperty(exports, name, { 41 | /******/ configurable: false, 42 | /******/ enumerable: true, 43 | /******/ get: getter 44 | /******/ }); 45 | /******/ } 46 | /******/ }; 47 | /******/ 48 | /******/ // getDefaultExport function for compatibility with non-harmony modules 49 | /******/ __webpack_require__.n = function(module) { 50 | /******/ var getter = module && module.__esModule ? 51 | /******/ function getDefault() { return module['default']; } : 52 | /******/ function getModuleExports() { return module; }; 53 | /******/ __webpack_require__.d(getter, 'a', getter); 54 | /******/ return getter; 55 | /******/ }; 56 | /******/ 57 | /******/ // Object.prototype.hasOwnProperty.call 58 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 59 | /******/ 60 | /******/ // __webpack_public_path__ 61 | /******/ __webpack_require__.p = ""; 62 | /******/ 63 | /******/ // Load entry module and return exports 64 | /******/ return __webpack_require__(__webpack_require__.s = 6); 65 | /******/ }) 66 | /************************************************************************/ 67 | /******/ ([ 68 | /* 0 */ 69 | /***/ (function(module, exports) { 70 | 71 | eval("module.exports = require(\"cheerio\");\n\n//////////////////\n// WEBPACK FOOTER\n// external \"cheerio\"\n// module id = 0\n// module chunks = 0\n\n//# sourceURL=webpack:///external_%22cheerio%22?"); 72 | 73 | /***/ }), 74 | /* 1 */ 75 | /***/ (function(module, exports, __webpack_require__) { 76 | 77 | "use strict"; 78 | eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\n\nvar _lang = __webpack_require__(3);\n\nvar _lang2 = _interopRequireDefault(_lang);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\n/** @description responsible of generating request options.\n * @param {string} URL - HTTP URL.\n * @param {string} lang - Subtitle language.\n * @param {string} method - HTTP METHOD (GET/HEAD/POST/PUT).\n * @param {string} body - HTTP request body.\n * @param {boolean} followRedirect - to follow redirects or not (true/false)\n @return {Promise.}\n */\nfunction genHttpOptions(URL, lang, method, body, followRedirect) {\n var settings = Object.seal({\n followRedirect: false,\n method: 'GET',\n url: '',\n body: '',\n encoding: 'utf-8',\n gzip: true,\n headers: {\n 'User-Agent': 'Mozilla/5.0',\n 'Cookie': 'LanguageFilter='\n }\n });\n settings.url = URL;\n settings.followRedirect = followRedirect || false;\n settings.method = method || 'GET';\n settings.headers.Cookie += (0, _lang2.default)(lang) || '13';\n settings.body = body || '';\n debugger;\n return settings;\n}\n\nexports.default = genHttpOptions;\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/http_options.js\n// module id = 1\n// module chunks = 0\n\n//# sourceURL=webpack:///./src/http_options.js?"); 79 | 80 | /***/ }), 81 | /* 2 */ 82 | /***/ (function(module, exports, __webpack_require__) { 83 | 84 | "use strict"; 85 | eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\n\n/** @description wrapper around a promisifed request function that retries.\n * @param {Object} options - request optipons.\n @return {Respones}\n*/\nvar req = function () {\n var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(options) {\n var response, i;\n return regeneratorRuntime.wrap(function _callee$(_context) {\n while (1) {\n switch (_context.prev = _context.next) {\n case 0:\n _context.next = 2;\n return reqP(options);\n\n case 2:\n response = _context.sent;\n i = 0;\n\n case 4:\n if (!(response.statusCode === blockCode && i < retry)) {\n _context.next = 13;\n break;\n }\n\n _context.next = 7;\n return sleep(sleeptime);\n\n case 7:\n _context.next = 9;\n return reqP(options);\n\n case 9:\n response = _context.sent;\n\n i += 1;\n _context.next = 4;\n break;\n\n case 13:\n if (!(response.statusCode === blockCode)) {\n _context.next = 15;\n break;\n }\n\n return _context.abrupt('return', Promise.reject(new Error('script got blocked 409 resCode')));\n\n case 15:\n return _context.abrupt('return', response);\n\n case 16:\n case 'end':\n return _context.stop();\n }\n }\n }, _callee, this);\n }));\n\n return function req(_x) {\n return _ref.apply(this, arguments);\n };\n}();\n\nvar _request = __webpack_require__(10);\n\nvar _request2 = _interopRequireDefault(_request);\n\nvar _util = __webpack_require__(11);\n\nvar _util2 = _interopRequireDefault(_util);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nfunction _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step(\"next\", value); }, function (err) { step(\"throw\", err); }); } } return step(\"next\"); }); }; }\n\n__webpack_require__(12).shim();\n\nvar reqP = _util2.default.promisify(_request2.default);\nvar retry = 3;\nvar blockCode = 409;\nvar sleeptime = 800;\n\n/** @description sleep//setTimeout.\n * @param {string} seconds - number of seconds to sleep.\n @return {Respones}\n*/\nfunction sleep(seconds) {\n return new Promise(function (resolve, reject) {\n setTimeout(function () {\n resolve(seconds);\n }, seconds);\n });\n}exports.default = req;\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/req.js\n// module id = 2\n// module chunks = 0\n\n//# sourceURL=webpack:///./src/req.js?"); 86 | 87 | /***/ }), 88 | /* 3 */ 89 | /***/ (function(module, exports, __webpack_require__) { 90 | 91 | "use strict"; 92 | eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nvar languageSet = Object.freeze({\n 'arabic': '2',\n 'brazillianportuguese': '4',\n 'danish': '10',\n 'dutch': '11',\n 'english': '13',\n 'farsi/persian': '46',\n 'finnish': '17',\n 'french': '18',\n 'hebrew': '22',\n 'indonesian': '44',\n 'italian': '26',\n 'norwegian': '30',\n 'romanian': '33',\n 'spanish': '38',\n 'swedish': '39',\n 'vietnamese': '45',\n 'albanian': '1',\n 'armenian': '73',\n 'azerbaijani': '55',\n 'basque': '74',\n 'belarusian': '68',\n 'bengali': '54',\n 'big5 code': '3',\n 'bosnian': '60',\n 'bulgarian': '5',\n 'bulgarian/english': '6',\n 'burmese': '61',\n 'catalan': '49',\n 'chinesebg code': '7',\n 'croatian': '8',\n 'czech': '9',\n 'dutch/english': '12',\n 'english/german': '15',\n 'esperanto': '47',\n 'estonian': '16',\n 'georgian': '62',\n 'german': '19',\n 'greek': '21',\n 'greenlandic': '57',\n 'hindi': '51',\n 'hungarian': '23',\n 'hungarian/english': '24',\n 'icelandic': '25',\n 'japanese': '27',\n 'korean': '28',\n 'kurdish': '52',\n 'latvian': '29',\n 'lithuanian': '43',\n 'macedonian': '48',\n 'malay': '50',\n 'malayalam': '64',\n 'manipuri': '65',\n 'mongolian': '72',\n 'pashto': '67',\n 'polish': '31',\n 'portuguese': '32',\n 'punjabi': '66',\n 'russian': '34',\n 'serbian': '35',\n 'sinhala': '58',\n 'slovak': '36',\n 'slovenian': '37',\n 'somali': '70',\n 'tagalog': '53',\n 'tamil': '59',\n 'telugu': '63',\n 'thai': '40',\n 'turkish': '41',\n 'urdu': '42',\n 'ukrainian': '56'\n});\n\n/** @description map string language with it's subscene langCode.\n * @param {string} lang - location to save file.\n @return {boolean}\n */\nfunction getLanguageCode(lang) {\n var l = lang.toLowerCase().trim();\n if (languageSet[l] != undefined) {\n return languageSet[l];\n } else {\n return false;\n }\n}\n\nexports.default = getLanguageCode;\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/lang.js\n// module id = 3\n// module chunks = 0\n\n//# sourceURL=webpack:///./src/lang.js?"); 93 | 94 | /***/ }), 95 | /* 4 */ 96 | /***/ (function(module, exports, __webpack_require__) { 97 | 98 | "use strict"; 99 | eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.getExactTitleList = exports.getExactTitlePassive = exports.getTitleSubtitles = undefined;\n\n/** @description responsible of choosing first title movie! when in passive\n mode.\n * @param {string} movieList - pack\n @return {string}\n */\n/** @description responsible of handling movie subtitles of type title.\n * @param {string} data - pack\n * @param {string} passive - pack\n @return {Promise.}\n */\nvar getExactTitleList = function () {\n var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(data) {\n var _ref2, movieList;\n\n return regeneratorRuntime.wrap(function _callee$(_context) {\n while (1) {\n switch (_context.prev = _context.next) {\n case 0:\n _context.next = 2;\n return getTitles(data);\n\n case 2:\n _ref2 = _context.sent;\n movieList = _ref2.movies;\n return _context.abrupt('return', movieList);\n\n case 5:\n case 'end':\n return _context.stop();\n }\n }\n }, _callee, this);\n }));\n\n return function getExactTitleList(_x) {\n return _ref.apply(this, arguments);\n };\n}();\n/** @description responsible of handling movie subtitles of type title.\n * @param {string} data - pack\n * @param {string} passive - pack\n @return {Promise.}\n */\n\n\nvar getExactTitlePassive = function () {\n var _ref3 = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee2(data) {\n var _ref4, movieList, language, result;\n\n return regeneratorRuntime.wrap(function _callee2$(_context2) {\n while (1) {\n switch (_context2.prev = _context2.next) {\n case 0:\n _context2.next = 2;\n return getTitles(data);\n\n case 2:\n _ref4 = _context2.sent;\n movieList = _ref4.movies;\n language = _ref4.lang;\n result = chooseTitleMoviePassive(movieList);\n _context2.next = 8;\n return getTitleSubLink({ lang: language, result: result });\n\n case 8:\n return _context2.abrupt('return', _context2.sent);\n\n case 9:\n case 'end':\n return _context2.stop();\n }\n }\n }, _callee2, this);\n }));\n\n return function getExactTitlePassive(_x2) {\n return _ref3.apply(this, arguments);\n };\n}();\n\n/** @description responsible of handling movie subtitles of type title.\n * @param {string} data - pack\n @return {Array}\n */\n\n\nvar getTitleSubtitles = function () {\n var _ref5 = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee3(data) {\n var url, lang, op, response, $, subtitles, lastLink;\n return regeneratorRuntime.wrap(function _callee3$(_context3) {\n while (1) {\n switch (_context3.prev = _context3.next) {\n case 0:\n url = data.url;\n lang = data.lang;\n op = (0, _http_options2.default)(url, lang, 'GET');\n _context3.next = 5;\n return (0, _req2.default)(op);\n\n case 5:\n response = _context3.sent;\n $ = _cheerio2.default.load(response.body);\n subtitles = [];\n lastLink = $('table').eq(0).children('tbody').children('tr').children('td.a1');\n\n lastLink.map(function (index, val) {\n var releaseUrl = $(val).children('a').attr('href');\n var releaseName = $(val).children('a').children('span').eq(1).text().trim();\n subtitles.push({\n url: domain + releaseUrl,\n name: releaseName\n });\n });\n return _context3.abrupt('return', subtitles);\n\n case 11:\n case 'end':\n return _context3.stop();\n }\n }\n }, _callee3, this);\n }));\n\n return function getTitleSubtitles(_x3) {\n return _ref5.apply(this, arguments);\n };\n}();\n\n/** @description responsible of handling movie subtitles of type title.\n * @param {string} data - pack\n @return {Promise.}\n */\n\n\nvar getTitles = function () {\n var _ref6 = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee4(data) {\n var language, $, movieTitleList;\n return regeneratorRuntime.wrap(function _callee4$(_context4) {\n while (1) {\n switch (_context4.prev = _context4.next) {\n case 0:\n // Exact>close>popular>tv-series\n language = data.lang;\n $ = _cheerio2.default.load(data.body);\n movieTitleList = {};\n\n $('div .search-result').children('h2').map(function (n, element) {\n var header = $(element).text();\n movieTitleList[header] = [];\n $(element).next().children('li').map(function (n, value) {\n $(value).children('div .title').children('a').map(function (n, Movie) {\n var val = {\n name: $(Movie).text(),\n link: domain + $(Movie).attr('href')\n };\n\n movieTitleList[header].push(val);\n });\n });\n });\n\n return _context4.abrupt('return', { movies: movieTitleList, lang: language });\n\n case 5:\n case 'end':\n return _context4.stop();\n }\n }\n }, _callee4, this);\n }));\n\n return function getTitles(_x4) {\n return _ref6.apply(this, arguments);\n };\n}();\n\nvar _cheerio = __webpack_require__(0);\n\nvar _cheerio2 = _interopRequireDefault(_cheerio);\n\nvar _http_options = __webpack_require__(1);\n\nvar _http_options2 = _interopRequireDefault(_http_options);\n\nvar _req = __webpack_require__(2);\n\nvar _req2 = _interopRequireDefault(_req);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nfunction _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step(\"next\", value); }, function (err) { step(\"throw\", err); }); } } return step(\"next\"); }); }; }\n\nvar domain = 'https://subscene.com';\nvar TitleOptions = ['Exact', 'Close', 'Popular', 'TV-Series'];\n\n/** @description responsible of choosing first title movie! when in passive\n mode.\n * @param {string} movieList - pack\n @return {string}\n */\nfunction chooseTitleMoviePassive(movieList) {\n var i = 0;\n for (var movieType in movieList) {\n if (TitleOptions[i] === movieType) {\n return domain + movieList[movieType][0].link;\n }\n i += 1;\n }\n};\n\nexports.getTitleSubtitles = getTitleSubtitles;\nexports.getExactTitlePassive = getExactTitlePassive;\nexports.getExactTitleList = getExactTitleList;\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/handle_title.js\n// module id = 4\n// module chunks = 0\n\n//# sourceURL=webpack:///./src/handle_title.js?"); 100 | 101 | /***/ }), 102 | /* 5 */ 103 | /***/ (function(module, exports) { 104 | 105 | eval("module.exports = require(\"path\");\n\n//////////////////\n// WEBPACK FOOTER\n// external \"path\"\n// module id = 5\n// module chunks = 0\n\n//# sourceURL=webpack:///external_%22path%22?"); 106 | 107 | /***/ }), 108 | /* 6 */ 109 | /***/ (function(module, exports, __webpack_require__) { 110 | 111 | eval("__webpack_require__(7);\nmodule.exports = __webpack_require__(8);\n\n\n//////////////////\n// WEBPACK FOOTER\n// multi babel-polyfill ./src/main.js\n// module id = 6\n// module chunks = 0\n\n//# sourceURL=webpack:///multi_babel-polyfill_./src/main.js?"); 112 | 113 | /***/ }), 114 | /* 7 */ 115 | /***/ (function(module, exports) { 116 | 117 | eval("module.exports = require(\"babel-polyfill\");\n\n//////////////////\n// WEBPACK FOOTER\n// external \"babel-polyfill\"\n// module id = 7\n// module chunks = 0\n\n//# sourceURL=webpack:///external_%22babel-polyfill%22?"); 118 | 119 | /***/ }), 120 | /* 8 */ 121 | /***/ (function(module, exports, __webpack_require__) { 122 | 123 | "use strict"; 124 | eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.passiveDownloader = exports.interactiveDownloader = undefined;\n\n/**\n * @typedef MovieTypeData\n * @property {string} type type of movie (title/release).\n * @property {string} lang language needed for the movie subtitle.\n * @property {string} body html response of search request.\n */\n/** @description Detirmines movies type (title/release).\n * @param {string} filename - the name of the movie.\n * @param {string} lang - the language desired\n @return {Promise.}\n */\nvar determineMovieNameType = function () {\n var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(filename, lang) {\n var langCode, url, response, type;\n return regeneratorRuntime.wrap(function _callee$(_context) {\n while (1) {\n switch (_context.prev = _context.next) {\n case 0:\n langCode = (0, _lang2.default)(lang);\n\n if (langCode) {\n _context.next = 3;\n break;\n }\n\n return _context.abrupt('return', Promise.reject(new Error('language not supported!')));\n\n case 3:\n url = domain + '/subtitles/searchbytitle';\n _context.next = 6;\n return (0, _req2.default)({ url, method: \"POST\", form: { query: encodeURIComponent(filename) } });\n\n case 6:\n response = _context.sent;\n type = response.request._redirect.redirects.length === 0 ? 'title' : 'release';\n return _context.abrupt('return', { '_name': filename,\n 'type': type,\n 'lang': langCode,\n 'body': response.body });\n\n case 9:\n case 'end':\n return _context.stop();\n }\n }\n }, _callee, this);\n }));\n\n return function determineMovieNameType(_x, _x2) {\n return _ref.apply(this, arguments);\n };\n}();\n\n/** @description extract subtitle's download link.\n * @param {string} URL - the name of the movie.\n @return {Promise.}\n */\nvar getSubtitleDownloadLink = function () {\n var _ref2 = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee2(URL) {\n var url, response, $, downloadLink;\n return regeneratorRuntime.wrap(function _callee2$(_context2) {\n while (1) {\n switch (_context2.prev = _context2.next) {\n case 0:\n // until we apply this to handleTitle\n // it will always be an array.\n url = Array.isArray(URL) ? URL[0] : URL;\n _context2.next = 3;\n return (0, _req2.default)((0, _http_options2.default)(url, 'GET', ''));\n\n case 3:\n response = _context2.sent;\n $ = _cheerio2.default.load(response.body);\n downloadLink = domain + $('.download a').attr('href');\n return _context2.abrupt('return', downloadLink);\n\n case 7:\n case 'end':\n return _context2.stop();\n }\n }\n }, _callee2, this);\n }));\n\n return function getSubtitleDownloadLink(_x3) {\n return _ref2.apply(this, arguments);\n };\n}();\n\n/** @description main function.\n * @param {string} movieName - the name of the movie.\n * @param {string} language - the name of the movie.\n * @param {string} path - the name of the movie.\n @return {Promise.}\n */\n\n\nvar getMovieSubtitleDetails = function () {\n var _ref3 = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee3(movieName, language) {\n var movieInfo, result;\n return regeneratorRuntime.wrap(function _callee3$(_context3) {\n while (1) {\n switch (_context3.prev = _context3.next) {\n case 0:\n _context3.next = 2;\n return determineMovieNameType(movieName, language);\n\n case 2:\n movieInfo = _context3.sent;\n _context3.next = 5;\n return (0, _handle_type2.default)(movieInfo);\n\n case 5:\n result = _context3.sent;\n return _context3.abrupt('return', {\n type: movieInfo.type,\n result: result\n });\n\n case 7:\n case 'end':\n return _context3.stop();\n }\n }\n }, _callee3, this);\n }));\n\n return function getMovieSubtitleDetails(_x4, _x5) {\n return _ref3.apply(this, arguments);\n };\n}();\n\n/** @description download's subtitle passivly, choose's first subtitle found.\n* @param {string} movieName - the name of the movie.\n* @param {string} lang - the name of the movie.\n* @param {string} path - the name of the movie.\n @return {Promise.}\n*/\n\n\nvar passiveDownloader = function () {\n var _ref4 = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee4(movieName, lang, path) {\n var language, movieInfo, movieURL, list, releaseURL, result, _releaseURL, _result;\n\n return regeneratorRuntime.wrap(function _callee4$(_context4) {\n while (1) {\n switch (_context4.prev = _context4.next) {\n case 0:\n language = lang || 'english';\n _context4.next = 3;\n return getMovieSubtitleDetails(movieName, language);\n\n case 3:\n movieInfo = _context4.sent;\n\n if (!(movieInfo.type === 'title')) {\n _context4.next = 16;\n break;\n }\n\n movieURL = chooseTitleMoviePassive(movieInfo.result); // 1\n\n _context4.next = 8;\n return (0, _handle_title.getTitleSubtitles)({ url: movieURL, lang: language });\n\n case 8:\n list = _context4.sent;\n releaseURL = list[0].url; // 2\n\n _context4.next = 12;\n return downloadReleaseSubtitle(releaseURL, path);\n\n case 12:\n result = _context4.sent;\n return _context4.abrupt('return', result);\n\n case 16:\n _releaseURL = movieInfo.result[0].url;\n _context4.next = 19;\n return downloadReleaseSubtitle(_releaseURL, path);\n\n case 19:\n _result = _context4.sent;\n return _context4.abrupt('return', _result);\n\n case 21:\n case 'end':\n return _context4.stop();\n }\n }\n }, _callee4, this);\n }));\n\n return function passiveDownloader(_x6, _x7, _x8) {\n return _ref4.apply(this, arguments);\n };\n}();\n\n/** @description download's and save's subtitle of releaseURL.\n* @param {string} releaseURL - url of release.\n* @param {string} location - location to save.\n @return {Object}\n*/\n\n\nvar downloadReleaseSubtitle = function () {\n var _ref5 = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee5(releaseURL, location) {\n var downloadLink, file, unPackedFile;\n return regeneratorRuntime.wrap(function _callee5$(_context5) {\n while (1) {\n switch (_context5.prev = _context5.next) {\n case 0:\n _context5.next = 2;\n return getSubtitleDownloadLink(releaseURL);\n\n case 2:\n downloadLink = _context5.sent;\n _context5.next = 5;\n return (0, _download_subtitle2.default)(downloadLink);\n\n case 5:\n file = _context5.sent;\n _context5.next = 8;\n return (0, _unzip_sub_buffer2.default)(file);\n\n case 8:\n unPackedFile = _context5.sent;\n _context5.next = 11;\n return (0, _save_subtitle2.default)(location || '.', unPackedFile);\n\n case 11:\n return _context5.abrupt('return', _context5.sent);\n\n case 12:\n case 'end':\n return _context5.stop();\n }\n }\n }, _callee5, this);\n }));\n\n return function downloadReleaseSubtitle(_x9, _x10) {\n return _ref5.apply(this, arguments);\n };\n}();\n\n/** @description simple async emitter....\n* @param {string} emitter - the name of the movie.\n* @param {string} e - event.\n @return {Object}\n*/\n\n\nvar _cheerio = __webpack_require__(0);\n\nvar _cheerio2 = _interopRequireDefault(_cheerio);\n\nvar _http_options = __webpack_require__(1);\n\nvar _http_options2 = _interopRequireDefault(_http_options);\n\nvar _handle_type = __webpack_require__(9);\n\nvar _handle_type2 = _interopRequireDefault(_handle_type);\n\nvar _download_subtitle = __webpack_require__(14);\n\nvar _download_subtitle2 = _interopRequireDefault(_download_subtitle);\n\nvar _unzip_sub_buffer = __webpack_require__(15);\n\nvar _unzip_sub_buffer2 = _interopRequireDefault(_unzip_sub_buffer);\n\nvar _save_subtitle = __webpack_require__(17);\n\nvar _save_subtitle2 = _interopRequireDefault(_save_subtitle);\n\nvar _lang = __webpack_require__(3);\n\nvar _lang2 = _interopRequireDefault(_lang);\n\nvar _req = __webpack_require__(2);\n\nvar _req2 = _interopRequireDefault(_req);\n\nvar _handle_title = __webpack_require__(4);\n\nvar _events = __webpack_require__(19);\n\nvar _events2 = _interopRequireDefault(_events);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nfunction _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step(\"next\", value); }, function (err) { step(\"throw\", err); }); } } return step(\"next\"); }); }; }\n\nvar domain = 'https://subscene.com/';\nvar TitleOptions = Object.freeze(['Exact', 'Close', 'Popular', 'TV-Series']);;\n\n/** @description return's the first title in the available options\n [exact,close,popular,tv-series].\n * @param {string} movieList - the name of the movie.\n @return {String}\n */\nfunction chooseTitleMoviePassive(movieList) {\n var i = 0;\n for (var movieType in movieList) {\n if (TitleOptions[i] === movieType) {\n return movieList[movieType][0].link;\n }\n i += 1;\n }\n}function asyncemit(emitter, e) {\n for (var _len = arguments.length, params = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {\n params[_key - 2] = arguments[_key];\n }\n\n return new Promise(function (resolve) {\n emitter.emit.apply(emitter, [e].concat(params, [function (c) {\n resolve(c);\n }]));\n });\n}\n/** @description download's subtitle passivly, choose's first subtitle found.\n* @param {string} movieName - the name of the movie.\n* @param {string} lang - the name of the movie.\n* @param {string} path - the name of the movie.\n @return {Object}\n*/\nfunction interactiveDownloader(movieName, lang, path) {\n var _this = this;\n\n var emitter = new _events2.default();\n process.nextTick(_asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee6() {\n var language, movieInfo, titleURL, list, releaseURL, result, _releaseURL2, _result2;\n\n return regeneratorRuntime.wrap(function _callee6$(_context6) {\n while (1) {\n switch (_context6.prev = _context6.next) {\n case 0:\n language = lang || 'english';\n _context6.next = 3;\n return getMovieSubtitleDetails(movieName, language);\n\n case 3:\n movieInfo = _context6.sent;\n\n if (!(movieInfo.type === 'title')) {\n _context6.next = 20;\n break;\n }\n\n _context6.next = 7;\n return asyncemit(emitter, 'info', movieInfo);\n\n case 7:\n titleURL = _context6.sent;\n _context6.next = 10;\n return (0, _handle_title.getTitleSubtitles)({ url: titleURL, lang: language });\n\n case 10:\n list = _context6.sent;\n _context6.next = 13;\n return asyncemit(emitter, 'title', list);\n\n case 13:\n releaseURL = _context6.sent;\n _context6.next = 16;\n return downloadReleaseSubtitle(releaseURL, path);\n\n case 16:\n result = _context6.sent;\n\n emitter.emit('done', result, movieName);\n _context6.next = 27;\n break;\n\n case 20:\n _context6.next = 22;\n return asyncemit(emitter, 'info', movieInfo);\n\n case 22:\n _releaseURL2 = _context6.sent;\n _context6.next = 25;\n return downloadReleaseSubtitle(_releaseURL2, path);\n\n case 25:\n _result2 = _context6.sent;\n\n emitter.emit('done', _result2, movieName);\n\n case 27:\n case 'end':\n return _context6.stop();\n }\n }\n }, _callee6, _this);\n })));\n return emitter;\n}\n\nexports.interactiveDownloader = interactiveDownloader;\nexports.passiveDownloader = passiveDownloader;\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/main.js\n// module id = 8\n// module chunks = 0\n\n//# sourceURL=webpack:///./src/main.js?"); 125 | 126 | /***/ }), 127 | /* 9 */ 128 | /***/ (function(module, exports, __webpack_require__) { 129 | 130 | "use strict"; 131 | eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\n\n/**\n * @typedef MovieTypeData\n * @property {string} type type of movie (title/release).\n * @property {string} lang language needed for the movie subtitle.\n * @property {string} body html response of search request.\n */\n/** @description handle's movies type (title/release).\n * @param {MovieTypeData} data\n * @param {MovieTypeData} isPassive\n @return {handleRelease|handleTitle}\n */\nvar handleType = function () {\n var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(data, isPassive) {\n var handleTitle, handler;\n return regeneratorRuntime.wrap(function _callee$(_context) {\n while (1) {\n switch (_context.prev = _context.next) {\n case 0:\n handleTitle = isPassive ? _handle_title.getExactTitlePassive : _handle_title.getExactTitleList;\n handler = data.type === 'release' ? _handle_release2.default : handleTitle;\n return _context.abrupt('return', handler(data));\n\n case 3:\n case 'end':\n return _context.stop();\n }\n }\n }, _callee, this);\n }));\n\n return function handleType(_x, _x2) {\n return _ref.apply(this, arguments);\n };\n}();\n\nvar _handle_title = __webpack_require__(4);\n\nvar _handle_release = __webpack_require__(13);\n\nvar _handle_release2 = _interopRequireDefault(_handle_release);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nfunction _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step(\"next\", value); }, function (err) { step(\"throw\", err); }); } } return step(\"next\"); }); }; }\n\nexports.default = handleType;\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/handle_type.js\n// module id = 9\n// module chunks = 0\n\n//# sourceURL=webpack:///./src/handle_type.js?"); 132 | 133 | /***/ }), 134 | /* 10 */ 135 | /***/ (function(module, exports) { 136 | 137 | eval("module.exports = require(\"request\");\n\n//////////////////\n// WEBPACK FOOTER\n// external \"request\"\n// module id = 10\n// module chunks = 0\n\n//# sourceURL=webpack:///external_%22request%22?"); 138 | 139 | /***/ }), 140 | /* 11 */ 141 | /***/ (function(module, exports) { 142 | 143 | eval("module.exports = require(\"util\");\n\n//////////////////\n// WEBPACK FOOTER\n// external \"util\"\n// module id = 11\n// module chunks = 0\n\n//# sourceURL=webpack:///external_%22util%22?"); 144 | 145 | /***/ }), 146 | /* 12 */ 147 | /***/ (function(module, exports) { 148 | 149 | eval("module.exports = require(\"util.promisify\");\n\n//////////////////\n// WEBPACK FOOTER\n// external \"util.promisify\"\n// module id = 12\n// module chunks = 0\n\n//# sourceURL=webpack:///external_%22util.promisify%22?"); 150 | 151 | /***/ }), 152 | /* 13 */ 153 | /***/ (function(module, exports, __webpack_require__) { 154 | 155 | "use strict"; 156 | eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\n\nvar _cheerio = __webpack_require__(0);\n\nvar _cheerio2 = _interopRequireDefault(_cheerio);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nvar domain = 'https://subscene.com';\n\n/** @description responsible of handling movie subtitles of type release.\n * @param {string} data - pack\n @return {Promise.}\n */\nfunction handleRelease(data) {\n var $ = _cheerio2.default.load(data.body);\n var releaseTable = $('table tbody tr');\n var releaseLinks = Array.prototype.slice.call(releaseTable.map(function (index, value) {\n var path = $(value).children('.a1').children('a').eq(0).attr('href');\n var name = $(value).children('.a1').children('a').eq(0).children('span').eq(1).text().trim();\n return {\n name: name,\n url: domain + path\n };\n }));\n return releaseLinks;\n}\n\nexports.default = handleRelease;\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/handle_release.js\n// module id = 13\n// module chunks = 0\n\n//# sourceURL=webpack:///./src/handle_release.js?"); 157 | 158 | /***/ }), 159 | /* 14 */ 160 | /***/ (function(module, exports, __webpack_require__) { 161 | 162 | "use strict"; 163 | eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\n\n/** @description responsible of downloading subtitle.\n * @param {string} downloadURL - download link\n @return {Promise.}\n */\nvar downloadSubtitle = function () {\n var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(downloadURL) {\n var op, response, filename, Package;\n return regeneratorRuntime.wrap(function _callee$(_context) {\n while (1) {\n switch (_context.prev = _context.next) {\n case 0:\n op = (0, _http_options2.default)(downloadURL, 'english', 'GET');\n\n op.encoding = null;\n _context.next = 4;\n return (0, _req2.default)(op);\n\n case 4:\n response = _context.sent;\n filename = response.headers['content-disposition'].split(';')[1].split('=')[1];\n Package = {\n filename: filename,\n data: response.body\n };\n return _context.abrupt('return', Package);\n\n case 8:\n case 'end':\n return _context.stop();\n }\n }\n }, _callee, this);\n }));\n\n return function downloadSubtitle(_x) {\n return _ref.apply(this, arguments);\n };\n}();\n\nvar _req = __webpack_require__(2);\n\nvar _req2 = _interopRequireDefault(_req);\n\nvar _http_options = __webpack_require__(1);\n\nvar _http_options2 = _interopRequireDefault(_http_options);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nfunction _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step(\"next\", value); }, function (err) { step(\"throw\", err); }); } } return step(\"next\"); }); }; }\n\nexports.default = downloadSubtitle;\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/download_subtitle.js\n// module id = 14\n// module chunks = 0\n\n//# sourceURL=webpack:///./src/download_subtitle.js?"); 164 | 165 | /***/ }), 166 | /* 15 */ 167 | /***/ (function(module, exports, __webpack_require__) { 168 | 169 | "use strict"; 170 | eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\n\nvar _path = __webpack_require__(5);\n\nvar _path2 = _interopRequireDefault(_path);\n\nvar _admZip = __webpack_require__(16);\n\nvar _admZip2 = _interopRequireDefault(_admZip);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\n/** @description responsible of unpacking subtitles.\n * @param {string} data - pack\n @return {Promise.}\n */\nfunction unzipSubtitleBuffer(data) {\n return new Promise(function (resolve, reject) {\n var files = [];\n // if rar neglict .. ******FEATURE Neeeded*****\n if (_path2.default.extname(data.filename) == '.rar') {\n files.push({ 'fileName': data.filename, 'data': data.data });\n resolve(files);\n }\n\n try {\n var zip = new _admZip2.default(data.data);\n zip.getEntries().forEach(function (entry) {\n var entryName = entry.entryName;\n // decompressed buffer of the entry\n var decompressedData = zip.readFile(entry);\n files.push({ 'fileName': entryName, 'data': decompressedData });\n });\n resolve(files);\n } catch (e) {\n reject(new Error('Error while unzipping subtitle+\\n' + e));\n }\n });\n}\n\nexports.default = unzipSubtitleBuffer;\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/unzip_sub_buffer.js\n// module id = 15\n// module chunks = 0\n\n//# sourceURL=webpack:///./src/unzip_sub_buffer.js?"); 171 | 172 | /***/ }), 173 | /* 16 */ 174 | /***/ (function(module, exports) { 175 | 176 | eval("module.exports = require(\"adm-zip\");\n\n//////////////////\n// WEBPACK FOOTER\n// external \"adm-zip\"\n// module id = 16\n// module chunks = 0\n\n//# sourceURL=webpack:///external_%22adm-zip%22?"); 177 | 178 | /***/ }), 179 | /* 17 */ 180 | /***/ (function(module, exports, __webpack_require__) { 181 | 182 | "use strict"; 183 | eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\n\nvar _path = __webpack_require__(5);\n\nvar _path2 = _interopRequireDefault(_path);\n\nvar _fs = __webpack_require__(18);\n\nvar _fs2 = _interopRequireDefault(_fs);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nfunction _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step(\"next\", value); }, function (err) { step(\"throw\", err); }); } } return step(\"next\"); }); }; }\n\n/** @description responsible of saving subtitle files to disk.\n * @param {string} savePath - location to save file.\n * @param {array} files - array of subtitle files.\n @return {Promise.}\n */\nfunction saveSubtitle(savePath, files) {\n return new Promise(function () {\n var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(resolve, reject) {\n var log;\n return regeneratorRuntime.wrap(function _callee$(_context) {\n while (1) {\n switch (_context.prev = _context.next) {\n case 0:\n log = [];\n\n files.map(function (file, n) {\n var saveFile = _path2.default.join(savePath, file.fileName);\n _fs2.default.writeFile(saveFile, file.data, function (err) {\n if (err) {\n reject(err);\n } else {\n log.push(saveFile);\n }\n if (n == files.length - 1) {\n resolve(log);\n }\n });\n });\n\n case 2:\n case 'end':\n return _context.stop();\n }\n }\n }, _callee, this);\n }));\n\n return function (_x, _x2) {\n return _ref.apply(this, arguments);\n };\n }());\n}\n\nexports.default = saveSubtitle;\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/save_subtitle.js\n// module id = 17\n// module chunks = 0\n\n//# sourceURL=webpack:///./src/save_subtitle.js?"); 184 | 185 | /***/ }), 186 | /* 18 */ 187 | /***/ (function(module, exports) { 188 | 189 | eval("module.exports = require(\"fs\");\n\n//////////////////\n// WEBPACK FOOTER\n// external \"fs\"\n// module id = 18\n// module chunks = 0\n\n//# sourceURL=webpack:///external_%22fs%22?"); 190 | 191 | /***/ }), 192 | /* 19 */ 193 | /***/ (function(module, exports) { 194 | 195 | eval("module.exports = require(\"events\");\n\n//////////////////\n// WEBPACK FOOTER\n// external \"events\"\n// module id = 19\n// module chunks = 0\n\n//# sourceURL=webpack:///external_%22events%22?"); 196 | 197 | /***/ }) 198 | /******/ ]); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "subscene_scraper", 3 | "version": "1.3.6", 4 | "description": "Automate subtitle downloading from subscene.com", 5 | "main": "./dist/main.bundle.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "eslint src/* && WEBPACK_ENV=build webpack --progress --colors", 9 | "dev": "WEBPACK_ENV=dev webpack --progress --colors --watch", 10 | "eslint": "watch -n 1 eslint src/" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/jodevsa/subscene_scraper.git" 15 | }, 16 | "keywords": [ 17 | "sub", 18 | "subscene", 19 | "fun", 20 | "subtitles", 21 | "subtitle", 22 | "movie", 23 | "movies", 24 | "api" 25 | ], 26 | "author": "jodevsa", 27 | "license": "ISC", 28 | "bugs": { 29 | "url": "https://github.com/jodevsa/subscene_scraper/issues" 30 | }, 31 | "homepage": "https://github.com/jodevsa/subscene_scraper#readme", 32 | "devDependencies": { 33 | "babel-core": "^6.26.0", 34 | "babel-eslint": "^8.0.1", 35 | "babel-loader": "^7.1.2", 36 | "babel-preset-env": "^1.6.1", 37 | "babel-preset-es2015": "^6.24.1", 38 | "babel-preset-import-export": "^1.0.2", 39 | "eslint": "^4.10.0", 40 | "eslint-config-google": "^0.9.1", 41 | "jsdoc": "^3.5.5", 42 | "webpack": "^3.8.1", 43 | "webpack-node-externals": "^1.6.0" 44 | }, 45 | "dependencies": { 46 | "adm-zip": "^0.4.7", 47 | "babel-polyfill": "^6.26.0", 48 | "cheerio": "^1.0.0-rc.2", 49 | "request": "^2.83.0", 50 | "util.promisify": "^1.0.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/download_subtitle.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import req from './req'; 4 | import httpOptions from './http_options'; 5 | 6 | /** @description responsible of downloading subtitle. 7 | * @param {string} downloadURL - download link 8 | @return {Promise.} 9 | */ 10 | async function downloadSubtitle(downloadURL) { 11 | const op = httpOptions(downloadURL, 'english', 'GET'); 12 | op.encoding = null; 13 | const response = await req(op); 14 | const filename=response.headers['content-disposition'] 15 | .split(';')[1].split('=')[1]; 16 | let Package = { 17 | filename: filename, 18 | data: response.body, 19 | }; 20 | return Package; 21 | } 22 | 23 | export default downloadSubtitle; 24 | -------------------------------------------------------------------------------- /src/handle_release.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import cheerio from 'cheerio'; 4 | 5 | const domain = 'https://subscene.com'; 6 | 7 | /** @description responsible of handling movie subtitles of type release. 8 | * @param {string} data - pack 9 | @return {Promise.} 10 | */ 11 | function handleRelease(data) { 12 | const $ = cheerio.load(data.body); 13 | const releaseTable = $('table tbody tr'); 14 | const releaseLinks = Array.prototype.slice.call(releaseTable.map( 15 | (index, value) => { 16 | const path = $(value).children('.a1').children('a').eq(0).attr('href'); 17 | const name = $(value).children('.a1').children('a') 18 | .eq(0).children('span').eq(1).text().trim(); 19 | return { 20 | name: name, 21 | url: (domain + path), 22 | }; 23 | })); 24 | return releaseLinks; 25 | } 26 | 27 | export default handleRelease; 28 | -------------------------------------------------------------------------------- /src/handle_title.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import cheerio from 'cheerio'; 4 | import httpOptions from './http_options'; 5 | import req from './req'; 6 | 7 | const domain = 'https://subscene.com'; 8 | const TitleOptions = ['Exact', 'Close', 'Popular', 'TV-Series']; 9 | 10 | /** @description responsible of choosing first title movie! when in passive 11 | mode. 12 | * @param {string} movieList - pack 13 | @return {string} 14 | */ 15 | function chooseTitleMoviePassive(movieList) { 16 | let i = 0; 17 | for (const movieType in movieList) { 18 | if (TitleOptions[i] === movieType) { 19 | return domain + movieList[movieType][0].link; 20 | } 21 | i += 1; 22 | } 23 | } 24 | /** @description responsible of choosing first title movie! when in passive 25 | mode. 26 | * @param {string} movieList - pack 27 | @return {string} 28 | */ 29 | /** @description responsible of handling movie subtitles of type title. 30 | * @param {string} data - pack 31 | * @param {string} passive - pack 32 | @return {Promise.} 33 | */ 34 | async function getExactTitleList(data) { 35 | const {movies: movieList} = await getTitles(data); 36 | return movieList; 37 | } 38 | /** @description responsible of handling movie subtitles of type title. 39 | * @param {string} data - pack 40 | * @param {string} passive - pack 41 | @return {Promise.} 42 | */ 43 | async function getExactTitlePassive(data) { 44 | const {movies: movieList, lang: language} = await getTitles(data); 45 | const result = chooseTitleMoviePassive(movieList); 46 | return await getTitleSubLink({lang: language, result: result}); 47 | } 48 | 49 | /** @description responsible of handling movie subtitles of type title. 50 | * @param {string} data - pack 51 | @return {Array} 52 | */ 53 | async function getTitleSubtitles(data) { 54 | const url = data.url; 55 | const lang = data.lang; 56 | const op = httpOptions(url, lang, 'GET'); 57 | const response = await req(op); 58 | const $ = cheerio.load(response.body); 59 | const subtitles = []; 60 | const lastLink = $('table').eq(0).children('tbody') 61 | .children('tr').children('td.a1'); 62 | lastLink.map((index, val) => { 63 | const releaseUrl = $(val).children('a').attr('href'); 64 | const releaseName = $(val).children('a') 65 | .children('span').eq(1).text().trim(); 66 | subtitles.push({ 67 | url: domain + releaseUrl, 68 | name: releaseName, 69 | }); 70 | }); 71 | return subtitles; 72 | } 73 | 74 | /** @description responsible of handling movie subtitles of type title. 75 | * @param {string} data - pack 76 | @return {Promise.} 77 | */ 78 | async function getTitles(data) { 79 | // Exact>close>popular>tv-series 80 | const language = data.lang; 81 | const $ = cheerio.load(data.body); 82 | const movieTitleList = {}; 83 | $('div .search-result').children('h2').map(function(n, element) { 84 | const header = $(element).text(); 85 | movieTitleList[header] = []; 86 | $(element).next().children('li').map((n, value) => { 87 | $(value).children('div .title').children('a').map((n, Movie) => { 88 | const val = { 89 | name: $(Movie).text(), 90 | link: domain + $(Movie).attr('href'), 91 | }; 92 | 93 | movieTitleList[header].push(val); 94 | }); 95 | }); 96 | }); 97 | 98 | return {movies: movieTitleList, lang: language}; 99 | }; 100 | 101 | export {getTitleSubtitles, getExactTitlePassive, getExactTitleList}; 102 | -------------------------------------------------------------------------------- /src/handle_type.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import {getExactTitleList, getExactTitlePassive} from './handle_title'; 4 | import handleRelease from './handle_release'; 5 | /** 6 | * @typedef MovieTypeData 7 | * @property {string} type type of movie (title/release). 8 | * @property {string} lang language needed for the movie subtitle. 9 | * @property {string} body html response of search request. 10 | */ 11 | /** @description handle's movies type (title/release). 12 | * @param {MovieTypeData} data 13 | * @param {MovieTypeData} isPassive 14 | @return {handleRelease|handleTitle} 15 | */ 16 | async function handleType(data, isPassive) { 17 | const handleTitle=isPassive?getExactTitlePassive:getExactTitleList; 18 | const handler=data.type === 'release' ? handleRelease : handleTitle; 19 | return handler(data); 20 | } 21 | 22 | export default handleType; 23 | -------------------------------------------------------------------------------- /src/http_options.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import getLanguageCode from './lang'; 4 | 5 | 6 | /** @description responsible of generating request options. 7 | * @param {string} URL - HTTP URL. 8 | * @param {string} lang - Subtitle language. 9 | * @param {string} method - HTTP METHOD (GET/HEAD/POST/PUT). 10 | * @param {string} body - HTTP request body. 11 | * @param {boolean} followRedirect - to follow redirects or not (true/false) 12 | @return {Promise.} 13 | */ 14 | function genHttpOptions(URL, lang, method, body, followRedirect) { 15 | const settings =Object.seal({ 16 | followRedirect: false, 17 | method: 'GET', 18 | url: '', 19 | body: '', 20 | encoding: 'utf-8', 21 | gzip: true, 22 | headers: { 23 | 'User-Agent': 'Mozilla/5.0', 24 | 'Cookie': 'LanguageFilter=', 25 | }, 26 | }); 27 | settings.url = URL; 28 | settings.followRedirect = followRedirect || false; 29 | settings.method = method || 'GET'; 30 | settings.headers.Cookie += (getLanguageCode(lang) || '13'); 31 | settings.body = body || ''; 32 | debugger; 33 | return settings; 34 | } 35 | 36 | export default genHttpOptions; 37 | -------------------------------------------------------------------------------- /src/lang.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const languageSet =Object.freeze({ 4 | 'arabic': '2', 5 | 'brazillianportuguese': '4', 6 | 'danish': '10', 7 | 'dutch': '11', 8 | 'english': '13', 9 | 'farsi/persian': '46', 10 | 'finnish': '17', 11 | 'french': '18', 12 | 'hebrew': '22', 13 | 'indonesian': '44', 14 | 'italian': '26', 15 | 'norwegian': '30', 16 | 'romanian': '33', 17 | 'spanish': '38', 18 | 'swedish': '39', 19 | 'vietnamese': '45', 20 | 'albanian': '1', 21 | 'armenian': '73', 22 | 'azerbaijani': '55', 23 | 'basque': '74', 24 | 'belarusian': '68', 25 | 'bengali': '54', 26 | 'big5 code': '3', 27 | 'bosnian': '60', 28 | 'bulgarian': '5', 29 | 'bulgarian/english': '6', 30 | 'burmese': '61', 31 | 'catalan': '49', 32 | 'chinesebg code': '7', 33 | 'croatian': '8', 34 | 'czech': '9', 35 | 'dutch/english': '12', 36 | 'english/german': '15', 37 | 'esperanto': '47', 38 | 'estonian': '16', 39 | 'georgian': '62', 40 | 'german': '19', 41 | 'greek': '21', 42 | 'greenlandic': '57', 43 | 'hindi': '51', 44 | 'hungarian': '23', 45 | 'hungarian/english': '24', 46 | 'icelandic': '25', 47 | 'japanese': '27', 48 | 'korean': '28', 49 | 'kurdish': '52', 50 | 'latvian': '29', 51 | 'lithuanian': '43', 52 | 'macedonian': '48', 53 | 'malay': '50', 54 | 'malayalam': '64', 55 | 'manipuri': '65', 56 | 'mongolian': '72', 57 | 'pashto': '67', 58 | 'polish': '31', 59 | 'portuguese': '32', 60 | 'punjabi': '66', 61 | 'russian': '34', 62 | 'serbian': '35', 63 | 'sinhala': '58', 64 | 'slovak': '36', 65 | 'slovenian': '37', 66 | 'somali': '70', 67 | 'tagalog': '53', 68 | 'tamil': '59', 69 | 'telugu': '63', 70 | 'thai': '40', 71 | 'turkish': '41', 72 | 'urdu': '42', 73 | 'ukrainian': '56', 74 | }); 75 | 76 | /** @description map string language with it's subscene langCode. 77 | * @param {string} lang - location to save file. 78 | @return {boolean} 79 | */ 80 | function getLanguageCode(lang) { 81 | let l = lang.toLowerCase().trim(); 82 | if (languageSet[l] != undefined) { 83 | return languageSet[l]; 84 | } else { 85 | return false; 86 | } 87 | } 88 | 89 | export default getLanguageCode; 90 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import cheerio from 'cheerio'; 4 | import genHttpOptions from './http_options'; 5 | import handleType from './handle_type'; 6 | import downloadSubtitle from './download_subtitle'; 7 | import unzipSubtitleBuffer from './unzip_sub_buffer'; 8 | import saveSubtitle from './save_subtitle'; 9 | import getLangCode from './lang'; 10 | import req from './req'; 11 | import {getTitleSubtitles} from './handle_title'; 12 | import EventEmitter from 'events'; 13 | 14 | const domain = 'https://subscene.com/'; 15 | const TitleOptions = Object.freeze([ 16 | 'Exact', 17 | 'Close', 18 | 'Popular', 19 | 'TV-Series', 20 | ]); 21 | 22 | /** 23 | * @typedef MovieTypeData 24 | * @property {string} type type of movie (title/release). 25 | * @property {string} lang language needed for the movie subtitle. 26 | * @property {string} body html response of search request. 27 | */ 28 | /** @description Detirmines movies type (title/release). 29 | * @param {string} filename - the name of the movie. 30 | * @param {string} lang - the language desired 31 | @return {Promise.} 32 | */ 33 | async function determineMovieNameType(filename, lang) { 34 | const langCode = getLangCode(lang); 35 | if (!langCode) { 36 | return Promise.reject(new Error('language not supported!')); 37 | } 38 | const url = domain + '/subtitles/searchbytitle'; 39 | const response = await req({url,method:"POST",form:{query:encodeURIComponent(filename)}}); 40 | const type = response.request._redirect.redirects.length === 0 41 | ? 'title' 42 | : 'release'; 43 | return {'_name': filename, 44 | 'type': type, 45 | 'lang': langCode, 46 | 'body': response.body}; 47 | }; 48 | 49 | /** @description return's the first title in the available options 50 | [exact,close,popular,tv-series]. 51 | * @param {string} movieList - the name of the movie. 52 | @return {String} 53 | */ 54 | function chooseTitleMoviePassive(movieList) { 55 | let i=0; 56 | for (const movieType in movieList) { 57 | if (TitleOptions[i] === movieType) { 58 | return movieList[movieType][0].link; 59 | } 60 | i+=1; 61 | } 62 | } 63 | 64 | /** @description extract subtitle's download link. 65 | * @param {string} URL - the name of the movie. 66 | @return {Promise.} 67 | */ 68 | async function getSubtitleDownloadLink(URL) { 69 | // until we apply this to handleTitle 70 | // it will always be an array. 71 | const url = Array.isArray(URL) ? 72 | URL[0] : 73 | URL; 74 | const response = await req(genHttpOptions(url, 'GET', '')); 75 | let $ = cheerio.load(response.body); 76 | let downloadLink = domain + $('.download a').attr('href'); 77 | return downloadLink; 78 | } 79 | 80 | /** @description main function. 81 | * @param {string} movieName - the name of the movie. 82 | * @param {string} language - the name of the movie. 83 | * @param {string} path - the name of the movie. 84 | @return {Promise.} 85 | */ 86 | async function getMovieSubtitleDetails(movieName, language) { 87 | let movieInfo = await determineMovieNameType(movieName, language); 88 | let result = await handleType(movieInfo); 89 | return { 90 | type: movieInfo.type, 91 | result: result, 92 | }; 93 | } 94 | 95 | /** @description download's subtitle passivly, choose's first subtitle found. 96 | * @param {string} movieName - the name of the movie. 97 | * @param {string} lang - the name of the movie. 98 | * @param {string} path - the name of the movie. 99 | @return {Promise.} 100 | */ 101 | async function passiveDownloader(movieName, lang, path) { 102 | const language = lang || 'english'; 103 | const movieInfo = await getMovieSubtitleDetails(movieName, language); 104 | if (movieInfo.type === 'title') { 105 | const movieURL = chooseTitleMoviePassive(movieInfo.result); // 1 106 | const list = await getTitleSubtitles({url: movieURL, lang: language}); 107 | const releaseURL = list[0].url; // 2 108 | const result = await downloadReleaseSubtitle(releaseURL, path); 109 | return result; 110 | } else { 111 | const releaseURL = movieInfo.result[0].url; 112 | const result = await downloadReleaseSubtitle(releaseURL, path); 113 | return result; 114 | } 115 | } 116 | 117 | /** @description download's and save's subtitle of releaseURL. 118 | * @param {string} releaseURL - url of release. 119 | * @param {string} location - location to save. 120 | @return {Object} 121 | */ 122 | async function downloadReleaseSubtitle(releaseURL, location) { 123 | const downloadLink = await getSubtitleDownloadLink(releaseURL); 124 | const file = await downloadSubtitle(downloadLink); 125 | const unPackedFile = await unzipSubtitleBuffer(file); 126 | return await saveSubtitle(location || '.', unPackedFile); 127 | } 128 | 129 | /** @description simple async emitter.... 130 | * @param {string} emitter - the name of the movie. 131 | * @param {string} e - event. 132 | @return {Object} 133 | */ 134 | function asyncemit(emitter, e, ...params) { 135 | return new Promise((resolve)=>{ 136 | emitter.emit(e, ...params, (c)=>{ 137 | resolve(c); 138 | }); 139 | }); 140 | } 141 | /** @description download's subtitle passivly, choose's first subtitle found. 142 | * @param {string} movieName - the name of the movie. 143 | * @param {string} lang - the name of the movie. 144 | * @param {string} path - the name of the movie. 145 | @return {Object} 146 | */ 147 | function interactiveDownloader(movieName, lang, path) { 148 | let emitter = new EventEmitter(); 149 | process.nextTick(async ()=>{ 150 | const language = lang || 'english'; 151 | const movieInfo = await getMovieSubtitleDetails(movieName, language); 152 | if (movieInfo.type === 'title') { 153 | const titleURL=await asyncemit(emitter, 'info', movieInfo); 154 | const list = await getTitleSubtitles({url: titleURL, lang: language}); 155 | const releaseURL =await asyncemit(emitter, 'title', list); 156 | const result = await downloadReleaseSubtitle(releaseURL, path); 157 | emitter.emit('done', result, movieName); 158 | } else { 159 | const releaseURL=await asyncemit(emitter, 'info', movieInfo); 160 | const result = await downloadReleaseSubtitle(releaseURL, path); 161 | emitter.emit('done', result, movieName); 162 | } 163 | }); 164 | return emitter; 165 | } 166 | 167 | export {interactiveDownloader, 168 | passiveDownloader}; 169 | -------------------------------------------------------------------------------- /src/req.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | require('util.promisify').shim(); 3 | import request from 'request'; 4 | import util from 'util'; 5 | const reqP = util.promisify(request); 6 | const retry = 3; 7 | const blockCode = 409; 8 | const sleeptime = 800; 9 | 10 | /** @description sleep//setTimeout. 11 | * @param {string} seconds - number of seconds to sleep. 12 | @return {Respones} 13 | */ 14 | function sleep(seconds) { 15 | return new Promise((resolve, reject) => { 16 | setTimeout(() => { 17 | resolve(seconds); 18 | }, seconds); 19 | }); 20 | } 21 | 22 | /** @description wrapper around a promisifed request function that retries. 23 | * @param {Object} options - request optipons. 24 | @return {Respones} 25 | */ 26 | async function req(options) { 27 | let response = await reqP(options); 28 | let i = 0; 29 | while (response.statusCode === blockCode && i < retry) { 30 | await sleep(sleeptime); 31 | response = await reqP(options); 32 | i += 1; 33 | } 34 | if (response.statusCode === blockCode) { 35 | return Promise.reject(new Error('script got blocked 409 resCode')); 36 | } 37 | return response; 38 | } 39 | 40 | export default req; 41 | -------------------------------------------------------------------------------- /src/save_subtitle.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import path from 'path'; 4 | import fs from 'fs'; 5 | 6 | /** @description responsible of saving subtitle files to disk. 7 | * @param {string} savePath - location to save file. 8 | * @param {array} files - array of subtitle files. 9 | @return {Promise.} 10 | */ 11 | function saveSubtitle(savePath, files) { 12 | return new Promise(async function(resolve, reject) { 13 | const log = []; 14 | files.map(function(file, n) { 15 | const saveFile = path.join(savePath, file.fileName); 16 | fs.writeFile(saveFile, file.data, function(err) { 17 | if (err) { 18 | reject(err); 19 | } else { 20 | log.push(saveFile); 21 | } 22 | if (n == files.length - 1) { 23 | resolve(log); 24 | } 25 | }); 26 | }); 27 | }); 28 | } 29 | 30 | export default saveSubtitle; 31 | -------------------------------------------------------------------------------- /src/unzip_sub_buffer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import path from 'path'; 4 | import AdmZip from 'adm-zip'; 5 | 6 | /** @description responsible of unpacking subtitles. 7 | * @param {string} data - pack 8 | @return {Promise.} 9 | */ 10 | function unzipSubtitleBuffer(data) { 11 | return new Promise(function(resolve, reject) { 12 | const files = []; 13 | // if rar neglict .. ******FEATURE Neeeded***** 14 | if (path.extname(data.filename) == '.rar') { 15 | files.push({'fileName': data.filename, 'data': data.data}); 16 | resolve(files); 17 | } 18 | 19 | try { 20 | const zip = new AdmZip(data.data); 21 | zip.getEntries().forEach(function(entry) { 22 | const entryName = entry.entryName; 23 | // decompressed buffer of the entry 24 | const decompressedData = zip.readFile(entry); 25 | files.push({'fileName': entryName, 'data': decompressedData}); 26 | }); 27 | resolve(files); 28 | } catch (e) { 29 | reject(new Error('Error while unzipping subtitle+\n' + e)); 30 | } 31 | }); 32 | } 33 | 34 | export default unzipSubtitleBuffer; 35 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var nodeExternals = require('webpack-node-externals'); 2 | const path = require('path') 3 | const config = { 4 | entry: [ 5 | "babel-polyfill", "./src/main.js" 6 | ], 7 | externals: [nodeExternals()], 8 | target: "node", 9 | output: { 10 | path: path.join(__dirname, 'dist'), 11 | filename: 'main.bundle.js', 12 | libraryTarget: 'commonjs2' 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.js$/, 18 | exclude: [/node_modules/], 19 | use: { 20 | loader: 'babel-loader', 21 | options: { 22 | presets: [ 23 | [ 24 | "env", { 25 | "targets": { 26 | "node": "4" 27 | } 28 | } 29 | ] 30 | ] 31 | } 32 | } 33 | } 34 | ] 35 | } 36 | }; 37 | if (process.env.WEBPACK_ENV === "dev") { 38 | config.devtool = "eval"; 39 | } 40 | 41 | module.exports = config; 42 | --------------------------------------------------------------------------------