UNPKG

es6-booru

Version:

Search a bunch of different boorus using package magic!

602 lines (526 loc) 22.1 kB
'use strict'; function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var fetch = _interopDefault(require('node-fetch')); var xml2js = require('xml2js'); var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; var classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; var createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }; var possibleConstructorReturn = function (self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }; var BooruError = function (_Error) { inherits(BooruError, _Error); function BooruError(message) { classCallCheck(this, BooruError); var _this = possibleConstructorReturn(this, (BooruError.__proto__ || Object.getPrototypeOf(BooruError)).call(this, message || 'Error message unspecified.')); _this.name = 'BooruError'; return _this; } return BooruError; }(Error); var ArrayUtil = function () { function ArrayUtil() { classCallCheck(this, ArrayUtil); } createClass(ArrayUtil, null, [{ key: "randInt", // Thanks mdn and damnit derpibooru value: function randInt(min, max) { min = Math.ceil(min); max = Math.floor(max); return Math.floor(Math.random() * (max - min + 1)) + min; } /** * Yay fisher-bates * Taken from http://stackoverflow.com/a/2450976 * @private * @param {Array} array Array of something * @return {Array} Shuffled array of something */ }, { key: "shuffle", value: function shuffle(array) { var currentIndex = array.length; var temporaryValue = void 0; var randomIndex = void 0; // While there remain elements to shuffle... while (currentIndex !== 0) { // Pick a remaining element... randomIndex = Math.floor(Math.random() * currentIndex); currentIndex -= 1; // And swap it with the current element. temporaryValue = array[currentIndex]; array[currentIndex] = array[randomIndex]; array[randomIndex] = temporaryValue; } return array; } }]); return ArrayUtil; }(); var sites = { "e621.net": { aliases: ["e6", "e621"], nsfw: true, api: "/post/index.json?", postView: "/post/show/", random: true }, "e926.net": { aliases: ["e9", "e926"], nsfw: false, api: "/post/index.json?", postView: "/post/show/", random: true }, "hypnohub.net": { aliases: ["hh", "hypo", "hypohub"], nsfw: true, api: "/post/index.json?", postView: "/post/show/", random: true }, "danbooru.donmai.us": { aliases: ["db", "dan", "danbooru"], nsfw: true, api: "/posts.json?", postView: "/posts/", random: true }, "konachan.com": { aliases: ["kc", "konac", "kcom"], nsfw: true, api: "/post.json?", postView: "/post/show/", random: true }, "konachan.net": { aliases: ["kn", "konan", "knet"], nsfw: false, api: "/post.json?", postView: "/post/show/", random: true }, "yande.re": { aliases: ["yd", "yand", "yandere"], nsfw: true, api: "/post.json?", postView: "/post/show/", random: true }, "gelbooru.com": { aliases: ["gb", "gel", "gelbooru"], nsfw: true, api: "/index.php?page=dapi&s=post&q=index&", postView: "/index.php?page=post&s=view&id=", random: false }, "rule34.xxx": { aliases: ["r34", "rule34"], nsfw: true, api: "/index.php?page=dapi&s=post&q=index&", postView: "/index.php?page=post&s=view&id=", random: false }, "safebooru.org": { aliases: ["sb", "safe", "safebooru"], nsfw: false, api: "/index.php?page=dapi&s=post&q=index&", postView: "/index.php?page=post&s=view&id=", random: false }, "tbib.org": { aliases: ["tb", "tbib", "big"], nsfw: false, api: "/index.php?page=dapi&s=post&q=index&", postView: "/index.php?page=post&s=view&id=", random: false }, "xbooru.com": { aliases: ["xb", "xbooru"], nsfw: true, api: "/index.php?page=dapi&s=post&q=index&", postView: "/index.php?page=post&s=view&id=", random: false }, "youhate.us": { aliases: ["yh", "you", "youhate"], nsfw: true, api: "/index.php?page=dapi&s=post&q=index&", postView: "/index.php?page=post&s=view&id=", random: false }, "dollbooru.org": { aliases: ["do", "doll", "dollbooru"], nsfw: false, api: "/api/danbooru/find_posts/index.xml?", postView: "/post/view/", random: false }, "rule34.paheal.net": { aliases: ["pa", "paheal"], nsfw: true, api: "/api/danbooru/find_posts/index.xml?", postView: "/post/view/", random: false }, "lolibooru.moe": { aliases: ["lb", "lol", "loli", "lolibooru"], nsfw: true, api: "/post/index.json?", postView: "/post/show/", random: true }, "derpibooru.org": { aliases: ["dp", "derp", "derpi", "derpibooru"], nsfw: true, api: "/search.json?", tagQuery: "q", postView: "/images/", random: "sf=random%" } }; /** * Search options to use with booru.search() * @typedef {Object} SearchOptions * @property {Number} [limit=1] The number of images to return * @property {Boolean} [random=false] If it should randomly grab results */ /** * An image from a booru, has a few props and stuff * Properties vary per booru * @typedef {Object} Image */ /** * An image from a booru with a few common props * @typedef {Object} ImageCommon * @property {Object} common - Contains several useful and common props for each booru * @property {String} common.file_url - The direct link to the image * @property {String} common.id - The id of the post * @property {String[]} common.tags - The tags of the image in an array * @property {Number} common.score - The score of the image * @property {String} common.source - Source of the image, if supplied * @property {String} common.rating - Rating of the image * * @example * common: { * file_url: 'https://aaaa.com/image.jpg', * id: '124125', * tags: ['cat', 'cute'], * score: 5, * source: 'https://giraffeduck.com/aaaa.png', * rating: 's' * } */ var Booru = function () { function Booru() { classCallCheck(this, Booru); this.parser = new xml2js.Parser(); } createClass(Booru, [{ key: 'search', /** * Searches a site for images with tags and returns the results * @param {String} site The site to search * @param {String[]} [tags=[]] Tags to search with * @param {SearchOptions} * @return {Promise} A promise with the images as an array of objects * * @example * booru.search('e926', ['glaceon', 'cute']) * //returns a promise with the latest cute glace pic from e926 */ value: function search(site) { var _this = this; var tags = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; var _ref = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}, _ref$limit = _ref.limit, limit = _ref$limit === undefined ? 1 : _ref$limit, _ref$random = _ref.random, random = _ref$random === undefined ? false : _ref$random; return new Promise(function (resolve, reject) { site = Booru.resolveSite(site); limit = parseInt(limit); if (site === false) { return reject(new BooruError('Site not supported')); } if (!(tags instanceof Array)) { return reject(new BooruError('`tags` should be an array')); } if (typeof limit !== 'number' || Number.isNaN(limit)) { return reject(new BooruError('`limit` should be an int')); } resolve(_this.searchPosts(site, tags, { limit: limit, random: random })); }); } /** * Actual searching code * @private * @param {String} site The full site url, name + tld * @param {Array} tags The array of tags to search for * @param {Number} limit Number of posts to fetch * @param {searchOptions} * @return {Promise} Response with the site's api */ }, { key: 'searchPosts', value: function searchPosts(site, tags) { var _ref2 = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}, _ref2$limit = _ref2.limit, limit = _ref2$limit === undefined ? 1 : _ref2$limit, _ref2$random = _ref2.random, random = _ref2$random === undefined ? false : _ref2$random; return new Promise(function (resolve, reject) { // derpibooru requires '*' to show all images if (tags[0] === undefined && site === 'derpibooru.org') { tags[0] = '*'; } // derpibooru requires spaces instead of _ if (site === 'derpibooru.org') { tags = tags.map(function (v) { return v.replace(/_/g, '%20'); }); } var uri = 'http://' + site + sites[site].api + (sites[site].tagQuery ? sites[site].tagQuery : 'tags') + '=' + tags.join('+') + '&limit=' + limit; var options = { headers: { 'User-Agent': 'Booru, a node package for booru searching (by AtlasTheBot)' }, gzip: true, json: true }; if (!random) { resolve(fetch(uri, options).then(function (result) { return result.json(); }).catch(function (err) { return reject(new BooruError(err.message || err.error && err.error.message || err.error)); })); } // If we request random images... // First check if the site supports order:random (or some other way to randomize it) if (sites[site].random) { // If it's a string it's (likely) randomized using a user-provided random hex if (typeof sites[site].random === 'string') { uri = 'http://' + site + sites[site].api + (sites[site].tagQuery ? sites[site].tagQuery : 'tags') + '=' + tags.join('+') + '&limit=' + limit + ('&' + sites[site].random + (sites[site].random.endsWith('%') ? Array(7).fill(0).map(function (v) { return ArrayUtil.randInt(0, 16); }).join('') : '')); // http://example.com/posts/?tags=some_example&limit=100&sf=random%AB43FF // Sorry, but derpibooru has an odd and confusing api that's not similar to the others at all } else { // We can just add `order:random` and get random results! uri = 'http://' + site + sites[site].api + 'tags=order:random+' + tags.join('+') + '&limit=' + limit; } fetch(uri, options) // Once again, derpi is weird and has it's results in body.search and not just in body .then(function (result) { return resolve((result.body.search ? result.body.search : result.body).slice(0, limit)); }).catch(function (err) { return reject(new BooruError(err.message || err.error)); }); } else { // The site doesn't support random sorting in any way, so we need to do it ourselves // This is done by just getting the 100 latest and randomly sorting those // Which isn't really an amazing way, but works well enough and doesn't require keeping track // of how many pages or whatever uri = 'http://' + site + sites[site].api + 'tags=' + tags.join('+') + '&limit=100'; // This does automatically jsonfy results, but that's because I can't really sort them otherwise fetch(uri, options).then(function (result) { return Booru.jsonfy(result.text); }).then(function (images) { return resolve(ArrayUtil.shuffle(images).slice(0, limit)); }).catch(function (err) { return resolve(new BooruError(err.message || err.error)); }); } }); } /** * For some reason, this won't return anything but `null` * @param {String} site * @param {String} md5 */ }, { key: 'show', value: function show(site, md5) { return new Promise(function (resolve, reject) { site = Booru.resolveSite(site); var uri = 'https://' + site + sites[site].api.replace('index', 'show') + 'md5=' + md5; var options = { headers: { 'User-Agent': 'Booru, a node package for booru searching (by AtlasTheBot)' } }; fetch(uri, options).then(function (result) { return result.json(); }).then(resolve).catch(function (err) { return reject(new BooruError(err.error && err.error.message || err.error || err)); }); }); } }], [{ key: 'jsonfy', /** * Parse images xml to json, which can be used with js * @static * @param {Image[]} images The images to convert to jsonfy * @return {Image[]} The images in JSON format */ value: function jsonfy(images) { var _this2 = this; return new Promise(function (resolve, reject) { // If it's an object, assume it's already jsonfied if ((typeof images === 'undefined' ? 'undefined' : _typeof(images)) !== 'object') { _this2.parser.parseString(images, function (err, res) { if (err) { return reject(err); } if (res.posts.post !== undefined) { resolve(res.posts.post.map(function (val) { return val.$; })); } else { resolve([]); } }); } else resolve(images); }); } /** * Takes an array of images and converts to json is needed, and add an extra property called "common" with a few common properties * Allow you to simply use "images[2].common.tags" and get the tags instead of having to check if it uses .tags then realizing it doesn't * then having to use "tag_string" instead and aaaa i hate xml aaaa * @param {Image[]} images Array of {@link Image} objects * @return {ImageCommon[]} Array of {@link ImageCommon} objects */ }, { key: 'commonfy', value: function commonfy(images) { return new Promise(function (resolve, reject) { if (typeof images[0] === 'undefined') { return reject(new BooruError('You didn\'t give any images')); } Booru.jsonfy(images).then(Booru.createCommon).then(resolve).catch(function (e) { return reject(new BooruError('This function should only receive images: ' + e)); }); }); } /** * Create the .common property for each {@link Image} passed and removes images without a link to the image * @param {Image[]} images The images to add common props to * @return {ImageCommon[]} The images with common props added */ }, { key: 'createCommon', value: function createCommon(images) { return new Promise(function (resolve, reject) { var finalImages = []; for (var i = 0; i < images.length; i++) { images[i].common = {}; images[i].common.file_url = images[i].file_url || images[i].image; images[i].common.id = images[i].id.toString(); images[i].common.tags = (images[i].tags !== undefined ? images[i].tags.split(' ') : images[i].tag_string.split(' ')).map(function (v) { return v.replace(/,/g, '').replace(/ /g, '_'); }); images[i].common.tags = images[i].common.tags.filter(function (v) { return v !== ''; }); images[i].common.score = parseInt(images[i].score); images[i].common.source = images[i].source; images[i].common.rating = images[i].rating || /(safe|suggestive|questionable|explicit)/i.exec(images[i].tags)[0]; if (images[i].common.rating === 'suggestive') { images[i].common.rating = 'q'; // i just give up at this point } images[i].common.rating = images[i].common.rating.charAt(0); if (images[i].common.file_url === undefined) { images[i].common.file_url = images[i].source; } // if the image's file_url is *still* undefined or the source is empty or it's deleted: don't use // thanks danbooru *grumble grumble* if (images[i].common.file_url === undefined || images[i].common.file_url.trim() === '' || images[i].is_deleted) { continue; } if (images[i].common.file_url.startsWith('/data')) { images[i].common.file_url = 'https://danbooru.donmai.us' + images[i].file_url; } if (images[i].common.file_url.startsWith('/cached')) { images[i].common.file_url = 'https://danbooru.donmai.us' + images[i].file_url; } if (images[i].common.file_url.startsWith('/_images')) { images[i].common.file_url = 'https://dollbooru.org' + images[i].file_url; } if (images[i].common.file_url.startsWith('//derpicdn.net')) { images[i].common.file_url = 'https:' + images[i].image; } if (!images[i].common.file_url.startsWith('http')) { images[i].common.file_url = 'https:' + images[i].file_url; } // lolibooru likes to shove all the tags into its urls, despite the fact you don't need the tags if (images[i].common.file_url.match(/https?:\/\/lolibooru.moe/)) { images[i].common.file_url = images[i].sample_url.replace(/(.*booru \d+ ).*(\..*)/, '$1sample$2'); } finalImages.push(images[i]); } resolve(finalImages); }); } /** * Check if `site` is a supported site (and check if it's an alias and return the sites's true name) * @param {String} siteToResolve The site to resolveSite * @return {(String|Boolean)} False if site is not supported, the site otherwise */ }, { key: 'resolveSite', value: function resolveSite(siteToResolve) { if (typeof siteToResolve !== 'string') { return false; } siteToResolve = siteToResolve.toLowerCase(); for (var site in sites) { if (site === siteToResolve || sites[site].aliases.includes(siteToResolve)) { return site; } } return false; } }]); return Booru; }(); module.exports = Booru;