mirror of
https://github.com/Moe-Sakura/Wrangler-API.git
synced 2026-03-15 04:13:18 +08:00
feat: 初始化项目,支持流式搜索与限流
* 初始化项目结构,包括配置、依赖和忽略文件。 * 引入核心搜索逻辑,支持流式响应以提供实时进度。 * 抽象化平台接口,并集成多个Galgame和补丁搜索源。 * 实现基于IP的速率限制功能,利用Cloudflare KV存储。 * 新增自动化脚本,用于生成平台索引文件。 * 统一HTTP请求客户端,增加超时和自定义User-Agent。 * 为部分平台添加了对`zypassword`参数的支持。
This commit is contained in:
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# Node.js dependencies
|
||||
node_modules/
|
||||
|
||||
# Wrangler CLI temporary files
|
||||
.wrangler/
|
||||
|
||||
# Build output (if any)
|
||||
# dist/
|
||||
# build/
|
||||
|
||||
*.md
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.*
|
||||
1571
package-lock.json
generated
Normal file
1571
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
package.json
Normal file
18
package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "new-project",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"generate": "node scripts/generate-indices.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20250821.0",
|
||||
"typescript": "^5.9.2",
|
||||
"wrangler": "^4.31.0"
|
||||
}
|
||||
}
|
||||
39
scripts/generate-indices.js
Normal file
39
scripts/generate-indices.js
Normal file
@@ -0,0 +1,39 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const platformsDir = path.join(__dirname, '../src/platforms');
|
||||
|
||||
function generateIndexFile(directory) {
|
||||
const dirPath = path.join(platformsDir, directory);
|
||||
if (!fs.existsSync(dirPath)) return;
|
||||
|
||||
const files = fs.readdirSync(dirPath)
|
||||
.filter(file => file.endsWith('.ts') && file !== 'index.ts');
|
||||
|
||||
if (files.length === 0) return;
|
||||
|
||||
const imports = files.map(file => {
|
||||
const platformName = path.basename(file, '.ts');
|
||||
return `import ${platformName} from "./${platformName}";`;
|
||||
}).join('\n');
|
||||
|
||||
const platformNames = files.map(file => path.basename(file, '.ts'));
|
||||
|
||||
const content = `import type { Platform } from "../../types";
|
||||
${imports}
|
||||
|
||||
const platforms: Platform[] = [
|
||||
${platformNames.join(',\n ')},
|
||||
];
|
||||
|
||||
export default platforms;
|
||||
`;
|
||||
|
||||
fs.writeFileSync(path.join(dirPath, 'index.ts'), content.trim() + '\n');
|
||||
console.log(`Generated index for ${directory} with ${files.length} platforms.`);
|
||||
}
|
||||
|
||||
console.log('Generating platform indices...');
|
||||
generateIndexFile('gal');
|
||||
generateIndexFile('patch');
|
||||
console.log('Done.');
|
||||
69
src/core.ts
Normal file
69
src/core.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import type { Platform, PlatformSearchResult, StreamProgress, StreamResult } from "./types";
|
||||
import platformsGal from "./platforms/gal";
|
||||
import platformsPatch from "./platforms/patch";
|
||||
|
||||
/**
|
||||
* 将平台搜索结果格式化为自定义的流事件字符串 (JSON + 换行符)。
|
||||
*/
|
||||
function formatStreamEvent(data: object): string {
|
||||
return `${JSON.stringify(data)}\n`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理搜索请求并以流的形式写入结果。
|
||||
* @param game 要搜索的游戏名称。
|
||||
* @param platforms 要使用的平台列表。
|
||||
* @param writer 用于写入 SSE 事件的 WritableStreamDefaultWriter。
|
||||
* @param zypassword (可选) 访问某些平台可能需要的密码。
|
||||
*/
|
||||
export async function handleSearchRequestStream(
|
||||
game: string,
|
||||
platforms: Platform[],
|
||||
writer: WritableStreamDefaultWriter<Uint8Array>,
|
||||
zypassword: string = "" // 添加 zypassword 参数
|
||||
): Promise<void> {
|
||||
const encoder = new TextEncoder();
|
||||
const total = platforms.length;
|
||||
let completed = 0;
|
||||
|
||||
// 发送初始的总数信息
|
||||
await writer.write(encoder.encode(formatStreamEvent({ total })));
|
||||
|
||||
const searchPromises = platforms.map(async (platform) => {
|
||||
try {
|
||||
// 传递 zypassword 给平台搜索函数
|
||||
const result = await platform.search(game, zypassword);
|
||||
completed++;
|
||||
|
||||
const progress: StreamProgress = { completed, total };
|
||||
|
||||
if (result.count > 0 || result.error) {
|
||||
const streamResult: StreamResult = {
|
||||
name: result.name,
|
||||
color: result.error ? 'red' : platform.color,
|
||||
items: result.items,
|
||||
error: result.error,
|
||||
};
|
||||
await writer.write(encoder.encode(formatStreamEvent({ progress, result: streamResult })));
|
||||
} else {
|
||||
// 即使没有结果或错误,也发送进度更新
|
||||
await writer.write(encoder.encode(formatStreamEvent({ progress })));
|
||||
}
|
||||
} catch (e) {
|
||||
completed++;
|
||||
// 记录平台内部的未知错误
|
||||
console.error(`Error searching platform ${platform.name}:`, e);
|
||||
const progress: StreamProgress = { completed, total };
|
||||
await writer.write(encoder.encode(formatStreamEvent({ progress })));
|
||||
}
|
||||
});
|
||||
|
||||
// 等待所有搜索完成
|
||||
await Promise.all(searchPromises);
|
||||
|
||||
// 发送完成信号
|
||||
await writer.write(encoder.encode(formatStreamEvent({ done: true })));
|
||||
}
|
||||
|
||||
export const PLATFORMS_GAL = platformsGal;
|
||||
export const PLATFORMS_PATCH = platformsPatch;
|
||||
89
src/index.ts
Normal file
89
src/index.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { handleSearchRequestStream, PLATFORMS_GAL, PLATFORMS_PATCH } from "./core";
|
||||
import { checkRateLimit } from "./ratelimit";
|
||||
|
||||
export interface Env {
|
||||
RATE_LIMIT_KV: KVNamespace;
|
||||
}
|
||||
|
||||
const corsHeaders = {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type",
|
||||
};
|
||||
|
||||
async function handleSearch(request: Request, env: Env, ctx: ExecutionContext, platforms: any[]) {
|
||||
try {
|
||||
const formData = await request.formData();
|
||||
const game = formData.get("game") as string;
|
||||
const zypassword = formData.get("zypassword") as string || ""; // 获取 zypassword
|
||||
|
||||
if (env.RATE_LIMIT_KV) {
|
||||
const ip = request.headers.get("CF-Connecting-IP") || "unknown";
|
||||
const { allowed, retryAfter } = await checkRateLimit(ip, env.RATE_LIMIT_KV);
|
||||
|
||||
if (!allowed) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: `搜索过于频繁, 请 ${retryAfter} 秒后再试` }),
|
||||
{
|
||||
status: 429,
|
||||
headers: { "Content-Type": "application/json", ...corsHeaders },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!game || typeof game !== 'string') {
|
||||
return new Response(JSON.stringify({ error: "Game name is required" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json", ...corsHeaders },
|
||||
});
|
||||
}
|
||||
|
||||
const { readable, writable } = new TransformStream();
|
||||
const writer = writable.getWriter();
|
||||
|
||||
// 将异步任务交给 waitUntil 来处理,确保它能完整执行
|
||||
ctx.waitUntil(
|
||||
handleSearchRequestStream(game.trim(), platforms, writer, zypassword) // 传递 zypassword
|
||||
.catch(err => console.error("Streaming error:", err))
|
||||
.finally(() => writer.close())
|
||||
);
|
||||
|
||||
return new Response(readable, {
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream; charset=utf-8",
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
...corsHeaders
|
||||
},
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "An unknown error occurred";
|
||||
return new Response(JSON.stringify({ error: errorMessage }), {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json", ...corsHeaders },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (request.method === "OPTIONS") {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
}
|
||||
|
||||
if (request.method === "POST") {
|
||||
if (url.pathname === "/gal") {
|
||||
return handleSearch(request, env, ctx, PLATFORMS_GAL);
|
||||
}
|
||||
if (url.pathname === "/patch") {
|
||||
return handleSearch(request, env, ctx, PLATFORMS_PATCH);
|
||||
}
|
||||
}
|
||||
|
||||
return new Response("Not Found", { status: 404 });
|
||||
},
|
||||
};
|
||||
58
src/platforms/gal/ACGYingYingGuai.ts
Normal file
58
src/platforms/gal/ACGYingYingGuai.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { fetchClient } from "../../utils/httpClient";
|
||||
import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types";
|
||||
|
||||
const API_URL = "https://acgyyg.ru/";
|
||||
const REGEX = /<a target="_blank" href="(?<URL>.*?)" title="(?<NAME>.*?)" class="post-overlay">/gs;
|
||||
|
||||
async function searchACGYingYingGuai(game: string): Promise<PlatformSearchResult> {
|
||||
const searchResult: PlatformSearchResult = {
|
||||
name: "ACG嘤嘤怪",
|
||||
count: 0,
|
||||
items: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const url = new URL(API_URL);
|
||||
url.searchParams.set("s", game);
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
const matches = html.matchAll(REGEX);
|
||||
|
||||
const items: SearchResultItem[] = [];
|
||||
for (const match of matches) {
|
||||
if (match.groups?.NAME && match.groups?.URL) {
|
||||
items.push({
|
||||
name: match.groups.NAME.trim(),
|
||||
url: match.groups.URL,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
searchResult.items = items;
|
||||
searchResult.count = items.length;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
searchResult.error = error.message;
|
||||
} else {
|
||||
searchResult.error = "An unknown error occurred";
|
||||
}
|
||||
searchResult.count = -1;
|
||||
}
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
const ACGYingYingGuai: Platform = {
|
||||
name: "ACG嘤嘤怪",
|
||||
color: "white",
|
||||
magic: false,
|
||||
search: searchACGYingYingGuai,
|
||||
};
|
||||
|
||||
export default ACGYingYingGuai;
|
||||
58
src/platforms/gal/BiAnXingLu.ts
Normal file
58
src/platforms/gal/BiAnXingLu.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { fetchClient } from "../../utils/httpClient";
|
||||
import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types";
|
||||
|
||||
const API_URL = "https://seve.yugal.cc";
|
||||
const REGEX = /<div class="post-info">\s*?<h2><a href="(?<URL>.*?)">(?<NAME>.*?)<\/a><\/h2>/gs;
|
||||
|
||||
async function searchBiAnXingLu(game: string): Promise<PlatformSearchResult> {
|
||||
const searchResult: PlatformSearchResult = {
|
||||
name: "彼岸星露",
|
||||
count: 0,
|
||||
items: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const url = new URL(API_URL);
|
||||
url.searchParams.set("s", game);
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
const matches = html.matchAll(REGEX);
|
||||
|
||||
const items: SearchResultItem[] = [];
|
||||
for (const match of matches) {
|
||||
if (match.groups?.NAME && match.groups?.URL) {
|
||||
items.push({
|
||||
name: match.groups.NAME.trim(),
|
||||
url: match.groups.URL,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
searchResult.items = items;
|
||||
searchResult.count = items.length;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
searchResult.error = error.message;
|
||||
} else {
|
||||
searchResult.error = "An unknown error occurred";
|
||||
}
|
||||
searchResult.count = -1;
|
||||
}
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
const BiAnXingLu: Platform = {
|
||||
name: "彼岸星露",
|
||||
color: "lime",
|
||||
magic: false,
|
||||
search: searchBiAnXingLu,
|
||||
};
|
||||
|
||||
export default BiAnXingLu;
|
||||
70
src/platforms/gal/DaoHeGal.ts
Normal file
70
src/platforms/gal/DaoHeGal.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { fetchClient } from "../../utils/httpClient";
|
||||
import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types";
|
||||
|
||||
const API_URL = "https://inarigal.com/api/home/list";
|
||||
const BASE_URL = "https://inarigal.com/detail/";
|
||||
|
||||
interface DaoHeGalItem {
|
||||
id: number;
|
||||
title_cn: string;
|
||||
}
|
||||
|
||||
interface DaoHeGalResponse {
|
||||
success: boolean;
|
||||
data: {
|
||||
list: DaoHeGalItem[];
|
||||
};
|
||||
}
|
||||
|
||||
async function searchDaoHeGal(game: string): Promise<PlatformSearchResult> {
|
||||
const searchResult: PlatformSearchResult = {
|
||||
name: "稻荷GAL",
|
||||
count: 0,
|
||||
items: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const url = new URL(API_URL);
|
||||
url.searchParams.set("page", "1");
|
||||
url.searchParams.set("pageSize", "18"); // Hardcoded as per original script
|
||||
url.searchParams.set("search", game);
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as DaoHeGalResponse;
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error("API returned success: false");
|
||||
}
|
||||
|
||||
const items: SearchResultItem[] = data.data.list.map(item => ({
|
||||
name: item.title_cn.trim(),
|
||||
url: BASE_URL + item.id,
|
||||
}));
|
||||
|
||||
searchResult.items = items;
|
||||
searchResult.count = items.length;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
searchResult.error = error.message;
|
||||
} else {
|
||||
searchResult.error = "An unknown error occurred";
|
||||
}
|
||||
searchResult.count = -1;
|
||||
}
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
const DaoHeGal: Platform = {
|
||||
name: "稻荷GAL",
|
||||
color: "lime",
|
||||
magic: false,
|
||||
search: searchDaoHeGal,
|
||||
};
|
||||
|
||||
export default DaoHeGal;
|
||||
66
src/platforms/gal/FuFuACG.ts
Normal file
66
src/platforms/gal/FuFuACG.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { fetchClient } from "../../utils/httpClient";
|
||||
import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types";
|
||||
|
||||
const API_URL = "https://www.fufugal.com/so";
|
||||
const BASE_URL = "https://www.fufugal.com/detail";
|
||||
|
||||
interface FuFuACGItem {
|
||||
game_id: number;
|
||||
game_name: string;
|
||||
}
|
||||
|
||||
interface FuFuACGResponse {
|
||||
obj: FuFuACGItem[];
|
||||
}
|
||||
|
||||
async function searchFuFuACG(game: string): Promise<PlatformSearchResult> {
|
||||
const searchResult: PlatformSearchResult = {
|
||||
name: "FuFuACG",
|
||||
count: 0,
|
||||
items: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const url = new URL(API_URL);
|
||||
url.searchParams.set("query", game);
|
||||
|
||||
const response = await fetchClient(url, {
|
||||
headers: {
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as FuFuACGResponse;
|
||||
|
||||
const items: SearchResultItem[] = data.obj.map(item => ({
|
||||
name: item.game_name,
|
||||
url: `${BASE_URL}?id=${item.game_id}`,
|
||||
}));
|
||||
|
||||
searchResult.items = items;
|
||||
searchResult.count = items.length;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
searchResult.error = error.message;
|
||||
} else {
|
||||
searchResult.error = "An unknown error occurred";
|
||||
}
|
||||
searchResult.count = -1;
|
||||
}
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
const FuFuACG: Platform = {
|
||||
name: "FuFuACG",
|
||||
color: "white",
|
||||
magic: false,
|
||||
search: searchFuFuACG,
|
||||
};
|
||||
|
||||
export default FuFuACG;
|
||||
63
src/platforms/gal/GGBases.ts
Normal file
63
src/platforms/gal/GGBases.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { fetchClient } from "../../utils/httpClient";
|
||||
import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types";
|
||||
|
||||
const API_URL = "https://www.ggbases.com/search.so";
|
||||
const BASE_URL = "https://www.ggbases.com/view.so?id=";
|
||||
const REGEX = /<a index=\d+ id="bid(?<URL>\d*?)" name="title" c=".*?" target="_blank" href=".*?">(?<NAME>.*?)<\/a>/gs;
|
||||
|
||||
async function searchGGBases(game: string): Promise<PlatformSearchResult> {
|
||||
const searchResult: PlatformSearchResult = {
|
||||
name: "GGBases",
|
||||
count: 0,
|
||||
items: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const url = new URL(API_URL);
|
||||
url.searchParams.set("p", "0");
|
||||
url.searchParams.set("title", game);
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
// Pre-process the HTML to remove highlighting tags
|
||||
const processedHtml = html.replace(/<\/b>/g, "").replace(/<b style='color:red'>/g, "");
|
||||
|
||||
const matches = processedHtml.matchAll(REGEX);
|
||||
|
||||
const items: SearchResultItem[] = [];
|
||||
for (const match of matches) {
|
||||
if (match.groups?.NAME && match.groups?.URL) {
|
||||
items.push({
|
||||
name: match.groups.NAME.trim(),
|
||||
url: BASE_URL + match.groups.URL,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
searchResult.items = items;
|
||||
searchResult.count = items.length;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
searchResult.error = error.message;
|
||||
} else {
|
||||
searchResult.error = "An unknown error occurred";
|
||||
}
|
||||
searchResult.count = -1;
|
||||
}
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
const GGBases: Platform = {
|
||||
name: "GGBases",
|
||||
color: "lime",
|
||||
magic: false,
|
||||
search: searchGGBases,
|
||||
};
|
||||
|
||||
export default GGBases;
|
||||
56
src/platforms/gal/GGS.ts
Normal file
56
src/platforms/gal/GGS.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { fetchClient } from "../../utils/httpClient";
|
||||
import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types";
|
||||
|
||||
const DATA_URL = "https://gal.saop.cc/search.json";
|
||||
const BASE_URL = "https://gal.saop.cc";
|
||||
|
||||
interface GgsItem {
|
||||
title: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
async function searchGGS(game: string): Promise<PlatformSearchResult> {
|
||||
const searchResult: PlatformSearchResult = {
|
||||
name: "GGS",
|
||||
count: 0,
|
||||
items: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetchClient(DATA_URL);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch data from ${DATA_URL}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as GgsItem[];
|
||||
|
||||
const items: SearchResultItem[] = data
|
||||
.filter(item => item.title.includes(game))
|
||||
.map(item => ({
|
||||
name: item.title,
|
||||
url: BASE_URL + item.url,
|
||||
}));
|
||||
|
||||
searchResult.items = items;
|
||||
searchResult.count = items.length;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
searchResult.error = error.message;
|
||||
} else {
|
||||
searchResult.error = "An unknown error occurred";
|
||||
}
|
||||
searchResult.count = -1;
|
||||
}
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
const GGS: Platform = {
|
||||
name: "GGS",
|
||||
color: "lime",
|
||||
magic: false,
|
||||
search: searchGGS,
|
||||
};
|
||||
|
||||
export default GGS;
|
||||
88
src/platforms/gal/GalTuShuGuan.ts
Normal file
88
src/platforms/gal/GalTuShuGuan.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { fetchClient } from "../../utils/httpClient";
|
||||
import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types";
|
||||
|
||||
const API_URL = "https://gallibrary.pw/galgame/game/manyGame";
|
||||
const BASE_URL = "https://gallibrary.pw/game.html?id=";
|
||||
|
||||
interface GalTuShuGuanItem {
|
||||
id: number;
|
||||
listGameText: { data: string; type: number; version: number }[]; // Corrected type
|
||||
}
|
||||
|
||||
interface GalTuShuGuanResponse {
|
||||
code: number;
|
||||
data?: GalTuShuGuanItem[];
|
||||
}
|
||||
|
||||
// A helper function to strip HTML tags from a string
|
||||
function stripHtml(html: string): string {
|
||||
return html.replace(/<[^>]*>/g, "").trim();
|
||||
}
|
||||
|
||||
async function searchGalTuShuGuan(game: string): Promise<PlatformSearchResult> {
|
||||
const searchResult: PlatformSearchResult = {
|
||||
name: "GAL图书馆",
|
||||
count: 0,
|
||||
items: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const url = new URL(API_URL);
|
||||
url.searchParams.set("page", "1");
|
||||
url.searchParams.set("type", "1");
|
||||
url.searchParams.set("count", "1000"); // Hardcoded as per original script
|
||||
url.searchParams.set("keyWord", game);
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as GalTuShuGuanResponse;
|
||||
|
||||
// console.log("GAL图书馆 API Response Data:", JSON.stringify(data, null, 2)); // Keep for debugging if needed
|
||||
|
||||
if (data.code !== 200) {
|
||||
throw new Error(`API returned code ${data.code}`);
|
||||
}
|
||||
|
||||
if (!data.data) {
|
||||
throw new Error("API response 'data' field is missing or null.");
|
||||
}
|
||||
|
||||
const items: SearchResultItem[] = data.data.map(item => {
|
||||
// Access item.listGameText[1].data and strip HTML
|
||||
const name = item.listGameText[1]?.data; // Use optional chaining for safety
|
||||
if (!name) {
|
||||
console.warn(`GAL图书馆: Missing name data for item ID ${item.id}`);
|
||||
return null; // Skip this item
|
||||
}
|
||||
return {
|
||||
name: stripHtml(name),
|
||||
url: BASE_URL + item.id,
|
||||
};
|
||||
}).filter(Boolean) as SearchResultItem[]; // Filter out nulls
|
||||
|
||||
searchResult.items = items;
|
||||
searchResult.count = items.length;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
searchResult.error = error.message;
|
||||
} else {
|
||||
searchResult.error = "An unknown error occurred";
|
||||
}
|
||||
searchResult.count = -1;
|
||||
}
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
const GalTuShuGuan: Platform = {
|
||||
name: "GAL图书馆",
|
||||
color: "lime",
|
||||
magic: false,
|
||||
search: searchGalTuShuGuan,
|
||||
};
|
||||
|
||||
export default GalTuShuGuan;
|
||||
83
src/platforms/gal/GalgameX.ts
Normal file
83
src/platforms/gal/GalgameX.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { fetchClient } from "../../utils/httpClient";
|
||||
import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types";
|
||||
|
||||
const API_URL = "https://www.galgamex.net/api/search";
|
||||
const BASE_URL = "https://www.galgamex.net/";
|
||||
|
||||
interface GalgameXItem {
|
||||
name: string;
|
||||
uniqueId: string;
|
||||
}
|
||||
|
||||
interface GalgameXResponse {
|
||||
galgames: GalgameXItem[];
|
||||
}
|
||||
|
||||
async function searchGalgameX(game: string): Promise<PlatformSearchResult> {
|
||||
const searchResult: PlatformSearchResult = {
|
||||
name: "Galgamex",
|
||||
count: 0,
|
||||
items: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
queryString: JSON.stringify([{ type: "keyword", name: game }]),
|
||||
limit: 24, // Hardcoded as per original script
|
||||
searchOption: {
|
||||
searchInIntroduction: false,
|
||||
searchInAlias: true,
|
||||
searchInTag: false,
|
||||
},
|
||||
page: 1,
|
||||
selectedType: "all",
|
||||
selectedLanguage: "all",
|
||||
selectedPlatform: "all",
|
||||
sortField: "resource_update_time",
|
||||
sortOrder: "desc",
|
||||
selectedYears: ["all"],
|
||||
selectedMonths: ["all"],
|
||||
};
|
||||
|
||||
const response = await fetchClient(API_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as GalgameXResponse;
|
||||
|
||||
const items: SearchResultItem[] = data.galgames.map(item => ({
|
||||
name: item.name.trim(),
|
||||
url: BASE_URL + item.uniqueId,
|
||||
}));
|
||||
|
||||
searchResult.items = items;
|
||||
searchResult.count = items.length;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
searchResult.error = error.message;
|
||||
} else {
|
||||
searchResult.error = "An unknown error occurred";
|
||||
}
|
||||
searchResult.count = -1;
|
||||
}
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
const GalgameX: Platform = {
|
||||
name: "Galgamex",
|
||||
color: "lime",
|
||||
magic: false,
|
||||
search: searchGalgameX,
|
||||
};
|
||||
|
||||
export default GalgameX;
|
||||
58
src/platforms/gal/Hikarinagi.ts
Normal file
58
src/platforms/gal/Hikarinagi.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { fetchClient } from "../../utils/httpClient";
|
||||
import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types";
|
||||
|
||||
const API_URL = "https://www.hikarinagi.net/";
|
||||
const REGEX = /" class="lazyload fit-cover radius8">.*?<h2 class="item-heading"><a target="_blank" href="(?<URL>.*?)">(?<NAME>.*?)<\/a><\/h2>/gs;
|
||||
|
||||
async function searchHikarinagi(game: string): Promise<PlatformSearchResult> {
|
||||
const searchResult: PlatformSearchResult = {
|
||||
name: "Hikarinagi",
|
||||
count: 0,
|
||||
items: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const url = new URL(API_URL);
|
||||
url.searchParams.set("s", game);
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
const matches = html.matchAll(REGEX);
|
||||
|
||||
const items: SearchResultItem[] = [];
|
||||
for (const match of matches) {
|
||||
if (match.groups?.NAME && match.groups?.URL) {
|
||||
items.push({
|
||||
name: match.groups.NAME.trim(),
|
||||
url: match.groups.URL,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
searchResult.items = items;
|
||||
searchResult.count = items.length;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
searchResult.error = error.message;
|
||||
} else {
|
||||
searchResult.error = "An unknown error occurred";
|
||||
}
|
||||
searchResult.count = -1;
|
||||
}
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
const Hikarinagi: Platform = {
|
||||
name: "Hikarinagi",
|
||||
color: "white",
|
||||
magic: false,
|
||||
search: searchHikarinagi,
|
||||
};
|
||||
|
||||
export default Hikarinagi;
|
||||
70
src/platforms/gal/JiMengACG.ts
Normal file
70
src/platforms/gal/JiMengACG.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { fetchClient } from "../../utils/httpClient";
|
||||
import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types";
|
||||
|
||||
const API_URL = "https://game.acgs.one/api/posts";
|
||||
|
||||
interface JiMengACGItem {
|
||||
title: string;
|
||||
permalink: string;
|
||||
}
|
||||
|
||||
interface JiMengACGResponse {
|
||||
status: string;
|
||||
data: {
|
||||
dataSet: JiMengACGItem[];
|
||||
};
|
||||
}
|
||||
|
||||
async function searchJiMengACG(game: string): Promise<PlatformSearchResult> {
|
||||
const searchResult: PlatformSearchResult = {
|
||||
name: "绮梦ACG",
|
||||
count: 0,
|
||||
items: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const url = new URL(API_URL);
|
||||
url.searchParams.set("filterType", "search");
|
||||
url.searchParams.set("filterSlug", game);
|
||||
url.searchParams.set("page", "1");
|
||||
url.searchParams.set("pageSize", "999999"); // Corresponds to MAX_RESULTS
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as JiMengACGResponse;
|
||||
|
||||
if (data.status !== "success") {
|
||||
throw new Error(`API returned status: ${data.status}`);
|
||||
}
|
||||
|
||||
const items: SearchResultItem[] = data.data.dataSet.map(item => ({
|
||||
name: item.title.trim(),
|
||||
url: item.permalink,
|
||||
}));
|
||||
|
||||
searchResult.items = items;
|
||||
searchResult.count = items.length;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
searchResult.error = error.message;
|
||||
} else {
|
||||
searchResult.error = "An unknown error occurred";
|
||||
}
|
||||
searchResult.count = -1;
|
||||
}
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
const JiMengACG: Platform = {
|
||||
name: "绮梦ACG",
|
||||
color: "lime",
|
||||
magic: false,
|
||||
search: searchJiMengACG,
|
||||
};
|
||||
|
||||
export default JiMengACG;
|
||||
63
src/platforms/gal/Koyso.ts
Normal file
63
src/platforms/gal/Koyso.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { fetchClient } from "../../utils/httpClient";
|
||||
import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types";
|
||||
|
||||
const API_URL = "https://koyso.to/";
|
||||
const BASE_URL = "https://koyso.to";
|
||||
const REGEX = /<a class="game_item" href="(?<URL>.+?)"\s*>.*?<span style="background-color: rgba\(128,128,128,0\)">(?<NAME>.+?)<\/span>/gs;
|
||||
|
||||
async function searchKoyso(game: string): Promise<PlatformSearchResult> {
|
||||
const searchResult: PlatformSearchResult = {
|
||||
name: "Koyso",
|
||||
count: 0,
|
||||
items: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const url = new URL(API_URL);
|
||||
url.searchParams.set("keywords", game);
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
// --- DEBUGGING: Print the full HTML content ---
|
||||
// console.log("Koyso API HTML Response:", html.substring(0, 1000)); // Print first 1000 chars
|
||||
// --- END DEBUGGING ---
|
||||
|
||||
const matches = html.matchAll(REGEX);
|
||||
|
||||
const items: SearchResultItem[] = [];
|
||||
for (const match of matches) {
|
||||
if (match.groups?.NAME && match.groups?.URL) {
|
||||
items.push({
|
||||
name: match.groups.NAME.trim(),
|
||||
url: BASE_URL + encodeURIComponent(match.groups.URL),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
searchResult.items = items;
|
||||
searchResult.count = items.length;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
searchResult.error = error.message;
|
||||
} else {
|
||||
searchResult.error = "An unknown error occurred";
|
||||
}
|
||||
searchResult.count = -1;
|
||||
}
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
const Koyso: Platform = {
|
||||
name: "Koyso",
|
||||
color: "lime",
|
||||
magic: false,
|
||||
search: searchKoyso,
|
||||
};
|
||||
|
||||
export default Koyso;
|
||||
67
src/platforms/gal/KunGalgame.ts
Normal file
67
src/platforms/gal/KunGalgame.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { fetchClient } from "../../utils/httpClient";
|
||||
import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types";
|
||||
|
||||
const API_URL = "https://www.kungal.com/api/search";
|
||||
const BASE_URL = "https://www.kungal.com/zh-cn/galgame/";
|
||||
|
||||
interface KunGalgameItem {
|
||||
id: number;
|
||||
name: {
|
||||
"zh-cn": string;
|
||||
"ja-jp": string;
|
||||
};
|
||||
}
|
||||
|
||||
async function searchKunGalgame(game: string): Promise<PlatformSearchResult> {
|
||||
const searchResult: PlatformSearchResult = {
|
||||
name: "鲲Galgame",
|
||||
count: 0,
|
||||
items: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const url = new URL(API_URL);
|
||||
url.searchParams.set("keywords", game);
|
||||
url.searchParams.set("type", "galgame");
|
||||
url.searchParams.set("page", "1");
|
||||
url.searchParams.set("limit", "12"); // Hardcoded as per original script
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as KunGalgameItem[];
|
||||
|
||||
const items: SearchResultItem[] = data.map(item => {
|
||||
const zhName = item.name["zh-cn"]?.trim();
|
||||
const jpName = item.name["ja-jp"]?.trim();
|
||||
return {
|
||||
name: zhName || jpName,
|
||||
url: BASE_URL + item.id,
|
||||
};
|
||||
});
|
||||
|
||||
searchResult.items = items;
|
||||
searchResult.count = items.length;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
searchResult.error = error.message;
|
||||
} else {
|
||||
searchResult.error = "An unknown error occurred";
|
||||
}
|
||||
searchResult.count = -1;
|
||||
}
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
const KunGalgame: Platform = {
|
||||
name: "鲲Galgame",
|
||||
color: "lime",
|
||||
magic: false,
|
||||
search: searchKunGalgame,
|
||||
};
|
||||
|
||||
export default KunGalgame;
|
||||
59
src/platforms/gal/LiSiTanACG.ts
Normal file
59
src/platforms/gal/LiSiTanACG.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { fetchClient } from "../../utils/httpClient";
|
||||
import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types";
|
||||
|
||||
const DATA_URL = "https://www.limulu.moe/search.xml";
|
||||
const BASE_URL = "https://www.limulu.moe";
|
||||
const REGEX = /<entry>.*?<title>(.*?)<\/title>.*?<url>(.*?)<\/url>.*?<\/entry>/gs;
|
||||
|
||||
async function searchLiSiTanACG(game: string): Promise<PlatformSearchResult> {
|
||||
const searchResult: PlatformSearchResult = {
|
||||
name: "莉斯坦ACG",
|
||||
count: 0,
|
||||
items: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetchClient(DATA_URL);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch data from ${DATA_URL}`);
|
||||
}
|
||||
|
||||
const xmlText = await response.text();
|
||||
const matches = xmlText.matchAll(REGEX);
|
||||
|
||||
const items: SearchResultItem[] = [];
|
||||
for (const match of matches) {
|
||||
const title = match[1];
|
||||
const urlPath = match[2];
|
||||
|
||||
if (title && urlPath && title.includes(game)) {
|
||||
items.push({
|
||||
name: title.trim(),
|
||||
url: BASE_URL + urlPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
searchResult.items = items;
|
||||
searchResult.count = items.length;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
searchResult.error = error.message;
|
||||
} else {
|
||||
searchResult.error = "An unknown error occurred";
|
||||
}
|
||||
searchResult.count = -1;
|
||||
}
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
const LiSiTanACG: Platform = {
|
||||
name: "莉斯坦ACG",
|
||||
color: "lime",
|
||||
magic: false,
|
||||
search: searchLiSiTanACG,
|
||||
};
|
||||
|
||||
export default LiSiTanACG;
|
||||
58
src/platforms/gal/LiangZiACG.ts
Normal file
58
src/platforms/gal/LiangZiACG.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { fetchClient } from "../../utils/httpClient";
|
||||
import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types";
|
||||
|
||||
const API_URL = "https://lzacg.org/";
|
||||
const REGEX = />\s*<h2 class="item-heading"><a target="_blank" href="(?<URL>.*?)">(?<NAME>.*?)<\/a><\/h2>\s*<div/gs;
|
||||
|
||||
async function searchLiangZiACG(game: string): Promise<PlatformSearchResult> {
|
||||
const searchResult: PlatformSearchResult = {
|
||||
name: "量子acg",
|
||||
count: 0,
|
||||
items: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const url = new URL(API_URL);
|
||||
url.searchParams.set("s", game);
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
const matches = html.matchAll(REGEX);
|
||||
|
||||
const items: SearchResultItem[] = [];
|
||||
for (const match of matches) {
|
||||
if (match.groups?.NAME && match.groups?.URL) {
|
||||
items.push({
|
||||
name: match.groups.NAME.trim(),
|
||||
url: match.groups.URL,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
searchResult.items = items;
|
||||
searchResult.count = items.length;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
searchResult.error = error.message;
|
||||
} else {
|
||||
searchResult.error = "An unknown error occurred";
|
||||
}
|
||||
searchResult.count = -1;
|
||||
}
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
const LiangZiACG: Platform = {
|
||||
name: "量子acg",
|
||||
color: "white",
|
||||
magic: false,
|
||||
search: searchLiangZiACG,
|
||||
};
|
||||
|
||||
export default LiangZiACG;
|
||||
84
src/platforms/gal/MaoMaoWangPan.ts
Normal file
84
src/platforms/gal/MaoMaoWangPan.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { fetchClient } from "../../utils/httpClient";
|
||||
import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types";
|
||||
|
||||
const API_URL = "https://catcat.cloud/api/fs/search";
|
||||
const BASE_URL = "https://catcat.cloud";
|
||||
|
||||
interface MaoMaoItem {
|
||||
name: string;
|
||||
parent: string;
|
||||
}
|
||||
|
||||
interface MaoMaoResponse {
|
||||
message: string;
|
||||
data: {
|
||||
content: MaoMaoItem[];
|
||||
total: number;
|
||||
};
|
||||
}
|
||||
|
||||
async function searchMaoMaoWangPan(game: string): Promise<PlatformSearchResult> {
|
||||
const searchResult: PlatformSearchResult = {
|
||||
name: "猫猫网盘",
|
||||
count: 0,
|
||||
items: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
parent: "/GalGame/",
|
||||
keywords: game,
|
||||
scope: 0,
|
||||
page: 1,
|
||||
per_page: 999999, // Corresponds to MAX_RESULTS
|
||||
password: "",
|
||||
};
|
||||
|
||||
const response = await fetchClient(API_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as MaoMaoResponse;
|
||||
|
||||
if (data.message !== "success") {
|
||||
throw new Error(`API returned an error: ${data.message}`);
|
||||
}
|
||||
|
||||
const items: SearchResultItem[] = data.data.content
|
||||
.filter(item => item.parent.startsWith("/GalGame/SP后端1[GalGame分区]/"))
|
||||
.map(item => ({
|
||||
name: item.name.trim(),
|
||||
url: BASE_URL + item.parent + "/" + item.name,
|
||||
}));
|
||||
|
||||
searchResult.items = items;
|
||||
searchResult.count = items.length;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
searchResult.error = error.message;
|
||||
} else {
|
||||
searchResult.error = "An unknown error occurred";
|
||||
}
|
||||
searchResult.count = -1;
|
||||
}
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
const MaoMaoWangPan: Platform = {
|
||||
name: "猫猫网盘",
|
||||
color: "lime",
|
||||
magic: false,
|
||||
search: searchMaoMaoWangPan,
|
||||
};
|
||||
|
||||
export default MaoMaoWangPan;
|
||||
63
src/platforms/gal/MiaoYuanLingYu.ts
Normal file
63
src/platforms/gal/MiaoYuanLingYu.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { fetchClient } from "../../utils/httpClient";
|
||||
import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types";
|
||||
|
||||
const API_URL = "https://www.nyantaku.com/";
|
||||
const REGEX = /<div class="item-thumbnail">\s*<a target="_blank" href="(?<URL>.*?)">.+?" alt="(?<NAME>.*?)" class="lazyload/gs;
|
||||
|
||||
async function searchMiaoYuanLingYu(game: string): Promise<PlatformSearchResult> {
|
||||
const searchResult: PlatformSearchResult = {
|
||||
name: "喵源领域",
|
||||
count: 0,
|
||||
items: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const url = new URL(API_URL);
|
||||
url.searchParams.set("type", "post");
|
||||
url.searchParams.set("s", game);
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
const matches = html.matchAll(REGEX);
|
||||
|
||||
const items: SearchResultItem[] = [];
|
||||
for (const match of matches) {
|
||||
if (match.groups?.NAME && match.groups?.URL) {
|
||||
let name = match.groups.NAME.trim();
|
||||
if (name.endsWith("-喵源领域")) {
|
||||
name = name.substring(0, name.length - "-喵源领域".length).trim();
|
||||
}
|
||||
items.push({
|
||||
name: name,
|
||||
url: match.groups.URL,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
searchResult.items = items;
|
||||
searchResult.count = items.length;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
searchResult.error = error.message;
|
||||
} else {
|
||||
searchResult.error = "An unknown error occurred";
|
||||
}
|
||||
searchResult.count = -1;
|
||||
}
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
const MiaoYuanLingYu: Platform = {
|
||||
name: "喵源领域",
|
||||
color: "white",
|
||||
magic: false,
|
||||
search: searchMiaoYuanLingYu,
|
||||
};
|
||||
|
||||
export default MiaoYuanLingYu;
|
||||
67
src/platforms/gal/Nysoure.ts
Normal file
67
src/platforms/gal/Nysoure.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { fetchClient } from "../../utils/httpClient";
|
||||
import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types";
|
||||
|
||||
const API_URL = "https://res.nyne.dev/api/resource/search";
|
||||
const BASE_URL = "https://res.nyne.dev/resources/";
|
||||
|
||||
interface NysoureItem {
|
||||
id: number;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface NysoureResponse {
|
||||
success: boolean;
|
||||
data: NysoureItem[];
|
||||
}
|
||||
|
||||
async function searchNysoure(game: string): Promise<PlatformSearchResult> {
|
||||
const searchResult: PlatformSearchResult = {
|
||||
name: "Nysoure",
|
||||
count: 0,
|
||||
items: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const url = new URL(API_URL);
|
||||
url.searchParams.set("keyword", game);
|
||||
url.searchParams.set("page", "1");
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as NysoureResponse;
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error("API returned success: false");
|
||||
}
|
||||
|
||||
const items: SearchResultItem[] = data.data.map(item => ({
|
||||
name: item.title.trim(),
|
||||
url: BASE_URL + item.id,
|
||||
}));
|
||||
|
||||
searchResult.items = items;
|
||||
searchResult.count = items.length;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
searchResult.error = error.message;
|
||||
} else {
|
||||
searchResult.error = "An unknown error occurred";
|
||||
}
|
||||
searchResult.count = -1;
|
||||
}
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
const Nysoure: Platform = {
|
||||
name: "Nysoure",
|
||||
color: "gold",
|
||||
magic: true,
|
||||
search: searchNysoure,
|
||||
};
|
||||
|
||||
export default Nysoure;
|
||||
64
src/platforms/gal/QingJiACG.ts
Normal file
64
src/platforms/gal/QingJiACG.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { fetchClient } from "../../utils/httpClient";
|
||||
import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types";
|
||||
|
||||
const API_URL = "https://www.qingju.org/";
|
||||
const REGEX = /" class="lazyload fit-cover radius8">.*?<h2 class="item-heading"><a href="(?<URL>.*?)">(?<NAME>.*?)<\/a><\/h2>/gs;
|
||||
|
||||
async function searchQingJiACG(game: string): Promise<PlatformSearchResult> {
|
||||
const searchResult: PlatformSearchResult = {
|
||||
name: "青桔ACG",
|
||||
count: 0,
|
||||
items: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const url = new URL(API_URL);
|
||||
url.searchParams.set("s", game);
|
||||
url.searchParams.set("type", "post");
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
const matches = html.matchAll(REGEX);
|
||||
|
||||
const items: SearchResultItem[] = [];
|
||||
for (const match of matches) {
|
||||
if (match.groups?.NAME && match.groups?.URL) {
|
||||
// Original Python script had a filter: if "</p>" in i.group("URL"): continue
|
||||
// This is likely to filter out malformed URLs or irrelevant matches.
|
||||
if (match.groups.URL.includes("</p>")) {
|
||||
continue;
|
||||
}
|
||||
items.push({
|
||||
name: match.groups.NAME.trim(),
|
||||
url: match.groups.URL,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
searchResult.items = items;
|
||||
searchResult.count = items.length;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
searchResult.error = error.message;
|
||||
} else {
|
||||
searchResult.error = "An unknown error occurred";
|
||||
}
|
||||
searchResult.count = -1;
|
||||
}
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
const QingJiACG: Platform = {
|
||||
name: "青桔ACG",
|
||||
color: "lime",
|
||||
magic: false,
|
||||
search: searchQingJiACG,
|
||||
};
|
||||
|
||||
export default QingJiACG;
|
||||
58
src/platforms/gal/ShenShiTianTang.ts
Normal file
58
src/platforms/gal/ShenShiTianTang.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { fetchClient } from "../../utils/httpClient";
|
||||
import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types";
|
||||
|
||||
const API_URL = "https://www.chgal.com/";
|
||||
const REGEX = /<h2 class="post-list-title">\s*<a href="(?<URL>.*?)" title=".+?" class="text-reset">(?<NAME>.*?)<\/a>\s*<\/h2>\s*<span class="category-meta">/gs;
|
||||
|
||||
async function searchShenShiTianTang(game: string): Promise<PlatformSearchResult> {
|
||||
const searchResult: PlatformSearchResult = {
|
||||
name: "绅仕天堂",
|
||||
count: 0,
|
||||
items: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const url = new URL(API_URL);
|
||||
url.searchParams.set("s", game);
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
const matches = html.matchAll(REGEX);
|
||||
|
||||
const items: SearchResultItem[] = [];
|
||||
for (const match of matches) {
|
||||
if (match.groups?.NAME && match.groups?.URL) {
|
||||
items.push({
|
||||
name: match.groups.NAME.trim(),
|
||||
url: match.groups.URL,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
searchResult.items = items;
|
||||
searchResult.count = items.length;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
searchResult.error = error.message;
|
||||
} else {
|
||||
searchResult.error = "An unknown error occurred";
|
||||
}
|
||||
searchResult.count = -1;
|
||||
}
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
const ShenShiTianTang: Platform = {
|
||||
name: "绅仕天堂",
|
||||
color: "gold",
|
||||
magic: true,
|
||||
search: searchShenShiTianTang,
|
||||
};
|
||||
|
||||
export default ShenShiTianTang;
|
||||
55
src/platforms/gal/TaoHuaYuan.ts
Normal file
55
src/platforms/gal/TaoHuaYuan.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { fetchClient } from "../../utils/httpClient";
|
||||
import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types";
|
||||
|
||||
const DATA_URL = "https://peach.sslswwdx.top/page/search/index.json";
|
||||
|
||||
interface TaoHuaYuanItem {
|
||||
title: string;
|
||||
permalink: string;
|
||||
}
|
||||
|
||||
async function searchTaoHuaYuan(game: string): Promise<PlatformSearchResult> {
|
||||
const searchResult: PlatformSearchResult = {
|
||||
name: "桃花源",
|
||||
count: 0,
|
||||
items: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetchClient(DATA_URL);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch data from ${DATA_URL}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as TaoHuaYuanItem[];
|
||||
|
||||
const items: SearchResultItem[] = data
|
||||
.filter(item => item.title.includes(game))
|
||||
.map(item => ({
|
||||
name: item.title.trim(),
|
||||
url: item.permalink,
|
||||
}));
|
||||
|
||||
searchResult.items = items;
|
||||
searchResult.count = items.length;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
searchResult.error = error.message;
|
||||
} else {
|
||||
searchResult.error = "An unknown error occurred";
|
||||
}
|
||||
searchResult.count = -1;
|
||||
}
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
const TaoHuaYuan: Platform = {
|
||||
name: "桃花源",
|
||||
color: "lime",
|
||||
magic: false,
|
||||
search: searchTaoHuaYuan,
|
||||
};
|
||||
|
||||
export default TaoHuaYuan;
|
||||
57
src/platforms/gal/TianYouErCiYuan.ts
Normal file
57
src/platforms/gal/TianYouErCiYuan.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { fetchClient } from "../../utils/httpClient";
|
||||
import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types";
|
||||
|
||||
const API_URL = "https://www.tiangal.com/search/";
|
||||
const REGEX = /<\/i><\/a><h2><a href="(?<URL>.*?)" title="(?<NAME>.*?)"/gs;
|
||||
|
||||
async function searchTianYouErCiYuan(game: string): Promise<PlatformSearchResult> {
|
||||
const searchResult: PlatformSearchResult = {
|
||||
name: "天游二次元",
|
||||
count: 0,
|
||||
items: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const url = new URL(API_URL + encodeURIComponent(game)); // URL path parameter
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
const matches = html.matchAll(REGEX);
|
||||
|
||||
const items: SearchResultItem[] = [];
|
||||
for (const match of matches) {
|
||||
if (match.groups?.NAME && match.groups?.URL) {
|
||||
items.push({
|
||||
name: match.groups.NAME.trim(),
|
||||
url: match.groups.URL,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
searchResult.items = items;
|
||||
searchResult.count = items.length;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
searchResult.error = error.message;
|
||||
} else {
|
||||
searchResult.error = "An unknown error occurred";
|
||||
}
|
||||
searchResult.count = -1;
|
||||
}
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
const TianYouErCiYuan: Platform = {
|
||||
name: "天游二次元",
|
||||
color: "gold",
|
||||
magic: true,
|
||||
search: searchTianYouErCiYuan,
|
||||
};
|
||||
|
||||
export default TianYouErCiYuan;
|
||||
74
src/platforms/gal/TouchGal.ts
Normal file
74
src/platforms/gal/TouchGal.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { fetchClient } from "../../utils/httpClient";
|
||||
import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types";
|
||||
|
||||
const API_URL = "https://www.touchgal.us/api/search";
|
||||
const BASE_URL = "https://www.touchgal.us/";
|
||||
|
||||
async function searchTouchGal(game: string): Promise<PlatformSearchResult> {
|
||||
const searchResult: PlatformSearchResult = {
|
||||
name: "TouchGal",
|
||||
count: 0,
|
||||
items: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
queryString: JSON.stringify([{ type: "keyword", name: game }]),
|
||||
limit: 24, // Hardcoded as per original script
|
||||
searchOption: {
|
||||
searchInIntroduction: false,
|
||||
searchInAlias: true,
|
||||
searchInTag: false,
|
||||
},
|
||||
page: 1,
|
||||
selectedType: "all",
|
||||
selectedLanguage: "all",
|
||||
selectedPlatform: "all",
|
||||
sortField: "resource_update_time",
|
||||
sortOrder: "desc",
|
||||
selectedYears: ["all"],
|
||||
selectedMonths: ["all"],
|
||||
};
|
||||
|
||||
const response = await fetchClient(API_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as { galgames: { name: string; uniqueId: string }[] };
|
||||
|
||||
const items: SearchResultItem[] = data.galgames.map(item => ({
|
||||
name: item.name.trim(),
|
||||
url: BASE_URL + item.uniqueId,
|
||||
}));
|
||||
|
||||
searchResult.items = items;
|
||||
searchResult.count = items.length;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
searchResult.error = error.message;
|
||||
} else {
|
||||
searchResult.error = "An unknown error occurred";
|
||||
}
|
||||
searchResult.count = -1;
|
||||
}
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
const TouchGal: Platform = {
|
||||
name: "TouchGal",
|
||||
color: "lime",
|
||||
magic: false,
|
||||
search: searchTouchGal,
|
||||
};
|
||||
|
||||
export default TouchGal;
|
||||
103
src/platforms/gal/VikaACG.ts
Normal file
103
src/platforms/gal/VikaACG.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { fetchClient } from "../../utils/httpClient";
|
||||
import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types";
|
||||
|
||||
const API_URL = "https://www.vikacg.com/wp-json/b2/v1/getPostList";
|
||||
|
||||
// The Python code suggests the response text itself might be a JSON string
|
||||
// that contains escaped HTML. Let's try to parse it as JSON first.
|
||||
// The regex is applied to the *unescaped* string.
|
||||
const REGEX = /<h2><a target="_blank" href="(?<URL>.*?)" title="(?<NAME>.*?)"/gs;
|
||||
|
||||
async function searchVikaACG(game: string): Promise<PlatformSearchResult> {
|
||||
const searchResult: PlatformSearchResult = {
|
||||
name: "VikaACG",
|
||||
count: 0,
|
||||
items: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
paged: 1,
|
||||
post_paged: 1,
|
||||
post_count: 1000, // Corresponds to MAX_RESULTS
|
||||
post_type: "post-1",
|
||||
post_cat: [6],
|
||||
post_order: "modified",
|
||||
post_meta: [
|
||||
"user", "date", "des", "cats", "like", "comment", "views", "video", "download", "hide",
|
||||
],
|
||||
metas: {},
|
||||
search: game,
|
||||
};
|
||||
|
||||
const response = await fetchClient(API_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
}
|
||||
|
||||
const rawText = await response.text();
|
||||
let htmlContent: string;
|
||||
|
||||
try {
|
||||
// Attempt to parse as JSON first. If it's a JSON string containing HTML,
|
||||
// JSON.parse will handle standard escapes like \uXXXX.
|
||||
const parsedJson = JSON.parse(rawText);
|
||||
// Assuming the HTML content is directly the value of the JSON, or a specific field.
|
||||
// The Python code implies the entire response text, after unescaping, is the HTML.
|
||||
// So, if parsedJson is a string, use it. Otherwise, stringify it.
|
||||
htmlContent = typeof parsedJson === 'string' ? parsedJson : JSON.stringify(parsedJson);
|
||||
} catch (jsonError) {
|
||||
// If JSON.parse fails, it might be due to non-standard Python escapes like \\/ or \\\\
|
||||
// Attempt a simple unescape for these specific cases.
|
||||
// Note: This is a simplified unescape and might not cover all Python's unicode_escape nuances.
|
||||
const unescapedText = rawText.replace(/\\(.)/g, '$1'); // Replaces \\/ with / and \\\\ with \
|
||||
try {
|
||||
htmlContent = JSON.parse(unescapedText); // Try parsing as JSON again
|
||||
} catch (finalError) {
|
||||
// If still fails, assume it's raw HTML that just needs basic unescaping
|
||||
htmlContent = unescapedText;
|
||||
}
|
||||
}
|
||||
|
||||
const matches = htmlContent.matchAll(REGEX);
|
||||
|
||||
const items: SearchResultItem[] = [];
|
||||
for (const match of matches) {
|
||||
if (match.groups?.NAME && match.groups?.URL) {
|
||||
items.push({
|
||||
name: match.groups.NAME.trim(),
|
||||
url: match.groups.URL,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
searchResult.items = items;
|
||||
searchResult.count = items.length;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
searchResult.error = error.message;
|
||||
} else {
|
||||
searchResult.error = "An unknown error occurred";
|
||||
}
|
||||
searchResult.count = -1;
|
||||
}
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
const VikaACG: Platform = {
|
||||
name: "VikaACG",
|
||||
color: "gold",
|
||||
magic: true,
|
||||
search: searchVikaACG,
|
||||
};
|
||||
|
||||
export default VikaACG;
|
||||
82
src/platforms/gal/WeiZhiYunPan.ts
Normal file
82
src/platforms/gal/WeiZhiYunPan.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { fetchClient } from "../../utils/httpClient";
|
||||
import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types";
|
||||
|
||||
const API_URL = "https://www.nullcloud.top/api/fs/search";
|
||||
const BASE_URL = "https://www.nullcloud.top";
|
||||
|
||||
interface WeiZhiYunPanItem {
|
||||
name: string;
|
||||
parent: string;
|
||||
}
|
||||
|
||||
interface WeiZhiYunPanResponse {
|
||||
message: string;
|
||||
data: {
|
||||
content: WeiZhiYunPanItem[];
|
||||
total: number;
|
||||
};
|
||||
}
|
||||
|
||||
async function searchWeiZhiYunPan(game: string): Promise<PlatformSearchResult> {
|
||||
const searchResult: PlatformSearchResult = {
|
||||
name: "未知云盘",
|
||||
count: 0,
|
||||
items: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
parent: "/",
|
||||
keywords: game,
|
||||
scope: 0,
|
||||
page: 1,
|
||||
per_page: 999999, // Corresponds to MAX_RESULTS
|
||||
password: "",
|
||||
};
|
||||
|
||||
const response = await fetchClient(API_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as WeiZhiYunPanResponse;
|
||||
|
||||
if (data.message !== "success") {
|
||||
throw new Error(`API returned an error: ${data.message}`);
|
||||
}
|
||||
|
||||
const items: SearchResultItem[] = data.data.content.map(item => ({
|
||||
name: item.name.trim(),
|
||||
url: BASE_URL + item.parent + "/" + item.name,
|
||||
}));
|
||||
|
||||
searchResult.items = items;
|
||||
searchResult.count = items.length;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
searchResult.error = error.message;
|
||||
} else {
|
||||
searchResult.error = "An unknown error occurred";
|
||||
}
|
||||
searchResult.count = -1;
|
||||
}
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
const WeiZhiYunPan: Platform = {
|
||||
name: "未知云盘",
|
||||
color: "lime",
|
||||
magic: false,
|
||||
search: searchWeiZhiYunPan,
|
||||
};
|
||||
|
||||
export default WeiZhiYunPan;
|
||||
59
src/platforms/gal/YingZhiGuang.ts
Normal file
59
src/platforms/gal/YingZhiGuang.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { fetchClient } from "../../utils/httpClient";
|
||||
import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types";
|
||||
|
||||
const DATA_URL = "https://yinghu.netlify.app/search.xml";
|
||||
const BASE_URL = "https://yinghu.netlify.app";
|
||||
const REGEX = /<entry>.*?<title>(.*?)<\/title>.*?<url>(.*?)<\/url>.*?<\/entry>/gs;
|
||||
|
||||
async function searchYingZhiGuang(game: string): Promise<PlatformSearchResult> {
|
||||
const searchResult: PlatformSearchResult = {
|
||||
name: "萤ノ光",
|
||||
count: 0,
|
||||
items: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetchClient(DATA_URL);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch data from ${DATA_URL}`);
|
||||
}
|
||||
|
||||
const xmlText = await response.text();
|
||||
const matches = xmlText.matchAll(REGEX);
|
||||
|
||||
const items: SearchResultItem[] = [];
|
||||
for (const match of matches) {
|
||||
const title = match[1];
|
||||
const urlPath = match[2];
|
||||
|
||||
if (title && urlPath && title.includes(game)) {
|
||||
items.push({
|
||||
name: title.trim(),
|
||||
url: BASE_URL + urlPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
searchResult.items = items;
|
||||
searchResult.count = items.length;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
searchResult.error = error.message;
|
||||
} else {
|
||||
searchResult.error = "An unknown error occurred";
|
||||
}
|
||||
searchResult.count = -1;
|
||||
}
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
const YingZhiGuang: Platform = {
|
||||
name: "萤ノ光",
|
||||
color: "lime",
|
||||
magic: false,
|
||||
search: searchYingZhiGuang,
|
||||
};
|
||||
|
||||
export default YingZhiGuang;
|
||||
61
src/platforms/gal/YouYuDeloli.ts
Normal file
61
src/platforms/gal/YouYuDeloli.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { fetchClient } from "../../utils/httpClient";
|
||||
import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types";
|
||||
|
||||
const API_URL = "https://www.ttloli.com/";
|
||||
const REGEX = /<p style="text-align: center;"> <a href=".*?" target="_blank">.*?<p style="text-align: center;"> <a href="(?<URL>.*?)" title="(?<NAME>.*?)"> <img src=/gs;
|
||||
|
||||
async function searchYouYuDeloli(game: string): Promise<PlatformSearchResult> {
|
||||
const searchResult: PlatformSearchResult = {
|
||||
name: "忧郁的loli",
|
||||
count: 0,
|
||||
items: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const url = new URL(API_URL);
|
||||
url.searchParams.set("s", game);
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
const matches = html.matchAll(REGEX);
|
||||
|
||||
const items: SearchResultItem[] = [];
|
||||
for (const match of matches) {
|
||||
if (match.groups?.NAME && match.groups?.URL) {
|
||||
if (match.groups.NAME === "详细更新日志") {
|
||||
continue;
|
||||
}
|
||||
items.push({
|
||||
name: match.groups.NAME.trim(),
|
||||
url: match.groups.URL,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
searchResult.items = items;
|
||||
searchResult.count = items.length;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
searchResult.error = error.message;
|
||||
} else {
|
||||
searchResult.error = "An unknown error occurred";
|
||||
}
|
||||
searchResult.count = -1;
|
||||
}
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
const YouYuDeloli: Platform = {
|
||||
name: "忧郁的loli",
|
||||
color: "lime",
|
||||
magic: false,
|
||||
search: searchYouYuDeloli,
|
||||
};
|
||||
|
||||
export default YouYuDeloli;
|
||||
59
src/platforms/gal/YueYao.ts
Normal file
59
src/platforms/gal/YueYao.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { fetchClient } from "../../utils/httpClient";
|
||||
import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types";
|
||||
|
||||
const DATA_URL = "https://www.sayafx.vip/search.xml";
|
||||
const BASE_URL = "https://www.sayafx.vip";
|
||||
const REGEX = /<entry>.*?<title>(.*?)<\/title>.*?<url>(.*?)<\/url>.*?<\/entry>/gs;
|
||||
|
||||
async function searchYueYao(game: string): Promise<PlatformSearchResult> {
|
||||
const searchResult: PlatformSearchResult = {
|
||||
name: "月谣",
|
||||
count: 0,
|
||||
items: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetchClient(DATA_URL);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch data from ${DATA_URL}`);
|
||||
}
|
||||
|
||||
const xmlText = await response.text();
|
||||
const matches = xmlText.matchAll(REGEX);
|
||||
|
||||
const items: SearchResultItem[] = [];
|
||||
for (const match of matches) {
|
||||
const title = match[1];
|
||||
const urlPath = match[2];
|
||||
|
||||
if (title && urlPath && title.includes(game)) {
|
||||
items.push({
|
||||
name: title.trim(),
|
||||
url: BASE_URL + urlPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
searchResult.items = items;
|
||||
searchResult.count = items.length;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
searchResult.error = error.message;
|
||||
} else {
|
||||
searchResult.error = "An unknown error occurred";
|
||||
}
|
||||
searchResult.count = -1;
|
||||
}
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
const YueYao: Platform = {
|
||||
name: "月谣",
|
||||
color: "lime",
|
||||
magic: false,
|
||||
search: searchYueYao,
|
||||
};
|
||||
|
||||
export default YueYao;
|
||||
82
src/platforms/gal/ZeroFive.ts
Normal file
82
src/platforms/gal/ZeroFive.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { fetchClient } from "../../utils/httpClient";
|
||||
import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types";
|
||||
|
||||
const API_URL = "https://05fx.022016.xyz/api/fs/search";
|
||||
const BASE_URL = "https://05fx.022016.xyz";
|
||||
|
||||
interface ZeroFiveItem {
|
||||
name: string;
|
||||
parent: string;
|
||||
}
|
||||
|
||||
interface ZeroFiveResponse {
|
||||
message: string;
|
||||
data: {
|
||||
content: ZeroFiveItem[];
|
||||
total: number;
|
||||
};
|
||||
}
|
||||
|
||||
async function searchZeroFive(game: string): Promise<PlatformSearchResult> {
|
||||
const searchResult: PlatformSearchResult = {
|
||||
name: "05的资源小站",
|
||||
count: 0,
|
||||
items: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
parent: "/",
|
||||
keywords: game,
|
||||
scope: 0,
|
||||
page: 1,
|
||||
per_page: 999999, // Corresponds to MAX_RESULTS
|
||||
password: "",
|
||||
};
|
||||
|
||||
const response = await fetchClient(API_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as ZeroFiveResponse;
|
||||
|
||||
if (data.message !== "success") {
|
||||
throw new Error(`API returned an error: ${data.message}`);
|
||||
}
|
||||
|
||||
const items: SearchResultItem[] = data.data.content.map(item => ({
|
||||
name: item.name.trim(),
|
||||
url: BASE_URL + item.parent + "/" + item.name,
|
||||
}));
|
||||
|
||||
searchResult.items = items;
|
||||
searchResult.count = items.length;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
searchResult.error = error.message;
|
||||
} else {
|
||||
searchResult.error = "An unknown error occurred";
|
||||
}
|
||||
searchResult.count = -1;
|
||||
}
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
const ZeroFive: Platform = {
|
||||
name: "05的资源小站",
|
||||
color: "lime",
|
||||
magic: false,
|
||||
search: searchZeroFive,
|
||||
};
|
||||
|
||||
export default ZeroFive;
|
||||
59
src/platforms/gal/ZhenHongXiaoZhan.ts
Normal file
59
src/platforms/gal/ZhenHongXiaoZhan.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { fetchClient } from "../../utils/httpClient";
|
||||
import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types";
|
||||
|
||||
const API_URL = "https://www.shinnku.com/search";
|
||||
const BASE_URL = "https://www.shinnku.com";
|
||||
const REGEX = /hover:underline" href="(?<URL>.+?)">\s*(?<NAME>.+?)\s*<\/a>/gs;
|
||||
|
||||
async function searchZhenHongXiaoZhan(game: string): Promise<PlatformSearchResult> {
|
||||
const searchResult: PlatformSearchResult = {
|
||||
name: "真红小站",
|
||||
count: 0,
|
||||
items: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const url = new URL(API_URL);
|
||||
url.searchParams.set("q", game);
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
const matches = html.matchAll(REGEX);
|
||||
|
||||
const items: SearchResultItem[] = [];
|
||||
for (const match of matches) {
|
||||
if (match.groups?.NAME && match.groups?.URL) {
|
||||
items.push({
|
||||
name: match.groups.NAME.trim(),
|
||||
url: BASE_URL + encodeURIComponent(match.groups.URL),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
searchResult.items = items;
|
||||
searchResult.count = items.length;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
searchResult.error = error.message;
|
||||
} else {
|
||||
searchResult.error = "An unknown error occurred";
|
||||
}
|
||||
searchResult.count = -1;
|
||||
}
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
const ZhenHongXiaoZhan: Platform = {
|
||||
name: "真红小站",
|
||||
color: "lime",
|
||||
magic: false,
|
||||
search: searchZhenHongXiaoZhan,
|
||||
};
|
||||
|
||||
export default ZhenHongXiaoZhan;
|
||||
82
src/platforms/gal/ZiLingDeMiaoMiaoWu.ts
Normal file
82
src/platforms/gal/ZiLingDeMiaoMiaoWu.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { fetchClient } from "../../utils/httpClient";
|
||||
import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types";
|
||||
|
||||
const API_URL = "https://zi0.cc/api/fs/search";
|
||||
const BASE_URL = "https://zi0.cc";
|
||||
|
||||
interface ZiLingDeMiaoMiaoWuItem {
|
||||
name: string;
|
||||
parent: string;
|
||||
}
|
||||
|
||||
interface ZiLingDeMiaoMiaoWuResponse {
|
||||
message: string;
|
||||
data: {
|
||||
content: ZiLingDeMiaoMiaoWuItem[];
|
||||
total: number;
|
||||
};
|
||||
}
|
||||
|
||||
async function searchZiLingDeMiaoMiaoWu(game: string): Promise<PlatformSearchResult> {
|
||||
const searchResult: PlatformSearchResult = {
|
||||
name: "梓澪の妙妙屋",
|
||||
count: 0,
|
||||
items: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
parent: "/",
|
||||
keywords: game,
|
||||
scope: 0,
|
||||
page: 1,
|
||||
per_page: 999999, // Corresponds to MAX_RESULTS
|
||||
password: "",
|
||||
};
|
||||
|
||||
const response = await fetchClient(API_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as ZiLingDeMiaoMiaoWuResponse;
|
||||
|
||||
if (data.message !== "success") {
|
||||
throw new Error(`API returned an error: ${data.message}`);
|
||||
}
|
||||
|
||||
const items: SearchResultItem[] = data.data.content.map(item => ({
|
||||
name: item.name.trim(),
|
||||
url: BASE_URL + item.parent + "/" + item.name,
|
||||
}));
|
||||
|
||||
searchResult.items = items;
|
||||
searchResult.count = items.length;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
searchResult.error = error.message;
|
||||
} else {
|
||||
searchResult.error = "An unknown error occurred";
|
||||
}
|
||||
searchResult.count = -1;
|
||||
}
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
const ZiLingDeMiaoMiaoWu: Platform = {
|
||||
name: "梓澪の妙妙屋",
|
||||
color: "lime",
|
||||
magic: false,
|
||||
search: searchZiLingDeMiaoMiaoWu,
|
||||
};
|
||||
|
||||
export default ZiLingDeMiaoMiaoWu;
|
||||
82
src/platforms/gal/ZiYuanShe.ts
Normal file
82
src/platforms/gal/ZiYuanShe.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { fetchClient } from "../../utils/httpClient";
|
||||
import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types";
|
||||
|
||||
const API_URL = "https://galzy.eu.org/api/fs/search";
|
||||
const BASE_URL = "https://galzy.eu.org";
|
||||
|
||||
interface ZiYuanSheItem {
|
||||
name: string;
|
||||
parent: string;
|
||||
}
|
||||
|
||||
interface ZiYuanSheResponse {
|
||||
message: string;
|
||||
data: {
|
||||
content: ZiYuanSheItem[];
|
||||
total: number;
|
||||
};
|
||||
}
|
||||
|
||||
async function searchZiYuanShe(game: string, zypassword: string = ""): Promise<PlatformSearchResult> {
|
||||
const searchResult: PlatformSearchResult = {
|
||||
name: "紫缘Gal",
|
||||
count: 0,
|
||||
items: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
parent: "/",
|
||||
keywords: game,
|
||||
scope: 0,
|
||||
page: 1,
|
||||
per_page: 999999, // Corresponds to MAX_RESULTS
|
||||
password: zypassword, // Pass the zypassword here
|
||||
};
|
||||
|
||||
const response = await fetchClient(API_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as ZiYuanSheResponse;
|
||||
|
||||
if (data.message !== "success") {
|
||||
throw new Error(`API returned an error: ${data.message}`);
|
||||
}
|
||||
|
||||
const items: SearchResultItem[] = data.data.content.map(item => ({
|
||||
name: item.name.trim(),
|
||||
url: BASE_URL + item.parent + "/" + item.name,
|
||||
}));
|
||||
|
||||
searchResult.items = items;
|
||||
searchResult.count = items.length;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
searchResult.error = error.message;
|
||||
} else {
|
||||
searchResult.error = "An unknown error occurred";
|
||||
}
|
||||
searchResult.count = -1;
|
||||
}
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
const ZiYuanShe: Platform = {
|
||||
name: "紫缘Gal",
|
||||
color: "white",
|
||||
magic: false,
|
||||
search: searchZiYuanShe,
|
||||
};
|
||||
|
||||
export default ZiYuanShe;
|
||||
70
src/platforms/gal/index.ts
Normal file
70
src/platforms/gal/index.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import type { Platform } from "../../types";
|
||||
import ACGYingYingGuai from "./ACGYingYingGuai";
|
||||
import BiAnXingLu from "./BiAnXingLu";
|
||||
import DaoHeGal from "./DaoHeGal";
|
||||
import FuFuACG from "./FuFuACG";
|
||||
import GalgameX from "./GalgameX";
|
||||
import GalTuShuGuan from "./GalTuShuGuan";
|
||||
import GGBases from "./GGBases";
|
||||
import GGS from "./GGS";
|
||||
import Hikarinagi from "./Hikarinagi";
|
||||
import JiMengACG from "./JiMengACG";
|
||||
import Koyso from "./Koyso";
|
||||
import KunGalgame from "./KunGalgame";
|
||||
import LiangZiACG from "./LiangZiACG";
|
||||
import LiSiTanACG from "./LiSiTanACG";
|
||||
import MaoMaoWangPan from "./MaoMaoWangPan";
|
||||
import MiaoYuanLingYu from "./MiaoYuanLingYu";
|
||||
import Nysoure from "./Nysoure";
|
||||
import QingJiACG from "./QingJiACG";
|
||||
import ShenShiTianTang from "./ShenShiTianTang";
|
||||
import TaoHuaYuan from "./TaoHuaYuan";
|
||||
import TianYouErCiYuan from "./TianYouErCiYuan";
|
||||
import TouchGal from "./TouchGal";
|
||||
import VikaACG from "./VikaACG";
|
||||
import WeiZhiYunPan from "./WeiZhiYunPan";
|
||||
import xxacg from "./xxacg";
|
||||
import YingZhiGuang from "./YingZhiGuang";
|
||||
import YouYuDeloli from "./YouYuDeloli";
|
||||
import YueYao from "./YueYao";
|
||||
import ZeroFive from "./ZeroFive";
|
||||
import ZhenHongXiaoZhan from "./ZhenHongXiaoZhan";
|
||||
import ZiLingDeMiaoMiaoWu from "./ZiLingDeMiaoMiaoWu";
|
||||
import ZiYuanShe from "./ZiYuanShe";
|
||||
|
||||
const platforms: Platform[] = [
|
||||
ACGYingYingGuai,
|
||||
BiAnXingLu,
|
||||
DaoHeGal,
|
||||
FuFuACG,
|
||||
GalgameX,
|
||||
GalTuShuGuan,
|
||||
GGBases,
|
||||
GGS,
|
||||
Hikarinagi,
|
||||
JiMengACG,
|
||||
Koyso,
|
||||
KunGalgame,
|
||||
LiangZiACG,
|
||||
LiSiTanACG,
|
||||
MaoMaoWangPan,
|
||||
MiaoYuanLingYu,
|
||||
Nysoure,
|
||||
QingJiACG,
|
||||
ShenShiTianTang,
|
||||
TaoHuaYuan,
|
||||
TianYouErCiYuan,
|
||||
TouchGal,
|
||||
VikaACG,
|
||||
WeiZhiYunPan,
|
||||
xxacg,
|
||||
YingZhiGuang,
|
||||
YouYuDeloli,
|
||||
YueYao,
|
||||
ZeroFive,
|
||||
ZhenHongXiaoZhan,
|
||||
ZiLingDeMiaoMiaoWu,
|
||||
ZiYuanShe,
|
||||
];
|
||||
|
||||
export default platforms;
|
||||
69
src/platforms/gal/xxacg.ts
Normal file
69
src/platforms/gal/xxacg.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { fetchClient } from "../../utils/httpClient";
|
||||
import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types";
|
||||
|
||||
const REGEX = /<h4 class="entry-title title"><a href="(?<URL>.*?)">(?<NAME>.*?)<\/a><\/h4>/gs;
|
||||
|
||||
function stripHtml(html: string): string {
|
||||
return html.replace(/<[^>]*>/g, "").trim();
|
||||
}
|
||||
|
||||
async function searchXxacg(game: string): Promise<PlatformSearchResult> {
|
||||
const searchResult: PlatformSearchResult = {
|
||||
name: "xxacg",
|
||||
count: 0,
|
||||
items: [],
|
||||
};
|
||||
|
||||
let html = ""; // 将 html 变量提升到 try 块外部
|
||||
|
||||
try {
|
||||
const url = new URL("https://xxacg.net/");
|
||||
url.searchParams.set("s", game);
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Search API response status code is ${response.status}`);
|
||||
}
|
||||
|
||||
html = await response.text();
|
||||
const matches = html.matchAll(REGEX);
|
||||
|
||||
const items: SearchResultItem[] = [];
|
||||
for (const match of matches) {
|
||||
if (match.groups?.NAME && match.groups?.URL) {
|
||||
items.push({
|
||||
name: stripHtml(match.groups.NAME),
|
||||
url: match.groups.URL,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (items.length === 0 && html.length > 0) {
|
||||
// 如果没有匹配项,但我们确实收到了 HTML,这可能意味着页面结构已更改。
|
||||
// 将部分 HTML 包含在错误中以供调试。
|
||||
throw new Error(`No matches found on page. HTML starts with: ${html.substring(0, 500)}`);
|
||||
}
|
||||
|
||||
searchResult.items = items;
|
||||
searchResult.count = items.length;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
searchResult.error = error.message;
|
||||
} else {
|
||||
searchResult.error = "An unknown error occurred";
|
||||
}
|
||||
searchResult.count = -1;
|
||||
}
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
const xxacg: Platform = {
|
||||
name: "xxacg",
|
||||
color: "gold",
|
||||
magic: true,
|
||||
search: searchXxacg,
|
||||
};
|
||||
|
||||
export default xxacg;
|
||||
76
src/platforms/patch/KunGalgameBuDing.ts
Normal file
76
src/platforms/patch/KunGalgameBuDing.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { fetchClient } from "../../utils/httpClient";
|
||||
import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types";
|
||||
|
||||
const API_URL = "https://www.moyu.moe/api/search";
|
||||
const BASE_URL = "https://www.moyu.moe/patch/";
|
||||
|
||||
interface KunGalgameBuDingItem {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface KunGalgameBuDingResponse {
|
||||
galgames: KunGalgameBuDingItem[];
|
||||
}
|
||||
|
||||
async function searchKunGalgameBuDing(game: string): Promise<PlatformSearchResult> {
|
||||
const searchResult: PlatformSearchResult = {
|
||||
name: "鲲Galgame补丁",
|
||||
count: 0,
|
||||
items: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
limit: 24, // Hardcoded as per original script
|
||||
page: 1,
|
||||
query: game.split(/\s+/), // Split by whitespace
|
||||
searchOption: {
|
||||
searchInAlias: true,
|
||||
searchInIntroduction: false,
|
||||
searchInTag: false,
|
||||
},
|
||||
};
|
||||
|
||||
const response = await fetchClient(API_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as KunGalgameBuDingResponse;
|
||||
|
||||
const items: SearchResultItem[] = data.galgames.map(item => ({
|
||||
name: item.name,
|
||||
url: `${BASE_URL}${item.id}/introduction`,
|
||||
}));
|
||||
|
||||
searchResult.items = items;
|
||||
searchResult.count = items.length;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
searchResult.error = error.message;
|
||||
} else {
|
||||
searchResult.error = "An unknown error occurred";
|
||||
}
|
||||
searchResult.count = -1;
|
||||
}
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
const KunGalgameBuDing: Platform = {
|
||||
name: "鲲Galgame补丁",
|
||||
color: "lime",
|
||||
magic: false,
|
||||
search: searchKunGalgameBuDing,
|
||||
};
|
||||
|
||||
export default KunGalgameBuDing;
|
||||
65
src/platforms/patch/TWOdfan.ts
Normal file
65
src/platforms/patch/TWOdfan.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { fetchClient } from "../../utils/httpClient";
|
||||
import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types";
|
||||
|
||||
const API_URL = "https://2dfan.com/subjects/search";
|
||||
const BASE_URL = "https://2dfan.com";
|
||||
const REGEX = /<h4 class="media-heading"><a target="_blank" href="(?<URL>.*?)">(?<NAME>.*?)<\/a><\/h4>/gs;
|
||||
|
||||
interface TwoDFanResponse {
|
||||
subjects: string; // This is an HTML string
|
||||
}
|
||||
|
||||
async function searchTWOdfan(game: string): Promise<PlatformSearchResult> {
|
||||
const searchResult: PlatformSearchResult = {
|
||||
name: "2dfan",
|
||||
count: 0,
|
||||
items: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const url = new URL(API_URL);
|
||||
url.searchParams.set("keyword", game);
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as TwoDFanResponse;
|
||||
const html = data.subjects;
|
||||
|
||||
const matches = html.matchAll(REGEX);
|
||||
|
||||
const items: SearchResultItem[] = [];
|
||||
for (const match of matches) {
|
||||
if (match.groups?.NAME && match.groups?.URL) {
|
||||
items.push({
|
||||
name: match.groups.NAME.trim(),
|
||||
url: BASE_URL + match.groups.URL,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
searchResult.items = items;
|
||||
searchResult.count = items.length;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
searchResult.error = error.message;
|
||||
} else {
|
||||
searchResult.error = "An unknown error occurred";
|
||||
}
|
||||
searchResult.count = -1;
|
||||
}
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
const TWOdfan: Platform = {
|
||||
name: "2dfan",
|
||||
color: "lime",
|
||||
magic: false,
|
||||
search: searchTWOdfan,
|
||||
};
|
||||
|
||||
export default TWOdfan;
|
||||
10
src/platforms/patch/index.ts
Normal file
10
src/platforms/patch/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { Platform } from "../../types";
|
||||
import KunGalgameBuDing from "./KunGalgameBuDing";
|
||||
import TWOdfan from "./TWOdfan";
|
||||
|
||||
const platforms: Platform[] = [
|
||||
KunGalgameBuDing,
|
||||
TWOdfan,
|
||||
];
|
||||
|
||||
export default platforms;
|
||||
36
src/ratelimit.ts
Normal file
36
src/ratelimit.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
// --- 速率限制常量 ---
|
||||
const SEARCH_INTERVAL_SECONDS = 15;
|
||||
// KV 条目将在此秒数后自动过期,以防止存储膨胀
|
||||
const IP_ENTRY_TTL_SECONDS = 60;
|
||||
|
||||
/**
|
||||
* 检查给定 IP 地址是否超出了速率限制。
|
||||
* @param ip 客户端的 IP 地址。
|
||||
* @param kvNamespace 用于存储 IP 时间戳的 KV 命名空间。
|
||||
* @returns 返回一个对象,包含是否允许请求以及剩余的等待秒数。
|
||||
*/
|
||||
export async function checkRateLimit(
|
||||
ip: string,
|
||||
kvNamespace: KVNamespace
|
||||
): Promise<{ allowed: boolean; retryAfter: number }> {
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
const lastSearchTimeStr = await kvNamespace.get(ip);
|
||||
const lastSearchTime = lastSearchTimeStr ? parseInt(lastSearchTimeStr, 10) : 0;
|
||||
|
||||
if (lastSearchTime && (currentTime - lastSearchTime) < SEARCH_INTERVAL_SECONDS) {
|
||||
return {
|
||||
allowed: false,
|
||||
retryAfter: SEARCH_INTERVAL_SECONDS - (currentTime - lastSearchTime),
|
||||
};
|
||||
}
|
||||
|
||||
// 更新 IP 的最后搜索时间,并设置 TTL
|
||||
await kvNamespace.put(ip, currentTime.toString(), {
|
||||
expirationTtl: IP_ENTRY_TTL_SECONDS,
|
||||
});
|
||||
|
||||
return {
|
||||
allowed: true,
|
||||
retryAfter: 0,
|
||||
};
|
||||
}
|
||||
34
src/types.ts
Normal file
34
src/types.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
// 单个搜索结果
|
||||
export interface SearchResultItem {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
// 平台搜索的返回值
|
||||
export interface PlatformSearchResult {
|
||||
items: SearchResultItem[];
|
||||
count: number;
|
||||
name: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// 平台对象的接口
|
||||
export interface Platform {
|
||||
name: string;
|
||||
color: string;
|
||||
magic: boolean;
|
||||
search: (game: string, ...args: any[]) => Promise<PlatformSearchResult>;
|
||||
}
|
||||
|
||||
// SSE 事件流中的数据结构
|
||||
export interface StreamResult {
|
||||
name: string;
|
||||
color: string;
|
||||
items: SearchResultItem[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface StreamProgress {
|
||||
completed: number;
|
||||
total: number;
|
||||
}
|
||||
41
src/utils/httpClient.ts
Normal file
41
src/utils/httpClient.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
const TIMEOUT_SECONDS = 15;
|
||||
|
||||
const HEADERS = {
|
||||
"Connection": "close",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 (From SearchGal.homes) (https://github.com/Moe-Sakura/SearchGal)",
|
||||
};
|
||||
|
||||
/**
|
||||
* 一个封装了原生 fetch 并增加了超时功能的 HTTP 客户端。
|
||||
* @param url 请求的 URL。
|
||||
* @param options fetch 的请求选项。
|
||||
* @returns 返回一个 Promise<Response>。
|
||||
*/
|
||||
export async function fetchClient(
|
||||
url: string | URL,
|
||||
options: RequestInit = {}
|
||||
): Promise<Response> {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_SECONDS * 1000);
|
||||
|
||||
const finalOptions: RequestInit = {
|
||||
...options,
|
||||
headers: {
|
||||
...HEADERS,
|
||||
...options.headers,
|
||||
},
|
||||
signal: controller.signal,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(url, finalOptions);
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
throw new Error(`Request timed out after ${TIMEOUT_SECONDS} seconds`);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"lib": ["esnext"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"types": ["@cloudflare/workers-types"]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
11
wrangler.toml
Normal file
11
wrangler.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
name = "searchgal-worker"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2023-10-30"
|
||||
|
||||
[vars]
|
||||
# 如果有需要,可以在这里添加环境变量
|
||||
|
||||
# --- KV 命名空间绑定 ---
|
||||
[[kv_namespaces]]
|
||||
binding = "RATE_LIMIT_KV"
|
||||
id = "7ed4c4f36baf419bb9ed54538a61f473"
|
||||
Reference in New Issue
Block a user