├── .gitignore ├── .editorconfig ├── db ├── lib │ ├── upsert.js │ ├── search.js │ ├── data.js │ └── load.js ├── package.json ├── actions │ ├── list.js │ ├── menu.js │ ├── update.js │ ├── remove.js │ └── add.js ├── index.js └── monsters.db ├── proxy ├── decryptor.js └── index.js └── .eslintrc.json /.gitignore: -------------------------------------------------------------------------------- 1 | # sublime text files 2 | *.sublime* 3 | *.*~*.TMP 4 | 5 | # temp files 6 | .DS_Store 7 | Thumbs.db 8 | Desktop.ini 9 | npm-debug.log 10 | 11 | # project files 12 | .project 13 | .idea 14 | 15 | # vim swap files 16 | *.sw* 17 | 18 | # emacs temp files 19 | *~ 20 | \#*# 21 | 22 | # project ignores 23 | !.gitkeep 24 | *__temp 25 | .env 26 | node_modules/ 27 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs. 2 | # More information at http://EditorConfig.org 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | indent_style = space 10 | indent_size = 4 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [{package.json,bower.json,*.yml}] 16 | indent_size = 2 -------------------------------------------------------------------------------- /db/lib/upsert.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const merge = require('merge'); 4 | 5 | module.exports = function (db, store, obj, isAdd) { 6 | let val = store.by('postId', obj.postId) 7 | 8 | if (val) { 9 | let c = val.count; 10 | merge(val, obj); 11 | val.count = isAdd ? c + 1 : c; 12 | 13 | store.update(val); 14 | } 15 | else { 16 | obj.count = 1; 17 | store.insert(obj); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /db/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sw-mons", 3 | "version": "1.0.0", 4 | "description": "Scripts to manage my monster db", 5 | "main": "index.js", 6 | "author": "Chad Engler ", 7 | "license": "MIT", 8 | "dependencies": { 9 | "async": "^1.5.2", 10 | "cheerio": "^0.20.0", 11 | "cli-table": "^0.3.1", 12 | "inquirer": "^0.12.0", 13 | "lokijs": "^1.3.15", 14 | "merge": "^1.2.0", 15 | "minimist": "^1.2.0", 16 | "request": "^2.69.0" 17 | }, 18 | "devDependencies": { 19 | "eslint": "^2.2.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /db/actions/list.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Table = require('cli-table'); 4 | 5 | const data = require('../lib/data'); 6 | 7 | module.exports = function (argv, db, store, changeState) { 8 | const table = new Table({ 9 | head: ['Count', 'Type', 'Name', 'Awakened Name', 'Rating', 'User Rating', 'Tags'] 10 | }); 11 | 12 | const results = store.find(); 13 | 14 | table.push.apply(table, results.map((v) => { 15 | return [ 16 | v.count || 1, 17 | v.type, 18 | v.name, 19 | v.awakenedName, 20 | v.ratings.review || '', 21 | v.ratings.users || '', 22 | v.details.tags ? v.details.tags.join(', ') : '' 23 | ]; 24 | })); 25 | 26 | console.log(table.toString()); 27 | 28 | changeState(data.STATES.MENU); 29 | }; 30 | -------------------------------------------------------------------------------- /db/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const argv = require('minimist')(process.argv.slice(2)); 6 | const Loki = require('lokijs'); 7 | const data = require('./lib/data'); 8 | const db = new Loki('monsters.db'); 9 | 10 | db.loadDatabase({}, function () { 11 | let store = db.getCollection('monsters') || db.addCollection('monsters', data.collectionSettings); 12 | 13 | handleState(data.STATES.MENU); 14 | 15 | function handleState(state, err) { 16 | db.saveDatabase(function () { 17 | if (state === data.STATES.EXIT) 18 | return process.exit(0); 19 | 20 | if (state === data.STATES.ERROR) { 21 | console.error(err); 22 | state = data.STATES.MENU; 23 | } 24 | 25 | require(`./actions/${state}`)(argv, db, store, handleState); 26 | }); 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /db/actions/menu.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const inquirer = require('inquirer'); 4 | const async = require('async'); 5 | const merge = require('merge'); 6 | 7 | const data = require('../lib/data'); 8 | 9 | module.exports = function (argv, db, store, changeState) { 10 | inquirer.prompt([ 11 | { 12 | type: 'list', 13 | name: 'action', 14 | message: 'Choose an Action', 15 | choices: [ 16 | { name: 'Add Monster', value: data.STATES.ADD }, 17 | { name: 'Remove Monster', value: data.STATES.REMOVE }, 18 | { name: 'List Monsters', value: data.STATES.LIST }, 19 | { name: 'Update All Monsters', value: data.STATES.UPDATE }, 20 | new inquirer.Separator(), 21 | { name: 'Exit', value: data.STATES.EXIT } 22 | ] 23 | } 24 | ], function (answers) { 25 | changeState(answers.action); 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /db/actions/update.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const async = require('async'); 4 | 5 | const data = require('../lib/data'); 6 | const upsert = require('../lib/upsert'); 7 | const load = require('../lib/load'); 8 | 9 | module.exports = function (argv, db, store, changeState) { 10 | const results = store.find(); 11 | const updateStart = Date.now(); 12 | 13 | console.log('Updating monster list...'); 14 | 15 | async.each(results, function (monster, _done) { 16 | load(monster.url, true, function (err, result) { 17 | if (err) return _done(err); 18 | 19 | upsert(db, store, result); 20 | _done(); 21 | }); 22 | }, function (err) { 23 | if (err) { 24 | changeState(data.STATES.ERROR, err); 25 | } 26 | else { 27 | console.log('✓ Update completed in %dms', Date.now() - updateStart); 28 | changeState(data.STATES.MENU); 29 | } 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /db/lib/search.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const request = require('request'); 4 | const cheerio = require('cheerio'); 5 | const data = require('./data'); 6 | 7 | module.exports = function (query, cb) { 8 | let searchStartTime = Date.now(); 9 | 10 | console.log('Searching for "%s"...', query); 11 | 12 | request.post(data.getRequestOptions(query), function onResponse(err, res, body) { 13 | if (err) return cb(err); 14 | 15 | const $ = cheerio.load(body.replace(data.rgxCleanup, '')); 16 | const $results = $('.asp_result_pagepost'); 17 | 18 | console.log('Search completed, got %d results in %dms', $results.length, (Date.now() - searchStartTime)); 19 | 20 | let results = []; 21 | 22 | $results.each(function (i, element) { 23 | const $elm = $(element).find('h3 > a').eq(0); 24 | 25 | results.push({ 26 | name: $elm.text().trim(), 27 | value: $elm.prop('href') 28 | }); 29 | }); 30 | 31 | cb(null, results); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /proxy/decryptor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const crypto = require('crypto'); 4 | const zlib = require('zlib'); 5 | const Buffer = require('buffer').Buffer; 6 | 7 | const key = new Buffer(process.env.DECRYPTION_KEY, 'hex'); 8 | 9 | module.exports = { 10 | decryptRequest: function (body) { 11 | return _decrypt(body); 12 | }, 13 | decryptResponse: function (body) { 14 | return zlib.inflateSync(_decrypt(body)); 15 | } 16 | }; 17 | 18 | function getIv() { 19 | return new Buffer([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); 20 | } 21 | 22 | function _decrypt(msg) { 23 | if (!msg) { 24 | return new Buffer(0); 25 | } 26 | 27 | const buffer = new Buffer(msg, 'base64'); 28 | const decipher = crypto.createDecipheriv('AES-128-CBC', key, getIv()); 29 | 30 | decipher.setAutoPadding(false); 31 | 32 | const result = Buffer.concat([ 33 | decipher.update(buffer), 34 | decipher.final() 35 | ]); 36 | 37 | const padding = result.readUInt8(result.length - 1); 38 | 39 | return result.slice(0, result.length - padding); 40 | } 41 | -------------------------------------------------------------------------------- /db/monsters.db: -------------------------------------------------------------------------------- 1 | {"filename":"monsters.db","collections":[{"name":"monsters","data":[{"postId":542,"type":"water","name":"Warbear","awakenedName":"Dagora","url":"http://summonerswar.co/water-warbear-dagora/","details":{"grade":3,"type":"hp","get":["unknown scroll","mystical scroll","mystical summon","social summon","mt. white ragon"],"hp":11850,"atk":417,"def":604,"spd":101},"ratings":{"review":6,"users":7.7},"reactions":{"keep":68,"food":8,"best":18,"meh":5},"runes":[{"name":"standard hp","runes":["energy"],"stats":["hp%","hp%","hp%"]},{"name":"arena","runes":["energy"],"stats":["hp%","hp%","resist%"]}],"count":1,"meta":{"revision":0,"created":1456794480895,"version":0},"$loki":1}],"idIndex":[1],"binaryIndices":{"name":{"name":"name","dirty":true,"values":[]}},"constraints":null,"uniqueNames":["postId","awakenedName"],"transforms":{},"objType":"monsters","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableChangesApi":true,"autoupdate":false,"ttl":{"age":null,"ttlInterval":null,"daemon":null},"maxId":1,"DynamicViews":[],"events":{"insert":[null],"update":[null],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[]}],"databaseVersion":1,"engineVersion":1.1,"autosave":false,"autosaveInterval":5000,"autosaveHandle":null,"options":{},"persistenceMethod":"fs","persistenceAdapter":null,"verbose":false,"events":{"init":[null],"loaded":[],"flushChanges":[],"close":[],"changes":[],"warning":[]},"ENV":"NODEJS"} -------------------------------------------------------------------------------- /db/lib/data.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var data = module.exports = { 4 | rgxCleanup: /!!ASP(START|END)!!/g, 5 | 6 | star: '★', 7 | 8 | STATES: { 9 | ADD: 'add', 10 | REMOVE: 'remove', 11 | LIST: 'list', 12 | MENU: 'menu', 13 | ERROR: 'error', 14 | UPDATE: 'update', 15 | EXIT: 'exit' 16 | }, 17 | 18 | CATEGORIES: { 19 | STAR_2: 93, 20 | STAR_3: 94, 21 | STAR_4: 91, 22 | STAR_5: 90, 23 | ARENA_DEFENSE: 89, 24 | ARENA_OFFENSE: 95, 25 | BLOG: 3, 26 | DARK: 75, 27 | DUNGEON: 96, 28 | FIRE: 72, 29 | FOOD: 88, 30 | KEEPER: 97, 31 | LIGHT: 76, 32 | MONSTERS: 81, 33 | UNCATEGORIZED: 1, 34 | WATER: 80, 35 | WIND: 74 36 | }, 37 | 38 | collectionSettings: { 39 | indices: ['name'], 40 | unique: ['postId', 'awakenedName'], 41 | disableChangesApi: true 42 | }, 43 | 44 | getRequestOptions: function (query) { 45 | return { 46 | url: 'http://summonerswar.co/wp-admin/admin-ajax.php', 47 | timeout: 30000, 48 | form: { 49 | action: 'ajaxsearchpro_search', 50 | aspp: query, // search term 51 | asid: 1, 52 | asp_inst_id: '1_1', 53 | options: { 54 | qtranslate_lang: 0, 55 | set_intitle: 'None', 56 | set_incontent: 'None', 57 | set_inposts: 'None', 58 | set_inpages: 'None', 59 | categoryset: data.CATEGORIES.MONSTERS 60 | }, 61 | asp_preview_options: 0 62 | } 63 | }; 64 | } 65 | }; 66 | 67 | data.CATEGORIES.ALL = Object.keys(data.CATEGORIES).map((k) => data.CATEGORIES[k]); 68 | -------------------------------------------------------------------------------- /db/actions/remove.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const inquirer = require('inquirer'); 4 | 5 | const data = require('../lib/data'); 6 | 7 | module.exports = function (argv, db, store, changeState) { 8 | inquirer.prompt([ 9 | { 10 | type: 'input', 11 | name: 'query', 12 | message: 'Search term:' 13 | } 14 | ], function (answers) { 15 | const where = { '$regex': new RegExp(answers.query, 'i') }; 16 | const results = store.find({ 17 | '$or': [ 18 | { name: where }, 19 | { awakenedName: where }, 20 | { type: where } 21 | ] 22 | }); 23 | 24 | if (!results.length) { 25 | console.log('No results found.'); 26 | return changeState(data.STATES.MENU); 27 | } 28 | 29 | let choices = results.map((v) => { 30 | let c = v.count > 1 ? '(' + v.count + ') ' : ''; 31 | return { 32 | name: `${c}${v.type} - ${v.name} (${v.awakenedName})`, 33 | value: v 34 | }; 35 | }); 36 | 37 | choices.push(new inquirer.Separator()); 38 | choices.push({ name: 'Cancel', value: null }); 39 | 40 | inquirer.prompt([ 41 | { 42 | type: 'list', 43 | name: 'monster', 44 | message: 'Choose monster to remove:', 45 | choices: choices 46 | } 47 | ], function (answers) { 48 | const obj = answers.monster; 49 | 50 | if (obj) { 51 | if (obj.count > 1) { 52 | obj.count--; 53 | } 54 | else { 55 | store.remove(obj); 56 | } 57 | console.log('Monster removed.'); 58 | } 59 | 60 | changeState(data.STATES.MENU); 61 | }); 62 | }); 63 | }; 64 | -------------------------------------------------------------------------------- /db/actions/add.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const inquirer = require('inquirer'); 4 | const async = require('async'); 5 | const merge = require('merge'); 6 | 7 | const data = require('../lib/data'); 8 | const upsert = require('../lib/upsert'); 9 | const search = require('../lib/search'); 10 | const load = require('../lib/load'); 11 | 12 | module.exports = function (argv, db, store, changeState) { 13 | inquirer.prompt([ 14 | { 15 | type: 'input', 16 | name: 'query', 17 | message: 'Search term:' 18 | } 19 | ], function (answers) { 20 | async.waterfall([ 21 | // Search 22 | function (_done) { 23 | search(answers.query, _done); 24 | }, 25 | // Prompt 26 | function (results, _done) { 27 | if (!results || !results.length) 28 | return _done(new Error('No results')); 29 | 30 | if (results.length === 1) { 31 | _done(null, results[0].value); 32 | } 33 | else { 34 | results.push(new inquirer.Separator()); 35 | inquirer.prompt([ 36 | { 37 | type: 'list', 38 | name: 'mon', 39 | message: 'Which monster', 40 | choices: results 41 | } 42 | ], function (answers) { 43 | _done(null, answers.mon); 44 | }); 45 | } 46 | }, 47 | // Load Data 48 | function (url, _done) { 49 | load(url, _done); 50 | } 51 | ], function (err, result) { 52 | if (err) return changeState(data.STATES.ERROR, err); 53 | 54 | upsert(db, store, result, true); 55 | 56 | console.log('Monster stored!'); 57 | 58 | changeState(data.STATES.MENU); 59 | }); 60 | }); 61 | }; 62 | -------------------------------------------------------------------------------- /proxy/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('dotenv').load({ path: '../.env' }); 4 | 5 | const http = require('http'); 6 | const url = require('url'); 7 | const Buffer = require('buffer').Buffer; 8 | const decrypt = require('./decryptor'); 9 | 10 | // create a new http proxy server 11 | // req - http.IncomingMessage 12 | // res - http.ServerResponse 13 | const server = http.createServer(function (req, res) { 14 | // setup the request options 15 | const reqOptions = url.parse(req.url); 16 | const host = reqOptions.hostname || reqOptions.host || req.headers.host; 17 | 18 | reqOptions.hostname = host; 19 | reqOptions.method = req.method; 20 | reqOptions.path = reqOptions.path || reqOptions.pathname; 21 | reqOptions.headers = req.headers; 22 | 23 | // console.log(reqOptions.protocol, reqOptions.protocol === 'https:' ? 443 : 80); 24 | 25 | let requestChunks = []; 26 | let responseChunks = []; 27 | let trackRequest = host.startsWith('summonerswar') && host.endsWith('com2us.net') && reqOptions.path.startsWith('/api/'); 28 | 29 | // proxyRes - http.IncomingMessage 30 | const proxyReq = http.request(reqOptions, (proxyRes) => { 31 | // pipe response back to requester 32 | proxyRes.pipe(res); 33 | 34 | // parse body of response for our own tracking if it matches a summoner request 35 | if (trackRequest) { 36 | proxyRes 37 | .on('data', (chunk) => { 38 | responseChunks.push(chunk); 39 | }) 40 | .on('end', () => { 41 | requestComplete(req, Buffer.concat(requestChunks), proxyRes, Buffer.concat(responseChunks)); 42 | }); 43 | } 44 | }); 45 | 46 | // pipe request to the target 47 | req.pipe(proxyReq); 48 | 49 | if (trackRequest) { 50 | // also buffer request for our own purposes 51 | req.on('data', (chunk) => { 52 | requestChunks.push(chunk); 53 | }); 54 | } 55 | }); 56 | 57 | server.listen(process.env.PORT || 8080, '0.0.0.0'); 58 | 59 | console.log('Server listening...'); 60 | 61 | function requestComplete(req, reqBody, res, resBody) { 62 | console.log('REQUEST:\n', req.method, req.url, '\n', req.headers, '\n'); 63 | const reqBodyStr = decrypt.decryptRequest(reqBody.toString()).toString(); 64 | console.log(reqBodyStr ? JSON.parse(reqBodyStr) : ''); 65 | 66 | console.log('\nRESPONSE:\n', res.headers, '\n'); 67 | const resBodyStr = decrypt.decryptResponse(resBody.toString()).toString(); 68 | console.log(resBodyStr ? JSON.parse(resBodyStr) : ''); 69 | console.log('-----------------------------------'); 70 | } 71 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es6": true 5 | }, 6 | "globals": { 7 | }, 8 | "rules": { 9 | // Possible Errors 10 | "comma-dangle": [2, "never"], 11 | "no-cond-assign": [2, "except-parens"], 12 | "no-console": 0, 13 | "no-constant-condition": 2, 14 | "no-control-regex": 2, 15 | "no-debugger": 2, 16 | "no-dupe-args": 2, 17 | "no-dupe-keys": 2, 18 | "no-duplicate-case": 2, 19 | "no-empty-character-class": 2, 20 | "no-empty": 2, 21 | "no-ex-assign": 2, 22 | "no-extra-boolean-cast": 2, 23 | "no-extra-parens": 0, 24 | "no-extra-semi": 2, 25 | "no-func-assign": 2, 26 | "no-inner-declarations": 2, 27 | "no-invalid-regexp": 2, 28 | "no-irregular-whitespace": 2, 29 | "no-negated-in-lhs": 2, 30 | "no-obj-calls": 2, 31 | "no-regex-spaces": 2, 32 | "no-sparse-arrays": 2, 33 | "no-unexpected-multiline": 2, 34 | "no-unreachable": 2, 35 | "use-isnan": 2, 36 | "valid-jsdoc": 0, // jscs does this already. 37 | "valid-typeof": 2, 38 | 39 | // Best Practices 40 | "accessor-pairs": 2, 41 | "block-scoped-var": 2, 42 | "complexity": 0, 43 | "consistent-return": 0, 44 | "curly": 0, // jscs does this already. 45 | "default-case": 0, 46 | "dot-location": 0, // jscs does this already. 47 | "dot-notation": 2, 48 | "eqeqeq": 2, 49 | "guard-for-in": 0, 50 | "no-alert": 2, 51 | "no-caller": 2, 52 | "no-case-declarations": 2, 53 | "no-div-regex": 2, 54 | "no-else-return": 2, 55 | "no-empty-pattern": 2, 56 | "no-eq-null": 2, 57 | "no-eval": 2, 58 | "no-extend-native": 2, 59 | "no-extra-bind": 2, 60 | "no-fallthrough": 2, 61 | "no-floating-decimal": 2, 62 | "no-implicit-coercion": 0, // jscs does this already. 63 | "no-implied-eval": 2, 64 | "no-invalid-this": 0, 65 | "no-iterator": 2, 66 | "no-labels": 2, 67 | "no-lone-blocks": 2, 68 | "no-loop-func": 0, 69 | "no-magic-numbers": 0, 70 | "no-multi-spaces": 0, // jscs does this already. 71 | "no-multi-str": 2, 72 | "no-native-reassign": 2, 73 | "no-new-func": 2, 74 | "no-new-wrappers": 2, 75 | "no-new": 2, 76 | "no-octal-escape": 2, 77 | "no-octal": 2, 78 | "no-param-reassign": 0, 79 | "no-process-env": 0, 80 | "no-proto": 2, 81 | "no-redeclare": 2, 82 | "no-return-assign": 2, 83 | "no-script-url": 2, 84 | "no-self-compare": 2, 85 | "no-sequences": 2, 86 | "no-throw-literal": 2, 87 | "no-unused-expressions": 2, 88 | "no-useless-call": 2, 89 | "no-useless-concat": 2, 90 | "no-void": 2, 91 | "no-warning-comments": 0, 92 | "no-with": 0, // jscs does this already. 93 | "radix": 2, 94 | "vars-on-top": 0, 95 | "wrap-iife": 0, // jscs does this already. 96 | "yoda": 0, // jscs does this already. 97 | 98 | // Strict Mode 99 | "strict": [2, "global"], 100 | 101 | // Variables 102 | "init-declarations": 0, 103 | "no-catch-shadow": 2, 104 | "no-delete-var": 2, 105 | "no-label-var": 2, 106 | "no-shadow-restricted-names": 2, 107 | "no-shadow": 0, 108 | "no-undef-init": 2, 109 | "no-undef": 2, 110 | "no-undefined": 2, 111 | "no-unused-vars": 2, 112 | "no-use-before-define": [2, "nofunc"], 113 | 114 | // Node.js and CommonJS 115 | "callback-return": 0, 116 | "global-require": 0, 117 | "handle-callback-err": 2, 118 | "no-mixed-requires": 2, 119 | "no-new-require": 2, 120 | "no-path-concat": 2, 121 | "no-process-exit": 0, 122 | "no-restricted-modules": 0, 123 | "no-sync": 0 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /db/lib/load.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const request = require('request'); 4 | const cheerio = require('cheerio'); 5 | 6 | module.exports = function (url, silent, cb) { 7 | if (typeof silent === 'function') { 8 | cb = silent; 9 | silent = false; 10 | } 11 | 12 | let loadStartTime = Date.now(); 13 | 14 | if (!silent) 15 | console.log('Loading monster data from %s', url); 16 | 17 | request.get(url, function (err, res, body) { 18 | if (err) return cb(err); 19 | 20 | const $ = cheerio.load(body); 21 | const title = $('.main-title').text().trim(); 22 | const titleParts = title.match(/([^ ]+) ([^\(]+) \(([^\)]+)\)/); 23 | 24 | if (!titleParts) { 25 | return cb(new Error('Failed to load monster title.')); 26 | } 27 | 28 | let monsterData = { 29 | postId: $('#page-content').data('postid'), 30 | type: titleParts[1].toLowerCase(), 31 | name: titleParts[2], 32 | awakenedName: titleParts[3], 33 | url: url, 34 | details: {}, 35 | ratings: {}, 36 | reactions: {}, 37 | runes: [] 38 | }; 39 | 40 | parseDetails($('.details-wrapper'), monsterData.details); 41 | parseRatings($('.total-info'), monsterData.ratings); 42 | parseReactions($('.reactions-wrapper'), monsterData.reactions); 43 | parseRunes($('.the-content'), monsterData.runes); 44 | 45 | if (!silent) 46 | console.log('Load completed in %dms.', (Date.now() - loadStartTime)); 47 | 48 | cb(null, monsterData); 49 | }); 50 | } 51 | 52 | function parseDetails($details, out) { 53 | $details.find('.detail-item').each(function (i, element) { 54 | const $elm = cheerio(element); 55 | const $value = $elm.find('.detail-content'); 56 | const title = $elm.find('.detail-label').text().trim().toLowerCase(); 57 | 58 | let value = $value.children().first().text().trim().toLowerCase(); 59 | 60 | switch (title) { 61 | case 'grade': 62 | out.grade = value.length; 63 | break; 64 | 65 | case 'type': 66 | out.type = value; 67 | break; 68 | 69 | case 'get from': 70 | out.get = value.split(', '); 71 | break; 72 | 73 | case 'stats': 74 | value = value.split('\n'); 75 | value.forEach(function (v) { 76 | let vs = v.split(':'); 77 | if (vs.length !== 2) return; 78 | 79 | out[vs[0].trim()] = parseInt(vs[1].trim(), 10); 80 | }); 81 | break; 82 | 83 | case 'good for': 84 | out.goodFor = value; 85 | break; 86 | 87 | case 'badges': 88 | out.tags = $value.children().map((i, v) => cheerio(v).attr('title')).get(); 89 | break; 90 | } 91 | }); 92 | } 93 | 94 | function parseRatings($info, out) { 95 | $info.find('.total-rating-wrapper').each(function (i, element) { 96 | const $elm = cheerio(element); 97 | const type = $elm.find('.section-subtitle').text().trim().toLowerCase(); 98 | const value = parseFloat($elm.find('.number').text().trim(), 10); 99 | 100 | out[type === 'total score' ? 'review' : 'users'] = value; 101 | }); 102 | } 103 | 104 | function parseReactions($reactions, out) { 105 | $reactions.find('.reaction').each(function (i, element) { 106 | const $elm = cheerio(element); 107 | const type = $elm.data('reaction'); 108 | const value = parseInt($elm.find('.reaction-percentage').text().trim(), 10); 109 | 110 | out[type] = value; 111 | }); 112 | } 113 | 114 | function parseRunes($content, out) { 115 | const runeFilter = function (i, e) { return cheerio(e).text().trim().toLowerCase() === 'rune recommendations' }; 116 | const $rune = $content.find('h2 strong').filter(runeFilter).parent(); 117 | const $runes = $rune.nextUntil('h1, h2, h3, h4, h5, h6', 'p'); 118 | 119 | $runes.each(function (i, element) { 120 | const $elm = cheerio(element); 121 | const text = $elm.text().trim().toLowerCase(); 122 | 123 | if (!text) return; 124 | 125 | const values = text.split('–'); 126 | const details = values[(values.length === 1 ? 0 : 1)].trim().split('('); 127 | 128 | let build = { 129 | name: values.length === 1 ? '' : values[0].trim(), 130 | runes: details[0].split('/').map(cleanupRuneNames), 131 | stats: details[1].replace(')', '').split('/').map(cleanupRuneStats) 132 | }; 133 | 134 | out.push(build); 135 | }); 136 | } 137 | 138 | function cleanupRuneNames(val) { 139 | return val 140 | .replace(/x\d/g, '') 141 | .trim(); 142 | } 143 | 144 | function cleanupRuneStats(val) { 145 | return val 146 | .replace('crit r', 'crit rate') 147 | .replace('crit d', 'crit damage') 148 | .trim(); 149 | } 150 | --------------------------------------------------------------------------------