diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml deleted file mode 100644 index 0050e0db..00000000 --- a/.github/workflows/node.js.yml +++ /dev/null @@ -1,41 +0,0 @@ -# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions - -name: Node.js CI - -on: - push: - branches: ["**"] - pull_request: - branches: ["**"] - -jobs: - test: - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [18.x, 20.x] - - steps: - - uses: actions/checkout@v4.1.7 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - - - name: Get Dependencies - run: yarn - - name: Get CodeClimate Coverage - run: | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter - chmod +x ./cc-test-reporter - ./cc-test-reporter before-build - - - name: Run Coverage and Lint - run: | - yarn lint - yarn coverage - ./cc-test-reporter after-build --exit-code $(echo $?) - env: - CC_TEST_REPORTER_ID: bda3746e9ecd039cee2dcfb3c1e4efc59c712a40fa3b9316e281eb00d61c613e diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..8001e5c1 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,41 @@ +# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +name: BunJS CI + +on: + push: + branches: ["**"] + pull_request: + branches: ["**"] + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + matrix: + bun-version: [latest, 1.1] + + steps: + - uses: actions/checkout@v4.1.7 + - name: Use Bun.js ${{ matrix.bun-version }} + uses: oven-sh/setup-bun@v2 + with: + bun-version: ${{ matrix.bun-version }} + + - name: Get Dependencies + run: bun install +# - name: Get CodeClimate Coverage +# run: | +# curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter +# chmod +x ./cc-test-reporter +# ./cc-test-reporter before-build + + - name: Run Coverage and Lint + run: | + echo 'SAUCENAO_TOKEN=${{ secrets.SAUCENAO_TOKEN }}' > ./.env.test.local + bun run coverage +# ./cc-test-reporter after-build --exit-code $(echo $?) +# env: +# CC_TEST_REPORTER_ID: bda3746e9ecd039cee2dcfb3c1e4efc59c712a40fa3b9316e281eb00d61c613e diff --git a/.idx/dev.nix b/.idx/dev.nix index a2c57565..de3c5329 100644 --- a/.idx/dev.nix +++ b/.idx/dev.nix @@ -4,5 +4,6 @@ pkgs.vim pkgs.git pkgs.bun + pkgs.deno ]; } diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..5543bc0a --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,37 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "bun", + "internalConsoleOptions": "neverOpen", + "request": "launch", + "name": "Debug File", + "program": "${file}", + "cwd": "${workspaceFolder}", + "envFile": ".env.test.local", + "stopOnEntry": false, + "watchMode": false + }, + { + "type": "bun", + "internalConsoleOptions": "neverOpen", + "request": "launch", + "name": "Run File", + "program": "${file}", + "cwd": "${workspaceFolder}", + "noDebug": true, + "watchMode": false + }, + { + "type": "bun", + "internalConsoleOptions": "neverOpen", + "request": "attach", + "name": "Attach Bun", + "url": "ws://localhost:6499/", + "stopOnEntry": false + } + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fc8b715..b4cd138b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **BREAKING** - Interfaces have been renamed to use the hungarian notation style (e.g. `SagiriResult` is now `ISagiriResult`). Please adapt your code accordingly. - **BREAKING** - ESM support has been added to allow the module to be used on Node.js 16+ code that uses ESM code. -- **NEW** - Package is now published in the [JSR](https://jsr.io) registry. To install the package, run `npm install jsr:@clarity/sagiri`. +- **BREAKING** - Bun and Deno support has been added. Library contributors are encouraged to try the library on these platforms and avoid any Node.js specific code. +- **NEW** - Package is now published in the [JSR](https://jsr.io) registry. To install the package, run `deno install jsr:@clarity/sagiri`. +- **NEW** - Library now uses the native [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). A [node-fetch](https://npmjs.com/package/node-fetch) fallback is provided for LTS versions below 21.x - however this will be removed once 18 and 20.x versions become deprecated. ## [3.6.0] - 2024-04-10 diff --git a/bun.lockb b/bun.lockb index a67122a0..6f889cc0 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/eslint.config.mjs b/eslint.config.mjs index 6b169bba..99cb5916 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -10,7 +10,8 @@ export default [ { ignores: [ "*.config.*", - "tests/**" + "tests/**", + "dist/**" ] } ]; diff --git a/jest.config.cjs b/jest.config.cjs deleted file mode 100644 index a93551fc..00000000 --- a/jest.config.cjs +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - preset: "ts-jest", - testEnvironment: "node", -}; diff --git a/jsr.json b/jsr.json index 2e85211f..b6e60a6b 100644 --- a/jsr.json +++ b/jsr.json @@ -1,5 +1,5 @@ { "name": "@clarity/sagiri", - "version": "4.0.1", - "exports": "./lib/index.ts" + "version": "4.0.2", + "exports": "./lib/sagiri.ts" } diff --git a/lib/errors.ts b/lib/errors.ts index 7b4e3472..66b7014f 100644 --- a/lib/errors.ts +++ b/lib/errors.ts @@ -1,3 +1,6 @@ +/** + * Represents an error specific to the Sagiri library. + */ export class SagiriError extends Error { constructor(code: number, message: string) { super(`${message} (${code})`); @@ -5,6 +8,9 @@ export class SagiriError extends Error { } } +/** + * Represents an error specific to the Sagiri client. + */ export class SagiriClientError extends SagiriError { constructor(code: number, message: string) { super(code, message); @@ -12,6 +18,10 @@ export class SagiriClientError extends SagiriError { } } +/** + * Represents a error specific to the SauceNAO API. + * @extends SagiriError + */ export class SagiriServerError extends SagiriError { constructor(code: number, message: string) { super(code, message); diff --git a/lib/index.ts b/lib/index.ts deleted file mode 100644 index e2f82be1..00000000 --- a/lib/index.ts +++ /dev/null @@ -1,162 +0,0 @@ -import bent from "./thirdparty/bent.js"; -import debug from "debug"; -import FormData from "form-data"; - -import { createReadStream } from "node:fs"; -import { Readable } from "node:stream"; -import { env } from "node:process"; -import * as NodeFetch from "node-fetch"; - -import { SagiriClientError, SagiriServerError } from "./errors.js"; -import { IResponse, IResult } from "./response.js"; -import sites from "./sites.js"; -import { generateMask, resolveResult } from "./util.js"; - -const log = debug("sagiri"); -let fetch; - -type File = string | Buffer | Readable; - -// check if fetch exists, if it does, use it, otherwise use node-fetch -if (typeof fetch === "undefined") { - fetch = NodeFetch.default; -} - -/** - * Creates a function to be used for finding potential sources for a given image. - */ -const sagiri = (token: string, defaultOptions: IOptions = { results: 5 }): (file: File, optionOverrides?: IOptions) => Promise => { - log("Created Sagiri function with default options:", defaultOptions); - - // do some token validation, tokens must be 40 chars long and alphanumeric - // make sure we're lenient during testing though to allow jest to pass. - if (env.NODE_ENV !== "test") - if (token.length < 40 || !/^[a-zA-Z0-9]+$/.test(token)) - throw new Error("Malformed SauceNAO Token. Fetch your own at https://saucenao.com/user.php"); - - const request = bent("https://saucenao.com", "json", "POST", 200); - - return async (file: File, optionOverrides: IOptions = {}): Promise => { - if (!file) throw new Error("Missing file to find source for"); - - log(`Requesting possible sources for ${typeof file === "string" ? file : "a stream or buffer"}`); - - const { - results: numberResults, - testMode, - mask, - excludeMask, - } = { - ...defaultOptions, - ...optionOverrides, - }; - const form = new FormData(); - - log(`Requesting ${numberResults!} results from SauceNAO`); - - form.append("api_key", token); - form.append("output_type", 2); - form.append("numres", numberResults); - - if (testMode) { - log("Enabling test mode"); - form.append("testmode", 1); - } - - if (mask && excludeMask) - throw new Error("It's redundant to set both mask and excludeMask. Choose one or the other."); - else if (mask) { - log(`Adding inclusive db mask with a value of ${generateMask(mask)} (from [${mask.join(", ")}])`); - form.append("dbmask", generateMask(mask)); - } else if (excludeMask) { - log(`Adding exclusive db mask with value of ${generateMask(excludeMask)} (from [${excludeMask.join(", ")}])`); - form.append("dbmaski", generateMask(excludeMask)); - } - - if (typeof file === "string" && /^https?:/.test(file)) { - log("Adding given file as a URL"); - form.append("url", file); - } else if (typeof file === "string") { - log("Adding given file from an fs.createReadStream"); - form.append("file", createReadStream(file)); - } else { - log("Adding file as stream or buffer"); - form.append("file", file); - } - - log("Sending request to SauceNAO"); - - const response = (await request("/search.php", form, form.getHeaders())) as IResponse; - const { - header: { status, message, results_returned: resultsReturned }, - } = response; - - log(`Received response, status ${status}`); - - // Server side error - if (status > 0) throw new SagiriServerError(status, message!); - // Client side error - else if (status < 0) throw new SagiriClientError(status, message!); - - const unknownIds = new Set( - response.results.filter((result) => !sites[result.header.index_id]).map((result) => result.header.index_id), - ); - - if (unknownIds.size > 0) { - console.warn( - `Some results were not resolved, because they were not found in the list of supported sites. -Please report this IDs to the author ${[...unknownIds.values()].join(", ")}`, - ); - } - - const results = response.results - .filter((result) => !unknownIds.has(result.header.index_id)) - .sort((a, b) => b.header.similarity - a.header.similarity); - - log( - `Expected ${numberResults} results. ` + - `SauceNAO says it sent ${resultsReturned}, actually sent ${response.results.length}. ` + - `Found ${results.length} acceptable results.`, - ); - - return results.map((result) => { - const { url, name, id, authorName, authorUrl } = resolveResult(result); - const { - header: { similarity, thumbnail }, - } = result; - - return { - url, - site: name, - index: parseInt(id), // These are actually numbers but they're typed as strings so they can be used to select from the sites map - similarity: Number(similarity), - thumbnail, - authorName, - authorUrl, - raw: result, - }; - }); - }; -}; - -export default sagiri; - -export interface IOptions { - results?: number; - mask?: number[]; - excludeMask?: number[]; - getRatings?: boolean; - testMode?: boolean; - db?: number; -} - -export interface ISagiriResult { - url: string; - site: string; - index: number; - similarity: number; - thumbnail: string; - authorName: string | null; - authorUrl: string | null; - raw: IResult; -} diff --git a/lib/interfaces.ts b/lib/interfaces.ts new file mode 100644 index 00000000..262001db --- /dev/null +++ b/lib/interfaces.ts @@ -0,0 +1,27 @@ +import type { IResult } from "./response"; + +/** + * Represents the valid options to pass on to SauceNAO. + */ +export interface IOptions { + results?: number; + mask?: number[]; + excludeMask?: number[]; + getRatings?: boolean; + testMode?: boolean; + db?: number; +} + +/** + * Represents the result of a Sagiri search. + */ +export interface ISagiriResult { + url: string; + site: string; + index: number; + similarity: number; + thumbnail: string; + authorName: string | null; + authorUrl: string | null; + raw: IResult; +} diff --git a/lib/response.ts b/lib/response.ts index 54f73236..8f1592ac 100644 --- a/lib/response.ts +++ b/lib/response.ts @@ -119,6 +119,11 @@ export interface IResponse { results: IResult[]; } +export interface IHeaderStatus { + status: number; + message: string; +} + export interface IHeader { account_type: string; index: { [id: string]: IHeaderIndex | undefined }; // do i wanna generic this to be id value? diff --git a/lib/sagiri.ts b/lib/sagiri.ts new file mode 100644 index 00000000..e66344f1 --- /dev/null +++ b/lib/sagiri.ts @@ -0,0 +1,145 @@ +import * as nodeFetch from 'node-fetch'; +import type { IOptions } from './interfaces'; +import { env } from 'node:process'; +import { Readable } from 'node:stream'; +import { createReadStream } from 'node:fs'; +import FormData from 'form-data'; +import { generateMask, resolveResult } from './util'; +import { SagiriClientError, SagiriServerError } from './errors'; +import type { IResponse, IResult } from './response'; +import sites from './sites'; + +let fetchFn; +// compatibility with older versions of nodejs. This will be removed in the future once LTS versions of nodejs has moved above 21.x +if (globalThis.fetch === undefined) { + fetchFn = nodeFetch.default; +} else { + fetchFn = globalThis.fetch; +} + +type File = string | Buffer | Readable; + +/** + * Creates a function to be used for finding potential sources for a given image. + * By default has options set to give 5 results from SauceNAO. + * @param token your saucenao token, get one from https://saucenao.com/user.php + * @param defaultOpts the default options that the client will use for querying + * @returns an `async function (file: File, optionOverrides?: Options)` which is loaded with the given token and default options to use. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const sagiri = (token: string, defaultOpts: IOptions = { results: 5 }): (file: File, opts?: IOptions) => Promise<{ url: any; site: any; index: number; similarity: number; thumbnail: string; authorName: any; authorUrl: any; raw: IResult; }[]> => { + console.debug(`Created sagiri instance with opts: ${JSON.stringify(defaultOpts)}`); + + // token validation to ensure the token is 40 characters long and alphanumeric + if (env.NODE_ENV !== "test") + if (token.length < 40 || !/^[a-zA-Z0-9]+$/.test(token)) + throw new Error("Malformed token. Get a token from https://saucenao.com/user.php"); + + + return async (file: File, opts: IOptions = {}) => { + if (!file) throw new Error("No file provided"); + + console.debug(`Searching for possible sources of image: ${typeof file === 'string' ? file : 'Buffer'}`); + + const form = new FormData(); + const { results, mask, excludeMask, testMode } = { ...defaultOpts, ...opts }; + + form.append("api_key", token); + form.append("output_type", 2); + form.append("numres", results); + + if (testMode) { + console.debug("Test mode enabled"); + form.append("testmode", 1); + } + + if (mask && excludeMask) { + throw new Error("Cannot have both mask and excludeMask"); + } else if (mask) { + console.log(`Adding inclusive db mask ${generateMask(mask)} from ${mask.join(", ")}`); + form.append("dbmask", generateMask(mask)); + } else if (excludeMask) { + console.log(`Adding exclusive db mask ${generateMask(excludeMask)} from ${excludeMask.join(", ")}`); + form.append("dbmaski", generateMask(excludeMask)); + } + + switch (typeof file) { + case 'string': + if (/^https?:\/\//.test(file)) { + form.append("url", file); + } else { + form.append("file", createReadStream(file), { filename: "image.jpg" }); + } + break; + case 'object': + if (file instanceof Buffer) { + form.append("file", file, { filename: "image.jpg" }); + } else if (file instanceof Readable) { + form.append("file", file, { filename: "image.jpg" }); + } else { + throw new Error("Invalid file type"); + } + break; + default: + throw new Error("Invalid file type"); + } + + const response = await fetchFn("https://saucenao.com/search.php", { + method: "POST", + body: form.getBuffer(), + headers: form.getHeaders() + }); + + // I'm sure there's a better way to do this but I'll just re-assign a new var + // because I am writing this in midnight and I want to go to sleep + const res = await response.json() as IResponse; + + const { + header: { status, message, results_returned: resultsReturned }, + } = res; + + + // server-side error + if (status > 0) throw new SagiriServerError(status, message!); + // client-side error + if (status < 0) throw new SagiriClientError(status, message!); + + // filter unknowns + const unknownIds = new Set( + res.results.filter((result) => !sites[result.header.index_id]).map((result) => result.header.index_id), + ); + + if (unknownIds.size > 0) + console.warn( + `Same results were not resolved, because they were not found in the list of supported sites. + Please report this IDs to the library maintainer: ${Array.from(unknownIds).join(", ")}`); + + const srcResults = res.results + .filter((res) => !unknownIds.has(res.header.index_id)) + .sort((a, b) => b.header.similarity - a.header.similarity); + + console.debug(`Exepcted ${results} results, got ${srcResults.length}, with saucenao reporting ${resultsReturned} results.`); + + // return the results + return srcResults.map((res) => { + const { url, name, id, authorName, authorUrl } = resolveResult(res); + const { + header: { similarity, thumbnail } + } = res; + + return { + url, + site: name, + index: parseInt(id), + similarity: Number(similarity), + thumbnail, + authorName, + authorUrl, + raw: res + } + }); + } +} + + +export default sagiri; diff --git a/lib/sites.ts b/lib/sites.ts index 49d07d21..302db8c5 100644 --- a/lib/sites.ts +++ b/lib/sites.ts @@ -1,4 +1,4 @@ -import { IResult, IResultData } from "./response"; +import type { IResult, IResultData } from "./response"; import { SagiriClientError } from "./errors"; interface AuthorData { diff --git a/lib/thirdparty/bent.ts b/lib/thirdparty/bent.ts deleted file mode 100644 index aefe263c..00000000 --- a/lib/thirdparty/bent.ts +++ /dev/null @@ -1,198 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { PassThrough } from "node:stream"; -import zlib from "node:zlib"; -import http from "node:http"; -import https from "node:https"; -import { URL } from "node:url"; -import { isStream } from "is-stream"; -import Caseless from "./caseless.js"; - -function bytesishFrom(_from: any, encoding?: any) { - if (Buffer.isBuffer(_from)) return _from - _from = bytesishFrom(_from, encoding) - return Buffer.from(_from.buffer, _from.byteOffset, _from.byteLength) -} - -const compression: any = { - deflate: function (): zlib.Inflate { - return zlib.createInflate(); - }, - gzip: function (): zlib.Gunzip { - return zlib.createGunzip(); - }, - br: function (): zlib.BrotliDecompress { - return zlib.createBrotliDecompress(); - } -} as const; - -const acceptEncoding = Object.keys(compression).join(', ') - -const getResponse = (resp: { statusCode: any; statusMessage: any; headers: any; pipe: any }) => { - const ret: any = new PassThrough() - ret.statusCode = resp.statusCode - ret.status = resp.statusCode - ret.statusMessage = resp.statusMessage - ret.headers = resp.headers - ret._response = resp - if (ret.headers['content-encoding']) { - const encodings = ret.headers['content-encoding'].split(', ').reverse() - while (encodings.length) { - const enc = encodings.shift() - if (compression[enc]) { - const decompress = compression[enc]() - decompress.on('error', (e: ErrorOptions | undefined) => ret.emit('error', new Error('ZBufError', e))) - resp = resp.pipe(decompress) - } else { - break - } - } - } - return resp.pipe(ret) -} - -class StatusError extends Error { - statusCode: number - json: any - text: string - arrayBuffer: ArrayBuffer - headers: any - constructor (res: { statusMessage: string; statusCode: number; json: any; text: string; arrayBuffer: () => ArrayBuffer; headers: any }, ...params: undefined[]) { - super(...params) - - Error.captureStackTrace(this, StatusError) - this.name = 'StatusError' - this.message = res.statusMessage; - this.statusCode = res.statusCode; - this.json = res.json; - this.text = res.text; - this.arrayBuffer = res.arrayBuffer(); - this.headers = res.headers; - let buffer: any; - - const get = () => { - if (!buffer) buffer = this.arrayBuffer; - return buffer - } - Object.defineProperty(this, 'responseBody', { get }) - } -} - -const getBuffer = (stream: any) => new Promise((resolve, reject) => { - const parts: any[] = [] - stream.on('error', reject) - stream.on('end', () => resolve(Buffer.concat(parts))) - stream.on('data', (d: any) => parts.push(d)) -}) - -const decodings = (res: { arrayBuffer: () => Promise; text: () => any; json: () => Promise }) => { - let _buffer: Promise - res.arrayBuffer = () => { - if (!_buffer) { - _buffer = getBuffer(res) - return _buffer - } else { - throw new Error('body stream is locked') - } - } - res.text = () => res.arrayBuffer().then((buff: { toString: () => any }) => buff.toString()) - res.json = async () => { - const str = await res.text() - try { - return JSON.parse(str) - } catch (e) { - (e as Error).message += `str"${str}"` - throw e - } - } -} - -const mkrequest = (statusCodes: any, method: any, encoding: string, baseurl: any, headers?: any, ) => (_url: any, body: any, _headers = {}) => { - _url = baseurl + (_url || '') - const parsed = new URL(_url) - let h - if (parsed.protocol === 'https:') { - h = https - } else if (parsed.protocol === 'http:') { - h = http - } else { - throw new Error(`Unknown protocol, ${parsed.protocol}`) - } - const request: { - path: any; - port: any; - method: any; - headers: any; - hostname: any; - auth?: string; - } = { - path: parsed.pathname + parsed.search, - port: parsed.port, - method: method, - headers: { ...(headers || {}), ..._headers }, - hostname: parsed.hostname - } - if (parsed.username || parsed.password) { - request.auth = [parsed.username, parsed.password].join(':') - } - const c = new Caseless(request.headers); - if (encoding === 'json') { - if (!c.get('accept')) { - c.set('accept', 'application/json') - } - } - if (!c.has('accept-encoding')) { - c.set('accept-encoding', acceptEncoding) - } - return new Promise((resolve, reject) => { - const req = h.request(request, async (res: any) => { - res = getResponse(res) - res.on('error', reject) - decodings(res) - res.status = res.statusCode - if (!statusCodes.has(res.statusCode)) { - return reject(new StatusError(res)) - } - - if (!encoding) return resolve(res) - else { - /* istanbul ignore else */ - if (encoding === 'buffer') { - resolve(res.arrayBuffer()) - } else if (encoding === 'json') { - resolve(res.json()) - } else if (encoding === 'string') { - resolve(res.text()) - } - } - }) - req.on('error', reject) - if (body) { - if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) { - body = bytesishFrom(body); - } - if (Buffer.isBuffer(body)) { - // noop - } else if (typeof body === 'string') { - body = Buffer.from(body) - } else if (isStream(body)) { - body.pipe(req) - body = null - } else if (typeof body === 'object') { - if (!c.has('content-type')) { - req.setHeader('content-type', 'application/json') - } - body = Buffer.from(JSON.stringify(body)) - } else { - reject(new Error('Unknown body type.')) - } - if (body) { - req.setHeader('content-length', body.length) - req.end(body) - } - } else { - req.end() - } - }) -} - -export default mkrequest; diff --git a/lib/thirdparty/caseless.ts b/lib/thirdparty/caseless.ts deleted file mode 100644 index eb939d13..00000000 --- a/lib/thirdparty/caseless.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -export default class Caseless { - dict: any; - constructor(dict: any) { - this.dict = dict || {} - } - set(name: any, value: any, clobber?: boolean) { - if (typeof name === 'object') { - for (const i in name) { - this.set(i, name[i], value) - } - } else { - if (typeof clobber === 'undefined') clobber = true - const has = this.has(name) - - if (!clobber && has) this.dict[has] = this.dict[has] + ',' + value - else this.dict[has || name] = value - return has - } - return null; - } - has(name: string) { - const keys = Object.keys(this.dict); - name = name.toLowerCase(); - for (let i=0;i any; hasHeader: (key: any) => any; getHeader: (key: any) => any; removeHeader: (key: any) => any; headers: any }, headers: any) { - const c = new Caseless(headers) - resp.setHeader = function (key, value, clobber) { - if (typeof value === 'undefined') return - return c.set(key, value, clobber) - } - resp.hasHeader = function (key) { - return c.has(key) - } - resp.getHeader = function (key) { - return c.get(key) - } - resp.removeHeader = function (key) { - return c.del(key) - } - resp.headers = c.dict - return c -} diff --git a/package.json b/package.json index b4de40a9..8619a080 100644 --- a/package.json +++ b/package.json @@ -3,17 +3,8 @@ "version": "4.0.0", "description": "A simple, lightweight and actually good JS wrapper for the SauceNAO API.", "license": "MIT", + "module": "./lib/sagiri.ts", "type": "module", - "exports": { - "require": "./dist/cjs/index.js", - "import": "./dist/esm/index.js" - }, - "main": "dist/cjs/index.js", - "contributors": [ - "sr229", - "Agent_RBY_", - "Kotosif" - ], "homepage": "https://github.com/ClarityCafe/Sagiri#readme", "repository": { "type": "git", @@ -28,15 +19,11 @@ "url": "https://ko-fi.com/capuccino" } ], - "scripts": { - "build": "tsc --module commonjs --outDir dist/cjs/ && tsc --module es2022 --outDir dist/esm/", - "coverage": "jest --coverage", - "lint": "eslint . --fix", - "format": "prettier . --write --config .prettierrc", - "prepublishOnly": "bun run build", - "test": "jest", - "test:debug": "cross-env DEBUG=sagiri jest" - }, + "contributors": [ + "sr229", + "Agent_RBY_", + "Kotosif" + ], "keywords": [ "hitorigoto", "sagiri", @@ -49,43 +36,29 @@ "source" ], "engines": { - "node": ">=12" + "node": ">=20" }, - "dependencies": { - "bent": "^7.3.12", - "debug": "^4.3.4", - "form-data": "^4.0.0", - "is-stream": "^4.0.1" + "files": [ + "dist" + ], + "scripts": { + "test": "bun --env-file='./.env.test.local' test bun", + "test:deno": "deno test", + "coverage": "bun --env-file='./.env.test.local' test bun --coverage", + "lint": "eslint --fix", + "prepublishOnly": "bun run build" }, "devDependencies": { - "@eslint/js": "^9.0.0", - "@types/bent": "7.3.8", "@types/bun": "latest", - "@types/caseless": "^0.12.5", - "@types/debug": "4.1.12", - "@types/jest": "29.5.12", - "@types/lodash.ismatch": "4.4.9", - "@types/node": "20.14.10", - "@typescript-eslint/eslint-plugin": "7.17.0", - "@typescript-eslint/parser": "6.21.0", - "cross-env": "7.0.3", - "eslint": "^9.0.0", - "eslint-config-prettier": "9.1.0", - "eslint-plugin-import": "2.29.1", - "eslint-plugin-prettier": "5.1.3", - "eslint-plugin-react": "7.35.0", - "globals": "^15.0.0", - "jest": "29.7.0", - "lodash.ismatch": "4.4.0", - "nock": "13.5.4", - "parse-multipart": "1.0.4", - "prettier": "3.3.3", - "ts-jest": "29.2.3", - "ts-node": "10.9.2", - "typescript": "5.5.4", - "typescript-eslint": "^8.2.0" + "bun": "^1.1.26", + "eslint": "^9.9.1", + "typescript-eslint": "^8.3.0" }, - "files": [ - "dist" - ] + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "form-data": "^4.0.0", + "node-fetch": "^3.3.2" + } } diff --git a/test/bun/index.spec.ts b/test/bun/index.spec.ts new file mode 100644 index 00000000..b5294f32 --- /dev/null +++ b/test/bun/index.spec.ts @@ -0,0 +1,19 @@ +import { expect, test } from "bun:test"; +import sagiri from "../../lib/sagiri"; +import { env } from "node:process"; + +test("Should fail on invalid characters", () => { + env.NODE_ENV = "production"; + expect(() => {sagiri("!!!!!*&#@(!)")}).toThrow(Error); +}) + +test("Should fail on invalid length", () => { + env.NODE_ENV = "production"; + expect(() => {sagiri("7".repeat(27))}).toThrow(Error); +}) + +test("Resolve with results", async () => { + const result = await sagiri(env.SAUCENAO_TOKEN as string, { results: 5 })("https://i.imgur.com/F9QSgPx.jpeg"); + console.log(result); + expect(result).toBeDefined(); +}) diff --git a/test/deno/index.spec.ts b/test/deno/index.spec.ts new file mode 100644 index 00000000..94247708 --- /dev/null +++ b/test/deno/index.spec.ts @@ -0,0 +1,14 @@ +import { assertEquals, assertExists } from "@std/assert"; +import sagiri from "../../lib/sagiri"; + +Deno.test("should fail on invalid characters", () => { + assertEquals(() => {sagiri("!!!!!*&#@(!)")}, Error); +}); + +Deno.test("should fail on invalid length", () => { + assertEquals(() => {sagiri("7".repeat(27))}, Error); +}); + +Deno.test("Resolve with results", () => { + assertExists(()=> {}); +}) diff --git a/test/testarticle.jpg b/test/testarticle.jpg new file mode 100644 index 00000000..36920562 Binary files /dev/null and b/test/testarticle.jpg differ diff --git a/tests/README.md b/tests/README.md deleted file mode 100644 index 531417db..00000000 --- a/tests/README.md +++ /dev/null @@ -1,11 +0,0 @@ -When testing, make sure you have added a value called `SAUCENAO_TOKEN` to your enviroment variables with the value of your SauceNao token. - -## How to - -- Remove `.sample` from the `.env` file in the `fixtures` folder (`tests/fixtures/.env` from project root). -- Add your token after the `=` at `SAUCENAO_TOKEN=` - -## For CI environments - -- Add the environment variable to the CI enviroment settings. - How this works depends on the CI service, so look up documentation for your service. diff --git a/tests/fixtures/data.ts b/tests/fixtures/data.ts deleted file mode 100644 index e223341b..00000000 --- a/tests/fixtures/data.ts +++ /dev/null @@ -1,345 +0,0 @@ - - -// SauceNao output of https://owo.whats-th.is/6MtFNmm.png (upload of image.png) -export const normalData = { - header: { - user_id: "14555", - account_type: "1", - short_limit: "6", - long_limit: "200", - long_remaining: 196, - short_remaining: 5, - status: 0, - results_requested: 5, - index: { - "0": { status: 0, parent_id: 0, id: 0, results: 5 }, - "2": { status: 0, parent_id: 2, id: 2, results: 5 }, - "3": { status: 0, parent_id: 3, id: 3, results: 5 }, - "4": { status: 0, parent_id: 4, id: 4, results: 5 }, - "5": { status: 0, parent_id: 5, id: 5, results: 5 }, - "51": { status: 0, parent_id: 5, id: 51, results: 5 }, - "52": { status: 0, parent_id: 5, id: 52, results: 5 }, - "53": { status: 0, parent_id: 5, id: 53, results: 5 }, - "6": { status: 0, parent_id: 6, id: 6, results: 5 }, - "8": { status: 0, parent_id: 8, id: 8, results: 5 }, - "9": { status: 0, parent_id: 9, id: 9, results: 40 }, - "10": { status: 0, parent_id: 10, id: 10, results: 5 }, - "11": { status: 0, parent_id: 11, id: 11, results: 5 }, - "12": { status: 1, parent_id: 9, id: 12 }, - "16": { status: 0, parent_id: 16, id: 16, results: 5 }, - "18": { status: 0, parent_id: 18, id: 18, results: 5 }, - "19": { status: 0, parent_id: 19, id: 19, results: 2 }, - "20": { status: 0, parent_id: 20, id: 20, results: 5 }, - "21": { status: 0, parent_id: 21, id: 21, results: 5 }, - "211": { status: 0, parent_id: 21, id: 211, results: 5 }, - "22": { status: 0, parent_id: 22, id: 22, results: 5 }, - "23": { status: 0, parent_id: 23, id: 23, results: 5 }, - "24": { status: 0, parent_id: 24, id: 24, results: 5 }, - "25": { status: 1, parent_id: 9, id: 25 }, - "26": { status: 1, parent_id: 9, id: 26 }, - "27": { status: 1, parent_id: 9, id: 27 }, - "28": { status: 1, parent_id: 9, id: 28 }, - "29": { status: 1, parent_id: 9, id: 29 }, - "30": { status: 1, parent_id: 9, id: 30 }, - "31": { status: 0, parent_id: 31, id: 31, results: 5 }, - "32": { status: 0, parent_id: 32, id: 32, results: 5 }, - "33": { status: 0, parent_id: 33, id: 33, results: 5 }, - "34": { status: 0, parent_id: 34, id: 34, results: 5 }, - "35": { status: 0, parent_id: 35, id: 35, results: 5 }, - "36": { status: 0, parent_id: 36, id: 36, results: 5 }, - "37": { status: 0, parent_id: 37, id: 37, results: 5 }, - }, - search_depth: "128", - minimum_similarity: 46.15, - query_image_display: "userdata/R8YKDaCcl.png.png", - query_image: "R8YKDaCcl.png", - results_returned: 5, - }, - results: [ - { - header: { - similarity: "94.52", - thumbnail: "https://img1.saucenao.com/res/pixiv/4181/41817184_m.jpg?auth=EZ-kzUSPWzobHq0lx8bRMA&exp=1571293428", - index_id: 5, - index_name: "Index #5: Pixiv Images - 41817184_m.jpg", - }, - data: { - ext_urls: ["https://www.pixiv.net/member_illust.php?mode=medium&illust_id=41817184"], - title: "準備を!", - pixiv_id: 41817184, - member_name: "Gz", - member_id: 5508193, - }, - }, - { - header: { - similarity: "91.6", - thumbnail: - "https://img1.saucenao.com/res/pixiv/6023/60231445_p0_master1200.jpg?auth=izM6Chn8mNB9eOVkuP-hww&exp=1571293428", - index_id: 5, - index_name: "Index #5: Pixiv Images - 60231445_p0_master1200.jpg", - }, - data: { - ext_urls: ["https://www.pixiv.net/member_illust.php?mode=medium&illust_id=60231445"], - title: "no title", - pixiv_id: 60231445, - member_name: "miguelonn", - member_id: 21353907, - }, - }, - { - header: { - similarity: "95.28", - thumbnail: "https://img3.saucenao.com/dA/50781/507811345.jpg", - index_id: 34, - index_name: "Index #34: deviantArt - 507811345.jpg", - }, - data: { - ext_urls: ["https://deviantart.com/view/507811345"], - title: "A cute pair of glasses", - da_id: 507811345, - author_name: "Ninjacooncat", - author_url: "http://ninjacooncat.deviantart.com", - }, - }, - { - header: { - similarity: "95.11", - thumbnail: "https://img3.saucenao.com/dA/65328/653284939.jpg", - index_id: 34, - index_name: "Index #34: deviantArt - 653284939.jpg", - }, - data: { - ext_urls: ["https://deviantart.com/view/653284939"], - title: "Look at what my friend drew me!", - da_id: 653284939, - author_name: "XxCloverwindyxX", - author_url: "http://xxcloverwindyxx.deviantart.com", - }, - }, - { - header: { - similarity: "95.01", - thumbnail: "https://img3.saucenao.com/dA/60579/605799146.jpg", - index_id: 34, - index_name: "Index #34: deviantArt - 605799146.jpg", - }, - data: { - ext_urls: ["https://deviantart.com/view/605799146"], - title: "Neighbors TG", - da_id: 605799146, - author_name: "lolmastersadow", - author_url: "http://lolmastersadow.deviantart.com", - }, - }, - ], -}; - -// SauceNao output for http://saucenao.com/images/static/banner.gif with dbmask of 32 (index 5) -export const regularMaskData = { - header: { - user_id: "14555", - account_type: "1", - short_limit: "6", - long_limit: "200", - long_remaining: 192, - short_remaining: 3, - status: 0, - results_requested: 5, - index: { - "5": { status: 0, parent_id: 5, id: 5, results: 1 }, - "51": { status: 0, parent_id: 5, id: 51, results: 1 }, - "52": { status: 0, parent_id: 5, id: 52, results: 1 }, - "53": { status: 0, parent_id: 5, id: 53, results: 1 }, - }, - search_depth: "128", - minimum_similarity: 55, - query_image_display: "userdata/lq0TTvvBi.gif.png", - query_image: "lq0TTvvBi.gif", - results_returned: 3, - }, - results: [ - { - header: { - similarity: "23.50", - thumbnail: "https://img1.saucenao.com/res/pixiv/493/4933944_s.jpg?auth=Up9BDClLy7R-a_jVr10ZpA&exp=1571746128", - index_id: 5, - index_name: "Index #5: Pixiv Images - 4933944_s.jpg", - }, - data: { - ext_urls: ["https://www.pixiv.net/member_illust.php?mode=medium&illust_id=4933944"], - title: "妖キャラをカリスマ化してみた。", - pixiv_id: 4933944, - member_name: "佳虫", - member_id: 724886, - }, - }, - { - header: { - similarity: "21.15", - thumbnail: - "https://img1.saucenao.com/res/pixiv/6070/manga/60706368_p2.jpg?auth=fit_Th-DmLczFvl6SdK53A&exp=1571746128", - index_id: 5, - index_name: "Index #5: Pixiv Images - 60706368_p2.jpg", - }, - data: { - ext_urls: ["https://www.pixiv.net/member_illust.php?mode=medium&illust_id=60706368"], - title: "MHAまとめ9", - pixiv_id: 60706368, - member_name: "まゆ", - member_id: 361946, - }, - }, - { - header: { - similarity: "19.13", - thumbnail: - "https://img1.saucenao.com/res/pixiv/7567/75673655_p0_master1200.jpg?auth=P0-k9fSmN-76d95T42l5bw&exp=1571746128", - index_id: 5, - index_name: "Index #5: Pixiv Images - 75673655_p0_master1200.jpg", - }, - data: { - ext_urls: ["https://www.pixiv.net/member_illust.php?mode=medium&illust_id=75673655"], - title: "ポーカーフェイク", - pixiv_id: 75673655, - member_name: "桜夜(サヨ)", - member_id: 26124403, - }, - }, - ], -}; - -// SauceNao output for http://saucenao.com/images/static/banner.gif with dbmaski of 32 (index 5) -export const inverseMaskData = { - header: { - user_id: "14555", - account_type: "1", - short_limit: "6", - long_limit: "200", - long_remaining: 194, - short_remaining: 5, - status: 0, - results_requested: 5, - index: { - "0": { status: 0, parent_id: 0, id: 0, results: 1 }, - "2": { status: 0, parent_id: 2, id: 2, results: 1 }, - "3": { status: 0, parent_id: 3, id: 3, results: 1 }, - "4": { status: 0, parent_id: 4, id: 4, results: 1 }, - "6": { status: 0, parent_id: 6, id: 6, results: 1 }, - "8": { status: 0, parent_id: 8, id: 8, results: 1 }, - "9": { status: 0, parent_id: 9, id: 9, results: 8 }, - "10": { status: 0, parent_id: 10, id: 10, results: 1 }, - "11": { status: 0, parent_id: 11, id: 11, results: 1 }, - "12": { status: 1, parent_id: 9, id: 12 }, - "15": { status: 0, parent_id: 15, id: 15, results: 1 }, - "16": { status: 0, parent_id: 16, id: 16, results: 1 }, - "18": { status: 0, parent_id: 18, id: 18, results: 1 }, - "19": { status: 0, parent_id: 19, id: 19, results: 1 }, - "20": { status: 0, parent_id: 20, id: 20, results: 1 }, - "21": { status: 0, parent_id: 21, id: 21, results: 1 }, - "211": { status: 0, parent_id: 21, id: 211, results: 1 }, - "22": { status: 0, parent_id: 22, id: 22, results: 1 }, - "23": { status: 0, parent_id: 23, id: 23, results: 1 }, - "24": { status: 0, parent_id: 24, id: 24, results: 1 }, - "25": { status: 1, parent_id: 9, id: 25 }, - "26": { status: 1, parent_id: 9, id: 26 }, - "27": { status: 1, parent_id: 9, id: 27 }, - "28": { status: 1, parent_id: 9, id: 28 }, - "29": { status: 1, parent_id: 9, id: 29 }, - "30": { status: 1, parent_id: 9, id: 30 }, - "31": { status: 0, parent_id: 31, id: 31, results: 1 }, - "32": { status: 0, parent_id: 32, id: 32, results: 1 }, - "33": { status: 0, parent_id: 33, id: 33, results: 1 }, - "34": { status: 0, parent_id: 34, id: 34, results: 1 }, - "35": { status: 0, parent_id: 35, id: 35, results: 1 }, - "36": { status: 0, parent_id: 36, id: 36, results: 1 }, - "37": { status: 0, parent_id: 37, id: 37, results: 1 }, - }, - search_depth: "128", - minimum_similarity: 24.6, - query_image_display: "userdata/NWQqfFbzx.gif.png", - query_image: "NWQqfFbzx.gif", - results_returned: 5, - }, - results: [ - { - header: { - similarity: "23.60", - thumbnail: "https://img3.saucenao.com/dA/51571/515715132.jpg", - index_id: 34, - index_name: "Index #34: deviantArt - 515715132.jpg", - }, - data: { - ext_urls: ["https://deviantart.com/view/515715132"], - title: "Koshitantan + video link+stagedl", - da_id: 515715132, - author_name: "SliverRose0916", - author_url: "http://sliverrose0916.deviantart.com", - }, - }, - { - header: { - similarity: "22.06", - thumbnail: - "https://thumb1.shutterstock.com/thumb_large/862396/157296953/stock-photo-bows-seamless-background-157296953.jpg", - index_id: 15, - index_name: "Index #15: Shutterstock - 157296953_t.jpg", - }, - data: { - shutterstock_id: 157296953, - contributor_id: "862396", - date: "2013-10-07", - }, - }, - { - header: { - similarity: "21.48", - thumbnail: - "https://img1.saucenao.com/res/pixiv_historical/383/3836606_s.jpg?auth=onLJgrY36duFEf7jkGVWOA&exp=1571746122", - index_id: 6, - index_name: "Index #6: Pixiv Historical - 3836606_s.jpg", - }, - data: { - ext_urls: ["https://www.pixiv.net/member_illust.php?mode=medium&illust_id=3836606"], - title: "【pixiv袁紹軍】", - pixiv_id: 3836606, - member_name: "白菜", - member_id: 58228, - }, - }, - { - header: { - similarity: "21.0", - thumbnail: - "https://img1.saucenao.com/res/bcy/illust/69/manga/694527_p9-48.jpg?auth=NQw6JD9RNXuOqbRflxXa4A&exp=1571746122", - index_id: 31, - index_name: "Index #31: bcy.net Illust - 694527_p9-48.jpg", - }, - data: { - ext_urls: ["https://bcy.net/illust/detail/55206"], - title: "|*美食方言*|", - bcy_id: 694527, - member_name: "第四存档点", - member_id: 1767173, - member_link_id: 55206, - bcy_type: "illust", - }, - }, - { - header: { - similarity: "19.58", - thumbnail: "https://img1.saucenao.com/res/pawoo/468/46871064_1.jpg?auth=5I_e3su5TWkQexCx2JX46w&exp=1571746122", - index_id: 35, - index_name: "Index #35: Pawoo.net - 46871064_1.jpg", - }, - data: { - ext_urls: ["https://pawoo.net/@nez_ebi"], - created_at: "2017-10-11T12:29:20.000Z", - pawoo_id: 46871064, - pawoo_user_acct: "nez_ebi", - pawoo_user_username: "nez_ebi", - pawoo_user_display_name: "🦐ねづ🦐", - }, - }, - ], -}; diff --git a/tests/fixtures/expectations.ts b/tests/fixtures/expectations.ts deleted file mode 100644 index 7c1a90a3..00000000 --- a/tests/fixtures/expectations.ts +++ /dev/null @@ -1,311 +0,0 @@ - - -export const normalExpectations = [ - { - url: "https://deviantart.com/view/507811345", - site: "deviantArt", - index: 34, - similarity: 95.28, - thumbnail: "https://img3.saucenao.com/dA/50781/507811345.jpg", - authorName: "Ninjacooncat", - authorUrl: "http://ninjacooncat.deviantart.com", - raw: { - header: { - similarity: "95.28", - thumbnail: "https://img3.saucenao.com/dA/50781/507811345.jpg", - index_id: 34, - index_name: "Index #34: deviantArt - 507811345.jpg", - }, - data: { - ext_urls: ["https://deviantart.com/view/507811345"], - title: "A cute pair of glasses", - da_id: 507811345, - author_name: "Ninjacooncat", - author_url: "http://ninjacooncat.deviantart.com", - }, - }, - }, - { - url: "https://deviantart.com/view/653284939", - site: "deviantArt", - index: 34, - similarity: 95.11, - thumbnail: "https://img3.saucenao.com/dA/65328/653284939.jpg", - authorName: "XxCloverwindyxX", - authorUrl: "http://xxcloverwindyxx.deviantart.com", - raw: { - header: { - similarity: "95.11", - thumbnail: "https://img3.saucenao.com/dA/65328/653284939.jpg", - index_id: 34, - index_name: "Index #34: deviantArt - 653284939.jpg", - }, - data: { - ext_urls: ["https://deviantart.com/view/653284939"], - title: "Look at what my friend drew me!", - da_id: 653284939, - author_name: "XxCloverwindyxX", - author_url: "http://xxcloverwindyxx.deviantart.com", - }, - }, - }, - { - url: "https://deviantart.com/view/605799146", - site: "deviantArt", - index: 34, - similarity: 95.01, - thumbnail: "https://img3.saucenao.com/dA/60579/605799146.jpg", - authorName: "lolmastersadow", - authorUrl: "http://lolmastersadow.deviantart.com", - raw: { - header: { - similarity: "95.01", - thumbnail: "https://img3.saucenao.com/dA/60579/605799146.jpg", - index_id: 34, - index_name: "Index #34: deviantArt - 605799146.jpg", - }, - data: { - ext_urls: ["https://deviantart.com/view/605799146"], - title: "Neighbors TG", - da_id: 605799146, - author_name: "lolmastersadow", - author_url: "http://lolmastersadow.deviantart.com", - }, - }, - }, - { - url: "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=41817184", - site: "Pixiv", - index: 5, - similarity: 94.52, - thumbnail: "https://img1.saucenao.com/res/pixiv/4181/41817184_m.jpg?auth=EZ-kzUSPWzobHq0lx8bRMA&exp=1571293428", - authorName: "Gz", - authorUrl: "https://www.pixiv.net/users/5508193", - raw: { - header: { - similarity: "94.52", - thumbnail: "https://img1.saucenao.com/res/pixiv/4181/41817184_m.jpg?auth=EZ-kzUSPWzobHq0lx8bRMA&exp=1571293428", - index_id: 5, - index_name: "Index #5: Pixiv Images - 41817184_m.jpg", - }, - data: { - ext_urls: ["https://www.pixiv.net/member_illust.php?mode=medium&illust_id=41817184"], - title: "準備を!", - pixiv_id: 41817184, - member_name: "Gz", - member_id: 5508193, - }, - }, - }, - { - url: "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=60231445", - site: "Pixiv", - index: 5, - similarity: 91.6, - thumbnail: - "https://img1.saucenao.com/res/pixiv/6023/60231445_p0_master1200.jpg?auth=izM6Chn8mNB9eOVkuP-hww&exp=1571293428", - authorName: "miguelonn", - authorUrl: "https://www.pixiv.net/users/21353907", - raw: { - header: { - similarity: "91.6", - thumbnail: - "https://img1.saucenao.com/res/pixiv/6023/60231445_p0_master1200.jpg?auth=izM6Chn8mNB9eOVkuP-hww&exp=1571293428", - index_id: 5, - index_name: "Index #5: Pixiv Images - 60231445_p0_master1200.jpg", - }, - data: { - ext_urls: ["https://www.pixiv.net/member_illust.php?mode=medium&illust_id=60231445"], - title: "no title", - pixiv_id: 60231445, - member_name: "miguelonn", - member_id: 21353907, - }, - }, - }, -]; - -export const regularMaskExpectations = [ - { - url: "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=4933944", - site: "Pixiv", - index: 5, - similarity: 23.5, - thumbnail: "https://img1.saucenao.com/res/pixiv/493/4933944_s.jpg?auth=Up9BDClLy7R-a_jVr10ZpA&exp=1571746128", - authorName: "佳虫", - authorUrl: "https://www.pixiv.net/users/724886", - raw: { - header: { - similarity: "23.50", - thumbnail: "https://img1.saucenao.com/res/pixiv/493/4933944_s.jpg?auth=Up9BDClLy7R-a_jVr10ZpA&exp=1571746128", - index_id: 5, - index_name: "Index #5: Pixiv Images - 4933944_s.jpg", - }, - data: { - ext_urls: ["https://www.pixiv.net/member_illust.php?mode=medium&illust_id=4933944"], - title: "妖キャラをカリスマ化してみた。", - pixiv_id: 4933944, - member_name: "佳虫", - member_id: 724886, - }, - }, - }, - { - url: "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=60706368", - site: "Pixiv", - index: 5, - similarity: 21.15, - thumbnail: - "https://img1.saucenao.com/res/pixiv/6070/manga/60706368_p2.jpg?auth=fit_Th-DmLczFvl6SdK53A&exp=1571746128", - authorName: "まゆ", - authorUrl: "https://www.pixiv.net/users/361946", - raw: { - header: { - similarity: "21.15", - thumbnail: - "https://img1.saucenao.com/res/pixiv/6070/manga/60706368_p2.jpg?auth=fit_Th-DmLczFvl6SdK53A&exp=1571746128", - index_id: 5, - index_name: "Index #5: Pixiv Images - 60706368_p2.jpg", - }, - data: { - ext_urls: ["https://www.pixiv.net/member_illust.php?mode=medium&illust_id=60706368"], - title: "MHAまとめ9", - pixiv_id: 60706368, - member_name: "まゆ", - member_id: 361946, - }, - }, - }, - { - url: "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=75673655", - site: "Pixiv", - index: 5, - similarity: 19.13, - thumbnail: - "https://img1.saucenao.com/res/pixiv/7567/75673655_p0_master1200.jpg?auth=P0-k9fSmN-76d95T42l5bw&exp=1571746128", - authorName: "桜夜(サヨ)", - authorUrl: "https://www.pixiv.net/users/26124403", - raw: { - header: { - similarity: "19.13", - thumbnail: - "https://img1.saucenao.com/res/pixiv/7567/75673655_p0_master1200.jpg?auth=P0-k9fSmN-76d95T42l5bw&exp=1571746128", - index_id: 5, - index_name: "Index #5: Pixiv Images - 75673655_p0_master1200.jpg", - }, - data: { - ext_urls: ["https://www.pixiv.net/member_illust.php?mode=medium&illust_id=75673655"], - title: "ポーカーフェイク", - pixiv_id: 75673655, - member_name: "桜夜(サヨ)", - member_id: 26124403, - }, - }, - }, -]; - -export const inverseMaskExpectations = [ - { - url: "https://deviantart.com/view/515715132", - site: "deviantArt", - index: 34, - similarity: 23.6, - thumbnail: "https://img3.saucenao.com/dA/51571/515715132.jpg", - authorName: "SliverRose0916", - authorUrl: "http://sliverrose0916.deviantart.com", - raw: { - header: { - similarity: "23.60", - thumbnail: "https://img3.saucenao.com/dA/51571/515715132.jpg", - index_id: 34, - index_name: "Index #34: deviantArt - 515715132.jpg", - }, - data: { - ext_urls: ["https://deviantart.com/view/515715132"], - title: "Koshitantan + video link+stagedl", - da_id: 515715132, - author_name: "SliverRose0916", - author_url: "http://sliverrose0916.deviantart.com", - }, - }, - }, - { - url: "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=3836606", - site: "Pixiv", - index: 6, - similarity: 21.48, - thumbnail: - "https://img1.saucenao.com/res/pixiv_historical/383/3836606_s.jpg?auth=onLJgrY36duFEf7jkGVWOA&exp=1571746122", - authorName: "白菜", - authorUrl: "https://www.pixiv.net/users/58228", - raw: { - header: { - similarity: "21.48", - thumbnail: - "https://img1.saucenao.com/res/pixiv_historical/383/3836606_s.jpg?auth=onLJgrY36duFEf7jkGVWOA&exp=1571746122", - index_id: 6, - index_name: "Index #6: Pixiv Historical - 3836606_s.jpg", - }, - data: { - ext_urls: ["https://www.pixiv.net/member_illust.php?mode=medium&illust_id=3836606"], - title: "【pixiv袁紹軍】", - pixiv_id: 3836606, - member_name: "白菜", - member_id: 58228, - }, - }, - }, - { - url: "https://bcy.net/illust/detail/55206", - site: "bcy.net Illust", - index: 31, - similarity: 21, - thumbnail: - "https://img1.saucenao.com/res/bcy/illust/69/manga/694527_p9-48.jpg?auth=NQw6JD9RNXuOqbRflxXa4A&exp=1571746122", - authorName: "第四存档点", - authorUrl: "https://bcy.net/u/1767173", - raw: { - header: { - similarity: "21.0", - thumbnail: - "https://img1.saucenao.com/res/bcy/illust/69/manga/694527_p9-48.jpg?auth=NQw6JD9RNXuOqbRflxXa4A&exp=1571746122", - index_id: 31, - index_name: "Index #31: bcy.net Illust - 694527_p9-48.jpg", - }, - data: { - ext_urls: ["https://bcy.net/illust/detail/55206"], - title: "|*美食方言*|", - bcy_id: 694527, - member_name: "第四存档点", - member_id: 1767173, - member_link_id: 55206, - bcy_type: "illust", - }, - }, - }, - { - url: "https://pawoo.net/@nez_ebi", - site: "Pawoo", - index: 35, - similarity: 19.58, - thumbnail: "https://img1.saucenao.com/res/pawoo/468/46871064_1.jpg?auth=5I_e3su5TWkQexCx2JX46w&exp=1571746122", - authorName: null, - authorUrl: null, - raw: { - header: { - similarity: "19.58", - thumbnail: "https://img1.saucenao.com/res/pawoo/468/46871064_1.jpg?auth=5I_e3su5TWkQexCx2JX46w&exp=1571746122", - index_id: 35, - index_name: "Index #35: Pawoo.net - 46871064_1.jpg", - }, - data: { - ext_urls: ["https://pawoo.net/@nez_ebi"], - created_at: "2017-10-11T12:29:20.000Z", - pawoo_id: 46871064, - pawoo_user_acct: "nez_ebi", - pawoo_user_username: "nez_ebi", - pawoo_user_display_name: "🦐ねづ🦐", - }, - }, - }, -]; diff --git a/tests/fixtures/image.png b/tests/fixtures/image.png deleted file mode 100644 index a13eee4b..00000000 Binary files a/tests/fixtures/image.png and /dev/null differ diff --git a/tests/fixtures/remoteData.ts b/tests/fixtures/remoteData.ts deleted file mode 100644 index 7ee90e6a..00000000 --- a/tests/fixtures/remoteData.ts +++ /dev/null @@ -1,519 +0,0 @@ - - -const remoteData = { - "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=41817184": ` - #Original µ║ûÕéÖÒéÆ´╝ü - YuzaÒü«ÒéñÒâ®Òé╣Òâê - pixiv -
`, - - "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=60231445": ` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ÒéñÒâ®Òé╣ÒâêÒé│ÒâƒÒâÑÒâïÒé▒Òâ╝ÒéÀÒâºÒâ│ÒéÁÒâ╝ÒâôÒé╣[pixiv] - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

