diff --git a/.codeclimate.yml b/.codeclimate.yml index 018bf5b8..ef9019f4 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -11,7 +11,4 @@ engines: enabled: true ratings: paths: - - "lib/**.js" -exclude_paths: -- data/* -- assets/* \ No newline at end of file + - "lib/**.js" \ No newline at end of file diff --git a/index.js b/index.js index 640978c2..1616d77c 100644 --- a/index.js +++ b/index.js @@ -1 +1,13 @@ -module.exports = require('./lib/Sagiri'); \ No newline at end of file +const Handler = require('./lib/Handler'); +const Constants = require('./lib/Constants'); +const Ratelimiter = require('./lib/Ratelimiter'); + +function Sagiri(key, options) { + return new Handler(key, options); +} + +Sagiri.Handler = Handler; +Sagiri.Constants = Constants; +Sagiri.Ratelimiter = Ratelimiter; + +module.exports = Sagiri; \ No newline at end of file diff --git a/lib/Constants.js b/lib/Constants.js index 29cdc5cd..d6109637 100644 --- a/lib/Constants.js +++ b/lib/Constants.js @@ -18,11 +18,7 @@ const SITE_LIST = { name: 'Danbooru', backupURL: data => `https://danbooru.donmai.us/posts/${data.data.danbooru_id}`, URLRegex: /(?:https?:\/\/)?danbooru\.donmai\.us\/(?:posts|post\/show)\/\d{7}/i, - getRating(body) { - let rating = body.match(/
  • Rating: (.*?)<\/li>/i)[1]; - - return [null, 'Safe', 'Questionable', 'Explicit'].indexOf(rating); - } + getRating: body => [null, 'Safe', 'Questionable', 'Explicit'].indexOf(body.match(/
  • Rating: (.*?)<\/li>/i)[1]) }, 10: { name: 'drawr', @@ -38,21 +34,19 @@ const SITE_LIST = { name: 'Yande.re', backupURL: data => `https://yande.re/post/show/${data.data.yandere_id}`, URLRegex: /(?:https?:\/\/)?yande\.re\/post\/show\/\d{6}/i, - getRating(body) { - let rating = body.match(/
  • Rating: (.*?)<\/li>/i)[1].split(' ')[0]; - - return [null, 'Safe', 'Questionable', 'Explicit'].indexOf(rating); - } + getRating: body => [null, 'Safe', 'Questionable', 'Explicit'].indexOf(body.match(/
  • Rating: (.*?)<\/li>/i)[1]) }, 16: { name: 'FAKKU', backupURL: data => `https://www.fakku.net/hentai/${data.data.source.toLowerCase().replace(' ', '-')}`, - URLRegex: /(?:https?:\/\/)?(www\.)?fakku\.net\/hentai\/[a-z-]+\d{10}/i + URLRegex: /(?:https?:\/\/)?(www\.)?fakku\.net\/hentai\/[a-z-]+\d{10}/i, + getRating: () => RATINGS.NSFW }, 18: { name: 'nHentai', backupURL: data => `https://nhentai.net/g/${data.header.thumbnail.match(/nhentai\/(\d+)/)[1]}`, - URLRegex: /(?:https?:\/\/)nhentai.net\/g\/\d+/i + URLRegex: /(?:https?:\/\/)nhentai.net\/g\/\d+/i, + getRating: () => RATINGS.NSFW }, 19: { name: '2D-Market', @@ -78,11 +72,7 @@ const SITE_LIST = { name: 'Gelbooru', backupURL: data => `https://gelbooru.com/index.php?page=post&s=view&id=${data.data.gelbooru_id}`, URLRegex: /(?:https?:\/\/)gelbooru\.com\/index\.php\?page=post&s=view&id=\d{7}/i, - getRating(body) { - let rating = body.match(/
  • Rating: (.*?)<\/li>/i)[1]; - - return [null, 'Safe', 'Questionable', 'Explicit'].indexOf(rating); - } + getRating: body => [null, 'Safe', 'Questionable', 'Explicit'].indexOf(body.match(/
  • Rating: (.*?)<\/li>/i)[1]) }, 26: { name: 'Konachan', @@ -151,4 +141,9 @@ const RATINGS = Object.freeze({ NSFW: 3 }); -module.exports = {SITE_LIST, RATINGS}; \ No newline at end of file +const PERIODS = Object.freeze({ + SHORT: 1000 * 30, // 30 seconds + LONG: 1000 * 60 * 60 * 24 // 24 hours +}); + +module.exports = {SITE_LIST, RATINGS, PERIODS}; \ No newline at end of file diff --git a/lib/Sagiri.js b/lib/Handler.js similarity index 58% rename from lib/Sagiri.js rename to lib/Handler.js index 65930436..dd561a46 100644 --- a/lib/Sagiri.js +++ b/lib/Handler.js @@ -8,19 +8,18 @@ const FormData = require('form-data'); const fs = require('fs'); const http = require('http'); const https = require('https'); -const {SITE_LIST, RATINGS} = require('./Constants'); +const {SITE_LIST, RATINGS, PERIODS} = require('./Constants'); +const Ratelimiter = require('./Ratelimiter'); /** * Query handler for SauceNAO. * * @prop {String} key API key * @prop {Number} numRes Amount of responses returned from the API. - * @prop {?Number} shortLimit Ratelimit for the "short" period, currently the last 30 seconds. Will be null before the first request. - * @prop {?Number} longLimit Ratelimit for the "long" period, currently 24 hours. Will be null before the first request. * @prop {?Number} shortRemaining Amount of requests left during the "short" period before you get ratelimited. Will be null before the first request. * @prop {?Number} longRemaining Amount of requests left during the "long" period before you get ratelimited. Will be null before the first request. */ -class Sagiri { +class Handler { /** * @param {String} key API Key for SauceNAO * @param {Object} [options] Optional options @@ -34,10 +33,8 @@ class Sagiri { this.key = key, this.numRes = options.numRes != null ? options.numRes : 5; this.getRating = options.getRating || false; - this.shortLimit = null; - this.longLimit = null; - this.shortRemaining = null; - this.longRemaining = null; + this.shortLimiter = new Ratelimiter(20, PERIODS.SHORT); // 20 uses every 30 seconds + this.longLimiter = new Ratelimiter(300, PERIODS.SHORT); // 300 uses every 24 hours } /** @@ -49,61 +46,59 @@ class Sagiri { */ getSauce(file) { return new Promise((resolve, reject) => { - if (typeof file !== 'string') { - reject(new Error('file is not a string.')); - } else { - let form = new FormData(); - - form.append('api_key', this.key); - form.append('output_type', 2); - form.append('numres', this.numRes); - - if (fs.existsSync(file)) form.append('file', fs.createReadStream(file)); - else form.append('url', file); - - sendForm(form).then(res => { - this.shortLimit = res.header.short_limit; - this.longLimit = res.header.long_limit; - this.shortRemaining = res.header.short_remaining; - this.longRemaining = res.header.long_remaining; - - if (this.shortLimit === 0) throw new Error('Short duration ratelimit exceeded.'); - if (this.longLimit === 0) throw new Error('Long duration ratelimit exceeded.'); - - if (res.header.status > 0) throw new Error(`Server-side error occurred. Error Code: ${res.header.status}`); - if (res.header.status < 0) throw new Error(`Client-side error occurred. Error code: ${res.header.status}`); - - if (res.results.length === 0) throw new Error('No results.'); - - let results = res.results.sort((a, b) => Number(b.header.similarity) - Number(a.header.similarity)); - let returnData = []; - - for (let result of results) { - let data = resolveSauceData(result); - returnData.push({ - url: data.url, - site: data.name, - index: data.id, - similarity: Number(result.header.similarity), - thumbnail: result.header.thumbnail, - rating: RATINGS.UNKNOWN, - original: result - }); - } - - return returnData; - }).then(res => { - if (!this.getRating) return res; - return Promise.all([Promise.all(res.map(v => getRating(v.url))), res]); - }).then(res => { - if (!this.getRating) return res; - - let [ratings, original] = res; - - ratings.forEach((v, i) => original[i].rating = v); - return original; - }).then(resolve).catch(reject); - } + if (typeof file !== 'string') return reject(new Error('file is not a string.')); + if (this.shortLimiter.ratelimited) return reject(new Error('Short duration ratelimit exceeded.')); + if (this.longLimiter.ratelimited) return reject(new Error('Long duration ratelimit exceeded.')); + + let form = new FormData(); + + form.append('api_key', this.key); + form.append('output_type', 2); + form.append('numres', this.numRes); + + if (fs.existsSync(file)) form.append('file', fs.createReadStream(file)); + else form.append('url', file); + + sendForm(form).then(res => { + if (Number(res.header.short_limit) !== this.shortLimiter.totalUses) this.shortLimiter.totalUses = Number(res.header.short_limit); + if (Number(res.header.long_limit) !== this.longLimiter.totalUses) this.longLimiter.totalUses = Number(res.header.long_limit); + + this.shortLimiter.use(); + this.longLimiter.use(); + + if (res.header.status > 0) throw new Error(`Server-side error occurred. Error Code: ${res.header.status}`); + if (res.header.status < 0) throw new Error(`Client-side error occurred. Error code: ${res.header.status}`); + + if (res.results.length === 0) throw new Error('No results.'); + + let results = res.results.sort((a, b) => Number(b.header.similarity) - Number(a.header.similarity)); + let returnData = []; + + for (let result of results) { + let data = resolveSauceData(result); + returnData.push({ + url: data.url, + site: data.name, + index: data.id, + similarity: Number(result.header.similarity), + thumbnail: result.header.thumbnail, + rating: RATINGS.UNKNOWN, + original: result + }); + } + + return returnData; + }).then(res => { + if (!this.getRating) return res; + return Promise.all([Promise.all(res.map(v => getRating(v.url))), res]); + }).then(res => { + if (!this.getRating) return res; + + let [ratings, original] = res; + + ratings.forEach((v, i) => original[i].rating = v); + return original; + }).then(resolve).catch(reject); }); } @@ -185,4 +180,4 @@ function getRating(url) { }); } -module.exports = Sagiri; \ No newline at end of file +module.exports = Handler; \ No newline at end of file diff --git a/lib/Ratelimiter.js b/lib/Ratelimiter.js new file mode 100644 index 00000000..81e98812 --- /dev/null +++ b/lib/Ratelimiter.js @@ -0,0 +1,37 @@ +/** + * Ratelimiter class. + * + * @prop {Number} totalUses Amount of times a entity can be used before being ratelimited. + * @prop {Number} interval Interval between resetting amount of uses. + * @prop {Number} uses Number of current uses this interval has. + */ +class Ratelimiter { + /** + * Constructs a new ratelimiter. + * + * @param {Number} totalUses The total amount of uses the ratelimiter can be used before + * @param {Number} interval Time in milliseoncds between resettings uses. + */ + constructor(totalUses, interval) { + if (typeof totalUses !== 'number') throw new TypeError('totalUses is not not a number.'); + if (typeof interval !== 'number') throw new TypeError('interval is not not a number.'); + + this.totalUses = totalUses; + this.interval = interval; + this.uses = 0; + this._timer = setInterval(() => this.uses = 0, interval); + } + + /** + * Add a use to the ratelimiter. + */ + use() { + if (this.uses < this.totalUses) this.uses++; + } + + get ratelimited() { + return this.uses === this.totalUses; + } +} + +module.exports = Ratelimiter; \ No newline at end of file diff --git a/package.json b/package.json index 9eb62902..841e44d0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "sagiri", - "version": "1.2.5", - "description": "A wrapper for the SauceNAO API.", + "version": "1.3.0", + "description": "A simple, lightweight and actually good JS wrapper for the SauceNAO API.", "main": "index.js", "repository": { "type": "git", @@ -10,7 +10,10 @@ "keywords": [ "hitorigoto", "sagiri", - "saucenao" + "saucenao", + "simple", + "easy", + "api" ], "author": "sr229", "license": "MIT", @@ -27,10 +30,6 @@ "devDependencies": { "eslint": "^4.7.2" }, - "bin": { - "Sagiri.js": "lib/Sagiri.js", - "Constants.js": "lib/Constants.js" - }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }