UNPKG

booru

Version:

Search (and do other things) on a bunch of different boorus!

403 lines 14.9 kB
"use strict"; /** * @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 undici_1 = require("undici"); const Constants_1 = require("../Constants"); const Post_1 = __importDefault(require("../structures/Post")); const SearchResults_1 = __importDefault(require("../structures/SearchResults")); const Tag_1 = __importDefault(require("../structures/Tag")); const TagListResults_1 = __importDefault(require("../structures/TagListResults")); const Utils_1 = require("../Utils"); 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 */ 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 total number of posts for a specific tag or tag combination * @param {String|String[]} tags The tag(s) to search for * @return {Promise<number>} The total number of posts */ async getPostCount(tags) { const tagArray = this.normalizeTags(tags); try { const postCountResult = await this.doPostCountRequest(tagArray, { limit: 1, }); return postCountResult; } 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}`; } /** * Gets a list of tags from the booru * @param {Partial<TagsURLParams>} [params] The parameters for the tags list * @param {number} [params.limit=100] The limit of tags to return * @param {number} [params.page=1] The page of tags to return * @return {Promise<any[]>} A promise with the tags as an array */ async tagList({ limit = 100, page = 1, } = {}) { const url = this.getTagListUrl({ limit, page }); const options = Constants_1.defaultOptions; try { const response = await resolvedFetch(url, 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."); } } if (!response.ok) { throw new Constants_1.BooruError(`Received HTTP ${response.status} from booru: '${await response.text()}'`); } const data = await response.text(); /** * Many boorus don't support JSON parameter for tag listing * So attempt JSON parsing, but if it fails default to XML parsing **/ let tags = []; try { tags = (0, Utils_1.tryParseJSON)(data); } catch (_e) { tags = (0, Utils_1.jsonifyTags)(data); } return this.parseTagListResult(tags, { limit, page }); } catch (err) { if (err.type === 'invalid-json') if (err instanceof Error) { throw new Constants_1.BooruError(err); } throw err; } } /** * 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.jsonifyPosts)(data).posts : (0, Utils_1.tryParseJSON)(data); if (!response.ok) { throw new Constants_1.BooruError(`Received HTTP ${response.status} ` + `from booru ${this.site.domain}: '${posts.message ?? posts.error ?? JSON.stringify(posts)}'`); } return posts; } catch (err) { if (err.type === 'invalid-json') return ''; throw err; } } /** * The internal & common postCount logic, pls dont use this use .postCount instead * * @protected * @param {String[]|String} tags The tags to search with * @param {InternalSearchParameters} searchArgs The arguments for the search * @return {Promise<Object>} */ async doPostCountRequest(tags, { uri = null, limit = 1 } = {}) { let searchTags = tags.slice(); if (this.site.defaultTags) { searchTags = searchTags.concat(this.site.defaultTags.filter((v) => !searchTags.includes(v))); } const fetchuri = uri ?? this.getPostCountUrl({ tags: searchTags, limit }); try { const response = await resolvedFetch(fetchuri, Constants_1.defaultOptions); // 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(); if (!response.ok) { throw new Constants_1.BooruError(`Received HTTP ${response.status} ` + `from booru: '${data.error ?? data.message ?? JSON.stringify(data)}'`); } const postCountType = this.site.postCountType; return this.getPostCountNumber(postCountType, data); } catch (err) { if (err.type === 'invalid-json') return 0; throw err; } } getPostCountNumber(postCountType, data) { if (postCountType === 'json') { return (0, Utils_1.tryParseJSON)(data).counts.posts; } if (postCountType === 'derpi') { return (0, Utils_1.tryParseJSON)(data).total; } if (postCountType === 'xml') { const jsonData = (0, Utils_1.jsonifyPosts)(data); if (jsonData.count !== undefined) { return jsonData.count; } } return -1; } /** * 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); } /** * 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 */ getPostCountUrl({ tags = [], limit = 1, } = {}) { return (0, Constants_1.postCountURI)(this.site, tags, limit, this.credentials); } /** * Generates a URL to get a list of tags from the booru * @param opt * @param {number} [opt.limit] The limit of tags to return * @param {number} [opt.page] The page of tags to return * @returns {string} A URL to get the tags list */ getTagListUrl({ limit = 100, page = 1, } = {}) { if (!this.site.api.tagList) { throw new Constants_1.BooruError(`This booru does not support tag listing: ${this.site.domain}`); } return (0, Constants_1.tagListURI)(this.site, 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); } /** * Parse the response from the booru for a tag list * * @param result * @param param1 * @returns */ parseTagListResult(result, { limit = 100, page = 1 } = {}) { if (result.success === false) { throw new Constants_1.BooruError(result.message ?? result.reason); } let tags = []; if (result) { if (Array.isArray(result)) { tags = result.map((v) => new Tag_1.default(v, this)); } else { tags = [new Tag_1.default(result, this)]; } } return new TagListResults_1.default(tags, { limit, page }, this); } } exports.Booru = Booru; exports.default = Booru; //# sourceMappingURL=Booru.js.map