Skip to content

Commit fa91fd0

Browse files
committed
feat: Fix MAL, dynamic requests
1 parent ba42c07 commit fa91fd0

File tree

4 files changed

+132
-34
lines changed

4 files changed

+132
-34
lines changed

anify-backend/src/mappings/impl/meta/impl/mal.ts

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,27 +31,24 @@ export default class MALMeta extends MetaProvider {
3131
return results;
3232
}
3333

34-
private async fetchResults(query: string, type: MediaType, proxyURL?: string): Promise<IProviderResult[] | undefined> {
34+
private async fetchResults(query: string, type: MediaType): Promise<IProviderResult[] | undefined> {
3535
const results: IProviderResult[] = [];
3636

37-
const requestConfig: IRequestConfig = {};
38-
if (proxyURL) {
39-
requestConfig.proxy = proxyURL;
40-
}
41-
42-
requestConfig.validateResponse = async (response) => {
43-
if (!response.ok) return false;
44-
try {
45-
const data = await response.text();
46-
const $ = load(data);
47-
return $("div.js-categories-seasonal table tr").length > 0;
48-
} catch {
49-
return false;
50-
}
51-
};
52-
5337
const url = `${this.url}/${type === MediaType.ANIME ? "anime" : "manga"}.php?q=${query}&c[]=a&c[]=b&c[]=c&c[]=f&c[]=d&c[]=e&c[]=g`;
54-
const data = await (await this.request(url, requestConfig)).text();
38+
const data = await (
39+
await this.request(url, {
40+
validateResponse: async (response) => {
41+
if (!response.ok) return false;
42+
try {
43+
const data = await response.text();
44+
const $ = load(data);
45+
return $("div.js-categories-seasonal table tr").length > 0;
46+
} catch {
47+
return false;
48+
}
49+
},
50+
})
51+
).text();
5552
const $ = load(data);
5653

5754
const searchResults = $("div.js-categories-seasonal table tr").first();
@@ -72,7 +69,20 @@ export default class MALMeta extends MetaProvider {
7269

7370
promises.push(
7471
new Promise(async (resolve) => {
75-
const data = await (await this.request(`${this.url}/${type === MediaType.ANIME ? "anime" : "manga"}/${id}`, requestConfig)).text();
72+
const data = await (
73+
await this.request(`${this.url}/${type === MediaType.ANIME ? "anime" : "manga"}/${id}`, {
74+
validateResponse: async (response) => {
75+
if (!response.ok) return false;
76+
try {
77+
const data = await response.text();
78+
const $ = load(data);
79+
return $("title").text().includes("MyAnimeList");
80+
} catch {
81+
return false;
82+
}
83+
},
84+
})
85+
).text();
7686
const $$ = load(data);
7787

7888
const published =

anify-backend/src/mappings/index.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,6 @@ export const ANIME_PROVIDERS = [
1818
const { default: AnimePahe } = await import("./impl/anime/impl/animepahe");
1919
return new AnimePahe();
2020
},
21-
async () => {
22-
const { default: GogoAnime } = await import("./impl/anime/impl/gogoanime");
23-
return new GogoAnime();
24-
},
2521
async () => {
2622
const { default: HiAnime } = await import("./impl/anime/impl/hianime");
2723
return new HiAnime();

anify-backend/src/proxies/impl/request/customRequest.ts

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ const errorTracker = new Map<
1313
}
1414
>();
1515

16+
// Track rotation state
17+
const rotationState = {
18+
isRotating: false,
19+
lastRotationStart: 0,
20+
rotationAttempts: new Map<string, number>(),
21+
maxAttempts: 3,
22+
};
23+
1624
const ROTATION_THRESHOLD = 5;
1725
const ROTATION_COOLDOWN = 60000;
1826
const STATUS_CODE_WEIGHTS: Record<number, number> = {
@@ -29,8 +37,33 @@ const MIN_OCCURRENCES: Record<number, number> = {
2937
999: 2, // Need couple timeouts
3038
};
3139

40+
// Dynamic backoff calculation based on recent errors and rotation history
41+
function calculateBackoff(providerKey: string): number {
42+
const attempts = rotationState.rotationAttempts.get(providerKey) || 0;
43+
const baseDelay = 1000; // 1 second base
44+
const jitter = Math.random() * 500; // Add some randomness
45+
return Math.min(baseDelay * Math.pow(1.5, attempts) + jitter, 10000); // Cap at 10 seconds
46+
}
47+
48+
async function waitForRotation(providerKey: string): Promise<void> {
49+
if (!rotationState.isRotating) return;
50+
51+
const backoff = calculateBackoff(providerKey);
52+
await new Promise((resolve) => setTimeout(resolve, backoff));
53+
54+
// Increment attempt counter
55+
const attempts = (rotationState.rotationAttempts.get(providerKey) || 0) + 1;
56+
rotationState.rotationAttempts.set(providerKey, attempts);
57+
58+
// If we've waited too long, reset rotation state
59+
if (Date.now() - rotationState.lastRotationStart > 30000) {
60+
rotationState.isRotating = false;
61+
rotationState.rotationAttempts.clear();
62+
}
63+
}
64+
3265
function shouldRotateIP(errorState: { count: number; lastRotation: number; statusCodes: Map<number, number> }): boolean {
33-
if (Date.now() - errorState.lastRotation < ROTATION_COOLDOWN) {
66+
if (Date.now() - errorState.lastRotation < ROTATION_COOLDOWN || rotationState.isRotating) {
3467
return false;
3568
}
3669

@@ -69,12 +102,17 @@ async function makeRequest(url: string, options: RequestInit, timeout: number =
69102
export async function customRequest(url: string, options: IRequestConfig = {}): Promise<Response | null> {
70103
const { isChecking, proxy, useGoogleTranslate, timeout, providerType, providerId, maxRetries, validateResponse } = options;
71104
const retryAttempts = isChecking ? 1 : maxRetries || 3;
105+
const providerKey = `${providerType}-${providerId}`;
72106

73107
// Try different request strategies in sequence
74108
for (let attempt = 1; attempt <= retryAttempts; attempt++) {
109+
// Wait if IP rotation is in progress
110+
if (rotationState.isRotating) {
111+
await waitForRotation(providerKey);
112+
}
113+
75114
// 1. Try WireGuard proxy first if enabled
76115
if (!useGoogleTranslate && !isChecking && env.USE_WIREGUARD) {
77-
const providerKey = `${providerType}-${providerId}`;
78116
const errorState = errorTracker.get(providerKey) || {
79117
count: 0,
80118
lastRotation: 0,
@@ -83,16 +121,25 @@ export async function customRequest(url: string, options: IRequestConfig = {}):
83121

84122
if (shouldRotateIP(errorState)) {
85123
try {
124+
rotationState.isRotating = true;
125+
rotationState.lastRotationStart = Date.now();
126+
86127
await wireguardProxyManager.rotate();
128+
87129
errorTracker.set(providerKey, {
88130
count: 0,
89131
lastRotation: Date.now(),
90132
statusCodes: new Map(),
91133
});
134+
92135
console.log(`Rotated IP for provider ${providerKey} due to error threshold`);
93-
await new Promise((resolve) => setTimeout(resolve, 1000));
136+
137+
// Clear rotation state after successful rotation
138+
rotationState.isRotating = false;
139+
rotationState.rotationAttempts.delete(providerKey);
94140
} catch (error) {
95141
console.warn("Failed to rotate WireGuard IP:", error);
142+
rotationState.isRotating = false;
96143
}
97144
}
98145

@@ -165,7 +212,22 @@ export async function customRequest(url: string, options: IRequestConfig = {}):
165212
}
166213
}
167214

168-
// If all strategies failed, log warning and return null
169-
console.warn(`All request strategies failed for ${url}. Provider: ${providerId}/${providerType}`);
215+
// If all strategies failed, update error tracking and return null
216+
if (providerType && providerId) {
217+
const errorState = errorTracker.get(providerKey) || {
218+
count: 0,
219+
lastRotation: 0,
220+
statusCodes: new Map(),
221+
};
222+
223+
// Add to error count and track as a 503 error (service unavailable)
224+
const currentCount = errorState.statusCodes.get(503) || 0;
225+
errorState.statusCodes.set(503, currentCount + 1);
226+
errorState.count++;
227+
errorTracker.set(providerKey, errorState);
228+
229+
console.warn(`All request strategies failed for ${url}. Provider: ${providerId}/${providerType}`);
230+
}
231+
170232
return null;
171233
}

anify-backend/src/types/impl/mappings/impl/mediaProvider.ts

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@ import type { IRequestConfig } from "../../proxies";
44
import { selectProxy, proxyToUrl } from "../../../../proxies/impl/manager";
55
import { customRequest } from "../../../../proxies/impl/request/customRequest";
66

7+
export class RequestError extends Error {
8+
constructor(
9+
message: string,
10+
public readonly url: string,
11+
public readonly providerId: string,
12+
public readonly providerType: string,
13+
public readonly statusCode?: number,
14+
) {
15+
super(message);
16+
this.name = "RequestError";
17+
}
18+
}
19+
720
export abstract class MediaProvider {
821
private static limiterMap: Map<string, Bottleneck> = new Map();
922

@@ -20,8 +33,8 @@ export abstract class MediaProvider {
2033

2134
/**
2235
* Queued request function that respects this.rateLimit (seconds/10).
23-
* Returns Response if successful, null if request failed but was handled gracefully.
24-
* Throws an error only for unexpected failures that should halt execution.
36+
* Returns Response if successful, throws RequestError for handled failures.
37+
* Throws other errors only for unexpected failures that should halt execution.
2538
*/
2639
async request(url: string, config: IRequestConfig = {}, proxyRequest: boolean = false): Promise<Response> {
2740
if (!MediaProvider.limiterMap.has(this.id)) {
@@ -59,16 +72,33 @@ export abstract class MediaProvider {
5972

6073
const result = await customRequest(url, finalConfig);
6174
if (!result) {
62-
throw new Error(`Request failed for ${url}`);
75+
throw new RequestError(`Request failed after all retry attempts`, url, this.id, this.providerType);
6376
}
6477
return result;
6578
});
6679

6780
return response;
6881
} catch (error) {
69-
// Log the error but throw a standardized error to maintain type safety
70-
console.error(`Error in request for ${url} (${this.id}/${this.providerType}):`, error);
71-
throw new Error(`Failed to fetch ${url}`);
82+
if (error instanceof RequestError) {
83+
// Log the handled error but don't halt execution
84+
console.warn(`Request failed for ${url} (${this.id}/${this.providerType}):`, error.message);
85+
86+
// Return an empty 204 response instead of null
87+
return new Response(null, {
88+
status: 204,
89+
statusText: "No Content - Request Failed",
90+
headers: {
91+
"X-Error-Type": "RequestError",
92+
"X-Error-Message": error.message,
93+
"X-Provider-Id": this.id,
94+
"X-Provider-Type": this.providerType,
95+
},
96+
});
97+
}
98+
99+
// For unexpected errors, throw a standardized error
100+
console.error(`Unexpected error in request for ${url} (${this.id}/${this.providerType}):`, error);
101+
throw new RequestError(`Failed to fetch ${url}`, url, this.id, this.providerType);
72102
}
73103
}
74104
}

0 commit comments

Comments
 (0)