Skip to content

Commit e8d5941

Browse files
committed
another useless endpoint
1 parent 692aa2a commit e8d5941

File tree

3 files changed

+218
-0
lines changed

3 files changed

+218
-0
lines changed

src/handler.ts

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { getGameId } from "@/routes/games/get-game-id";
88
import { getAsset } from "@/routes/games/get-game-category";
99
import { getGames } from "@/routes/games/list-games";
1010
import { getChangelog } from "@/routes/discord/changelog";
11+
import { searchAssets } from "@/routes/search/global-asset-search";
1112

1213
const router = AutoRouter();
1314

@@ -16,6 +17,7 @@ router
1617
.get("/games", errorHandler(getGames))
1718
.get("/game/:gameId", errorHandler(getGameId))
1819
.get("/game/:gameId/:asset", errorHandler(getAsset))
20+
.get("/search/assets", errorHandler(searchAssets))
1921
.get("/discord/contributors", errorHandler(getContributors))
2022
.get("/discord/members", errorHandler(getMembers))
2123
.get("/discord/changelog", errorHandler(getChangelog))

src/routes/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ const routes: string[] = [
44
"https://api.wanderer.moe/games",
55
"https://api.wanderer.moe/game/{gameId}",
66
"https://api.wanderer.moe/game/{gameId}/{asset}",
7+
"https://api.wanderer.moe/search/assets?game={game}&query={searchQuery}",
78
"https://api.wanderer.moe/discord/contributors",
89
"https://api.wanderer.moe/discord/members",
10+
"https://api.wanderer.moe/discord/changelog",
911
];
1012

