From 06ecb8c07160b5f58f791bdad58dfb793c75c82a Mon Sep 17 00:00:00 2001 From: yeyang <746659424@qq.com> Date: Sat, 4 Feb 2023 00:46:42 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9A=97=EF=B8=8F=20=E5=A2=9E=E5=8A=A0SauceNAO?= =?UTF-8?q?=E6=90=9C=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +- apps/picSearch.js | 21 ++ components/Config.js | 5 + config/config/picSearch.yaml | 1 + config/default_config/picSearch.yaml | 1 + model/PicSearch.js | 46 ++++ model/index.js | 4 +- tools/sites.js | 381 +++++++++++++++++++++++++++ 8 files changed, 461 insertions(+), 2 deletions(-) create mode 100644 apps/picSearch.js create mode 100644 config/config/picSearch.yaml create mode 100644 config/default_config/picSearch.yaml create mode 100644 model/PicSearch.js create mode 100644 tools/sites.js diff --git a/.gitignore b/.gitignore index ff227fe..b163286 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ node_modules/ -data/ \ No newline at end of file +data/ +*_test.js +test.* \ No newline at end of file diff --git a/apps/picSearch.js b/apps/picSearch.js new file mode 100644 index 0000000..143ba17 --- /dev/null +++ b/apps/picSearch.js @@ -0,0 +1,21 @@ +import { common, PicSearch } from '../model/index.js' +export class newPicSearch extends plugin { + constructor () { + super({ + name: '椰奶图片搜索', + event: 'message', + priority: 2000, + rule: [ + { + reg: '^#?(椰奶)?搜图.*$', + fnc: 'search' + } + ] + }) + } + + async search (e) { + let msg = await PicSearch.SauceNAO(e.img[0]) + common.getforwardMsg(e, msg) + } +} diff --git a/components/Config.js b/components/Config.js index 3cf5a8e..23e1873 100644 --- a/components/Config.js +++ b/components/Config.js @@ -76,6 +76,11 @@ class Config { return this.getDefOrConfig('other') } + /** 搜图配置 */ + get picSearch () { + return this.getDefOrConfig('picSearch') + } + /** 默认配置和用户配置 */ getDefOrConfig (name) { let def = this.getdefSet(name) diff --git a/config/config/picSearch.yaml b/config/config/picSearch.yaml new file mode 100644 index 0000000..bc67a25 --- /dev/null +++ b/config/config/picSearch.yaml @@ -0,0 +1 @@ +SauceNAOApiKey: diff --git a/config/default_config/picSearch.yaml b/config/default_config/picSearch.yaml new file mode 100644 index 0000000..bc67a25 --- /dev/null +++ b/config/default_config/picSearch.yaml @@ -0,0 +1 @@ +SauceNAOApiKey: diff --git a/model/PicSearch.js b/model/PicSearch.js new file mode 100644 index 0000000..fc0c073 --- /dev/null +++ b/model/PicSearch.js @@ -0,0 +1,46 @@ +/* eslint-disable no-void */ +import fetch from 'node-fetch' +import { Config } from '../components/index.js' +import { common } from './index.js' +import sites from '../tools/sites.js' + +export default new class { + async SauceNAO (url) { + let apiKey = Config.picSearch.SauceNAOApiKey + if (!apiKey) return { error: '未配置SauceNAOApiKey,无法使用SauceNAO搜图,请在 https://saucenao.com/user.php?page=search-api 进行获取' } + + let params = { + url, + db: 999, + api_key: apiKey, + output_type: 2, + numres: 3 + } + let res = await this.request('https://saucenao.com/search.php', params) + if (!res) return { error: 'SauceNAO搜图请求失败' } + let msg = await Promise.all(sites(res).map(async item => [ + `SauceNAO (${item.similarity})\n`, + await common.proxyRequestImg(item.thumbnail), + `\nsite:${item.site}\n`, + `作者:${item.authorName}\n`, + `作者主页:${item.authorUrl}\n`, + `作品链接:${item.url[0]}` + ])) + if (res.headers.long_remaining < 10) { + msg.push(`SauceNAO 24h 内仅剩 ${res.headers.long_remaining} 次使用次数`) + } + } + + async request (url, params, headers) { + const qs = (obj) => { + let res = '' + for (const [k, v] of Object.entries(obj)) { res += `${k}=${encodeURIComponent(v)}&` } + return res.slice(0, res.length - 1) + } + let proxy = await common.getAgent() + return await fetch(url + '?' + qs(params), { + agent: proxy, + headers + }).then(res => res.json()).catch(err => console.log(err)) + } +}() diff --git a/model/index.js b/model/index.js index 1c3a57c..f5ea02e 100644 --- a/model/index.js +++ b/model/index.js @@ -9,6 +9,7 @@ import QQInterface from './QQInterface.js' import Interface from './Interface.js' import CronValidate from './CronValidate.js' import Bika from './Bika.js' +import PicSearch from './PicSearch.js' export { puppeteer, common, @@ -20,5 +21,6 @@ export { CronValidate, GroupAdmin, QQInterface, - Interface + Interface, + PicSearch } diff --git a/tools/sites.js b/tools/sites.js new file mode 100644 index 0000000..c91ef27 --- /dev/null +++ b/tools/sites.js @@ -0,0 +1,381 @@ +/* eslint-disable no-void */ +'use strict' +// Object.defineProperty(exports, '__esModule', { value: true }) +// const errors_1 = require('./errors') +// #region Site data objects +const DoujinMangaLexicon = { + name: 'The Doujinshi & Manga Lexicon', + index: 3, + urlMatcher: /(?:http:\/\/)?doujinshi\.mugimugi\.org\/index\.php?p=book&id=\d+/i, + backupUrl: ({ data: { ddb_id } }) => `http://doujinshi.mugimugi.org/index.php?P=BOOK&ID=${ddb_id}` +} +const Pixiv = { + name: 'Pixiv', + index: 5, + urlMatcher: /(?:https?:\/\/)?(?:www\.)?pixiv\.net\/member_illust\.php\?mode=.+&illust_id=\d+/i, + backupUrl: ({ data: { pixiv_id } }) => `https://www.pixiv.net/member_illust.php?mode=medium&illust_id=${pixiv_id}`, + authorData: ({ member_id, member_name }) => ({ + authorName: member_name, + authorUrl: `https://www.pixiv.net/users/${member_id}` + }) +} +const NicoNicoSeiga = { + name: 'Nico Nico Seiga', + index: 8, + urlMatcher: /(?:http:\/\/)?seiga\.nicovideo\.jp\/seiga\/im\d+/i, + backupUrl: ({ data: { seiga_id } }) => `http://seiga.nicovideo.jp/seiga/im${seiga_id}` +} +const Danbooru = { + name: 'Danbooru', + index: 9, + urlMatcher: /(?:https?:\/\/)?danbooru\.donmai\.us\/(?:posts|post\/show)\/\d+/i, + backupUrl: ({ data: { danbooru_id } }) => `https://danbooru.donmai.us/posts/${danbooru_id}` +} +const Drawr = { + name: 'drawr', + index: 10, + urlMatcher: /(?:http:\/\/)?(?:www\.)?drawr\.net\/show\.php\?id=\d+/i, + backupUrl: ({ data: { drawr_id } }) => `http://drawr.net/show.php?id=${drawr_id}` +} +const Nijie = { + name: 'Nijie', + index: 11, + urlMatcher: /(?:http:\/\/)?nijie\.info\/view\.php\?id=\d+/i, + backupUrl: (data) => `http://nijie.info/view.php?id=${data.data.nijie_id}` +} +const Yandere = { + name: 'Yande.re', + index: 12, + urlMatcher: /(?:https?:\/\/)?yande\.re\/post\/show\/\d+/i, + backupUrl: (data) => `https://yande.re/post/show/${data.data.yandere_id}` +} +const OpeningsMoe = { + name: 'Openings.moe', + index: 13, + urlMatcher: /(?:https?:\/\/)?openings\.moe\/\?video=.*/, + backupUrl: (data) => `https://openings.moe/?video=${data.data.file}` +} +const Fakku = { + name: 'FAKKU', + index: 16, + urlMatcher: /(?:https?:\/\/)?(www\.)?fakku\.net\/hentai\/[a-z-]+\d+}/i, + backupUrl: (data) => { let _a; return `https://www.fakku.net/hentai/${(_a = data.data.source) === null || _a === void 0 ? void 0 : _a.toLowerCase().replace(' ', '-')}` } +} +const NHentai = { + name: 'nHentai', + index: 18, + urlMatcher: /https?:\/\/nhentai.net\/g\/\d+/i, + backupUrl: (data) => { let _a; return `https://nhentai.net/g/${(_a = data.header.thumbnail.match(/nhentai\/(\d+)/)) === null || _a === void 0 ? void 0 : _a[1]}` } +} +const TwoDMarket = { + name: '2D-Market', + index: 19, + urlMatcher: /https?:\/\/2d-market\.com\/comic\/\d+/i, + backupUrl: (data) => { + let _a, _b + return `http://2d-market.com/Comic/${(_a = data.header.thumbnail.match(/2d_market\/(\d+)/i)) === null || _a === void 0 ? void 0 : _a[1]}-${(_b = data.data.source) === null || _b === void 0 ? void 0 : _b.replace(' ', '-')}` + } +} +const MediBang = { + name: 'MediBang', + index: 20, + urlMatcher: /(?:https?:\/\/)?medibang\.com\/picture\/[\da-z]+/i, + backupUrl: (data) => data.data.url +} +const AniDB = { + name: 'AniDB', + index: 21, + urlMatcher: /(?:https?:\/\/)?anidb\.net\/perl-bin\/animedb\.pl\?show=.+&aid=\d+/i, + backupUrl: (data) => `https://anidb.net/perl-bin/animedb.pl?show=anime&aid=${data.data.anidb_aid}` +} +const IMDb = { + name: 'IMDb', + index: 23, + urlMatcher: /(?:https?:\/\/)?(?:www\.)?imdb\.com\/title\/.+/i, + backupUrl: (data) => `https://www.imdb.com/title/${data.data.imdb_id}` +} +const Gelbooru = { + name: 'Gelbooru', + index: 25, + urlMatcher: /(?:https?:\/\/)gelbooru\.com\/index\.php\?page=post&s=view&id=\d+/i, + backupUrl: (data) => `https://gelbooru.com/index.php?page=post&s=view&id=${data.data.gelbooru_id}` +} +const Konachan = { + name: 'Konachan', + index: 26, + urlMatcher: /(?:http:\/\/)?konachan\.com\/post\/show\/\d+/i, + backupUrl: (data) => `https://konachan.com/post/show/${data.data.konachan_id}` +} +const SankakuChannel = { + name: 'Sankaku Channel', + index: 27, + urlMatcher: /(?:https?:\/\/)?chan\.sankakucomplex\.com\/post\/show\/\d+/i, + backupUrl: (data) => `https://chan.sankakucomplex.com/post/show/${data.data.sankaku_id}` +} +const AnimePictures = { + name: 'Anime-Pictures', + index: 28, + urlMatcher: /(?:https?:\/\/)?anime-pictures\.net\/pictures\/view_post\/\d+/i, + backupUrl: (data) => `https://anime-pictures.net/pictures/view_post/${data.data['anime-pictures_id']}` +} +const E621 = { + name: 'e621', + index: 29, + urlMatcher: /(?:https?:\/\/)?e621\.net\/post\/show\/\d+/i, + backupUrl: (data) => `https://e621.net/post/show/${data.data.e621_id}` +} +const IdolComplex = { + name: 'Idol Complex', + index: 30, + urlMatcher: /(?:https?:\/\/)?idol\.sankakucomplex\.com\/post\/show\/\d+/i, + backupUrl: (data) => `https://idol.sankakucomplex.com/post/show/${data.data.idol_id}` +} +const bcyIllust = { + name: 'bcy.net Illust', + index: 31, + urlMatcher: /(?:http:\/\/)?bcy.net\/illust\/detail\/\d+/i, + backupUrl: (data) => `https://bcy.net/${data.data.bcy_type}/detail/${data.data.member_link_id}/${data.data.bcy_id}`, + authorData: ({ member_id, member_name }) => ({ + authorName: member_name, + authorUrl: `https://bcy.net/u/${member_id}` + }) +} +const bcyCosplay = { + name: 'bcy.net Cosplay', + index: 32, + urlMatcher: /(?:http:\/\/)?bcy.net\/coser\/detail\/\d{5}/i, + backupUrl: (data) => `https://bcy.net/${data.data.bcy_type}/detail/${data.data.member_link_id}/${data.data.bcy_id}` +} +const PortalGraphics = { + name: 'PortalGraphics', + index: 33, + urlMatcher: /(?:http:\/\/)?web\.archive\.org\/web\/http:\/\/www\.portalgraphics\.net\/pg\/illust\/\?image_id=\d+/i, + backupUrl: (data) => `http://web.archive.org/web/http://www.portalgraphics.net/pg/illust/?image_id=${data.data.pg_id}` +} +const DeviantArt = { + name: 'deviantArt', + index: 34, + urlMatcher: /(?:https:\/\/)?deviantart\.com\/view\/\d+/i, + backupUrl: (data) => `https://deviantart.com/view/${data.data.da_id}`, + authorData: ({ author_name: authorName, author_url: authorUrl }) => ({ + authorName, + authorUrl + }) +} +const Pawoo = { + name: 'Pawoo', + index: 35, + urlMatcher: /(?:https?:\/\/)?pawoo\.net\/@.+/i, + backupUrl: (data) => `https://pawoo.net/@${data.data.user_acct}/${data.data.pawoo_id}` +} +const MangaUpdates = { + name: 'Manga Updates', + index: 36, + urlMatcher: /(?:https:\/\/)?www\.mangaupdates\.com\/series\.html\?id=\d+/gi, + backupUrl: (data) => `https://www.mangaupdates.com/series.html?id=${data.data.mu_id}` +} +const MangaDex = { + name: 'MangaDex', + index: 37, + urlMatcher: /(?:https?:\/\/)?mangadex\.org\/chapter\/(\w|-)+\/(?:\d+)?/gi, + backupUrl: (data) => `https://mangadex.org/chapter/${data.data.md_id}`, + authorData: (data) => ({ + authorName: data.author, + authorUrl: null + }) +} +const ArtStation = { + name: 'FurAffinity', + index: 39, + urlMatcher: /(?:https?:\/\/)?www\.artstation\.com\/artwork\/\w+/i, + backupUrl: (data) => `https://www.artstation.com/artwork/${data.data.as_project}`, + authorData: (data) => ({ + authorName: data.author_name, + authorUrl: data.author_url + }) +} +const FurAffinity = { + name: 'FurAffinity', + index: 40, + urlMatcher: /(?:https?:\/\/)?furaffinity\.net\/view\/\d+/i, + backupUrl: (data) => `https://furaffinity.net/view/${data.data.fa_id}`, + authorData: (data) => ({ + authorName: data.author_name, + authorUrl: data.author_url + }) +} +const Twitter = { + name: 'Twitter', + index: 41, + urlMatcher: /(?:https?:\/\/)?twitter\.com\/.+/i, + backupUrl: (data) => `https://twitter.com/i/web/status/${data.data.tweet_id}`, + authorData: (data) => ({ + authorName: data.twitter_user_handle, + authorUrl: `https://twitter.com/i/user/${data.twitter_user_id}` + }) +} +const FurryNetwork = { + name: 'Furry Network', + index: 42, + urlMatcher: /(?:https?:\/\/)?furrynetwork\.com\/artwork\/\d+/i, + backupUrl: (data) => `https://furrynetwork.com/artwork/${data.data.fn_id}`, + authorData: (data) => ({ + authorName: data.author_name, + authorUrl: data.author_url + }) +} +const Kemono = { + name: 'Kemono', + index: 43, + urlMatcher: /|(?:(?:https?:\/\/)?fantia\.jp\/posts\/\d+)|(?:(?:https?:\/\/)?subscribestar\.adult\/posts\/\d+)|(?:(?:https?:\/\/)?gumroad\.com\/l\/\w+)|(?:(?:https?:\/\/)?patreon\.com\/posts\/\d+)|(?:(?:https?:\/\/)?pixiv\.net\/fanbox\/creator\/\d+\/post\/\d+)|(?:(?:https?:\/\/)?dlsite\.com\/home\/work\/=\/product_id\/\w+\.\w+)/i, + backupUrl: (data) => { + switch (data.data.service) { + case 'fantia': + return `https://fantia.jp/posts/${data.data.id}` + case 'subscribestar': + return `https://subscribestar.adult/posts/${data.data.id}` + case 'gumroad': + return `https://gumroad.com/l/${data.data.id}` + case 'patreon': + return `https://patreon.com/posts/${data.data.id}` + case 'fanbox': + return `https://pixiv.net/fanbox/creator/${data.data.user_id}/post/${data.data.id}` + case 'dlsite': + return `https://dlsite.com/home/work/=/${data.data.id}` + default: + // throw new errors_1.SagiriClientError(999, `Unknown service type for Kemono: ${data.data.service}`) + console.error(999, `Unknown service type for Kemono: ${data.data.service}`) + } + }, + authorData: (data) => { + switch (data.service) { + case 'fantia': + return { + authorName: data.user_name, + authorUrl: `https://fantia.jp/fanclubs/${data.user_id}` + } + case 'subscribestar': + return { + authorName: data.user_name, + authorUrl: `https://subscribestar.adult/${data.user_id}` + } + case 'gumroad': + return { + authorName: data.user_name, + authorUrl: `https://gumroad.com/${data.user_id}` + } + case 'patreon': + return { + authorName: data.user_name, + authorUrl: `https://patreon.com/user?u=${data.user_id}` + } + case 'fanbox': + return { + authorName: data.user_name, + authorUrl: `https://pixiv.net/fanbox/creator/${data.user_id}` + } + case 'dlsite': + return { + authorName: data.user_name, + authorUrl: `https://dlsite.com/eng/cicrle/profile/=/marker_id/${data.user_id}` + } + default: + // throw new errors_1.SagiriClientError(999, `Unknown service type for Kemono: ${data.service}`) + console.error(999, `Unknown service type for Kemono: ${data.service}`) + } + } +} +const Skeb = { + name: 'Skeb', + index: 44, + urlMatcher: /(?:(?:https?:\/\/)?skeb\.jp\/@\w+\/works\/\d+)/i, + backupUrl: (data) => `https://skeb.jp${data.data.path}`, + authorData: (data) => ({ + authorName: data.creator_name, + authorUrl: data.author_url + }) +} +// #endregion +const sites = { + 3: DoujinMangaLexicon, + 4: DoujinMangaLexicon, + 5: Pixiv, + 6: Pixiv, + 8: NicoNicoSeiga, + 9: Danbooru, + 10: Drawr, + 11: Nijie, + 12: Yandere, + 13: OpeningsMoe, + 16: Fakku, + 18: NHentai, + 19: TwoDMarket, + 20: MediBang, + 21: AniDB, + 22: AniDB, + 23: IMDb, + 24: IMDb, + 25: Gelbooru, + 26: Konachan, + 27: SankakuChannel, + 28: AnimePictures, + 29: E621, + 30: IdolComplex, + 31: bcyIllust, + 32: bcyCosplay, + 33: PortalGraphics, + 34: DeviantArt, + 35: Pawoo, + 36: MangaUpdates, + 37: MangaDex, + 371: MangaDex, + // 38 + 39: ArtStation, + 40: FurAffinity, + 41: Twitter, + 42: FurryNetwork, + 43: Kemono, + 44: Skeb +} +const resolveResult = item => { + let _a + const { data, header } = item + const id = header.index_id + if (!sites[id]) { throw new Error(`Cannot resolve data for unknown index ${id}`) } + const { name, urlMatcher, backupUrl, authorData } = sites[id] + let url + if (data.ext_urls && data.ext_urls.length > 1) { + url = data.ext_urls.filter((url) => urlMatcher.test(url)) + } else if (data.ext_urls) { + url = data.ext_urls + } + if (!url) url = backupUrl(item) + return Object.assign({ + id, + url, + name + }, ((_a = authorData === null || authorData === void 0 ? void 0 : authorData(item.data)) !== null && _a !== void 0 ? _a : { authorName: null, authorUrl: null })) +} + +export default (response) => { + const unknownIds = new Set(response.results.filter((result) => !sites[result.header.index_id]).map((result) => result.header.index_id)) + const results = response.results + .filter((result) => !unknownIds.has(result.header.index_id)) + .sort((a, b) => b.header.similarity - a.header.similarity) + + return results.map((result) => { + const { url, name, id, authorName, authorUrl } = resolveResult(result) + const { header: { similarity, thumbnail } } = result + return { + url, + site: name, + index: id, + similarity: Number(similarity), + thumbnail, + authorName, + authorUrl, + raw: result + } + }) +} +// # sourceMappingURL=sites.js.map