pixivÒü»2018Õ╣┤5µ£ê16µùÑõ╗ÿÒüæÒüºÒâùÒâ®ÒéñÒâÉÒéÀÒâ╝ÒâØÒâ¬ÒéÀÒâ╝ÒéƵö╣Õ«ÜÒüùÒü¥ÒüÖÒÇéÕåàÕ«╣Òüîõ╗ÑÕëìÒéêÒéèÒééµÿÄþó║Òü½Òü¬ÒéèÒÇüEU(µ¼ºÕÀ×ÚÇúÕÉê)Òü«µû░ÒüùÒüäÒâùÒâ®ÒéñÒâÉÒéÀÒâ╝õ┐ØÞ¡Àµ│òÒü½ÒééÕ»¥Õ┐£ÒüùÒü¥ÒüÖÒÇé

Þ®│þ┤░
- -

pixivÒü©ÒéêÒüåÒüôÒüØ

pixiv(ÒâöÒé»ÒéÀÒâû)Òü»ÒÇüÒéñÒâ®Òé╣ÒâêÒâ╗µ╝½þö╗Òâ╗Õ░ÅÞ¬¼Òü«µèòþ¿┐ÒéäÚû▓ÞªºÒüîµÑ¢ÒüùÒéüÒéïÒÇîÒéñÒâ®Òé╣ÒâêÒé│ÒâƒÒâÑÒâïÒé▒Òâ╝ÒéÀÒâºÒâ│ÒéÁÒâ╝ÒâôÒé╣ÒÇìÒüºÒüÖÒÇéÕ░æÒüùÒüºÒééµ░ùÒü½Òü¬ÒüúÒüƒÒéèÒÇüÚØóþÖ¢ÒüØÒüåÒü¿µÇØÒüúÒüƒÒéëÒÇüÒü¥ÒüÜÒü»ÒâªÒâ╝ÒéÂÒâ╝þÖ╗Úî▓ÒéÆÒüùÒüªÒü┐ÒüªÒüÅÒüáÒüòÒüäÒÇé þÖ╗Úî▓ÒÇüÕê®þö¿Òü»þäíµûÖÒüºÒüÖÒÇéÒâóÒâÉÒéñÒâ½ÒüïÒéëÒüºÒééÒéóÒé»Òé╗Òé╣ÒüºÒüìÒü¥ÒüÖÒÇé
- -
- -
-
-
-
-
-
- - - - -
- - -
-