1113
export const index = async (): Promise<Response> => {
+214
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
import { responseHeaders } from "@/lib/responseHeaders";
2+
import { listBucket } from "@/lib/listBucket";
3+
import type { Image } from "@/lib/types/asset";
4+
5+
export const searchAssets = async (
6+
request: Request,
7+
env: Env,
8+
ctx: ExecutionContext
9+
): Promise<Response> => {
10+
const url = new URL(request.url);
11+
const query = url.searchParams.get("query")?.toLowerCase();
12+
const limit = Number(url.searchParams.get("limit") || "100");
13+
const game = url.searchParams.get("game");
14+
15+
if (!query || query.length < 2) {
16+
return new Response(
17+
JSON.stringify({
18+
success: false,
19+
status: "error",
20+
error: "Search query must be at least 2 characters",
21+
}),
22+
{
23+
headers: responseHeaders,
24+
status: 400,
25+
}
26+
);
27+
}
28+
29+
if (!game) {
30+
return new Response(
31+
JSON.stringify({
32+
success: false,
33+
status: "error",
34+
error: "Game parameter is required (e.g., ?game=genshin-impact)",
35+
}),
36+
{
37+
headers: responseHeaders,
38+
status: 400,
39+
}
40+
);
41+
}
42+
43+
const validGames = [
44+
"genshin-impact",
45+
"persona-3-reload",
46+
"zenless-zone-zero",
47+
"honkai-star-rail",
48+
"honkai-impact-3rd",
49+
"needy-streamer-overload",
50+
"dislyte",
51+
"strinova",
52+
"cookie-run",
53+
"blue-archive",
54+
"project-sekai",
55+
"tower-of-fantasy",
56+
"wuthering-waves",
57+
"reverse-1999",
58+
"sino-alice",
59+
"goddess-of-victory-nikke",
60+
];
61+
62+
if (!validGames.includes(game)) {
63+
return new Response(
64+
JSON.stringify({
65+
success: false,
66+
status: "error",
67+
error: `Invalid game parameter. Must be one of: ${validGames.join(", ")}`,
68+
}),
69+
{
70+
headers: responseHeaders,
71+
status: 400,
72+
}
73+
);
74+
}
75+
76+
const cacheKey = `search:${query}:${limit}:${game}`;
77+
const cache = caches.default;
78+
let response = await cache.match(request.url);
79+
80+
if (response) {
81+
return response;
82+
}
83+
84+
try {
85+
const controller = new AbortController();
86+
const timeoutId = setTimeout(() => {
87+
controller.abort('Search operation timed out');
88+
}, 3000);
89+
90+
try {
91+
const matchingFiles: Image[] = [];
92+
let totalScanned = 0;
93+
94+
let cursor: string | undefined = undefined;
95+
let hasMore = true;
96+
97+
while (hasMore && totalScanned < 2000 && matchingFiles.length < limit) {
98+
const options: R2ListOptions = {
99+
prefix: `${game}/`,
100+
cursor,
101+
limit: 500,
102+
};
103+
104+
const result = await listBucket(env.bucket, options);
105+
const objects = result.objects || [];
106+
totalScanned += objects.length;
107+
108+
for (const obj of objects) {
109+
const key = obj.key.toLowerCase();
110+
const filename = key.split('/').pop() || '';
111+
112+
if (key.includes(query) || filename.includes(query)) {
113+
const nameParts = filename.split('.');
114+
const extension = nameParts.length > 1 ? nameParts.pop() || '' : '';
115+
const name = nameParts.join('.');
116+
117+
matchingFiles.push({
118+
name,
119+
nameWithExtension: filename,
120+
path: `https://cdn.wanderer.moe/${obj.key}`,
121+
uploaded: obj.uploaded,
122+
size: obj.size,
123+
});
124+
}
125+
}
126+
127+
if (result.truncated) {
128+
cursor = result.cursor;
129+
} else {
130+
hasMore = false;
131+
}
132+
133+
if (matchingFiles.length >= limit) {
134+
break;
135+
}
136+
}
137+
138+
clearTimeout(timeoutId);
139+
140+
matchingFiles.sort((a, b) => {
141+
const aExactMatch = a.name.toLowerCase() === query;
142+
const bExactMatch = b.name.toLowerCase() === query;
143+
144+
if (aExactMatch && !bExactMatch) return -1;
145+
if (!aExactMatch && bExactMatch) return 1;
146+
147+
const aNameMatch = a.name.toLowerCase().includes(query);
148+
const bNameMatch = b.name.toLowerCase().includes(query);
149+
150+
if (aNameMatch && !bNameMatch) return -1;
151+
if (!aNameMatch && bNameMatch) return 1;
152+
153+
return a.name.length - b.name.length;
154+
});
155+
156+
const limitedResults = matchingFiles.slice(0, limit);
157+
158+
response = new Response(
159+
JSON.stringify({
160+
success: true,
161+
status: "ok",
162+
query,
163+
game,
164+
results: limitedResults,
165+
count: limitedResults.length,
166+
totalScanned,
167+
hasMore: matchingFiles.length > limit,
168+
limit,
169+
}),
170+
{
171+
headers: responseHeaders,
172+
status: 200,
173+
}
174+
);
175+
176+
ctx.waitUntil(cache.put(request.url, response.clone()));
177+
178+
return response;
179+
} finally {
180+
clearTimeout(timeoutId);
181+
}
182+
} catch (error) {
183+
console.error('Search error:', error);
184+
185+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
186+
const isTimeout = errorMessage.includes('timed out') || error instanceof DOMException && error.name === 'AbortError';
187+
188+
if (isTimeout) {
189+
return new Response(
190+
JSON.stringify({
191+
success: false,
192+
status: "error",
193+
error: "Search timed out, try a more specific query",
194+
}),
195+
{
196+
headers: responseHeaders,
197+
status: 408,
198+
}
199+
);
200+
}
201+
202+
return new Response(
203+
JSON.stringify({
204+
success: false,
205+
status: "error",
206+
error: errorMessage,
207+
}),
208+
{
209+
headers: responseHeaders,
210+
status: 500,
211+
}
212+
);
213+
}
214+
};

0 commit comments

Comments
 (0)