ani-web 1.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ani-web might be problematic. Click here for more details.
- package/LICENSE +21 -0
- package/README.md +174 -0
- package/client/dist/assets/AnimeInfo-C7DQp7Oo.js +1 -0
- package/client/dist/assets/AnimeInfo-Sb3YiXHJ.css +1 -0
- package/client/dist/assets/AnimeInfoPage-DJA7AJQ8.js +2 -0
- package/client/dist/assets/Button-Fq9KaUOg.css +1 -0
- package/client/dist/assets/Button-o0l9V_NG.js +1 -0
- package/client/dist/assets/ErrorMessage-Ddf2zmRx.js +1 -0
- package/client/dist/assets/ErrorMessage-FOxXyZC9.css +1 -0
- package/client/dist/assets/Home-CKHJA97j.css +1 -0
- package/client/dist/assets/Home-Dey0azy1.js +1 -0
- package/client/dist/assets/Insights-BSRcCkDs.css +1 -0
- package/client/dist/assets/Insights-CogjPOd_.js +1 -0
- package/client/dist/assets/MAL-CYArH4yf.js +1 -0
- package/client/dist/assets/MAL-DeQNXXPx.css +1 -0
- package/client/dist/assets/Player-BWFN9gud.js +9 -0
- package/client/dist/assets/Player-CBCYW7uG.css +1 -0
- package/client/dist/assets/PlayerSettings-BgStUrrP.css +1 -0
- package/client/dist/assets/PlayerSettings-rWZuATQf.js +1 -0
- package/client/dist/assets/RemoveConfirmationModal-BBiogSdf.css +1 -0
- package/client/dist/assets/RemoveConfirmationModal-CLYqyGOv.js +1 -0
- package/client/dist/assets/Search-DZAWgKwq.js +1 -0
- package/client/dist/assets/Search-lWsVQ0Ke.css +1 -0
- package/client/dist/assets/Settings-Bv9fX-x3.css +1 -0
- package/client/dist/assets/Settings-DyisJGeD.js +1 -0
- package/client/dist/assets/ToggleSwitch-CLnWnAuY.js +1 -0
- package/client/dist/assets/ToggleSwitch-DInRb7iM.css +1 -0
- package/client/dist/assets/Watchlist-2dVYksxq.css +1 -0
- package/client/dist/assets/Watchlist-CuqJISI3.js +1 -0
- package/client/dist/assets/hls.light-DcbkToIY.js +27 -0
- package/client/dist/assets/index-BK_Zaqaw.css +1 -0
- package/client/dist/assets/index-CHVF4D4L.js +178 -0
- package/client/dist/assets/useAnimeInfoData-Cr58brCY.js +1 -0
- package/client/dist/assets/useIsMobile-gHo4t6g6.js +1 -0
- package/client/dist/assets/vendor-DdbgYKo4.js +3 -0
- package/client/dist/favicon.ico +0 -0
- package/client/dist/index.html +35 -0
- package/client/dist/logo.png +0 -0
- package/client/dist/placeholder.svg +4 -0
- package/client/dist/robots.txt +3 -0
- package/client/package.json +54 -0
- package/orchestrator.js +302 -0
- package/package.json +69 -0
- package/server/dist/config.js +86 -0
- package/server/dist/constants.json +1359 -0
- package/server/dist/controllers/auth.controller.js +213 -0
- package/server/dist/controllers/data.controller.js +126 -0
- package/server/dist/controllers/insights.controller.js +125 -0
- package/server/dist/controllers/proxy.controller.js +235 -0
- package/server/dist/controllers/settings.controller.js +147 -0
- package/server/dist/controllers/watchlist.controller.js +499 -0
- package/server/dist/db.js +231 -0
- package/server/dist/github-sync.js +279 -0
- package/server/dist/google.js +274 -0
- package/server/dist/logger.js +21 -0
- package/server/dist/providers/123anime.provider.js +229 -0
- package/server/dist/providers/allanime.provider.js +773 -0
- package/server/dist/providers/animepahe.provider.js +313 -0
- package/server/dist/providers/animeya.provider.js +799 -0
- package/server/dist/providers/provider.interface.js +2 -0
- package/server/dist/rclone.js +126 -0
- package/server/dist/repositories/insights.repository.js +30 -0
- package/server/dist/repositories/notifications.repository.js +22 -0
- package/server/dist/repositories/settings.repository.js +13 -0
- package/server/dist/repositories/shows-meta.repository.js +39 -0
- package/server/dist/repositories/watched-episodes.repository.js +60 -0
- package/server/dist/repositories/watchlist.repository.js +49 -0
- package/server/dist/routes/auth.routes.js +23 -0
- package/server/dist/routes/data.routes.js +43 -0
- package/server/dist/routes/insights.routes.js +11 -0
- package/server/dist/routes/proxy.routes.js +13 -0
- package/server/dist/routes/settings.routes.js +26 -0
- package/server/dist/routes/watchlist.routes.js +26 -0
- package/server/dist/server.js +179 -0
- package/server/dist/sync-config.js +28 -0
- package/server/dist/sync.js +383 -0
- package/server/dist/utils/db-utils.js +36 -0
- package/server/dist/utils/env.utils.js +70 -0
- package/server/package.json +54 -0
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.AllAnimeProvider = void 0;
|
|
40
|
+
const axios_1 = __importDefault(require("axios"));
|
|
41
|
+
const logger_1 = __importDefault(require("../logger"));
|
|
42
|
+
const crypto = __importStar(require("node:crypto"));
|
|
43
|
+
const cheerio = __importStar(require("cheerio"));
|
|
44
|
+
const API_BASE_URL = 'https://allanime.day';
|
|
45
|
+
const API_ENDPOINT = `https://api.allanime.day/api`;
|
|
46
|
+
const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/121.0';
|
|
47
|
+
const REFERER = 'https://youtu-chan.com';
|
|
48
|
+
const DEOBFUSCATION_MAP = {
|
|
49
|
+
'79': 'A',
|
|
50
|
+
'7a': 'B',
|
|
51
|
+
'7b': 'C',
|
|
52
|
+
'7c': 'D',
|
|
53
|
+
'7d': 'E',
|
|
54
|
+
'7e': 'F',
|
|
55
|
+
'7f': 'G',
|
|
56
|
+
'70': 'H',
|
|
57
|
+
'71': 'I',
|
|
58
|
+
'72': 'J',
|
|
59
|
+
'73': 'K',
|
|
60
|
+
'74': 'L',
|
|
61
|
+
'75': 'M',
|
|
62
|
+
'76': 'N',
|
|
63
|
+
'77': 'O',
|
|
64
|
+
'68': 'P',
|
|
65
|
+
'69': 'Q',
|
|
66
|
+
'6a': 'R',
|
|
67
|
+
'6b': 'S',
|
|
68
|
+
'6c': 'T',
|
|
69
|
+
'6d': 'U',
|
|
70
|
+
'6e': 'V',
|
|
71
|
+
'6f': 'W',
|
|
72
|
+
'60': 'X',
|
|
73
|
+
'61': 'Y',
|
|
74
|
+
'62': 'Z',
|
|
75
|
+
'59': 'a',
|
|
76
|
+
'5a': 'b',
|
|
77
|
+
'5b': 'c',
|
|
78
|
+
'5c': 'd',
|
|
79
|
+
'5d': 'e',
|
|
80
|
+
'5e': 'f',
|
|
81
|
+
'5f': 'g',
|
|
82
|
+
'50': 'h',
|
|
83
|
+
'51': 'i',
|
|
84
|
+
'52': 'j',
|
|
85
|
+
'53': 'k',
|
|
86
|
+
'54': 'l',
|
|
87
|
+
'55': 'm',
|
|
88
|
+
'56': 'n',
|
|
89
|
+
'57': 'o',
|
|
90
|
+
'48': 'p',
|
|
91
|
+
'49': 'q',
|
|
92
|
+
'4a': 'r',
|
|
93
|
+
'4b': 's',
|
|
94
|
+
'4c': 't',
|
|
95
|
+
'4d': 'u',
|
|
96
|
+
'4e': 'v',
|
|
97
|
+
'4f': 'w',
|
|
98
|
+
'40': 'x',
|
|
99
|
+
'41': 'y',
|
|
100
|
+
'42': 'z',
|
|
101
|
+
'08': '0',
|
|
102
|
+
'09': '1',
|
|
103
|
+
'0a': '2',
|
|
104
|
+
'0b': '3',
|
|
105
|
+
'0c': '4',
|
|
106
|
+
'0d': '5',
|
|
107
|
+
'0e': '6',
|
|
108
|
+
'0f': '7',
|
|
109
|
+
'00': '8',
|
|
110
|
+
'01': '9',
|
|
111
|
+
'15': '-',
|
|
112
|
+
'16': '.',
|
|
113
|
+
'67': '_',
|
|
114
|
+
'46': '~',
|
|
115
|
+
'02': ':',
|
|
116
|
+
'17': '/',
|
|
117
|
+
'07': '?',
|
|
118
|
+
'1b': '#',
|
|
119
|
+
'63': '[',
|
|
120
|
+
'65': ']',
|
|
121
|
+
'78': '@',
|
|
122
|
+
'19': '!',
|
|
123
|
+
'1c': '$',
|
|
124
|
+
'1e': '&',
|
|
125
|
+
'10': '(',
|
|
126
|
+
'11': ')',
|
|
127
|
+
'12': '*',
|
|
128
|
+
'13': '+',
|
|
129
|
+
'14': ',',
|
|
130
|
+
'03': ';',
|
|
131
|
+
'05': '=',
|
|
132
|
+
'1d': '%',
|
|
133
|
+
};
|
|
134
|
+
class AllAnimeProvider {
|
|
135
|
+
name = 'AllAnime';
|
|
136
|
+
cache;
|
|
137
|
+
constructor(cache) {
|
|
138
|
+
this.cache = cache;
|
|
139
|
+
}
|
|
140
|
+
decryptTobeparsed(encryptedBase64) {
|
|
141
|
+
try {
|
|
142
|
+
const secret = 'Xot36i3lK3:v1';
|
|
143
|
+
const key = crypto.createHash('sha256').update(secret).digest();
|
|
144
|
+
const encryptedBuffer = Buffer.from(encryptedBase64, 'base64');
|
|
145
|
+
if (encryptedBuffer.length < 30) {
|
|
146
|
+
throw new Error('Encrypted data too short');
|
|
147
|
+
}
|
|
148
|
+
const ivPart = encryptedBuffer.subarray(1, 13);
|
|
149
|
+
const iv = Buffer.concat([ivPart, Buffer.from('00000002', 'hex')]);
|
|
150
|
+
const ciphertext = encryptedBuffer.subarray(13, encryptedBuffer.length - 16);
|
|
151
|
+
const decipher = crypto.createDecipheriv('aes-256-ctr', key, iv);
|
|
152
|
+
let decrypted = decipher.update(ciphertext);
|
|
153
|
+
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
|
154
|
+
const decryptedString = decrypted.toString('utf8');
|
|
155
|
+
return JSON.parse(decryptedString);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
const err = error;
|
|
159
|
+
logger_1.default.error({ err: err.message, stack: err.stack }, 'Failed to decrypt tobeparsed field');
|
|
160
|
+
const e = new Error(`Decryption failed: ${err.message}`);
|
|
161
|
+
e.cause = error;
|
|
162
|
+
throw e;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
_hexDecode(obfuscatedBody) {
|
|
166
|
+
let result = '';
|
|
167
|
+
for (let i = 0; i < obfuscatedBody.length; i += 2) {
|
|
168
|
+
const chunk = obfuscatedBody.substring(i, i + 2);
|
|
169
|
+
result += DEOBFUSCATION_MAP[chunk] || chunk;
|
|
170
|
+
}
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
deobfuscateStreamUrl(obfuscatedUrl) {
|
|
174
|
+
if (!obfuscatedUrl)
|
|
175
|
+
return '';
|
|
176
|
+
if (!obfuscatedUrl.startsWith('--'))
|
|
177
|
+
return obfuscatedUrl;
|
|
178
|
+
let deobfuscated = this._hexDecode(obfuscatedUrl.slice(2));
|
|
179
|
+
deobfuscated = deobfuscated.replace(/([^:]\/)\/+/g, '$1');
|
|
180
|
+
if (deobfuscated.startsWith('/')) {
|
|
181
|
+
return `${API_BASE_URL}${deobfuscated}`;
|
|
182
|
+
}
|
|
183
|
+
return deobfuscated;
|
|
184
|
+
}
|
|
185
|
+
deobfuscateUrl(obfuscatedUrl) {
|
|
186
|
+
if (!obfuscatedUrl)
|
|
187
|
+
return '';
|
|
188
|
+
let finalUrl = obfuscatedUrl;
|
|
189
|
+
if (!obfuscatedUrl.startsWith('--') &&
|
|
190
|
+
(obfuscatedUrl.includes('s4.anilist.co') || obfuscatedUrl.startsWith('http'))) {
|
|
191
|
+
// Direct access works, proxy is blocked
|
|
192
|
+
finalUrl = obfuscatedUrl;
|
|
193
|
+
}
|
|
194
|
+
else if (obfuscatedUrl.startsWith('--')) {
|
|
195
|
+
const deobfuscated = this._hexDecode(obfuscatedUrl.slice(2));
|
|
196
|
+
if (deobfuscated.startsWith('/')) {
|
|
197
|
+
if (deobfuscated.startsWith('/s4.anilist.co')) {
|
|
198
|
+
finalUrl = `https:/${deobfuscated}`;
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
// Use API_BASE_URL instead of the blocked proxy
|
|
202
|
+
finalUrl = `${API_BASE_URL}${deobfuscated}`;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
finalUrl = deobfuscated;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// Handle relative markers and paths
|
|
210
|
+
if (!finalUrl.startsWith('http')) {
|
|
211
|
+
if (finalUrl.startsWith('__Show__')) {
|
|
212
|
+
finalUrl = `https://aln.youtube-anime.com/images/${finalUrl}`;
|
|
213
|
+
}
|
|
214
|
+
else if (finalUrl.startsWith('mcovers') || finalUrl.startsWith('images2')) {
|
|
215
|
+
finalUrl = `https://aln.youtube-anime.com/${finalUrl}`;
|
|
216
|
+
}
|
|
217
|
+
else if (finalUrl.startsWith('/')) {
|
|
218
|
+
finalUrl = `${API_BASE_URL}${finalUrl}`;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (finalUrl.includes('wp.youtube-anime.com') || finalUrl.includes('allanime.day')) {
|
|
222
|
+
// refererValue would be set here in the full context
|
|
223
|
+
}
|
|
224
|
+
// Final robust cleanup for aln host and path structure
|
|
225
|
+
if (finalUrl.includes('aln.youtube-anime.com')) {
|
|
226
|
+
// Ensure we use the correct host (remove allanime.day prefix if present)
|
|
227
|
+
finalUrl = finalUrl.replace(/https?:\/\/allanime\.day\/aln\.youtube-anime\.com/, 'https://aln.youtube-anime.com');
|
|
228
|
+
// Remove incorrect /images/ prefix for mcovers/images2
|
|
229
|
+
if (finalUrl.includes('/images/mcovers')) {
|
|
230
|
+
finalUrl = finalUrl.replace('/images/mcovers', '/mcovers');
|
|
231
|
+
}
|
|
232
|
+
if (finalUrl.includes('/images/images2')) {
|
|
233
|
+
finalUrl = finalUrl.replace('/images/images2', '/images2');
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// Don't use the allanime.day proxy for s4.anilist.co URLs
|
|
237
|
+
if (finalUrl.includes('allanime.day/s4.anilist.co')) {
|
|
238
|
+
finalUrl = finalUrl.replace('https://allanime.day/s4.anilist.co', 'https://s4.anilist.co');
|
|
239
|
+
finalUrl = finalUrl.replace('http://allanime.day/s4.anilist.co', 'https://s4.anilist.co');
|
|
240
|
+
}
|
|
241
|
+
// Strip any existing local proxy prefixes that might have been saved
|
|
242
|
+
if (finalUrl.includes('/api/image-proxy?url=')) {
|
|
243
|
+
const match = finalUrl.match(/url=([^&]+)/);
|
|
244
|
+
if (match) {
|
|
245
|
+
const unwrapped = decodeURIComponent(match[1]);
|
|
246
|
+
finalUrl = unwrapped;
|
|
247
|
+
// Recurse once to catch the anilist fix for the unwrapped URL
|
|
248
|
+
return this.deobfuscateUrl(finalUrl);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return finalUrl;
|
|
252
|
+
}
|
|
253
|
+
async _fetchShows(variables, extensions) {
|
|
254
|
+
const body = { variables };
|
|
255
|
+
const fullQuery = `
|
|
256
|
+
query ($search: SearchInput, $limit: Int, $page: Int, $translationType: VaildTranslationTypeEnumType, $countryOrigin: VaildCountryOriginEnumType) {
|
|
257
|
+
shows(search: $search, limit: $limit, page: $page, translationType: $translationType, countryOrigin: $countryOrigin) {
|
|
258
|
+
edges { _id name nativeName englishName thumbnail description type availableEpisodesDetail isAdult rating }
|
|
259
|
+
}
|
|
260
|
+
}`;
|
|
261
|
+
if (extensions) {
|
|
262
|
+
body.extensions = extensions;
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
body.query = fullQuery;
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
const response = await axios_1.default.post(API_ENDPOINT, body, {
|
|
269
|
+
headers: { 'User-Agent': USER_AGENT, Referer: REFERER },
|
|
270
|
+
timeout: 15000,
|
|
271
|
+
});
|
|
272
|
+
const responseData = response.data;
|
|
273
|
+
if (responseData?.data?.tobeparsed) {
|
|
274
|
+
responseData.data = this.decryptTobeparsed(responseData.data.tobeparsed);
|
|
275
|
+
}
|
|
276
|
+
if (responseData.errors && responseData.errors[0]?.message === 'PersistedQueryNotFound') {
|
|
277
|
+
throw new Error('PersistedQueryNotFound');
|
|
278
|
+
}
|
|
279
|
+
const shows = responseData?.data?.shows?.edges || [];
|
|
280
|
+
return shows.map((show) => ({
|
|
281
|
+
...show,
|
|
282
|
+
thumbnail: this.deobfuscateUrl(show.thumbnail || ''),
|
|
283
|
+
}));
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
const err = error;
|
|
287
|
+
if (err.message === 'PersistedQueryNotFound' && extensions) {
|
|
288
|
+
logger_1.default.info('Search hash expired, falling back to full query');
|
|
289
|
+
const response = await axios_1.default.post(API_ENDPOINT, { variables, query: fullQuery }, {
|
|
290
|
+
headers: { 'User-Agent': USER_AGENT, Referer: REFERER },
|
|
291
|
+
timeout: 15000,
|
|
292
|
+
});
|
|
293
|
+
const responseData = response.data;
|
|
294
|
+
if (responseData?.data?.tobeparsed) {
|
|
295
|
+
responseData.data = this.decryptTobeparsed(responseData.data.tobeparsed);
|
|
296
|
+
}
|
|
297
|
+
const shows = responseData?.data?.shows?.edges || [];
|
|
298
|
+
return shows.map((show) => ({
|
|
299
|
+
...show,
|
|
300
|
+
thumbnail: this.deobfuscateUrl(show.thumbnail || ''),
|
|
301
|
+
}));
|
|
302
|
+
}
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
async search(options) {
|
|
307
|
+
const { query, season, year, sortBy, page, type, country, translation, genres, excludeGenres, tags, excludeTags, studios, } = options;
|
|
308
|
+
const searchObj = { allowAdult: false };
|
|
309
|
+
if (query)
|
|
310
|
+
searchObj.query = query;
|
|
311
|
+
if (season && season !== 'ALL')
|
|
312
|
+
searchObj.season = season;
|
|
313
|
+
if (year && year !== 'ALL')
|
|
314
|
+
searchObj.year = parseInt(year);
|
|
315
|
+
if (sortBy)
|
|
316
|
+
searchObj.sortBy = sortBy;
|
|
317
|
+
if (type && type !== 'ALL')
|
|
318
|
+
searchObj.types = [type];
|
|
319
|
+
if (genres)
|
|
320
|
+
searchObj.genres = genres.split(',');
|
|
321
|
+
if (excludeGenres)
|
|
322
|
+
searchObj.excludeGenres = excludeGenres.split(',');
|
|
323
|
+
if (tags)
|
|
324
|
+
searchObj.tags = tags.split(',');
|
|
325
|
+
if (studios)
|
|
326
|
+
searchObj.studios = studios.split(',');
|
|
327
|
+
if (excludeTags)
|
|
328
|
+
searchObj.excludeTags = excludeTags.split(',');
|
|
329
|
+
const variables = {
|
|
330
|
+
search: searchObj,
|
|
331
|
+
limit: 28,
|
|
332
|
+
page: parseInt(page) || 1,
|
|
333
|
+
translationType: translation && translation !== 'ALL' ? translation : 'sub',
|
|
334
|
+
countryOrigin: country && country !== 'ALL' ? country : 'ALL',
|
|
335
|
+
};
|
|
336
|
+
return this._fetchShows(variables);
|
|
337
|
+
}
|
|
338
|
+
async getPopular(timeframe) {
|
|
339
|
+
let dateRange = 0;
|
|
340
|
+
switch (timeframe) {
|
|
341
|
+
case 'daily':
|
|
342
|
+
dateRange = 1;
|
|
343
|
+
break;
|
|
344
|
+
case 'weekly':
|
|
345
|
+
dateRange = 7;
|
|
346
|
+
break;
|
|
347
|
+
case 'monthly':
|
|
348
|
+
dateRange = 30;
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
const variables = {
|
|
352
|
+
type: 'anime',
|
|
353
|
+
size: 10,
|
|
354
|
+
page: 1,
|
|
355
|
+
allowAdult: false,
|
|
356
|
+
allowUnknown: false,
|
|
357
|
+
dateRange,
|
|
358
|
+
};
|
|
359
|
+
const extensions = {
|
|
360
|
+
persistedQuery: {
|
|
361
|
+
version: 1,
|
|
362
|
+
sha256Hash: '60f50b84bb545fa25ee7f7c8c0adbf8f5cea40f7b1ef8501cbbff70e38589489',
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
try {
|
|
366
|
+
const response = await axios_1.default.post(API_ENDPOINT, { variables, extensions }, {
|
|
367
|
+
headers: { 'User-Agent': USER_AGENT, Referer: REFERER },
|
|
368
|
+
timeout: 15000,
|
|
369
|
+
});
|
|
370
|
+
const responseData = response.data;
|
|
371
|
+
if (responseData?.data?.tobeparsed) {
|
|
372
|
+
responseData.data = this.decryptTobeparsed(responseData.data.tobeparsed);
|
|
373
|
+
}
|
|
374
|
+
if (responseData.errors && responseData.errors[0]?.message === 'PersistedQueryNotFound') {
|
|
375
|
+
throw new Error('PersistedQueryNotFound');
|
|
376
|
+
}
|
|
377
|
+
const recommendations = responseData?.data?.queryPopular?.recommendations || [];
|
|
378
|
+
return recommendations.map((rec) => {
|
|
379
|
+
const card = rec.anyCard;
|
|
380
|
+
return { ...card, thumbnail: this.deobfuscateUrl(card.thumbnail || '') };
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
const err = error;
|
|
385
|
+
if (err.message === 'PersistedQueryNotFound') {
|
|
386
|
+
logger_1.default.info('Popular hash expired, falling back to full query');
|
|
387
|
+
const fullQuery = `
|
|
388
|
+
query ($type: VaildPopularTypeEnumType!, $size: Int!, $dateRange: Int, $page: Int, $allowAdult: Boolean, $allowUnknown: Boolean) {
|
|
389
|
+
queryPopular(type: $type, size: $size, dateRange: $dateRange, page: $page, allowAdult: $allowAdult, allowUnknown: $allowUnknown) {
|
|
390
|
+
recommendations {
|
|
391
|
+
anyCard { _id name nativeName englishName thumbnail type availableEpisodesDetail isAdult rating }
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}`;
|
|
395
|
+
const response = await axios_1.default.post(API_ENDPOINT, { query: fullQuery, variables }, {
|
|
396
|
+
headers: { 'User-Agent': USER_AGENT, Referer: REFERER },
|
|
397
|
+
timeout: 15000,
|
|
398
|
+
});
|
|
399
|
+
const responseData = response.data;
|
|
400
|
+
if (responseData?.data?.tobeparsed) {
|
|
401
|
+
responseData.data = this.decryptTobeparsed(responseData.data.tobeparsed);
|
|
402
|
+
}
|
|
403
|
+
const recommendations = responseData?.data?.queryPopular?.recommendations || [];
|
|
404
|
+
return recommendations.map((rec) => {
|
|
405
|
+
const card = rec.anyCard;
|
|
406
|
+
return { ...card, thumbnail: this.deobfuscateUrl(card.thumbnail || '') };
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
throw error;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
async getSchedule(date) {
|
|
413
|
+
const startOfDay = new Date(date);
|
|
414
|
+
startOfDay.setUTCHours(0, 0, 0, 0);
|
|
415
|
+
const endOfDay = new Date(date);
|
|
416
|
+
endOfDay.setUTCHours(23, 59, 59, 999);
|
|
417
|
+
const variables = {
|
|
418
|
+
search: {
|
|
419
|
+
dateRangeStart: Math.floor(startOfDay.getTime() / 1000),
|
|
420
|
+
dateRangeEnd: Math.floor(endOfDay.getTime() / 1000),
|
|
421
|
+
sortBy: 'Latest_Update',
|
|
422
|
+
},
|
|
423
|
+
limit: 50,
|
|
424
|
+
page: 1,
|
|
425
|
+
translationType: 'sub',
|
|
426
|
+
countryOrigin: 'ALL',
|
|
427
|
+
};
|
|
428
|
+
return this._fetchShows(variables);
|
|
429
|
+
}
|
|
430
|
+
async getSeasonal(page) {
|
|
431
|
+
const month = new Date().getMonth();
|
|
432
|
+
const season = month >= 0 && month <= 2
|
|
433
|
+
? 'Winter'
|
|
434
|
+
: month >= 3 && month <= 5
|
|
435
|
+
? 'Spring'
|
|
436
|
+
: month >= 6 && month <= 8
|
|
437
|
+
? 'Summer'
|
|
438
|
+
: 'Fall';
|
|
439
|
+
const year = new Date().getFullYear();
|
|
440
|
+
const variables = {
|
|
441
|
+
search: { year, season, sortBy: 'Latest_Update', allowAdult: false },
|
|
442
|
+
limit: 14,
|
|
443
|
+
page,
|
|
444
|
+
translationType: 'sub',
|
|
445
|
+
countryOrigin: 'JP',
|
|
446
|
+
};
|
|
447
|
+
return this._fetchShows(variables);
|
|
448
|
+
}
|
|
449
|
+
async getLatestReleases() {
|
|
450
|
+
const variables = {
|
|
451
|
+
search: { sortBy: 'Latest_Update', allowAdult: false },
|
|
452
|
+
limit: 14,
|
|
453
|
+
page: 1,
|
|
454
|
+
translationType: 'sub',
|
|
455
|
+
countryOrigin: 'JP',
|
|
456
|
+
};
|
|
457
|
+
return this._fetchShows(variables);
|
|
458
|
+
}
|
|
459
|
+
async getShowMeta(showId) {
|
|
460
|
+
const response = await axios_1.default.post(API_ENDPOINT, {
|
|
461
|
+
query: `query($showId: String!) { show(_id: $showId) { _id, name, thumbnail, nativeName, englishName, type, availableEpisodesDetail, score, isAdult } }`,
|
|
462
|
+
variables: { showId },
|
|
463
|
+
}, {
|
|
464
|
+
headers: { 'User-Agent': USER_AGENT, Referer: REFERER },
|
|
465
|
+
timeout: 15000,
|
|
466
|
+
});
|
|
467
|
+
const responseData = response.data;
|
|
468
|
+
if (responseData?.data?.tobeparsed) {
|
|
469
|
+
responseData.data = this.decryptTobeparsed(responseData.data.tobeparsed);
|
|
470
|
+
}
|
|
471
|
+
const show = responseData.data.show;
|
|
472
|
+
if (show) {
|
|
473
|
+
return {
|
|
474
|
+
_id: show._id,
|
|
475
|
+
name: show.name,
|
|
476
|
+
thumbnail: this.deobfuscateUrl(show.thumbnail),
|
|
477
|
+
nativeName: show.nativeName,
|
|
478
|
+
englishName: show.englishName,
|
|
479
|
+
type: show.type,
|
|
480
|
+
availableEpisodesDetail: show.availableEpisodesDetail,
|
|
481
|
+
score: show.score,
|
|
482
|
+
isAdult: show.isAdult,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
async getEpisodes(showId, mode) {
|
|
488
|
+
const cacheKey = `episodes-${showId}-${mode}`;
|
|
489
|
+
const cachedData = this.cache.get(cacheKey);
|
|
490
|
+
if (cachedData)
|
|
491
|
+
return cachedData;
|
|
492
|
+
const response = await axios_1.default.post(API_ENDPOINT, {
|
|
493
|
+
query: `query($showId: String!) { show(_id: $showId) { availableEpisodesDetail, description } }`,
|
|
494
|
+
variables: { showId },
|
|
495
|
+
}, {
|
|
496
|
+
headers: { 'User-Agent': USER_AGENT, Referer: REFERER },
|
|
497
|
+
timeout: 15000,
|
|
498
|
+
});
|
|
499
|
+
const responseData = response.data;
|
|
500
|
+
if (responseData?.data?.tobeparsed) {
|
|
501
|
+
responseData.data = this.decryptTobeparsed(responseData.data.tobeparsed);
|
|
502
|
+
}
|
|
503
|
+
const showData = responseData.data.show;
|
|
504
|
+
if (showData) {
|
|
505
|
+
const episodeDetails = {
|
|
506
|
+
episodes: showData.availableEpisodesDetail[mode] || [],
|
|
507
|
+
description: showData.description,
|
|
508
|
+
};
|
|
509
|
+
this.cache.set(cacheKey, episodeDetails);
|
|
510
|
+
return episodeDetails;
|
|
511
|
+
}
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
async getSkipTimes(showId, episodeNumber) {
|
|
515
|
+
try {
|
|
516
|
+
const malIdResponse = await axios_1.default.post(API_ENDPOINT, {
|
|
517
|
+
query: `query($showId: String!) { show(_id: $showId) { malId } }`,
|
|
518
|
+
variables: { showId },
|
|
519
|
+
}, {
|
|
520
|
+
headers: { 'User-Agent': USER_AGENT, Referer: REFERER },
|
|
521
|
+
timeout: 10000,
|
|
522
|
+
});
|
|
523
|
+
const responseData = malIdResponse.data;
|
|
524
|
+
if (responseData?.data?.tobeparsed) {
|
|
525
|
+
responseData.data = this.decryptTobeparsed(responseData.data.tobeparsed);
|
|
526
|
+
}
|
|
527
|
+
const malId = responseData?.data?.show?.malId;
|
|
528
|
+
if (!malId)
|
|
529
|
+
return { found: false, results: [] };
|
|
530
|
+
const response = await axios_1.default.get(`https://api.aniskip.com/v1/skip-times/${malId}/${episodeNumber}?types=op&types=ed`, {
|
|
531
|
+
headers: { 'User-Agent': USER_AGENT },
|
|
532
|
+
timeout: 5000,
|
|
533
|
+
});
|
|
534
|
+
return response.data;
|
|
535
|
+
}
|
|
536
|
+
catch {
|
|
537
|
+
return { found: false, results: [] };
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
async getStreamUrls(showId, episodeNumber, mode) {
|
|
541
|
+
const { data: axiosResponse } = await axios_1.default.post(API_ENDPOINT, {
|
|
542
|
+
variables: { showId, translationType: mode, episodeString: episodeNumber },
|
|
543
|
+
extensions: {
|
|
544
|
+
persistedQuery: {
|
|
545
|
+
version: 1,
|
|
546
|
+
sha256Hash: 'd405d0edd690624b66baba3068e0edc3ac90f1597d898a1ec8db4e5c43c00fec',
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
}, {
|
|
550
|
+
headers: { 'User-Agent': USER_AGENT, Referer: REFERER },
|
|
551
|
+
timeout: 15000,
|
|
552
|
+
});
|
|
553
|
+
const responseData = axiosResponse;
|
|
554
|
+
if (responseData?.data?.tobeparsed) {
|
|
555
|
+
responseData.data = this.decryptTobeparsed(responseData.data.tobeparsed);
|
|
556
|
+
}
|
|
557
|
+
const sourceUrls = responseData.data?.episode?.sourceUrls;
|
|
558
|
+
if (!Array.isArray(sourceUrls))
|
|
559
|
+
return null;
|
|
560
|
+
const supportedSources = [
|
|
561
|
+
'Yt-mp4',
|
|
562
|
+
'S-mp4',
|
|
563
|
+
'wixmp',
|
|
564
|
+
'Default',
|
|
565
|
+
'Fm-Hls',
|
|
566
|
+
'Vg',
|
|
567
|
+
'Sw',
|
|
568
|
+
'Mp4',
|
|
569
|
+
'Ok',
|
|
570
|
+
'Uni',
|
|
571
|
+
];
|
|
572
|
+
const filteredSources = sourceUrls
|
|
573
|
+
.filter((s) => supportedSources.includes(s.sourceName))
|
|
574
|
+
.sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
|
575
|
+
const processedSources = await Promise.all(filteredSources.map(async (source) => {
|
|
576
|
+
try {
|
|
577
|
+
let videoLinks = [];
|
|
578
|
+
let subtitles = [];
|
|
579
|
+
if (['Yt-mp4', 'S-mp4', 'wixmp', 'Default'].includes(source.sourceName)) {
|
|
580
|
+
let decryptedUrl = this.deobfuscateStreamUrl(source.sourceUrl);
|
|
581
|
+
if (decryptedUrl.includes('/clock') && !decryptedUrl.includes('.json')) {
|
|
582
|
+
decryptedUrl = decryptedUrl.replace('/clock', '/clock.json');
|
|
583
|
+
}
|
|
584
|
+
if (decryptedUrl.includes('/clock.json')) {
|
|
585
|
+
const finalUrl = decryptedUrl.startsWith('http')
|
|
586
|
+
? decryptedUrl
|
|
587
|
+
: new URL(decryptedUrl, API_BASE_URL).href;
|
|
588
|
+
const resp = await axios_1.default.get(finalUrl, {
|
|
589
|
+
headers: { Referer: REFERER, 'User-Agent': USER_AGENT },
|
|
590
|
+
timeout: 10000,
|
|
591
|
+
});
|
|
592
|
+
const clockData = resp.data;
|
|
593
|
+
if (clockData && Array.isArray(clockData.links) && clockData.links.length > 0) {
|
|
594
|
+
const linkData = clockData.links[0];
|
|
595
|
+
if (linkData.hls) {
|
|
596
|
+
const hlsResp = await axios_1.default.get(linkData.link, {
|
|
597
|
+
headers: linkData.headers || { Referer: REFERER },
|
|
598
|
+
responseType: 'text',
|
|
599
|
+
timeout: 10000,
|
|
600
|
+
});
|
|
601
|
+
const lines = hlsResp.data.split('\n');
|
|
602
|
+
for (let i = 0; i < lines.length; i++) {
|
|
603
|
+
if (lines[i].startsWith('#EXT-X-STREAM-INF')) {
|
|
604
|
+
const resMatch = lines[i].match(/RESOLUTION=\d+x(\d+)/);
|
|
605
|
+
videoLinks.push({
|
|
606
|
+
resolutionStr: resMatch ? `${resMatch[1]}p` : 'Auto',
|
|
607
|
+
link: new URL(lines[i + 1], linkData.link).href,
|
|
608
|
+
hls: true,
|
|
609
|
+
headers: linkData.headers || { Referer: REFERER },
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
videoLinks = clockData.links
|
|
616
|
+
.map((l) => ({
|
|
617
|
+
resolutionStr: l.resolutionStr || 'Default',
|
|
618
|
+
link: l.link && l.link.startsWith('/')
|
|
619
|
+
? `${API_BASE_URL}${l.link}`
|
|
620
|
+
: l.link || '',
|
|
621
|
+
hls: !!l.hls,
|
|
622
|
+
headers: l.headers || { Referer: REFERER },
|
|
623
|
+
}))
|
|
624
|
+
.filter((l) => l.link !== '');
|
|
625
|
+
}
|
|
626
|
+
if (Array.isArray(linkData.subtitles)) {
|
|
627
|
+
subtitles = linkData.subtitles.map((s) => ({
|
|
628
|
+
language: s.lang || s.language || 'en',
|
|
629
|
+
label: s.label || 'Subtitle',
|
|
630
|
+
url: s.src && s.src.startsWith('/')
|
|
631
|
+
? `${API_BASE_URL}${s.src}`
|
|
632
|
+
: s.src || s.url || '',
|
|
633
|
+
}));
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
if (videoLinks.length === 0 && decryptedUrl && !decryptedUrl.includes('/clock')) {
|
|
638
|
+
videoLinks.push({
|
|
639
|
+
resolutionStr: 'Default',
|
|
640
|
+
link: decryptedUrl,
|
|
641
|
+
hls: decryptedUrl.includes('.m3u8'),
|
|
642
|
+
headers: { Referer: REFERER },
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
if (videoLinks.length > 0) {
|
|
646
|
+
return {
|
|
647
|
+
sourceName: source.sourceName,
|
|
648
|
+
links: videoLinks,
|
|
649
|
+
subtitles,
|
|
650
|
+
type: 'player',
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
else if (source.sourceName === 'Mp4') {
|
|
655
|
+
const decryptedUrl = this.deobfuscateStreamUrl(source.sourceUrl);
|
|
656
|
+
try {
|
|
657
|
+
const { data: embedHtml } = await axios_1.default.get(decryptedUrl, {
|
|
658
|
+
headers: {
|
|
659
|
+
'User-Agent': USER_AGENT,
|
|
660
|
+
Referer: 'https://allanime.day/',
|
|
661
|
+
},
|
|
662
|
+
timeout: 10000,
|
|
663
|
+
});
|
|
664
|
+
const match = embedHtml.match(/src:\s*"(https:\/\/.*?\.mp4)"/);
|
|
665
|
+
if (match) {
|
|
666
|
+
return {
|
|
667
|
+
sourceName: source.sourceName,
|
|
668
|
+
links: [
|
|
669
|
+
{
|
|
670
|
+
resolutionStr: 'Default',
|
|
671
|
+
link: match[1],
|
|
672
|
+
hls: false,
|
|
673
|
+
headers: { Referer: 'https://www.mp4upload.com/' },
|
|
674
|
+
},
|
|
675
|
+
],
|
|
676
|
+
type: 'player',
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
catch (e) {
|
|
681
|
+
// Ignore scrape errors
|
|
682
|
+
}
|
|
683
|
+
return {
|
|
684
|
+
sourceName: source.sourceName,
|
|
685
|
+
links: [{ resolutionStr: 'iframe', link: decryptedUrl, hls: false }],
|
|
686
|
+
type: 'iframe',
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
else {
|
|
690
|
+
return {
|
|
691
|
+
sourceName: source.sourceName,
|
|
692
|
+
links: [{ resolutionStr: 'iframe', link: source.sourceUrl, hls: false }],
|
|
693
|
+
type: source.type === 'iframe' ||
|
|
694
|
+
source.sourceName === 'Fm-Hls' ||
|
|
695
|
+
['Vg', 'Sw', 'Ok', 'Uni'].includes(source.sourceName)
|
|
696
|
+
? 'iframe'
|
|
697
|
+
: 'player',
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
catch (e) {
|
|
702
|
+
return null;
|
|
703
|
+
}
|
|
704
|
+
return null;
|
|
705
|
+
}));
|
|
706
|
+
const result = processedSources.filter((s) => s !== null);
|
|
707
|
+
return result.length > 0 ? result : null;
|
|
708
|
+
}
|
|
709
|
+
async getShowDetails(showId) {
|
|
710
|
+
const response = await axios_1.default.post(API_ENDPOINT, {
|
|
711
|
+
query: `query($showId: String!) { show(_id: $showId) { name } }`,
|
|
712
|
+
variables: { showId },
|
|
713
|
+
}, {
|
|
714
|
+
headers: { 'User-Agent': USER_AGENT, Referer: REFERER },
|
|
715
|
+
timeout: 10000,
|
|
716
|
+
});
|
|
717
|
+
const responseData = response.data;
|
|
718
|
+
if (responseData?.data?.tobeparsed) {
|
|
719
|
+
responseData.data = this.decryptTobeparsed(responseData.data.tobeparsed);
|
|
720
|
+
}
|
|
721
|
+
const showName = responseData?.data?.show?.name;
|
|
722
|
+
if (!showName)
|
|
723
|
+
throw new Error('Show not found');
|
|
724
|
+
const scheduleSearchUrl = `https://animeschedule.net/api/v3/anime?q=${encodeURIComponent(showName)}`;
|
|
725
|
+
const scheduleResponse = await axios_1.default.get(scheduleSearchUrl, { timeout: 10000 });
|
|
726
|
+
const firstResult = scheduleResponse.data?.anime?.[0];
|
|
727
|
+
if (firstResult) {
|
|
728
|
+
if (firstResult.status === 'Ongoing') {
|
|
729
|
+
try {
|
|
730
|
+
const pageResponse = await axios_1.default.get(`https://animeschedule.net/anime/${firstResult.route}`, { timeout: 10000 });
|
|
731
|
+
const countdownMatch = pageResponse.data.match(/countdown-time" datetime="([^"]*)"/);
|
|
732
|
+
if (countdownMatch) {
|
|
733
|
+
firstResult.nextEpisodeAirDate = countdownMatch[1];
|
|
734
|
+
const airingTime = new Date(countdownMatch[1]).getTime();
|
|
735
|
+
const now = Date.now();
|
|
736
|
+
firstResult.nextAiring = {
|
|
737
|
+
episode: firstResult.currentEpisode ? firstResult.currentEpisode + 1 : 1,
|
|
738
|
+
timeUntilAiring: Math.floor((airingTime - now) / 1000),
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
catch (e) {
|
|
743
|
+
logger_1.default.warn({ err: e }, 'Failed to fetch schedule page');
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
return firstResult;
|
|
747
|
+
}
|
|
748
|
+
throw new Error('Not Found on Schedule');
|
|
749
|
+
}
|
|
750
|
+
async getAllmangaDetails(showId) {
|
|
751
|
+
const url = `https://allmanga.to/bangumi/${showId}`;
|
|
752
|
+
const response = await axios_1.default.get(url, {
|
|
753
|
+
headers: { 'User-Agent': USER_AGENT, Referer: REFERER },
|
|
754
|
+
});
|
|
755
|
+
const $ = cheerio.load(response.data);
|
|
756
|
+
const details = {
|
|
757
|
+
Rating: 'N/A',
|
|
758
|
+
Season: 'N/A',
|
|
759
|
+
Episodes: 'N/A',
|
|
760
|
+
Date: 'N/A',
|
|
761
|
+
'Original Broadcast': 'N/A',
|
|
762
|
+
};
|
|
763
|
+
$('.info-season').each((_i, elem) => {
|
|
764
|
+
const label = $(elem).find('h4').text().trim();
|
|
765
|
+
const value = $(elem).find('li').text().trim();
|
|
766
|
+
if (Object.prototype.hasOwnProperty.call(details, label)) {
|
|
767
|
+
details[label] = value;
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
return details;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
exports.AllAnimeProvider = AllAnimeProvider;
|