booru
Version:
Search (and do other things) on a bunch of different boorus!
237 lines • 8.28 kB
JavaScript
;
/**
* @packageDocumentation
* @module Boorus
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Booru = void 0;
const Constants_1 = require("../Constants");
const Utils_1 = require("../Utils");
const undici_1 = require("undici");
const Post_1 = __importDefault(require("../structures/Post"));
const SearchResults_1 = __importDefault(require("../structures/SearchResults"));
const resolvedFetch = typeof window !== 'undefined' ? window.fetch.bind(window) : undici_1.fetch;
/*
- new Booru
=> Constructor, params {name, {nsfw, {search, postView, ...}, random}, {apiTokens...}}
=> .search([tags...], {limit, random})
=> .postView(id)
=> .site
*/
/**
* A basic, JSON booru
* @example
* ```
* const Booru = require('booru')
* // Aliases are supported
* const e9 = Booru('e9')
*
* // You can then search the site
* const imgs = await e9.search(['cat', 'cute'], {limit: 3})
*
* // And use the images
* imgs.forEach(i => console.log(i.fileUrl))
*
* // Or access other methods on the Booru
* e9.postView(imgs[0].id)
* ```
*/
class Booru {
/** The domain of the booru */
domain;
/** The site object representing this booru */
site;
/** The credentials to use for this booru */
credentials;
/**
* Create a new booru from a site
*
* @private
* @param site The site to use
* @param credentials Credentials for the API (Currently not used)
*/
constructor(site, credentials) {
const domain = (0, Utils_1.resolveSite)(site.domain);
if (domain === null) {
throw new Error(`Invalid site passed: ${site.domain}`);
}
this.domain = domain;
this.site = site;
this.credentials = credentials;
}
normalizeTags(tags) {
if (!Array.isArray(tags)) {
return [tags];
}
return tags.slice();
}
/**
* Search for images on this booru
* @param {String|String[]} tags The tag(s) to search for
* @param {SearchParameters} searchArgs The arguments for the search
* @return {Promise<SearchResults>} The results as an array of Posts
*/
async search(tags, { limit = 1, random = false, page = 0, showUnavailable = false, } = {}) {
const fakeLimit = random && !this.site.random ? 100 : 0;
const tagArray = this.normalizeTags(tags);
try {
const searchResult = await this.doSearchRequest(tagArray, {
limit,
random,
page,
showUnavailable,
});
return this.parseSearchResult(searchResult, {
fakeLimit,
tags: tagArray,
limit,
random,
page,
showUnavailable,
});
}
catch (err) {
if (err instanceof Error) {
throw new Constants_1.BooruError(err);
}
throw err;
}
}
/**
* Gets the url you'd see in your browser from a post id for this booru
*
* @param {String} id The id to get the postView for
* @return {String} The url to the post
*/
postView(id) {
if (typeof id === 'string' && Number.isNaN(Number.parseInt(id, 10))) {
throw new Constants_1.BooruError(`Not a valid id for postView: ${id}`);
}
return `http${this.site.insecure ? '' : 's'}://${this.domain}${this.site.api.postView}${id}`;
}
/**
* The internal & common searching logic, pls dont use this use .search instead
*
* @protected
* @param {String[]|String} tags The tags to search with
* @param {InternalSearchParameters} searchArgs The arguments for the search
* @return {Promise<Object>}
*/
async doSearchRequest(tags, { uri = null, limit = 1, random = false, page = 0, } = {}) {
// Used for random on sites without order:random
let fakeLimit;
let searchTags = tags.slice();
if (random) {
if (this.site.random) {
searchTags.push('order:random');
}
else {
fakeLimit = 100;
}
}
if (this.site.defaultTags) {
searchTags = searchTags.concat(this.site.defaultTags.filter((v) => !searchTags.includes(v)));
}
const fetchuri = uri ??
this.getSearchUrl({ tags: searchTags, limit: fakeLimit ?? limit, page });
const options = Constants_1.defaultOptions;
const xml = this.site.type === 'xml';
try {
const response = await resolvedFetch(fetchuri, options);
// Check for CloudFlare ratelimiting
if (response.status === 503) {
const body = await response.clone().text();
if (body.includes('cf-browser-verification')) {
throw new Constants_1.BooruError("Received a CloudFlare browser verification request. Can't proceed.");
}
}
const data = await response.text();
const posts = xml ? (0, Utils_1.jsonfy)(data) : (0, Utils_1.tryParseJSON)(data);
if (!response.ok) {
throw new Constants_1.BooruError(`Received HTTP ${response.status} ` +
`from booru: '${posts.error ??
posts.message ??
JSON.stringify(posts)}'`);
}
return posts;
}
catch (err) {
if (err.type === 'invalid-json')
return '';
throw err;
}
}
/**
* Generates a URL to search the booru with, mostly for debugging purposes
* @param opt
* @param {string[]} [opt.tags] The tags to search for
* @param {number} [opt.limit] The limit of results to return
* @param {number} [opt.page] The page of results to return
* @returns A URL to search the booru
*/
getSearchUrl({ tags = [], limit = 100, page = 1, } = {}) {
return (0, Constants_1.searchURI)(this.site, tags, limit, page, this.credentials);
}
/**
* Parse the response from the booru
*
* @protected
* @param {Object} result The response of the booru
* @param {InternalSearchParameters} searchArgs The arguments used for the search
* @return {SearchResults} The results of this search
*/
parseSearchResult(result, { fakeLimit, tags, limit, random, page, showUnavailable, }) {
let outResult = result;
if (outResult.success === false) {
throw new Constants_1.BooruError(outResult.message ?? outResult.reason);
}
// Gelbooru
if (outResult['@attributes']) {
const attributes = outResult['@attributes'];
if (attributes.count === '0' || !outResult.post) {
outResult = [];
}
else if (Array.isArray(outResult.post)) {
outResult = outResult.post;
}
else {
outResult = [outResult.post];
}
}
if (outResult.posts) {
outResult = outResult.posts;
}
if (outResult.images) {
outResult = outResult.images;
}
let r;
// If gelbooru/other booru decides to return *nothing* instead of an empty array
if (outResult === '') {
r = [];
}
else if (fakeLimit) {
r = (0, Utils_1.shuffle)(outResult);
}
else if (outResult.constructor === Object) {
// For XML based sites
r = [outResult];
}
let posts = (r ?? outResult)
.slice(0, limit)
.map((v) => new Post_1.default(v, this));
const options = { limit, random, page, showUnavailable };
if (tags === undefined) {
tags = [];
}
if (!showUnavailable) {
posts = posts.filter((p) => p.available);
}
return new SearchResults_1.default(posts, tags, options, this);
}
}
exports.Booru = Booru;
exports.default = Booru;
//# sourceMappingURL=Booru.js.map