es6-booru
Version:
Search a bunch of different boorus using package magic!
602 lines (526 loc) • 22.1 kB
JavaScript
;
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;