UNPKG

es6-booru

Version:

Search a bunch of different boorus using package magic!

296 lines (253 loc) 12.6 kB
import fetch from 'node-fetch' import { Parser } from 'xml2js' import BooruError from './error/BooruError' import ArrayUtil from './util/ArrayUtil' import sites from '../sites.json' /** * 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' * } */ export default class Booru { /** * 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 */ static jsonfy(images) { return new Promise((resolve, reject) => { // If it's an object, assume it's already jsonfied if (typeof images !== 'object') { this.parser.parseString(images, (err, res) => { if (err) { return reject(err) } if (res.posts.post !== undefined) { resolve(res.posts.post.map(val => 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 */ static commonfy(images) { return new Promise((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(e => 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 */ static createCommon(images) { return new Promise((resolve, reject) => { const finalImages = [] for (let 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(v => v.replace(/,/g, '').replace(/ /g, '_')) images[i].common.tags = images[i].common.tags.filter(v => 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 */ static resolveSite(siteToResolve) { if (typeof siteToResolve !== 'string') { return false } siteToResolve = siteToResolve.toLowerCase() for (let site in sites) { if (site === siteToResolve || sites[site].aliases.includes(siteToResolve)) { return site } } return false } parser = new Parser() /** * 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 */ search(site, tags = [], {limit = 1, random = false} = {}) { return new Promise((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, 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 */ searchPosts(site, tags, {limit = 1, random = false} = {}) { return new Promise((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(v => v.replace(/_/g, '%20')) } let uri = `http://${site}${sites[site].api}${(sites[site].tagQuery) ? sites[site].tagQuery : 'tags'}=${tags.join('+')}&limit=${limit}` let options = { headers: {'User-Agent': 'Booru, a node package for booru searching (by AtlasTheBot)'}, gzip: true, json: true } if (!random) { resolve( fetch(uri, options) .then(result => result.json()) .catch(err => 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(v => 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(result => resolve(((result.body.search) ? result.body.search : result.body).slice(0, limit))) .catch(err => 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(result => Booru.jsonfy(result.text)) .then(images => resolve(ArrayUtil.shuffle(images).slice(0, limit))) .catch(err => resolve(new BooruError(err.message || err.error))) } }) } /** * For some reason, this won't return anything but `null` * @param {String} site * @param {String} md5 */ show(site, md5) { return new Promise((resolve, reject) => { site = Booru.resolveSite(site) let uri = `https://${site}${sites[site].api.replace('index', 'show')}md5=${md5}` let options = { headers: { 'User-Agent': 'Booru, a node package for booru searching (by AtlasTheBot)' } } fetch(uri, options) .then(result => result.json()) .then(resolve) .catch(err => reject(new BooruError((err.error && err.error.message) || err.error || err))) }) } }