Òé¿Òâ®Òâ╝ÒüîþÖ║þöƒÒüùÒü¥ÒüùÒüƒ

-

Þ®▓Õ¢ôõ¢£ÕôüÒü»ÕëèÚÖñÒüòÒéîÒüƒÒüïÒÇüÕ¡ÿÕ£¿ÒüùÒü¬Òüäõ¢£ÕôüIDÒüºÒüÖÒÇé

- - -

µê╗Òéï

-
-
-
- -
- - - - - - `, - - "https://deviantart.com/view/507811345": ` - - - A cute pair of glasses by Ninjacooncat on DeviantArt - - - - - - - - - - - - - - - - - - - - - - -
Ninjacooncat's avatar
A cute pair of glasses
6 4 131 (1 Today)
By Ninjacooncat   |   Watch
Published: January 18, 2015
┬® 2015 - 2019 Ninjacooncat
Here is an anime girl with some darn cute glasses. Please enjoy!!!
Image size
2480x3507px 2.47 MB
Comments4
anonymous's avatar
Join the community to add your comment. Already a deviant? Sign In
xSweetSlayerx's avatar
xSweetSlayerxProfessional General Artist
This image was not made by this deviant.
The original art was made by Yuza.
www.pixiv.net/member_illust.ph…
saito602's avatar
saito602Hobbyist Traditional Artist
So cute >.<
Ninjacooncat's avatar
NinjacooncatHobbyist Digital Artist
Haha, thanks :3
- - - - - - - - - - - - - - - `, - - "https://deviantart.com/view/653284939": ` - - - Look at what my friend drew me! by XxCloverwindyxX on DeviantArt - - - - - - - - - - - - - - - - - - - - - - -
Recommended for you
XxCloverwindyxX's avatar
Look at what my friend drew me!
1 1 86 (2 Today)
By XxCloverwindyxX   |   Watch
Published: December 25, 2016
┬® 2016 - 2019 XxCloverwindyxX
I love this so much
It's legit a picture of what my personality looks like.
Huhuhu
This is Pure Loaf:happybounce: 
Image size
400x566px 38.74 KB
Recommended for you
Comments1
anonymous's avatar
Join the community to add your comment. Already a deviant? Sign In
faken820's avatar
woah, your friend is Yuza on pixiv? cause thats the original source of this picture 🤔🤔🤔
- - - - - - - - - - - - - - - `, -}; - -export default remoteData; diff --git a/tests/index.spec.ts b/tests/index.spec.ts deleted file mode 100644 index 7f733ea2..00000000 --- a/tests/index.spec.ts +++ /dev/null @@ -1,216 +0,0 @@ - - - -import isMatch from "lodash.ismatch"; -import nock from "nock"; - -import fs from "fs"; -import { promisify } from "util"; - -import sagiri from "../lib"; - -import { inverseMaskData, normalData, regularMaskData } from "./fixtures/data"; -import { inverseMaskExpectations, normalExpectations, regularMaskExpectations } from "./fixtures/expectations"; -// import remoteData from './fixtures/remoteData'; - -const client = sagiri(""); -const testImage = `${__dirname}/fixtures/image.png`; -const readFile = promisify(fs.readFile); -/* const ratingMatcher = expect.arrayContaining([ - expect.objectContaining({ - url: 'https://deviantart.com/view/507811345', - rating: 'QUESTIONABLE' - }), - expect.objectContaining({ - url: 'https://deviantart.com/view/653284939', - rating: 'QUESTIONABLE' - }), - expect.objectContaining({ - url: 'https://deviantart.com/view/605799146', - rating: 'UNKNOWN' - }), - expect.objectContaining({ - url: - 'https://www.pixiv.net/member_illust.php?mode=medium&illust_id=41817184', - rating: 'SAFE' - }), - expect.objectContaining({ - url: - 'https://www.pixiv.net/member_illust.php?mode=medium&illust_id=60231445', - rating: 'NSFW' - }) -]);*/ - -/** Dumb 'n stupid "parser" for multipart/form-data bodies using regex and a bunch of string functions. - Serves to provide an object for matching correct options in Nock calls.*/ -const parseMultipart = (body: string): { [key: string]: any } => { - const _ = body.match(/^(-+[^\r\n]*)/); // Try to find the first multipart boundary for the form. - const boundary = _ ? _[0] : /^-+.*$/gm; // Fallback on generic dash regex if can't find first boundary for whatever reason. May break in some cases. - - return body - .split(boundary) // Split on boundaries - .slice(1, -1) // Remove blank elements at start and end - .map( - (x) => - x - .trimLeft() // Remove padding at start - .split("\r\n") // Split based on ending \r\n to get [name, blank, value] - .slice(0, -1), // Remove blank element at end - ) - .map(([name, ...r]) => [name.match(/name="(.*?)"/)![1], ...r.slice(1)]) // Extract field name from content disposition - .reduce((prev, [name, value]) => ({ ...prev, [name]: value }), {}); // Reduce array into object of names and values -}; - -/** Convenience function for "parsing" encoded multipart/form-data bodies that occur when sending a file object. - Doesn't end up returning the file field as that was too hard to properly do. */ -const parseEncodedMultipart = (body: string) => parseMultipart(Buffer.from(body, "hex").toString()); - -const mockApi = (...args: [nock.RequestBodyMatcher?, nock.Options?]) => - nock("https://saucenao.com").post("/search.php", ...args); - -describe("Sagiri#constructor", () => { - test("Should fail on invalid characters", () => { - // momentarily set to production mode to force the error lmao - process.env.NODE_ENV = "production"; - - expect(() => {sagiri("!!!!!*&#@(!)")}).toThrow(Error); - }) - - test("Should fail on invalid length", () => { - // momentarily set to production mode to force the error lmao - process.env.NODE_ENV = "production"; - - expect(() => {sagiri("7".repeat(27))}).toThrow(Error); - }) -}) - -describe("Sagiri#getSauce", () => { - test("gets source from url", async () => { - mockApi((b) => { - const body = parseMultipart(b); - return isMatch(body, { - api_key: "", - output_type: "2", - numres: "5", - url: "https://owo.whats-th.is/6MtFNmm.png", - }); - }).reply(200, normalData); - - const results = await client("https://owo.whats-th.is/6MtFNmm.png"); - - expect(results).toEqual(normalExpectations); - }); - - describe("gets source from file", () => { - test("path", async () => { - mockApi((b) => { - const body = parseEncodedMultipart(b); - return isMatch(body, { - api_key: "", - output_type: "2", - numres: "5", - }); - }).reply(200, normalData); - - const results = await client(testImage); - - expect(results).toEqual(normalExpectations); - }); - - test("buffer", async () => { - mockApi((b) => { - const body = parseEncodedMultipart(b); - return isMatch(body, { - api_key: "", - output_type: "2", - numres: "5", - }); - }).reply(200, normalData); - - const results = await client(await readFile(testImage)); - - expect(results).toEqual(normalExpectations); - }); - }); - - describe("index masks", () => { - test("regular", async () => { - mockApi((b) => { - const body = parseMultipart(b); - return isMatch(body, { - api_key: "", - output_type: "2", - numres: "5", - dbmask: "32", - url: "http://saucenao.com/images/static/banner.gif", - }); - }).reply(200, regularMaskData); - - const results = await client("http://saucenao.com/images/static/banner.gif", { mask: [5] }); - - expect(results).toEqual(regularMaskExpectations); - }); - - test("inverse", async () => { - mockApi((b) => { - const body = parseMultipart(b); - return isMatch(body, { - api_key: "", - output_type: "2", - numres: "5", - dbmaski: "32", - url: "http://saucenao.com/images/static/banner.gif", - }); - }).reply(200, inverseMaskData); - - const results = await client("http://saucenao.com/images/static/banner.gif", { excludeMask: [5] }); - - expect(results).toEqual(inverseMaskExpectations); - }); - }); - - // Uncomment when fetching ratings is supported again - /* describe('fetching ratings', () => { - test('gets the right ratings', async () => { - // Mock SauceNao - mockApi(b => { - const body = parseMultipart(b); - return isMatch(body, { - api_key: '', - output_type: '2', - numres: '5', - url: 'https://owo.whats-th.is/6MtFNmm.png' - }); - }).reply(200, normalData); - - // Mock places to get data from - nock('https://www.pixiv.net') - .get('/member_illust.php?mode=medium&illust_id=41817184') - .reply( - 200, - remoteData[ - 'https://www.pixiv.net/member_illust.php?mode=medium&illust_id=41817184' - ] - ) - .get('/member_illust.php?mode=medium&illust_id=60231445') - .reply( - 200, - remoteData[ - 'https://www.pixiv.net/member_illust.php?mode=medium&illust_id=60231445' - ] - ); - - nock('https://deviantart.com') - .get('/view/507811345') - .reply(200, remoteData['https://deviantart.com/view/507811345']) - .get('/view/653284939') - .reply(200, remoteData['https://deviantart.com/view/653284939']); - - const results = await client('https://owo.whats-th.is/6MtFNmm.png', { - getRatings: true - }); - - expect(results).toEqual(ratingMatcher); - }); - });*/ -}); diff --git a/tsconfig.json b/tsconfig.json index 78a8dbda..238655f2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,24 +1,27 @@ { "compilerOptions": { - "outDir": "./dist/", - "rootDir": "./lib/", - "module": "nodenext", - "lib": ["esnext", "dom"], - "target": "es2022", - "moduleResolution": "node16", + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices "strict": true, - "noImplicitReturns": true, - "noImplicitThis": true, - "noUnusedLocals": true, + "skipLibCheck": true, "noFallthroughCasesInSwitch": true, - "removeComments": false, - "forceConsistentCasingInFileNames": true, - "esModuleInterop": true, - "declaration": true, - "noUnusedParameters": true, - "allowSyntheticDefaultImports": true, - "sourceMap": true, - "skipLibCheck": true - }, - "include": ["lib/**/*"] + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } }