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"
}