├── .node-version ├── src ├── boot │ ├── .gitkeep │ ├── i18n.ts │ └── axios.ts ├── css │ ├── app.scss │ └── quasar.variables.scss ├── i18n │ ├── index.ts │ └── locales │ │ └── zh-CN.json ├── App.vue ├── env.d.ts ├── shims-vue.d.ts ├── quasar.d.ts ├── router │ ├── routes.ts │ └── index.ts ├── pages │ ├── Error404.vue │ └── Index.vue ├── index.template.html ├── api │ ├── types.ts │ └── index.ts ├── layouts │ └── MainLayout.vue ├── components │ ├── SearchIqDB.vue │ ├── SearchSauceNAO.vue │ ├── SearchAscii2d.vue │ ├── SearchEHentai.vue │ └── SearchTraceMoe.vue └── assets │ └── quasar-logo-vertical.svg ├── .prettierrc ├── public ├── favicon.ico └── icons │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ └── favicon-128x128.png ├── .eslintignore ├── functions ├── api │ ├── routes │ │ ├── index.ts │ │ ├── TraceMoe.ts │ │ ├── SauceNAO.ts │ │ ├── ascii2d.ts │ │ ├── IqDB.ts │ │ └── E-Hentai.ts │ ├── router.ts │ ├── [[path]].ts │ ├── patch.ts │ └── utils.ts └── proxy.ts ├── .editorconfig ├── tsconfig.json ├── .postcssrc.js ├── babel.config.js ├── .vscode ├── extensions.json └── settings.json ├── .gitignore ├── package.json ├── README.md ├── README_en.md ├── .eslintrc.js ├── quasar.conf.js └── LICENSE /.node-version: -------------------------------------------------------------------------------- 1 | 16 -------------------------------------------------------------------------------- /src/boot/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/css/app.scss: -------------------------------------------------------------------------------- 1 | // app global css in SCSS form 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true 4 | } 5 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mixmoe/HikariSearch/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mixmoe/HikariSearch/HEAD/public/icons/favicon-16x16.png -------------------------------------------------------------------------------- /public/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mixmoe/HikariSearch/HEAD/public/icons/favicon-32x32.png -------------------------------------------------------------------------------- /public/icons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mixmoe/HikariSearch/HEAD/public/icons/favicon-96x96.png -------------------------------------------------------------------------------- /public/icons/favicon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mixmoe/HikariSearch/HEAD/public/icons/favicon-128x128.png -------------------------------------------------------------------------------- /src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | export default async () => ({ 2 | 'zh-CN': (await import('./locales/zh-CN.json')).default, 3 | }); 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /src-bex/www 3 | /src-capacitor 4 | /src-cordova 5 | /.quasar 6 | /node_modules 7 | .eslintrc.js 8 | babel.config.js 9 | /src-ssr -------------------------------------------------------------------------------- /functions/api/routes/index.ts: -------------------------------------------------------------------------------- 1 | import './SauceNAO'; 2 | import './IqDB'; 3 | import './ascii2d'; 4 | import './E-Hentai'; 5 | import './TraceMoe'; 6 | -------------------------------------------------------------------------------- /functions/api/router.ts: -------------------------------------------------------------------------------- 1 | import { ThrowableRouter } from 'itty-router-extras'; 2 | 3 | export const router = ThrowableRouter({ base: '/api' }); 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 11 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | interface ProcessEnv { 3 | NODE_ENV: string; 4 | VUE_ROUTER_MODE: 'hash' | 'history' | 'abstract' | undefined; 5 | VUE_ROUTER_BASE: string | undefined; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@quasar/app/tsconfig-preset", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "types": [ 6 | "@cloudflare/workers-types" 7 | ], 8 | "jsx": "preserve" 9 | }, 10 | } -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | plugins: [ 5 | // to edit target browsers: use "browserslist" field in package.json 6 | require('autoprefixer') 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | // Mocks all files ending in `.vue` showing them as plain Vue instances 2 | /* eslint-disable */ 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue'; 5 | const component: DefineComponent<{}, {}, any>; 6 | export default component; 7 | } -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | module.exports = api => { 4 | return { 5 | presets: [ 6 | [ 7 | '@quasar/babel-preset-app', 8 | api.caller(caller => caller && caller.target === 'node') 9 | ? { targets: { node: 'current' } } 10 | : {} 11 | ] 12 | ] 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /src/boot/i18n.ts: -------------------------------------------------------------------------------- 1 | import { boot } from 'quasar/wrappers'; 2 | import { createI18n } from 'vue-i18n'; 3 | 4 | import messages from 'src/i18n'; 5 | 6 | export default boot(async ({ app }) => { 7 | const i18n = createI18n({ 8 | locale: 'en-US', 9 | messages: await messages(), 10 | }); 11 | 12 | // Set i18n instance on app 13 | app.use(i18n); 14 | }); 15 | -------------------------------------------------------------------------------- /src/quasar.d.ts: -------------------------------------------------------------------------------- 1 | // Forces TS to apply `@quasar/app` augmentations of `quasar` package 2 | // Removing this would break `quasar/wrappers` imports as those typings are declared 3 | // into `@quasar/app` 4 | // As a side effect, since `@quasar/app` reference `quasar` to augment it, 5 | // this declaration also apply `quasar` own 6 | // augmentations (eg. adds `$q` into Vue component context) 7 | /// 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "editorconfig.editorconfig", 6 | "johnsoncodehk.volar", 7 | "wayou.vscode-todo-highlight", 8 | "scaukk.i18n-downloader" 9 | ], 10 | "unwantedRecommendations": [ 11 | "octref.vetur", 12 | "hookyqr.beautify", 13 | "dbaeumer.jshint", 14 | "ms-vscode.vscode-typescript-tslint-plugin" 15 | ] 16 | } -------------------------------------------------------------------------------- /src/boot/axios.ts: -------------------------------------------------------------------------------- 1 | import { boot } from 'quasar/wrappers'; 2 | import type { AxiosInstance } from 'axios'; 3 | import { axios, API } from 'src/api'; 4 | 5 | declare module '@vue/runtime-core' { 6 | interface ComponentCustomProperties { 7 | $axios: AxiosInstance; 8 | $api: typeof API; 9 | } 10 | } 11 | 12 | export default boot(({ app }) => { 13 | app.config.globalProperties.$axios = axios; 14 | app.config.globalProperties.$api = API; 15 | }); 16 | 17 | export { API as api }; 18 | -------------------------------------------------------------------------------- /functions/proxy.ts: -------------------------------------------------------------------------------- 1 | export const onRequestGet: PagesFunction = async ({ request }) => { 2 | const { searchParams } = new URL(request.url); 3 | const imageUrl = searchParams.get('url'); 4 | if (!imageUrl) { 5 | return new Response(null, { status: 400 }); 6 | } 7 | const { body, headers } = await fetch(imageUrl); 8 | if (!headers.get('Content-Type')?.startsWith('image/')) { 9 | return new Response(null, { status: 500 }); 10 | } 11 | return new Response(body, { headers }); 12 | }; 13 | -------------------------------------------------------------------------------- /src/router/routes.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router'; 2 | 3 | const routes: RouteRecordRaw[] = [ 4 | { 5 | path: '/', 6 | component: () => import('layouts/MainLayout.vue'), 7 | children: [{ path: '', component: () => import('pages/Index.vue') }], 8 | }, 9 | 10 | // Always leave this as last one, 11 | // but you can also remove it 12 | { 13 | path: '/:catchAll(.*)*', 14 | component: () => import('pages/Error404.vue'), 15 | }, 16 | ]; 17 | 18 | export default routes; 19 | -------------------------------------------------------------------------------- /functions/api/[[path]].ts: -------------------------------------------------------------------------------- 1 | import './patch'; 2 | import { error } from 'itty-router-extras'; 3 | 4 | export const onRequest: PagesFunction = async ({ request, ...extra }) => { 5 | const { router } = await import('./router').then( 6 | async (module) => (await import('./routes'), module) 7 | ); 8 | 9 | try { 10 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 11 | const response: Response | undefined = await router.handle(request, extra); 12 | return response ?? error(404, 'not found'); 13 | } catch (err) { 14 | return error(500, (err as Error).message); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .thumbs.db 3 | node_modules 4 | 5 | # Quasar core related directories 6 | .quasar 7 | /dist 8 | 9 | # Cordova related directories and files 10 | /src-cordova/node_modules 11 | /src-cordova/platforms 12 | /src-cordova/plugins 13 | /src-cordova/www 14 | 15 | # Capacitor related directories and files 16 | /src-capacitor/www 17 | /src-capacitor/node_modules 18 | 19 | # BEX related directories and files 20 | /src-bex/www 21 | /src-bex/js/core 22 | 23 | # Log files 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # Editor directories and files 29 | .idea 30 | *.suo 31 | *.ntvs* 32 | *.njsproj 33 | *.sln 34 | -------------------------------------------------------------------------------- /src/pages/Error404.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 32 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.bracketPairColorization.enabled": true, 3 | "editor.guides.bracketPairs": true, 4 | "editor.formatOnSave": true, 5 | "editor.defaultFormatter": "esbenp.prettier-vscode", 6 | "editor.codeActionsOnSave": [ 7 | "source.fixAll.eslint" 8 | ], 9 | "eslint.validate": [ 10 | "javascript", 11 | "javascriptreact", 12 | "typescript", 13 | "vue" 14 | ], 15 | "typescript.tsdk": "node_modules/typescript/lib", 16 | "cSpell.words": [ 17 | "Hentai", 18 | "Hikari" 19 | ], 20 | "i18n-ally.localesPaths": [ 21 | "src/i18n" 22 | ], 23 | "i18n-ally.namespace": true, 24 | "i18n-ally.enabledParsers": [ 25 | "json" 26 | ], 27 | "i18n-ally.pathMatcher": "locales/{locale}.{ext}", 28 | "i18n-ally.keystyle": "flat", 29 | "i18n-ally.sourceLanguage": "en-US" 30 | } -------------------------------------------------------------------------------- /src/css/quasar.variables.scss: -------------------------------------------------------------------------------- 1 | // Quasar SCSS (& Sass) Variables 2 | // -------------------------------------------------- 3 | // To customize the look and feel of this app, you can override 4 | // the Sass/SCSS variables found in Quasar's source Sass/SCSS files. 5 | 6 | // Check documentation for full list of Quasar variables 7 | 8 | // Your own variables (that are declared here) and Quasar's own 9 | // ones will be available out of the box in your .vue/.scss/.sass files 10 | 11 | // It's highly recommended to change the default colors 12 | // to match your app's branding. 13 | // Tip: Use the "Theme Builder" on Quasar's documentation website. 14 | 15 | $primary : #1976D2; 16 | $secondary : #26A69A; 17 | $accent : #9C27B0; 18 | 19 | $dark : #1D1D1D; 20 | 21 | $positive : #21BA45; 22 | $negative : #C10015; 23 | $info : #31CCEC; 24 | $warning : #F2C037; 25 | -------------------------------------------------------------------------------- /src/index.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= productName %> 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /functions/api/patch.ts: -------------------------------------------------------------------------------- 1 | import '@ssttevee/cfw-formdata-polyfill'; 2 | import * as poly from '@ssttevee/blob-ponyfill'; 3 | 4 | export const BuiltinBlob = Blob, 5 | BuiltinFile = File, 6 | BuiltinFormData = FormData; 7 | 8 | class OverrideFormData extends BuiltinFormData { 9 | public append(name: string, value: string | Blob, filename?: string): void { 10 | const reader = new poly.FileReaderSync(); 11 | 12 | if (value instanceof poly.File) { 13 | value = new BuiltinFile( 14 | [reader.readAsArrayBuffer(value)], 15 | filename ?? value.name, 16 | { 17 | type: value.type, 18 | lastModified: value.lastModified, 19 | } 20 | ); 21 | } else if (value instanceof poly.Blob) { 22 | value = new BuiltinBlob([reader.readAsArrayBuffer(value)]); 23 | } 24 | 25 | return filename 26 | ? super.append(name, value, filename) 27 | : super.append(name, value); 28 | } 29 | } 30 | 31 | globalThis.File = poly.File as typeof File; 32 | globalThis.Blob = poly.Blob as typeof Blob; 33 | globalThis.FormData = OverrideFormData as typeof FormData; 34 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { route } from 'quasar/wrappers'; 2 | import { 3 | createMemoryHistory, 4 | createRouter, 5 | createWebHashHistory, 6 | createWebHistory, 7 | } from 'vue-router'; 8 | import routes from './routes'; 9 | 10 | /* 11 | * If not building with SSR mode, you can 12 | * directly export the Router instantiation; 13 | * 14 | * The function below can be async too; either use 15 | * async/await or return a Promise which resolves 16 | * with the Router instance. 17 | */ 18 | 19 | export default route(function (/* { store, ssrContext } */) { 20 | const createHistory = process.env.SERVER 21 | ? createMemoryHistory 22 | : (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory); 23 | 24 | const Router = createRouter({ 25 | scrollBehavior: () => ({ left: 0, top: 0 }), 26 | routes, 27 | 28 | // Leave this as is and make changes in quasar.conf.js instead! 29 | // quasar.conf.js -> build -> vueRouterMode 30 | // quasar.conf.js -> build -> publicPath 31 | history: createHistory( 32 | process.env.MODE === 'ssr' ? void 0 : process.env.VUE_ROUTER_BASE 33 | ), 34 | }); 35 | 36 | return Router; 37 | }); 38 | -------------------------------------------------------------------------------- /src/api/types.ts: -------------------------------------------------------------------------------- 1 | import type * as SauceNAO from 'functions/api/routes/SauceNAO'; 2 | import type * as IqDB from 'functions/api/routes/IqDB'; 3 | import type * as ascii2d from 'functions/api/routes/ascii2d'; 4 | import type * as EHentai from 'functions/api/routes/E-Hentai'; 5 | import type * as TraceMoe from 'functions/api/routes/TraceMoe'; 6 | import type Schema from 'schemastery'; 7 | 8 | type RequestSchemaType = T extends Schema ? S : never; 9 | 10 | export type SauceNAOParseResult = ReturnType; 11 | export type IqDBParseResult = ReturnType; 12 | export type ascii2dParseResult = ReturnType; 13 | export type EHentaiParseResult = ReturnType; 14 | export type TraceMoeParseResult = ReturnType; 15 | 16 | export type SauceNAORequestSchema = RequestSchemaType; 17 | export type IqDBRequestSchema = RequestSchemaType; 18 | export type ascii2dRequestSchema = RequestSchemaType; 19 | export type EHentaiRequestSchema = RequestSchemaType; 20 | export type TraceMoeRequestSchema = RequestSchemaType; 21 | -------------------------------------------------------------------------------- /src/i18n/locales/zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "Search on ascii2d": "在 ascii2d 上搜索", 3 | "Search Options": "搜索选项", 4 | "Search mode": "搜索模式", 5 | "Color Mode": "色彩检索", 6 | "BOVW Mode": "特征检索", 7 | "Submit": "提交", 8 | "Image source:": "图片来源:", 9 | "Image author:": "图片作者:", 10 | "Search Image": "图片搜索", 11 | "Select an image": "选择一张图片", 12 | "Search on SauceNAO": "在 SauceNAO 上搜索", 13 | "Hide sensitive content": "隐藏敏感内容", 14 | "Image similarity:": "图片相似度:", 15 | "More sources:": "更多来源:", 16 | "Search on IqDB": "在 IqDB 上搜索", 17 | "Image databases": "图片数据来源", 18 | "Search without color": "搜索时去除颜色", 19 | "Image ranking level:": "图片评级:", 20 | "Search on EHentai": "在 EHentai 上搜索", 21 | "Search site": "搜索来源站", 22 | "Search cover only": "仅搜索封面", 23 | "Search deleted content": "搜索已删除的内容", 24 | "Search similar content": "搜索相似内容", 25 | "Image tags:": "图片标签:", 26 | "Search on TraceMoe": "在 TraceMoe 上搜索", 27 | "Cut image borders": "裁剪图片边缘", 28 | "Episode:": "章节:", 29 | "Unknown episode": "未知章节", 30 | "English name:": "英文名:", 31 | "Romaji name:": "罗马字名:", 32 | "Hikari Anime Image Search": "Hikari 动漫图片聚合搜索", 33 | "Choose language": "选择语言", 34 | "Please select a image to search": "请选择一张图片来进行搜索", 35 | "Please select a site to start searching": "请选择你要进行搜索的站点", 36 | "Paste Image": "粘贴图片", 37 | "Do you want to paste this image": "你想粘贴这张图片吗?" 38 | } 39 | -------------------------------------------------------------------------------- /src/layouts/MainLayout.vue: -------------------------------------------------------------------------------- 1 | 49 | 62 | -------------------------------------------------------------------------------- /functions/api/routes/TraceMoe.ts: -------------------------------------------------------------------------------- 1 | import { json, StatusError } from 'itty-router-extras'; 2 | import Schema from 'schemastery'; 3 | import { router } from '../router'; 4 | import { request, validate } from '../utils'; 5 | 6 | export const BASE_URL = 'https://api.trace.moe'; 7 | 8 | export interface TraceMoeResponse { 9 | frameCount: number; 10 | error: string; 11 | result: { 12 | anilist?: { 13 | id: number; 14 | idMal: number; 15 | title: { native: string; romaji: string | null; english: string | null }; 16 | synonyms: string[]; 17 | isAdult: boolean; 18 | }; 19 | filename: string; 20 | episode: null | number; 21 | from: number; 22 | to: number; 23 | similarity: number; 24 | video: string; 25 | image: string; 26 | }[]; 27 | } 28 | 29 | export function parse({ error, result }: TraceMoeResponse) { 30 | if (error.length > 0) throw new StatusError(502, error); 31 | return result 32 | .map((result) => ({ 33 | preview: result.image, 34 | similarity: result.similarity * 100, 35 | name: result.anilist?.title, 36 | nsfw: result.anilist?.isAdult, 37 | from: result.from * 1000, 38 | to: result.to * 1000, 39 | episode: result.episode, 40 | file: result.filename, 41 | })) 42 | .sort((a, b) => a.similarity - b.similarity) 43 | .reverse(); 44 | } 45 | 46 | export const schema = Schema.object({ 47 | image: Schema.is(File).required(), 48 | cutBorders: Schema.boolean().default(true), 49 | }); 50 | 51 | router.post('/TraceMoe', async (req: Request) => { 52 | const { image, cutBorders } = await validate(req, schema); 53 | 54 | const form = new FormData(), 55 | url = new URL('/search', BASE_URL); 56 | form.append('image', image!); 57 | url.searchParams.append('anilistInfo', '1'); 58 | if (cutBorders) url.searchParams.append('cutBorders', '1'); 59 | 60 | const response = await request 61 | .post(url, form) 62 | .then((res) => res.json()); 63 | 64 | return json(parse(response)); 65 | }); 66 | -------------------------------------------------------------------------------- /functions/api/routes/SauceNAO.ts: -------------------------------------------------------------------------------- 1 | import * as cheerio from 'cheerio'; 2 | import * as _ from 'lodash'; 3 | import { json } from 'itty-router-extras'; 4 | import Schema from 'schemastery'; 5 | import { router } from '../router'; 6 | import { request, validate } from '../utils'; 7 | 8 | export const BASE_URL = 'https://saucenao.com'; 9 | 10 | export function parse(body: string) { 11 | const $ = cheerio.load(body, { decodeEntities: true }); 12 | return _.map($('.result'), (result) => { 13 | const image = $('.resultimage img', result), 14 | title = $('.resulttitle', result), 15 | similarity = $('.resultsimilarityinfo', result), 16 | misc = $('.resultmiscinfo > a', result), 17 | content = $('.resultcontentcolumn > *', result); 18 | if (title.length <= 0) return; 19 | const hiddenImage = image.attr('data-src2'), 20 | imageUrl = image.attr('data-src') ?? hiddenImage ?? image.attr('src') ?? ''; 21 | return { 22 | image: new URL(imageUrl, BASE_URL).toString(), 23 | hidden: !!hiddenImage, 24 | title: title.text(), 25 | similarity: parseFloat(similarity.text()), 26 | misc: _.map(misc, (m) => m.attribs.href), 27 | content: _.map(content, (element) => ({ 28 | text: $(element).text(), 29 | link: element.attribs.href as string | undefined, 30 | })).filter(({ text }) => text.length > 0), 31 | }; 32 | }) 33 | .filter((v: T | undefined): v is T => v !== undefined) 34 | .sort((a, b) => a.similarity - b.similarity) 35 | .reverse(); 36 | } 37 | 38 | export const schema = Schema.object({ 39 | hide: Schema.boolean().default(true), 40 | image: Schema.is(File).required(), 41 | }); 42 | 43 | router.post('/SauceNAO', async (req: Request) => { 44 | const { hide, image } = await validate(req, schema); 45 | 46 | const form = new FormData(); 47 | form.append('file', image!); 48 | if (hide) form.append('hide', '3'); 49 | 50 | const url = new URL('/search.php', BASE_URL); 51 | const response = await request.post(url, form).then((res) => res.text()); 52 | 53 | return json(parse(response)); 54 | }); 55 | -------------------------------------------------------------------------------- /functions/api/routes/ascii2d.ts: -------------------------------------------------------------------------------- 1 | import * as cheerio from 'cheerio'; 2 | import { json } from 'itty-router-extras'; 3 | import * as _ from 'lodash'; 4 | import Schema from 'schemastery'; 5 | import { router } from '../router'; 6 | import { request, validate } from '../utils'; 7 | 8 | export const BASE_URL = 'https://ascii2d.obfs.dev/'; 9 | 10 | export function parse(body: string) { 11 | const $ = cheerio.load(body, { decodeEntities: true }); 12 | return _.map($('.item-box'), (item) => { 13 | const detail = $('.detail-box', item), 14 | hash = $('.hash', item), 15 | info = $('.info-box > .text-muted', item), 16 | [image] = $('.image-box > img', item); 17 | 18 | const [source, author] = $('a[rel=noopener]', detail); 19 | 20 | if (!source && !author) return; 21 | 22 | return { 23 | hash: hash.text(), 24 | info: info.text(), 25 | image: new URL( 26 | image.attribs['src'] ?? image.attribs['data-cfsrc'], 27 | BASE_URL 28 | ).toString(), 29 | source: source 30 | ? { link: source.attribs.href, text: $(source).text() } 31 | : undefined, 32 | author: author 33 | ? { link: author.attribs.href, text: $(author).text() } 34 | : undefined, 35 | }; 36 | }).filter((v: T | undefined): v is T => v !== undefined); 37 | } 38 | 39 | export const schema = Schema.object({ 40 | type: Schema.union(['color', 'bovw'] as const).default('color'), 41 | image: Schema.is(File).required(), 42 | }); 43 | 44 | router.post('/ascii2d', async (req: Request) => { 45 | const { type, image } = await validate(req, schema); 46 | 47 | const form = new FormData(); 48 | form.append('file', image!); 49 | 50 | const url = new URL('/search/file', BASE_URL); 51 | const colorResponse = await request.post(url, form); 52 | 53 | let response: string; 54 | if (type === 'color') { 55 | response = await colorResponse.text(); 56 | } else { 57 | const bovwUrl = colorResponse.url.replace('/color/', '/bovw/'); 58 | response = await request.get(bovwUrl).then((res) => res.text()); 59 | } 60 | return json(parse(response)); 61 | }); 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hikari-search", 3 | "version": "0.0.1", 4 | "description": "A collection of anime image search engine, based on Cloudflare Pages Function.", 5 | "productName": "Hikari Anime Image Search", 6 | "author": "Mix <32300164+mnixry@users.noreply.github.com>", 7 | "repository": "github:mixmoe/HikariSearch", 8 | "license": "LGPL-3.0", 9 | "scripts": { 10 | "lint": "eslint --ext .js,.ts,.vue ./", 11 | "format": "prettier --write \"**/*.{js,ts,vue,scss,html,md,json}\" --ignore-path .gitignore", 12 | "dev:worker": "wrangler pages dev ./public", 13 | "dev:front": "quasar dev" 14 | }, 15 | "dependencies": { 16 | "@quasar/extras": "^1.0.0", 17 | "@ssttevee/cfw-formdata-polyfill": "^0.2.1", 18 | "axios": "^0.27.2", 19 | "cheerio": "^1.0.0-rc.10", 20 | "core-js": "^3.6.5", 21 | "itty-router": "^2.4.10", 22 | "itty-router-extras": "^0.4.2", 23 | "lodash": "^4.17.21", 24 | "quasar": "^2.0.0", 25 | "schemastery": "^2.4.2", 26 | "vue": "^3.0.0", 27 | "vue-i18n": "^9.0.0", 28 | "vue-router": "^4.0.0" 29 | }, 30 | "devDependencies": { 31 | "@babel/eslint-parser": "^7.13.14", 32 | "@cloudflare/workers-types": "^3.4.0", 33 | "@quasar/app": "^3.0.0", 34 | "@types/itty-router-extras": "^0.4.0", 35 | "@types/lodash": "^4.14.178", 36 | "@types/node": "^12.20.21", 37 | "@typescript-eslint/eslint-plugin": "^5.10.0", 38 | "@typescript-eslint/parser": "^5.10.0", 39 | "eslint": "^7.14.0", 40 | "eslint-config-prettier": "^8.1.0", 41 | "eslint-plugin-vue": "^7.0.0", 42 | "prettier": "^2.5.1", 43 | "wrangler": "^0.0.19" 44 | }, 45 | "browserslist": [ 46 | "last 10 Chrome versions", 47 | "last 10 Firefox versions", 48 | "last 4 Edge versions", 49 | "last 7 Safari versions", 50 | "last 8 Android versions", 51 | "last 8 ChromeAndroid versions", 52 | "last 8 FirefoxAndroid versions", 53 | "last 10 iOS versions", 54 | "last 5 Opera versions" 55 | ], 56 | "engines": { 57 | "node": ">= 12.22.1", 58 | "npm": ">= 6.13.4", 59 | "yarn": ">= 1.21.1" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import _axios from 'axios'; 2 | import { Notify } from 'quasar'; 3 | import type * as types from './types'; 4 | 5 | export const axios = _axios.create({ 6 | baseURL: '/api/', 7 | timeout: 24 * 1000, 8 | responseType: 'json', 9 | headers: { 10 | 'Content-Type': 'multipart/form-data', 11 | }, 12 | }); 13 | 14 | export interface ErrorResponse { 15 | status?: number; 16 | error?: string; 17 | } 18 | 19 | // eslint-disable-next-line @typescript-eslint/no-namespace 20 | export namespace API { 21 | function factory(path: string): (schema: R) => Promise { 22 | return async (schema: R) => 23 | await axios 24 | .post(path, schema) 25 | .then((response) => response.data) 26 | .catch((error: Error) => { 27 | const response = _axios.isAxiosError(error) 28 | ? error.response?.data 29 | : undefined; 30 | Notify.create({ 31 | message: 32 | response?.status && response.error 33 | ? `${response.status} - ${response.error}` 34 | : error.message, 35 | color: 'negative', 36 | position: 'bottom', 37 | }); 38 | throw error; 39 | }); 40 | } 41 | 42 | export const SauceNAO = factory< 43 | types.SauceNAORequestSchema, 44 | types.SauceNAOParseResult 45 | >('./SauceNAO'); 46 | 47 | export const IqDB = factory< 48 | types.IqDBRequestSchema, // 49 | types.IqDBParseResult 50 | >('./IqDB'); 51 | 52 | export const ascii2d = factory< 53 | types.ascii2dRequestSchema, 54 | types.ascii2dParseResult 55 | >('./ascii2d'); 56 | 57 | export const EHentai = factory< 58 | types.EHentaiRequestSchema, 59 | types.EHentaiParseResult 60 | >('./E-Hentai'); 61 | 62 | export const TraceMoe = factory< 63 | types.TraceMoeRequestSchema, 64 | types.TraceMoeParseResult 65 | >('./TraceMoe'); 66 | } 67 | 68 | export function proxy(url: string) { 69 | const requestUrl = new URL('/proxy', window.location.href); 70 | requestUrl.searchParams.set('url', url); 71 | return requestUrl.toString(); 72 | } 73 | -------------------------------------------------------------------------------- /functions/api/routes/IqDB.ts: -------------------------------------------------------------------------------- 1 | import * as cheerio from 'cheerio'; 2 | import { json } from 'itty-router-extras'; 3 | import * as _ from 'lodash'; 4 | import Schema from 'schemastery'; 5 | import { router } from '../router'; 6 | import { request, validate } from '../utils'; 7 | 8 | export const BASE_URL = 'https://iqdb.org/'; 9 | 10 | export enum IqDBServices { 11 | danbooru = 1, 12 | konachan = 2, 13 | yandere = 3, 14 | gelbooru = 4, 15 | sankaku_channel = 5, 16 | e_shuushuu = 6, 17 | zerochan = 11, 18 | anime_pictures = 13, 19 | } 20 | 21 | export function parse(body: string) { 22 | const $ = cheerio.load(body); 23 | return _.map($('table'), (result) => { 24 | const content = $(result).text(), 25 | [link] = $('td.image > a', result), 26 | [image] = $('td.image img', result); 27 | if (!link) return; 28 | const [, similarity] = content.match(/(\d+%)\s*similarity/) ?? [], 29 | [, level] = content.match(/\[(\w+)\]/) ?? [], 30 | [, resolution] = content.match(/(\d+×\d+)/) ?? []; 31 | 32 | return { 33 | url: new URL(link.attribs.href, BASE_URL).toString(), 34 | image: new URL(image.attribs.src, BASE_URL).toString(), 35 | similarity: parseFloat(similarity), 36 | resolution: resolution, 37 | level: level, 38 | }; 39 | }) 40 | .filter((v: T | undefined): v is T => v !== undefined) 41 | .sort((a, b) => a.similarity - b.similarity) 42 | .reverse(); 43 | } 44 | 45 | export const schema = Schema.object({ 46 | services: Schema.array( 47 | Schema.transform( 48 | Schema.union( 49 | Object.values(IqDBServices).filter( 50 | (s: T | number): s is T => typeof s === 'string' 51 | ) as (keyof typeof IqDBServices)[] 52 | ), 53 | (v) => IqDBServices[v] 54 | ) 55 | ), 56 | discolor: Schema.boolean().default(false), 57 | image: Schema.is(File).required(), 58 | }); 59 | 60 | router.post('/IqDB', async (req: Request) => { 61 | const { services, discolor, image } = await validate(req, schema); 62 | 63 | const form = new FormData(); 64 | form.append('file', image!); 65 | if (services) services.forEach((s) => form.append('service[]', s.toString())); 66 | if (discolor) form.append('forcegray', 'on'); 67 | 68 | const response = await request.post(BASE_URL, form).then((res) => res.text()); 69 | 70 | return json(parse(response)); 71 | }); 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hikari 动漫图片聚合搜索 2 | 3 | [**English README**](./README_en.md) 4 | 5 | **_一款动漫图片搜索引擎聚合网站, 基于 Cloudflare 提供的 Pages Function_** 6 | 7 | ![screenshot-mockup](https://user-images.githubusercontent.com/32300164/158019817-a90109c1-96ec-406d-9a8c-8b2bcf7145fb.png) 8 | 9 | ## 亮点 10 | 11 | - 集成了多款动漫图片搜索引擎, 包括: 12 | 13 | - [SauceNAO](https://saucenao.com/) 14 | - [IqDB](https://iqdb.org/) 15 | - [ascii2d](https://ascii2d.net/) 16 | - [E-Hentai](https://e-hentai.org/) 17 | - [TraceMoe](https://trace.moe/) 18 | 19 | 有了它, 你就可以在这个网站上搜索到你想要的动漫图片来源, 无需同时打开这么多网页 20 | 21 | - 现代化的交互界面, 基于 [Quasar](https://quasar.dev) 和 Vue.js 22 | 23 | - 基于 Cloudflare 的无服务器云函数, 稳定, 快速, 可靠 (并且完全免费) 24 | 25 | ## 演示站点 26 | 27 | 请访问[演示站点](https://hikari.obfs.dev)来进行使用 28 | 29 | ## 进行开发 30 | 31 | ### 配置开发环境 32 | 33 | - 你需要: 34 | - Node.js 14+ 35 | - yarn 36 | - VSCode 37 | 38 | #### 安装依赖 39 | 40 | ```bash 41 | yarn install 42 | ``` 43 | 44 | #### 启动开发服务 45 | 46 | - 你需要在两个终端中同时执行这两个指令 47 | 48 | ```bash 49 | yarn run dev:worker 50 | ``` 51 | 52 | ```bash 53 | yarn run dev:front 54 | ``` 55 | 56 | 稍等一会, 然后在浏览器中打开由 Quasar 提供的开发模式服务器: `http://localhost:8080`来开始你的开发之旅 57 | 58 | ### 部署到 Cloudflare 59 | 60 | 该项目部署流程完全和一般的 Pages 部署流程几乎一致, 你总是可以参考 [Cloudflare Pages](https://developers.cloudflare.com/pages/get-started/) 文档来获得更多信息 61 | 62 | - 这里给出你几个需要配置的值: 63 | - 构建命令: `quasar build -P` 64 | - 公开目录: `dist/spa` 65 | 66 | ## 鸣谢 67 | 68 | - 提供上述搜索服务的网站们 69 | - Cloudflare Pages, 本项目基于的对象 70 | - Quasar, 交互界面的框架 71 | - [Schemastery](https://github.com/Shigma/schemastery), 一款轻量字段类型校验库, 用于 API 的传参验证 72 | 73 | ## 开源许可 74 | 75 | 本项目以 [LGPL-3.0](./LICENSE) 许可开源 76 | 77 | 78 | 79 | A site collects many anime image search engines. 80 | Copyright (C) 2022 Mix 81 | 82 | This program is free software: you can redistribute it and/or modify 83 | it under the terms of the GNU General Public License as published by 84 | the Free Software Foundation, either version 3 of the License, or 85 | (at your option) any later version. 86 | 87 | This program is distributed in the hope that it will be useful, 88 | but WITHOUT ANY WARRANTY; without even the implied warranty of 89 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 90 | GNU General Public License for more details. 91 | 92 | You should have received a copy of the GNU General Public License 93 | along with this program. If not, see http://www.gnu.org/licenses/. 94 | 95 | 96 | -------------------------------------------------------------------------------- /functions/api/routes/E-Hentai.ts: -------------------------------------------------------------------------------- 1 | import * as cheerio from 'cheerio'; 2 | import { json } from 'itty-router-extras'; 3 | import * as _ from 'lodash'; 4 | import Schema from 'schemastery'; 5 | import { router } from '../router'; 6 | import { request, validate } from '../utils'; 7 | 8 | export const BASE_URLs = { 9 | eh: new URL('https://upld.e-hentai.org/image_lookup.php'), 10 | ex: new URL('https://exhentai.org/upld/image_lookup.php'), 11 | }; 12 | 13 | export function parse(body: string) { 14 | const $ = cheerio.load(body); 15 | return _.map($('.glte > tbody > tr'), (result) => { 16 | const title = $('.glink', result), 17 | [image] = $('.gl1e img', result), 18 | [link] = $('.gl1e a', result), 19 | type = $('.gl3e .cn', result), 20 | date = $('.gl3e [id^=posted]', result), 21 | tags = $('.gl4e table td > div[class]', result); 22 | return { 23 | title: title.text(), 24 | image: image.attribs.src, 25 | link: link.attribs.href, 26 | type: type.text().toUpperCase(), 27 | date: date.text(), 28 | tags: _.map(tags, (tag) => $(tag).text()), 29 | }; 30 | }); 31 | } 32 | 33 | export const schema = Schema.object({ 34 | site: Schema.transform( 35 | Schema.union(Object.keys(BASE_URLs) as (keyof typeof BASE_URLs)[]), 36 | (v) => BASE_URLs[v] 37 | ), 38 | cover: Schema.boolean().default(false), 39 | deleted: Schema.boolean().default(false), 40 | similar: Schema.boolean().default(true), 41 | image: Schema.is(File).required(), 42 | }); 43 | 44 | router.post('/E-Hentai', async (req: Request, { env }) => { 45 | const { site, cover, deleted, similar, image } = await validate(req, schema); 46 | 47 | const form = new FormData(); 48 | form.append('sfile', image!); 49 | form.append('f_sfile', 'search'); 50 | if (cover) form.append('fs_covers', 'on'); 51 | if (similar) form.append('fs_similar', 'on'); 52 | if (deleted) form.append('fs_exp', 'on'); 53 | 54 | const { EH_COOKIE } = env as { EH_COOKIE?: string }; 55 | const cookies = new Map( 56 | (EH_COOKIE?.split(';') ?? []) 57 | .map( 58 | (cookie) => cookie.trim().split('=', 1) as [string, string | undefined] 59 | ) 60 | .map(([key, value]) => [key, value ?? '']) 61 | ); 62 | cookies.set('sl', 'dm_2'); 63 | const cookieStr = [...cookies.entries()] 64 | .map(([k, v]) => `${k}=${v}`) 65 | .join('; '); 66 | 67 | const response = await request 68 | .post(site ?? BASE_URLs['eh'], form, { headers: { Cookie: cookieStr } }) 69 | .then((res) => res.text()); 70 | return json(parse(response)); 71 | }); 72 | -------------------------------------------------------------------------------- /README_en.md: -------------------------------------------------------------------------------- 1 | # Hikari anime image aggregation search 2 | 3 | **_A collection of anime image search engines, based on Cloudflare Pages Function._** 4 | 5 | ![screenshot-mockup](https://user-images.githubusercontent.com/32300164/158019817-a90109c1-96ec-406d-9a8c-8b2bcf7145fb.png) 6 | 7 | ## Highlights 8 | 9 | - Integrated with several anime image search engines, including: 10 | 11 | - [SauceNAO](https://saucenao.com/) 12 | - [IqDB](https://iqdb.org/) 13 | - [ascii2d](https://ascii2d.net/) 14 | - [E-Hentai](https://e-hentai.org/) 15 | - [TraceMoe](https://trace.moe/) 16 | 17 | With it, you can search for your desired anime image source on this website, WITHOUT having to open so many pages at once! 18 | 19 | - Modern user interface, based on Quasar and Vue.js 20 | 21 | - Based on Cloudflare Pages Function, stable, fast, reliable (and completely free) 22 | 23 | ## Demo site 24 | 25 | Please visit the [demo site](https://hikari.obfs.dev) to use it 26 | 27 | ## Develop 28 | 29 | ### Configuring the development environment 30 | 31 | - You need: 32 | - Node.js 14+ 33 | - yarn 34 | - VSCode 35 | 36 | #### Install the dependencies 37 | 38 | ```bash 39 | yarn install 40 | ``` 41 | 42 | #### Start development services 43 | 44 | - You need to execute both commands in both terminals 45 | 46 | ```bash 47 | yarn run dev:worker 48 | ``` 49 | 50 | ```bash 51 | yarn run dev:front 52 | ``` 53 | 54 | Wait a moment, then open the development mode server provided by Quasar in your browser: `http://localhost:8080` to start your development journey 55 | 56 | ### Deploying to Cloudflare 57 | 58 | The project deployment process is almost identical to the general Pages deployment process. 59 | You can always refer to the [Cloudflare Pages](https://developers.cloudflare.com/pages/get-started/) documentation for more information. 60 | 61 | - Here are some of the values you may need to configure: 62 | - Build command: `quasar build -P` 63 | - Public directory: `dist/spa` 64 | 65 | ## Acknowledgements 66 | 67 | - The sites that provide the above search services 68 | - Cloudflare Pages, the base of this project 69 | - Quasar, a framework for user interfaces 70 | - [Schemastery](https://github.com/Shigma/schemastery), a lightweight schema type validation library for API schema validation 71 | 72 | ## License 73 | 74 | This project is licensed under the [LGPL-3.0](./LICENSE) license. 75 | 76 | 77 | 78 | A site collects many anime image search engines. 79 | Copyright (C) 2022 Mix 80 | 81 | This program is free software: you can redistribute it and/or modify 82 | it under the terms of the GNU General Public License as published by 83 | the Free Software Foundation, either version 3 of the License, or 84 | (at your option) any later version. 85 | 86 | This program is distributed in the hope that it will be useful, 87 | but WITHOUT ANY WARRANTY; without even the implied warranty of 88 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 89 | GNU General Public License for more details. 90 | 91 | You should have received a copy of the GNU General Public License 92 | along with this program. If not, see http://www.gnu.org/licenses/. 93 | 94 | 95 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | module.exports = { 3 | // https://eslint.org/docs/user-guide/configuring#configuration-cascading-and-hierarchy 4 | // This option interrupts the configuration hierarchy at this file 5 | // Remove this if you have an higher level ESLint config file (it usually happens into a monorepos) 6 | root: true, 7 | 8 | // https://eslint.vuejs.org/user-guide/#how-to-use-a-custom-parser 9 | // Must use parserOptions instead of "parser" to allow vue-eslint-parser to keep working 10 | // `parser: 'vue-eslint-parser'` is already included with any 'plugin:vue/**' config and should be omitted 11 | parserOptions: { 12 | // https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/parser#configuration 13 | // https://github.com/TypeStrong/fork-ts-checker-webpack-plugin#eslint 14 | // Needed to make the parser take into account 'vue' files 15 | extraFileExtensions: ['.vue'], 16 | parser: '@typescript-eslint/parser', 17 | project: resolve(__dirname, './tsconfig.json'), 18 | tsconfigRootDir: __dirname, 19 | ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features 20 | sourceType: 'module' // Allows for the use of imports 21 | }, 22 | 23 | env: { 24 | browser: true 25 | }, 26 | 27 | // Rules order is important, please avoid shuffling them 28 | extends: [ 29 | // Base ESLint recommended rules 30 | // 'eslint:recommended', 31 | 32 | // https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin#usage 33 | // ESLint typescript rules 34 | 'plugin:@typescript-eslint/recommended', 35 | // consider disabling this class of rules if linting takes too long 36 | 'plugin:@typescript-eslint/recommended-requiring-type-checking', 37 | 38 | // Uncomment any of the lines below to choose desired strictness, 39 | // but leave only one uncommented! 40 | // See https://eslint.vuejs.org/rules/#available-rules 41 | 'plugin:vue/vue3-essential', // Priority A: Essential (Error Prevention) 42 | // 'plugin:vue/vue3-strongly-recommended', // Priority B: Strongly Recommended (Improving Readability) 43 | // 'plugin:vue/vue3-recommended', // Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead) 44 | 45 | // https://github.com/prettier/eslint-config-prettier#installation 46 | // usage with Prettier, provided by 'eslint-config-prettier'. 47 | 'prettier' 48 | ], 49 | 50 | plugins: [ 51 | // required to apply rules which need type information 52 | '@typescript-eslint', 53 | 54 | // https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-files 55 | // required to lint *.vue files 56 | 'vue', 57 | 58 | // https://github.com/typescript-eslint/typescript-eslint/issues/389#issuecomment-509292674 59 | // Prettier has not been included as plugin to avoid performance impact 60 | // add it as an extension for your IDE 61 | ], 62 | 63 | globals: { 64 | ga: 'readonly', // Google Analytics 65 | cordova: 'readonly', 66 | __statics: 'readonly', 67 | __QUASAR_SSR__: 'readonly', 68 | __QUASAR_SSR_SERVER__: 'readonly', 69 | __QUASAR_SSR_CLIENT__: 'readonly', 70 | __QUASAR_SSR_PWA__: 'readonly', 71 | process: 'readonly', 72 | Capacitor: 'readonly', 73 | chrome: 'readonly' 74 | }, 75 | 76 | // add your custom rules here 77 | rules: { 78 | 'prefer-promise-reject-errors': 'off', 79 | 80 | // TypeScript 81 | quotes: ['warn', 'single', { avoidEscape: true }], 82 | '@typescript-eslint/explicit-function-return-type': 'off', 83 | 84 | // allow debugger during development only 85 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/components/SearchIqDB.vue: -------------------------------------------------------------------------------- 1 | 74 | 112 | -------------------------------------------------------------------------------- /src/components/SearchSauceNAO.vue: -------------------------------------------------------------------------------- 1 | 80 | 102 | -------------------------------------------------------------------------------- /functions/api/utils.ts: -------------------------------------------------------------------------------- 1 | import Schema from 'schemastery'; 2 | import { StatusError } from 'itty-router-extras'; 3 | 4 | export async function validate(request: Request, schema: Schema) { 5 | const obj = {} as { [key: string]: unknown }; 6 | const { searchParams } = new URL(request.url), 7 | formData = await request.formData(); 8 | try { 9 | for (const [key, value] of [ 10 | ...searchParams.entries(), 11 | ...formData.entries(), 12 | ]) { 13 | try { 14 | obj[key] = value instanceof File ? value : JSON.parse(value); 15 | } catch { 16 | obj[key] = value; 17 | } 18 | } 19 | return schema(obj as unknown as S); 20 | } catch (err) { 21 | throw new StatusError(400, (err as TypeError).message); 22 | } 23 | } 24 | 25 | export class RequestMaker { 26 | public readonly requestHooks: RequestMaker.HookFunction[] = []; 27 | public readonly responseHooks: RequestMaker.HookFunction[] = []; 28 | public readonly errorHooks: RequestMaker.HookFunction[] = []; 29 | 30 | constructor(protected readonly base?: RequestMaker.URLType) {} 31 | 32 | public async request( 33 | method: string, 34 | url: RequestMaker.URLType, 35 | body?: BodyInit, 36 | { headers, ...options }: RequestMaker.Options = {} 37 | ): Promise { 38 | let request = new Request(new URL(url, this.base).toString(), { 39 | method, 40 | body, 41 | ...options, 42 | }), 43 | response: Response; 44 | 45 | for (const [key, value] of typeof headers === 'object' 46 | ? headers instanceof Headers 47 | ? headers.entries() 48 | : Object.entries(headers) 49 | : []) 50 | request.headers.set(key, value); 51 | 52 | for (const hook of this.requestHooks.reverse()) 53 | request = await hook(request); 54 | 55 | try { 56 | response = await fetch(request); 57 | } catch (err) { 58 | for (const hook of this.errorHooks.reverse()) err = hook(err as Error); 59 | throw err; 60 | } 61 | 62 | for (const hook of this.responseHooks.reverse()) 63 | response = await hook(response); 64 | 65 | return response; 66 | } 67 | 68 | public get get() { 69 | return this.request.bind(this, 'GET'); 70 | } 71 | 72 | public get post() { 73 | return this.request.bind(this, 'POST'); 74 | } 75 | 76 | public get put() { 77 | return this.request.bind(this, 'PUT'); 78 | } 79 | 80 | public get delete() { 81 | return this.request.bind(this, 'DELETE'); 82 | } 83 | 84 | public get patch() { 85 | return this.request.bind(this, 'PATCH'); 86 | } 87 | 88 | public get head() { 89 | return this.request.bind(this, 'HEAD'); 90 | } 91 | 92 | public get options() { 93 | return this.request.bind(this, 'OPTIONS'); 94 | } 95 | } 96 | 97 | // eslint-disable-next-line @typescript-eslint/no-namespace 98 | export namespace RequestMaker { 99 | export type URLType = string | URL; 100 | 101 | export type HeaderType = Record | Headers; 102 | 103 | export type HookFunction = (request: T) => Promise | T; 104 | 105 | export interface Options { 106 | headers?: HeaderType; 107 | redirect?: RequestRedirect; 108 | fetcher?: Fetcher; 109 | cf?: IncomingRequestCfProperties | RequestInitCfProperties; 110 | signal?: AbortSignal; 111 | responseType?: ResponseType; 112 | } 113 | } 114 | 115 | export const request = new RequestMaker(); 116 | 117 | request.errorHooks.push((err) => { 118 | throw new StatusError(502, `Upstream request failed: ${err.message}`); 119 | }); 120 | 121 | request.responseHooks.push((response) => { 122 | if (!response.ok) 123 | throw new StatusError( 124 | 502, 125 | `Upstream returned error: ${response.status} ${response.statusText}` 126 | ); 127 | return response; 128 | }); 129 | -------------------------------------------------------------------------------- /src/components/SearchAscii2d.vue: -------------------------------------------------------------------------------- 1 | 78 | 105 | -------------------------------------------------------------------------------- /src/components/SearchEHentai.vue: -------------------------------------------------------------------------------- 1 | 78 | 109 | -------------------------------------------------------------------------------- /src/pages/Index.vue: -------------------------------------------------------------------------------- 1 | 70 | 114 | -------------------------------------------------------------------------------- /src/assets/quasar-logo-vertical.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 8 | 10 | 12 | 14 | 15 | -------------------------------------------------------------------------------- /src/components/SearchTraceMoe.vue: -------------------------------------------------------------------------------- 1 | 81 | 113 | 122 | -------------------------------------------------------------------------------- /quasar.conf.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file runs in a Node context (it's NOT transpiled by Babel), so use only 3 | * the ES6 features that are supported by your Node version. https://node.green/ 4 | */ 5 | 6 | // Configuration for your app 7 | // https://quasar.dev/quasar-cli/quasar-conf-js 8 | 9 | /* eslint-env node */ 10 | /* eslint-disable @typescript-eslint/no-var-requires */ 11 | const { configure } = require('quasar/wrappers'); 12 | 13 | module.exports = configure(function (ctx) { 14 | return { 15 | // https://quasar.dev/quasar-cli/supporting-ts 16 | supportTS: { 17 | tsCheckerConfig: { 18 | eslint: { 19 | enabled: true, 20 | files: './src/**/*.{ts,tsx,js,jsx,vue}', 21 | }, 22 | }, 23 | }, 24 | 25 | // https://quasar.dev/quasar-cli/prefetch-feature 26 | // preFetch: true, 27 | 28 | // app boot file (/src/boot) 29 | // --> boot files are part of "main.js" 30 | // https://quasar.dev/quasar-cli/boot-files 31 | boot: ['i18n', 'axios'], 32 | 33 | // https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-css 34 | css: ['app.scss'], 35 | 36 | // https://github.com/quasarframework/quasar/tree/dev/extras 37 | extras: [ 38 | // 'ionicons-v4', 39 | // 'mdi-v5', 40 | // 'fontawesome-v5', 41 | // 'eva-icons', 42 | // 'themify', 43 | // 'line-awesome', 44 | // 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both! 45 | 46 | 'roboto-font', // optional, you are not bound to it 47 | 'material-icons', // optional, you are not bound to it 48 | ], 49 | 50 | // Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-build 51 | build: { 52 | vueRouterMode: 'history', // available values: 'hash', 'history' 53 | 54 | // transpile: false, 55 | // publicPath: '/', 56 | 57 | // Add dependencies for transpiling with Babel (Array of string/regex) 58 | // (from node_modules, which are by default not transpiled). 59 | // Applies only if "transpile" is set to true. 60 | // transpileDependencies: [], 61 | 62 | // rtl: true, // https://quasar.dev/options/rtl-support 63 | // preloadChunks: true, 64 | // showProgress: false, 65 | // gzip: true, 66 | // analyze: true, 67 | 68 | // Options below are automatically set depending on the env, set them if you want to override 69 | // extractCSS: false, 70 | 71 | // https://quasar.dev/quasar-cli/handling-webpack 72 | // "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain 73 | chainWebpack(/* chain */) { 74 | // 75 | }, 76 | }, 77 | 78 | // Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-devServer 79 | devServer: { 80 | server: { 81 | type: 'http', 82 | }, 83 | port: 8080, 84 | open: false, 85 | proxy: { 86 | '/api': { target: 'http://localhost:8788', changeOrigin: true }, 87 | '/proxy': { target: 'http://localhost:8788', changeOrigin: true }, 88 | }, 89 | }, 90 | 91 | // https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-framework 92 | framework: { 93 | config: {}, 94 | 95 | // iconSet: 'material-icons', // Quasar icon set 96 | // lang: 'en-US', // Quasar language pack 97 | 98 | // For special cases outside of where the auto-import strategy can have an impact 99 | // (like functional components as one of the examples), 100 | // you can manually specify Quasar components/directives to be available everywhere: 101 | // 102 | // components: [], 103 | // directives: [], 104 | 105 | // Quasar plugins 106 | plugins: ['Notify', 'Dark', 'Dialog', 'LocalStorage'], 107 | }, 108 | 109 | // animations: 'all', // --- includes all animations 110 | // https://quasar.dev/options/animations 111 | animations: ['fadeIn', 'fadeOut'], 112 | 113 | // https://quasar.dev/quasar-cli/developing-ssr/configuring-ssr 114 | ssr: { 115 | pwa: false, 116 | 117 | // manualStoreHydration: true, 118 | // manualPostHydrationTrigger: true, 119 | 120 | prodPort: 3000, // The default port that the production server should use 121 | // (gets superseded if process.env.PORT is specified at runtime) 122 | 123 | maxAge: 1000 * 60 * 60 * 24 * 30, 124 | // Tell browser when a file from the server should expire from cache (in ms) 125 | 126 | chainWebpackWebserver(/* chain */) { 127 | // 128 | }, 129 | 130 | middlewares: [ 131 | ctx.prod ? 'compression' : '', 132 | 'render', // keep this as last one 133 | ], 134 | }, 135 | 136 | // https://quasar.dev/quasar-cli/developing-pwa/configuring-pwa 137 | pwa: { 138 | workboxPluginMode: 'GenerateSW', // 'GenerateSW' or 'InjectManifest' 139 | workboxOptions: {}, // only for GenerateSW 140 | 141 | // for the custom service worker ONLY (/src-pwa/custom-service-worker.[js|ts]) 142 | // if using workbox in InjectManifest mode 143 | chainWebpackCustomSW(/* chain */) { 144 | // 145 | }, 146 | 147 | manifest: { 148 | name: 'Hikari Anime Image Search', 149 | short_name: 'Hikari Search', 150 | description: 151 | 'A collection of anime image search engine, based on Cloudflare Pages Function.', 152 | display: 'standalone', 153 | orientation: 'portrait', 154 | background_color: '#ffffff', 155 | theme_color: '#027be3', 156 | icons: [ 157 | { 158 | src: 'icons/icon-128x128.png', 159 | sizes: '128x128', 160 | type: 'image/png', 161 | }, 162 | { 163 | src: 'icons/icon-192x192.png', 164 | sizes: '192x192', 165 | type: 'image/png', 166 | }, 167 | { 168 | src: 'icons/icon-256x256.png', 169 | sizes: '256x256', 170 | type: 'image/png', 171 | }, 172 | { 173 | src: 'icons/icon-384x384.png', 174 | sizes: '384x384', 175 | type: 'image/png', 176 | }, 177 | { 178 | src: 'icons/icon-512x512.png', 179 | sizes: '512x512', 180 | type: 'image/png', 181 | }, 182 | ], 183 | }, 184 | }, 185 | 186 | // Full list of options: https://quasar.dev/quasar-cli/developing-cordova-apps/configuring-cordova 187 | cordova: { 188 | // noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing 189 | }, 190 | 191 | // Full list of options: https://quasar.dev/quasar-cli/developing-capacitor-apps/configuring-capacitor 192 | capacitor: { 193 | hideSplashscreen: true, 194 | }, 195 | 196 | // Full list of options: https://quasar.dev/quasar-cli/developing-electron-apps/configuring-electron 197 | electron: { 198 | bundler: 'packager', // 'packager' or 'builder' 199 | 200 | packager: { 201 | // https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options 202 | // OS X / Mac App Store 203 | // appBundleId: '', 204 | // appCategoryType: '', 205 | // osxSign: '', 206 | // protocol: 'myapp://path', 207 | // Windows only 208 | // win32metadata: { ... } 209 | }, 210 | 211 | builder: { 212 | // https://www.electron.build/configuration/configuration 213 | 214 | appId: 'hikari-search', 215 | }, 216 | 217 | // "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain 218 | chainWebpack(/* chain */) { 219 | // do something with the Electron main process Webpack cfg 220 | // extendWebpackMain also available besides this chainWebpackMain 221 | }, 222 | 223 | // "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain 224 | chainWebpackPreload(/* chain */) { 225 | // do something with the Electron main process Webpack cfg 226 | // extendWebpackPreload also available besides this chainWebpackPreload 227 | }, 228 | }, 229 | }; 230 | }); 231 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | --------------------------------------------------------------------------------