@serifpersia/dango 3.0.3

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.
Files changed (141) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +247 -0
  3. package/client/dist/assets/AnimeInfo-CMxs0nRH.css +1 -0
  4. package/client/dist/assets/AnimeInfo-DVdaocCl.js +1 -0
  5. package/client/dist/assets/AnimeInfoPage-Bx5EvGt-.js +2 -0
  6. package/client/dist/assets/Asmr-C2i9GKD4.css +1 -0
  7. package/client/dist/assets/Asmr-CIjmFtRH.js +2 -0
  8. package/client/dist/assets/Asmr.module-DPG2JhDJ.js +1 -0
  9. package/client/dist/assets/AvSyncCalibrator-CIYcyon5.css +1 -0
  10. package/client/dist/assets/AvSyncCalibrator-vqErH7Cm.js +10 -0
  11. package/client/dist/assets/ErrorMessage-BwSjzblw.js +1 -0
  12. package/client/dist/assets/ErrorMessage-CXoRbrLN.css +1 -0
  13. package/client/dist/assets/Home-D7YfGBUk.js +1 -0
  14. package/client/dist/assets/Home-DKCfEiwl.css +1 -0
  15. package/client/dist/assets/Insights-BljI64Ua.css +1 -0
  16. package/client/dist/assets/Insights-CLQGrlpX.js +1 -0
  17. package/client/dist/assets/Manga-DQ9HYnpk.js +1 -0
  18. package/client/dist/assets/Manga-H8Wb04w3.css +1 -0
  19. package/client/dist/assets/Map-CXFqXmsZ.css +1 -0
  20. package/client/dist/assets/Map-L_ryVBTq.js +1 -0
  21. package/client/dist/assets/Mature-BeWzAJ7B.js +1 -0
  22. package/client/dist/assets/Mature-DJmUUtlF.css +1 -0
  23. package/client/dist/assets/Player-CYpd__FP.js +30 -0
  24. package/client/dist/assets/Player-DreAhybc.css +1 -0
  25. package/client/dist/assets/PlayerRedirect-fxUAp9JV.js +1 -0
  26. package/client/dist/assets/PlayerSettings-BcsotxP9.js +1 -0
  27. package/client/dist/assets/PlayerSettings-Cqua1CDD.css +1 -0
  28. package/client/dist/assets/QueueRail-B9Pgbqc3.js +1 -0
  29. package/client/dist/assets/QueueRail-BnaltOP8.css +1 -0
  30. package/client/dist/assets/Radio-BvdY5twD.js +1 -0
  31. package/client/dist/assets/Radio-CUuYy3DY.css +1 -0
  32. package/client/dist/assets/Search-BzO-aRP7.css +1 -0
  33. package/client/dist/assets/Search-R9YLAXS1.js +1 -0
  34. package/client/dist/assets/Search.module-D8ZbQAiK.js +1 -0
  35. package/client/dist/assets/Settings-9kYcXdTR.js +21 -0
  36. package/client/dist/assets/Settings-DF8JThUL.css +1 -0
  37. package/client/dist/assets/SkeletonGrid-D5vRvX5q.js +1 -0
  38. package/client/dist/assets/SkeletonGrid-DEbYtO4H.css +1 -0
  39. package/client/dist/assets/SynopsisText-BlFw0R-0.js +1 -0
  40. package/client/dist/assets/SynopsisText-CUgOjOxt.css +1 -0
  41. package/client/dist/assets/Trackers-DfZlnnVq.js +2 -0
  42. package/client/dist/assets/Trackers-Dt9lR_uP.css +1 -0
  43. package/client/dist/assets/Tv-CVKC2YBs.css +1 -0
  44. package/client/dist/assets/Tv-Db1Wx_b7.js +15 -0
  45. package/client/dist/assets/Watchlist-B2nkNZqM.css +1 -0
  46. package/client/dist/assets/Watchlist-a77CYtzu.js +1 -0
  47. package/client/dist/assets/hls-6Owgw2Aj.js +1 -0
  48. package/client/dist/assets/hls-BOLXTnjL.js +42 -0
  49. package/client/dist/assets/index-B6qdXFtl.css +1 -0
  50. package/client/dist/assets/index-B8TPVaIS.js +30354 -0
  51. package/client/dist/assets/index-CpjgU8ou.js +178 -0
  52. package/client/dist/assets/useAnimeInfoData-BOfyi9UJ.js +1 -0
  53. package/client/dist/assets/useIsMobile-CM15uQn8.css +1 -0
  54. package/client/dist/assets/useIsMobile-Cb8HcKRj.js +1 -0
  55. package/client/dist/assets/useMatureConsent-CZMYBVXd.js +1 -0
  56. package/client/dist/assets/vendor-rLNfZm7Y.js +1 -0
  57. package/client/dist/av-sync-test.mp4 +0 -0
  58. package/client/dist/favicon.ico +0 -0
  59. package/client/dist/index.html +31 -0
  60. package/client/dist/logo-mochi.png +0 -0
  61. package/client/dist/logo-sakura.png +0 -0
  62. package/client/dist/logo.png +0 -0
  63. package/client/dist/placeholder.svg +4 -0
  64. package/client/dist/robots.txt +3 -0
  65. package/orchestrator.js +416 -0
  66. package/package.json +89 -0
  67. package/server/dist/app-auth.js +231 -0
  68. package/server/dist/config.js +133 -0
  69. package/server/dist/controllers/auth.controller.js +247 -0
  70. package/server/dist/controllers/data.controller.js +785 -0
  71. package/server/dist/controllers/insights.controller.js +197 -0
  72. package/server/dist/controllers/proxy.controller.js +665 -0
  73. package/server/dist/controllers/settings.controller.js +267 -0
  74. package/server/dist/controllers/watchlist.controller.js +967 -0
  75. package/server/dist/db.js +152 -0
  76. package/server/dist/discord-gateway.js +381 -0
  77. package/server/dist/discord-rpc.js +424 -0
  78. package/server/dist/github-sync.js +403 -0
  79. package/server/dist/google.js +549 -0
  80. package/server/dist/lib/anilist.js +1519 -0
  81. package/server/dist/lib/anischedule.js +202 -0
  82. package/server/dist/lib/ipc.js +32 -0
  83. package/server/dist/lib/kitsu.js +552 -0
  84. package/server/dist/lib/mal.js +886 -0
  85. package/server/dist/lib/migration.js +335 -0
  86. package/server/dist/lib/temp-ids.js +11 -0
  87. package/server/dist/lib/tmdb.js +71 -0
  88. package/server/dist/lib/tracker/anilist-tracker.js +250 -0
  89. package/server/dist/lib/tracker/sync.service.js +312 -0
  90. package/server/dist/logger.js +19 -0
  91. package/server/dist/providers/123anime.provider.js +240 -0
  92. package/server/dist/providers/anilight.provider.js +366 -0
  93. package/server/dist/providers/animepahe.provider.js +531 -0
  94. package/server/dist/providers/animeya.provider.js +746 -0
  95. package/server/dist/providers/base-provider.js +45 -0
  96. package/server/dist/providers/hn.provider.js +473 -0
  97. package/server/dist/providers/ht.provider.js +439 -0
  98. package/server/dist/providers/jasmr.provider.js +504 -0
  99. package/server/dist/providers/kaa.provider.js +388 -0
  100. package/server/dist/providers/manga/manga.types.js +10 -0
  101. package/server/dist/providers/manga/mangadex.provider.js +227 -0
  102. package/server/dist/providers/manga/mangapill.provider.js +232 -0
  103. package/server/dist/providers/megaplay.provider.js +583 -0
  104. package/server/dist/providers/op.provider.js +440 -0
  105. package/server/dist/providers/provider.interface.js +2 -0
  106. package/server/dist/providers/title-matching.js +234 -0
  107. package/server/dist/providers/wh.provider.js +600 -0
  108. package/server/dist/rclone.js +126 -0
  109. package/server/dist/repositories/insights.repository.js +43 -0
  110. package/server/dist/repositories/mal-cache.repository.js +66 -0
  111. package/server/dist/repositories/notifications.repository.js +40 -0
  112. package/server/dist/repositories/queue.repository.js +64 -0
  113. package/server/dist/repositories/settings.repository.js +15 -0
  114. package/server/dist/repositories/shows-meta.repository.js +43 -0
  115. package/server/dist/repositories/temp-show-ids.repository.js +54 -0
  116. package/server/dist/repositories/watched-episodes.repository.js +54 -0
  117. package/server/dist/repositories/watchlist.repository.js +74 -0
  118. package/server/dist/routes/asmr.routes.js +72 -0
  119. package/server/dist/routes/auth.routes.js +28 -0
  120. package/server/dist/routes/data.routes.js +50 -0
  121. package/server/dist/routes/discord-gateway.routes.js +57 -0
  122. package/server/dist/routes/insights.routes.js +12 -0
  123. package/server/dist/routes/lan-auth.routes.js +49 -0
  124. package/server/dist/routes/manga.routes.js +115 -0
  125. package/server/dist/routes/proxy.routes.js +15 -0
  126. package/server/dist/routes/radio.routes.js +143 -0
  127. package/server/dist/routes/settings.routes.js +27 -0
  128. package/server/dist/routes/tracker.routes.js +143 -0
  129. package/server/dist/routes/translate.routes.js +90 -0
  130. package/server/dist/routes/tv.routes.js +843 -0
  131. package/server/dist/routes/watchlist.routes.js +312 -0
  132. package/server/dist/server.js +310 -0
  133. package/server/dist/shipped-defaults.js +8 -0
  134. package/server/dist/sync-config.js +32 -0
  135. package/server/dist/sync.js +506 -0
  136. package/server/dist/utils/cookie.utils.js +17 -0
  137. package/server/dist/utils/db-utils.js +15 -0
  138. package/server/dist/utils/env.utils.js +83 -0
  139. package/server/dist/utils/machine-id.js +46 -0
  140. package/server/dist/utils/request-context.js +5 -0
  141. package/server/dist/utils/security.utils.js +99 -0
@@ -0,0 +1,1519 @@
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.anilistUnavailable = anilistUnavailable;
40
+ exports.anilistIsDown = anilistIsDown;
41
+ exports.isAnilistRateLimited = isAnilistRateLimited;
42
+ exports.markAnilistDownAtBoot = markAnilistDownAtBoot;
43
+ exports.checkAnilistStatus = checkAnilistStatus;
44
+ exports.wasAnilistDownAtBoot = wasAnilistDownAtBoot;
45
+ exports.applyRateLimitHeaders = applyRateLimitHeaders;
46
+ exports.mediaFields = mediaFields;
47
+ exports.mediaFieldsLight = mediaFieldsLight;
48
+ exports.getBatchedHomeData = getBatchedHomeData;
49
+ exports.setCachedAnilist = setCachedAnilist;
50
+ exports.waitForAnilistSlot = waitForAnilistSlot;
51
+ exports.anilistRequest = anilistRequest;
52
+ exports.getGenreTagLists = getGenreTagLists;
53
+ exports.fromAnilistMedia = fromAnilistMedia;
54
+ exports.getLatestReleases = getLatestReleases;
55
+ exports.getSeasonal = getSeasonal;
56
+ exports.getTrending = getTrending;
57
+ exports.getSpotlightBanners = getSpotlightBanners;
58
+ exports.parseMalId = parseMalId;
59
+ exports.findPosterByTitle = findPosterByTitle;
60
+ exports.getShowMetaById = getShowMetaById;
61
+ exports.getShowMetaByMalId = getShowMetaByMalId;
62
+ exports.batchGetShowStatuses = batchGetShowStatuses;
63
+ exports.getAnilistEpisodes = getAnilistEpisodes;
64
+ exports.searchAnilistByTitle = searchAnilistByTitle;
65
+ exports.getAiredEpisodesForShows = getAiredEpisodesForShows;
66
+ exports.getAiredEpisodesForShow = getAiredEpisodesForShow;
67
+ exports.getSchedule = getSchedule;
68
+ exports.searchAnilist = searchAnilist;
69
+ const logger_1 = __importDefault(require("../logger"));
70
+ const tmdb_1 = require("./tmdb");
71
+ const anischedule_1 = require("./anischedule");
72
+ const kitsu_1 = require("./kitsu");
73
+ const mal_1 = require("./mal");
74
+ const mal_cache_repository_1 = require("../repositories/mal-cache.repository");
75
+ const ANILIST_API = 'https://graphql.anilist.co';
76
+ let anilistDownUntil = 0;
77
+ let anilistRateLimited = false;
78
+ let anilistWasDownAtBoot = false;
79
+ const DOWN_COOLDOWN_MS = 10 * 60 * 1000;
80
+ const NETWORK_COOLDOWN_MS = 2 * 60 * 1000;
81
+ const RATE_LIMIT_FALLBACK_THRESHOLD_MS = 30 * 1000;
82
+ function anilistUnavailable() {
83
+ if (anilistDownUntil > Date.now())
84
+ return true;
85
+ if (anilistRateLimited && anilistCooldownUntil - Date.now() > RATE_LIMIT_FALLBACK_THRESHOLD_MS) {
86
+ return true;
87
+ }
88
+ return false;
89
+ }
90
+ function anilistIsDown() {
91
+ return anilistDownUntil > Date.now();
92
+ }
93
+ function isAnilistRateLimited() {
94
+ return anilistRateLimited && anilistCooldownUntil > Date.now();
95
+ }
96
+ function markAnilistDownAtBoot() {
97
+ anilistWasDownAtBoot = anilistUnavailable();
98
+ }
99
+ async function checkAnilistStatus() {
100
+ const available = await performAnilistStatusCheck();
101
+ if (!available) {
102
+ anilistWasDownAtBoot = true;
103
+ }
104
+ return available;
105
+ }
106
+ async function performAnilistStatusCheck() {
107
+ try {
108
+ const controller = new AbortController();
109
+ const timer = setTimeout(() => controller.abort(), 5000);
110
+ const response = await fetch(ANILIST_API, {
111
+ method: 'POST',
112
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
113
+ body: JSON.stringify({ query: 'query { __typename }' }),
114
+ signal: controller.signal,
115
+ });
116
+ clearTimeout(timer);
117
+ const available = response.ok;
118
+ if (!available) {
119
+ anilistDownUntil = Date.now() + DOWN_COOLDOWN_MS;
120
+ anilistRateLimited = false;
121
+ }
122
+ else {
123
+ anilistDownUntil = 0;
124
+ anilistRateLimited = false;
125
+ }
126
+ return available;
127
+ }
128
+ catch {
129
+ anilistDownUntil = Date.now() + DOWN_COOLDOWN_MS;
130
+ anilistRateLimited = false;
131
+ return false;
132
+ }
133
+ }
134
+ function wasAnilistDownAtBoot() {
135
+ return anilistWasDownAtBoot;
136
+ }
137
+ const BURST_CAPACITY = 8;
138
+ const DEFAULT_REFILL_INTERVAL_MS = 1500;
139
+ let refillIntervalMs = DEFAULT_REFILL_INTERVAL_MS;
140
+ let tokens = BURST_CAPACITY;
141
+ let lastRefill = Date.now();
142
+ let anilistCooldownUntil = 0;
143
+ function refillTokens() {
144
+ const now = Date.now();
145
+ if (now < lastRefill)
146
+ return;
147
+ const elapsed = now - lastRefill;
148
+ const refillAmount = Math.floor(elapsed / refillIntervalMs);
149
+ if (refillAmount > 0) {
150
+ tokens = Math.min(BURST_CAPACITY, tokens + refillAmount);
151
+ lastRefill += refillAmount * refillIntervalMs;
152
+ }
153
+ }
154
+ function applyRateLimitHeaders(headers) {
155
+ const readHeader = (name) => {
156
+ const raw = headers.get(name);
157
+ if (raw === null || raw === '')
158
+ return null;
159
+ const value = Number.parseInt(raw, 10);
160
+ return Number.isFinite(value) && value >= 0 ? value : null;
161
+ };
162
+ const limit = readHeader('X-RateLimit-Limit');
163
+ const remaining = readHeader('X-RateLimit-Remaining');
164
+ if (limit !== null && limit > 0) {
165
+ const reserve = Math.max(1, Math.round(limit * 0.2));
166
+ const safeRate = Math.max(1, limit - reserve);
167
+ refillIntervalMs = Math.max(300, Math.min(60000, Math.round(60000 / safeRate)));
168
+ }
169
+ if (remaining !== null && remaining <= 2) {
170
+ const reset = readHeader('X-RateLimit-Reset');
171
+ if (reset !== null) {
172
+ const resetMs = reset * 1000;
173
+ if (resetMs > Date.now()) {
174
+ anilistCooldownUntil = Math.max(anilistCooldownUntil, resetMs);
175
+ }
176
+ }
177
+ }
178
+ }
179
+ const anilistMemoryCache = new Map();
180
+ const ANILIST_MEMORY_TTL = 60 * 60 * 1000;
181
+ const airedEpisodesCache = new Map();
182
+ const AIRED_EPISODES_TTL = 60 * 60 * 1000;
183
+ const inFlightAnilistRequests = new Map();
184
+ function mediaFields() {
185
+ return `
186
+ id
187
+ idMal
188
+ title { romaji english native }
189
+ bannerImage
190
+ coverImage { extraLarge large medium color }
191
+ description
192
+ genres
193
+ averageScore
194
+ meanScore
195
+ format
196
+ status
197
+ episodes
198
+ duration
199
+ season
200
+ seasonYear
201
+ startDate { year month day }
202
+ endDate { year month day }
203
+ countryOfOrigin
204
+ isAdult
205
+ synonyms
206
+ trending
207
+ popularity
208
+ favourites
209
+ tags { id name rank isMediaSpoiler }
210
+ studios(isMain: true) { nodes { id name } }
211
+ nextAiringEpisode { episode timeUntilAiring airingAt }
212
+ trailer { id site thumbnail }
213
+ rankings { id rank type format allTime context }
214
+ siteUrl
215
+ `;
216
+ }
217
+ function mediaFieldsLight() {
218
+ return `
219
+ id
220
+ title { romaji english native }
221
+ coverImage { extraLarge large }
222
+ status
223
+ averageScore
224
+ episodes
225
+ seasonYear
226
+ season
227
+ genres
228
+ isAdult
229
+ format
230
+ nextAiringEpisode { episode }
231
+ `;
232
+ }
233
+ async function tryFallback(malFn, kitsuFn) {
234
+ for (const fn of [malFn, kitsuFn]) {
235
+ try {
236
+ const result = await fn();
237
+ if (result && result.length > 0)
238
+ return result;
239
+ }
240
+ catch {
241
+ // ignore
242
+ }
243
+ }
244
+ return [];
245
+ }
246
+ function mapFallbackResults(fb) {
247
+ const seen = new Set();
248
+ const results = [];
249
+ for (const m of fb) {
250
+ if (!m.id || seen.has(m.id))
251
+ continue;
252
+ seen.add(m.id);
253
+ results.push(fromAnilistMedia(m));
254
+ }
255
+ return results;
256
+ }
257
+ async function getBatchedHomeData(format) {
258
+ const now = new Date();
259
+ const year = now.getFullYear();
260
+ const month = now.getMonth() + 1;
261
+ const currentSeason = month <= 3 ? 'WINTER' : month <= 6 ? 'SPRING' : month <= 9 ? 'SUMMER' : 'FALL';
262
+ const formatVar = format && format !== 'ALL' ? format.toUpperCase() : undefined;
263
+ const formatFilter = formatVar ? `format: $seasonFormat,` : '';
264
+ const query = `
265
+ query ($season: MediaSeason, $seasonYear: Int, $seasonFormat: MediaFormat) {
266
+ trending: Page(perPage: 20) {
267
+ media(sort: TRENDING_DESC, type: ANIME, isAdult: false, status: RELEASING) {
268
+ ${mediaFieldsLight()}
269
+ }
270
+ }
271
+ seasonal: Page(perPage: 14) {
272
+ media(season: $season, seasonYear: $seasonYear, type: ANIME, isAdult: false, ${formatFilter} sort: [POPULARITY_DESC, SCORE_DESC]) {
273
+ ${mediaFieldsLight()}
274
+ }
275
+ }
276
+ spotlight: Page(perPage: 20) {
277
+ media(sort: TRENDING_DESC, type: ANIME, isAdult: false) {
278
+ ${mediaFields()}
279
+ }
280
+ }
281
+ }
282
+ `;
283
+ const result = await anilistRequest(query, { season: currentSeason, seasonYear: year, seasonFormat: formatVar });
284
+ if (!result?.data) {
285
+ if (!anilistUnavailable())
286
+ return { trending: [], seasonal: [], spotlight: [] };
287
+ try {
288
+ const [trending, seasonal, spotlight] = await Promise.all([
289
+ tryFallback(() => (0, mal_1.malTop)((0, mal_cache_repository_1.malCacheStore)(), 'airing', 1).then((m) => m.map(fromAnilistMedia)), () => (0, kitsu_1.kitsuTrending)(1, 20).then((m) => m.map(fromAnilistMedia))),
290
+ tryFallback(() => (0, mal_1.malSeasonal)((0, mal_cache_repository_1.malCacheStore)(), currentSeason, year, formatVar, 1, 14).then((m) => m.map(fromAnilistMedia)), () => (0, kitsu_1.kitsuSeasonal)(currentSeason, year, formatVar, 1, 14).then((m) => m.map(fromAnilistMedia))),
291
+ tryFallback(() => (0, mal_1.malTop)((0, mal_cache_repository_1.malCacheStore)(), 'airing', 1).then((m) => m.map(fromAnilistMedia)), () => (0, kitsu_1.kitsuSpotlight)(1, 20).then((m) => m.map(fromAnilistMedia))),
292
+ ]);
293
+ return { trending, seasonal, spotlight };
294
+ }
295
+ catch {
296
+ return { trending: [], seasonal: [], spotlight: [] };
297
+ }
298
+ }
299
+ return {
300
+ trending: (result.data.trending?.media ?? []).map(fromAnilistMedia),
301
+ seasonal: (result.data.seasonal?.media ?? []).map(fromAnilistMedia),
302
+ spotlight: (result.data.spotlight?.media ?? []).map(fromAnilistMedia),
303
+ };
304
+ }
305
+ function getAnilistCacheKey(query, variables) {
306
+ return `${query}:${JSON.stringify(variables || {})}`;
307
+ }
308
+ function getCachedAnilist(key) {
309
+ const entry = anilistMemoryCache.get(key);
310
+ if (entry && Date.now() < entry.expiry)
311
+ return entry.data;
312
+ anilistMemoryCache.delete(key);
313
+ return null;
314
+ }
315
+ function setCachedAnilist(key, data) {
316
+ anilistMemoryCache.set(key, { data, expiry: Date.now() + ANILIST_MEMORY_TTL });
317
+ }
318
+ function getCachedAiredEpisodes(key) {
319
+ const entry = airedEpisodesCache.get(key);
320
+ if (entry && Date.now() < entry.expiry)
321
+ return entry.data;
322
+ airedEpisodesCache.delete(key);
323
+ return null;
324
+ }
325
+ function setCachedAiredEpisodes(key, data) {
326
+ airedEpisodesCache.set(key, { data, expiry: Date.now() + AIRED_EPISODES_TTL });
327
+ }
328
+ async function waitForAnilistSlot() {
329
+ while (true) {
330
+ const now = Date.now();
331
+ if (now < anilistCooldownUntil) {
332
+ await new Promise((resolve) => setTimeout(resolve, anilistCooldownUntil - now));
333
+ continue;
334
+ }
335
+ refillTokens();
336
+ if (tokens >= 1) {
337
+ tokens -= 1;
338
+ return;
339
+ }
340
+ await new Promise((resolve) => setTimeout(resolve, refillIntervalMs));
341
+ }
342
+ }
343
+ async function performAnilistRequest(query, variables, retryCount = 0) {
344
+ await waitForAnilistSlot();
345
+ try {
346
+ const response = await fetch(ANILIST_API, {
347
+ method: 'POST',
348
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
349
+ body: JSON.stringify({ query, variables }),
350
+ });
351
+ applyRateLimitHeaders(response.headers);
352
+ if (response.status === 429) {
353
+ anilistRateLimited = true;
354
+ if (retryCount >= 5) {
355
+ return { data: null, errors: [{ message: 'AniList rate limited' }] };
356
+ }
357
+ const retryAfterHeader = Number.parseInt(response.headers.get('Retry-After') || '', 10);
358
+ const retryAfter = Number.isFinite(retryAfterHeader) && retryAfterHeader >= 0 ? retryAfterHeader : 5;
359
+ const delay = Math.min(retryAfter * 1000, 60000);
360
+ anilistCooldownUntil = Math.max(anilistCooldownUntil, Date.now() + delay);
361
+ refillIntervalMs = Math.min(60000, Math.max(refillIntervalMs, 1500) * 1.15);
362
+ logger_1.default.warn({ retryAfter, retryCount }, 'AniList rate limited, retrying');
363
+ return performAnilistRequest(query, variables, retryCount + 1);
364
+ }
365
+ if (response.status === 403 || response.status >= 500) {
366
+ anilistDownUntil = Date.now() + DOWN_COOLDOWN_MS;
367
+ anilistRateLimited = false;
368
+ }
369
+ else if (response.status >= 200 && response.status < 300) {
370
+ anilistDownUntil = 0;
371
+ anilistRateLimited = false;
372
+ }
373
+ const json = (await response.json());
374
+ if (json.errors && !json.data) {
375
+ logger_1.default.warn({ status: response.status, errors: json.errors }, 'AniList request failed');
376
+ }
377
+ return { data: json.data ?? null, errors: json.errors };
378
+ }
379
+ catch (err) {
380
+ anilistDownUntil = Math.max(anilistDownUntil, Date.now() + NETWORK_COOLDOWN_MS);
381
+ logger_1.default.error({ err }, 'AniList request error');
382
+ return null;
383
+ }
384
+ }
385
+ function anilistRequest(query, variables) {
386
+ if (anilistUnavailable()) {
387
+ return Promise.resolve({ data: null, errors: [{ message: 'AniList unavailable' }] });
388
+ }
389
+ const cacheKey = getAnilistCacheKey(query, variables);
390
+ const existing = inFlightAnilistRequests.get(cacheKey);
391
+ if (existing)
392
+ return existing;
393
+ const request = performAnilistRequest(query, variables);
394
+ inFlightAnilistRequests.set(cacheKey, request);
395
+ request.then(() => inFlightAnilistRequests.delete(cacheKey), () => inFlightAnilistRequests.delete(cacheKey));
396
+ return request;
397
+ }
398
+ const STATIC_GENRES = [
399
+ 'Action',
400
+ 'Adventure',
401
+ 'Comedy',
402
+ 'Drama',
403
+ 'Fantasy',
404
+ 'Horror',
405
+ 'Mahou Shoujo',
406
+ 'Mecha',
407
+ 'Music',
408
+ 'Mystery',
409
+ 'Psychological',
410
+ 'Romance',
411
+ 'Sci-Fi',
412
+ 'Slice of Life',
413
+ 'Sports',
414
+ 'Supernatural',
415
+ 'Thriller',
416
+ ];
417
+ let genreTagCache = null;
418
+ const GENRE_TAG_TTL_MS = 24 * 3600 * 1000;
419
+ async function getGenreTagLists() {
420
+ if (genreTagCache && Date.now() - genreTagCache.at < GENRE_TAG_TTL_MS) {
421
+ return { genres: genreTagCache.genres, tags: genreTagCache.tags, studios: [] };
422
+ }
423
+ try {
424
+ const result = await anilistRequest(`query { GenreCollection TagCollection { name isAdult } }`);
425
+ const genres = result?.data?.GenreCollection?.filter((g) => typeof g === 'string');
426
+ const tags = (result?.data?.TagCollection ?? [])
427
+ .filter((t) => t && !t.isAdult && typeof t.name === 'string')
428
+ .map((t) => t.name);
429
+ if (genres && genres.length > 0) {
430
+ genreTagCache = { genres, tags, at: Date.now() };
431
+ return { genres, tags, studios: [] };
432
+ }
433
+ }
434
+ catch {
435
+ // ignore
436
+ }
437
+ return { genres: STATIC_GENRES, tags: [], studios: [] };
438
+ }
439
+ function stripHtml(input) {
440
+ if (!input)
441
+ return '';
442
+ return input
443
+ .replace(/<br\s*\/?>/gi, '\n')
444
+ .replace(/<[^>]+>/g, '')
445
+ .replace(/&amp;/g, '&')
446
+ .replace(/&quot;/g, '"')
447
+ .replace(/&#0?39;|&apos;/g, "'")
448
+ .replace(/&lt;/g, '<')
449
+ .replace(/&gt;/g, '>')
450
+ .trim();
451
+ }
452
+ function fromAnilistMedia(m) {
453
+ const id = m.id < 0 ? `mal-${Math.abs(m.id)}` : m.id.toString();
454
+ const title = m.title;
455
+ const name = title?.romaji || title?.english || title?.native || 'Unknown';
456
+ const nextEpisodeAirDate = m.nextAiringEpisode?.airingAt != null
457
+ ? new Date(m.nextAiringEpisode.airingAt * 1000).toLocaleDateString('en-US', {
458
+ month: 'short',
459
+ day: 'numeric',
460
+ year: 'numeric',
461
+ timeZone: 'UTC',
462
+ })
463
+ : undefined;
464
+ return {
465
+ _id: id,
466
+ id,
467
+ anilistId: m.id > 0 ? m.id : undefined,
468
+ name,
469
+ englishName: title?.english,
470
+ nativeName: title?.native,
471
+ names: {
472
+ romaji: title?.romaji,
473
+ english: title?.english,
474
+ native: title?.native,
475
+ synonyms: m.synonyms,
476
+ },
477
+ thumbnail: m.coverImage?.extraLarge || m.coverImage?.large || '',
478
+ bannerImage: m.bannerImage || undefined,
479
+ description: stripHtml(m.description),
480
+ genres: (m.genres?.slice(0, 6) || []).map((g) => ({ name: g })),
481
+ score: m.averageScore != null ? Number((m.averageScore / 10).toFixed(1)) : undefined,
482
+ type: m.format,
483
+ status: m.status,
484
+ episodeCount: m.episodes ?? undefined,
485
+ year: m.seasonYear ?? undefined,
486
+ season: m.season
487
+ ? { season: m.season, title: m.season, year: m.seasonYear ?? undefined }
488
+ : undefined,
489
+ nextAiring: m.nextAiringEpisode
490
+ ? {
491
+ episode: m.nextAiringEpisode.episode,
492
+ timeUntilAiring: m.nextAiringEpisode.timeUntilAiring,
493
+ }
494
+ : undefined,
495
+ nextEpisodeAirDate,
496
+ studios: m.studios?.nodes?.map((n) => ({ name: n.name })),
497
+ isAdult: m.isAdult,
498
+ tags: m.tags?.map((t) => ({ name: t.name })),
499
+ averageScore: m.averageScore ?? undefined,
500
+ country: m.countryOfOrigin ?? undefined,
501
+ airedStart: m.startDate
502
+ ? {
503
+ year: m.startDate.year ?? undefined,
504
+ month: m.startDate.month ?? undefined,
505
+ date: m.startDate.day ?? undefined,
506
+ }
507
+ : undefined,
508
+ airedEnd: m.endDate
509
+ ? {
510
+ year: m.endDate.year ?? undefined,
511
+ month: m.endDate.month ?? undefined,
512
+ date: m.endDate.day ?? undefined,
513
+ }
514
+ : undefined,
515
+ };
516
+ }
517
+ async function getLatestReleases(format = 'TV', page = 1, size = 12) {
518
+ if (format === 'ADULT') {
519
+ return getAdultLatest(page, size);
520
+ }
521
+ const now = Math.floor(Date.now() / 1000);
522
+ const thirtyDaysAgo = now - 30 * 86400;
523
+ const needed = page * size;
524
+ const accumulated = [];
525
+ const seen = new Set();
526
+ let anilistPage = 1;
527
+ let apiFailed = false;
528
+ while (accumulated.length < needed && anilistPage <= 10) {
529
+ const query = `
530
+ query ($page: Int, $perPage: Int, $airingAtGt: Int, $airingAtLt: Int) {
531
+ Page(page: $page, perPage: $perPage) {
532
+ pageInfo { hasNextPage }
533
+ airingSchedules(sort: TIME_DESC, airingAt_lesser: $airingAtLt, airingAt_greater: $airingAtGt) {
534
+ id episode airingAt
535
+ media {
536
+ ${mediaFieldsLight()}
537
+ }
538
+ }
539
+ }
540
+ }
541
+ `;
542
+ const result = await anilistRequest(query, { page: anilistPage, perPage: 50, airingAtLt: now, airingAtGt: thirtyDaysAgo });
543
+ if (!result?.data) {
544
+ apiFailed = true;
545
+ break;
546
+ }
547
+ const schedules = result?.data?.Page?.airingSchedules;
548
+ if (!schedules || schedules.length === 0)
549
+ break;
550
+ for (const s of schedules) {
551
+ if (!s.media)
552
+ continue;
553
+ if (format !== 'ALL' && s.media.format !== format)
554
+ continue;
555
+ if (s.media.isAdult)
556
+ continue;
557
+ if (!s.media.id || seen.has(s.media.id))
558
+ continue;
559
+ seen.add(s.media.id);
560
+ accumulated.push(fromAnilistMedia(s.media));
561
+ if (accumulated.length >= needed)
562
+ break;
563
+ }
564
+ if (!result.data?.Page?.pageInfo?.hasNextPage)
565
+ break;
566
+ anilistPage++;
567
+ }
568
+ if (apiFailed) {
569
+ return (await tryFallback(() => (0, mal_1.malLatestReleases)((0, mal_cache_repository_1.malCacheStore)(), format, page, size), () => (0, kitsu_1.kitsuLatestReleases)(format, page, size))).map(fromAnilistMedia);
570
+ }
571
+ const start = (page - 1) * size;
572
+ return accumulated.slice(start, start + size);
573
+ }
574
+ async function getAdultLatest(page, size) {
575
+ const query = `
576
+ query ($page: Int, $perPage: Int) {
577
+ Page(page: $page, perPage: $perPage) {
578
+ pageInfo { total hasNextPage }
579
+ media(sort: [START_DATE_DESC, ID_DESC], type: ANIME, isAdult: true) {
580
+ ${mediaFieldsLight()}
581
+ }
582
+ }
583
+ }
584
+ `;
585
+ const result = await anilistRequest(query, { page, perPage: size });
586
+ if (!result?.data) {
587
+ return [];
588
+ }
589
+ const media = result?.data?.Page?.media;
590
+ if (!media)
591
+ return [];
592
+ return media.map(fromAnilistMedia);
593
+ }
594
+ async function getSeasonal(page = 1, size = 14, format) {
595
+ if (format === 'ADULT') {
596
+ return getAdultLatest(page, size);
597
+ }
598
+ const now = new Date();
599
+ const year = now.getFullYear();
600
+ const month = now.getMonth() + 1;
601
+ const currentSeason = month <= 3 ? 'WINTER' : month <= 6 ? 'SPRING' : month <= 9 ? 'SUMMER' : 'FALL';
602
+ const query = `
603
+ query ($page: Int, $perPage: Int, $season: MediaSeason, $seasonYear: Int, $format: MediaFormat) {
604
+ Page(page: $page, perPage: $perPage) {
605
+ pageInfo { hasNextPage total }
606
+ media(season: $season, seasonYear: $seasonYear, type: ANIME, format: $format, isAdult: false, sort: [POPULARITY_DESC, SCORE_DESC]) {
607
+ ${mediaFieldsLight()}
608
+ }
609
+ }
610
+ }
611
+ `;
612
+ const formatVar = format && format !== 'ALL' ? format.toUpperCase() : undefined;
613
+ const result = await anilistRequest(query, { page, perPage: size, season: currentSeason, seasonYear: year, format: formatVar });
614
+ if (!result?.data) {
615
+ if (anilistUnavailable()) {
616
+ return (await tryFallback(() => (0, mal_1.malSeasonal)((0, mal_cache_repository_1.malCacheStore)(), currentSeason, year, format, page, size), () => (0, kitsu_1.kitsuSeasonal)(currentSeason, year, format, page, size))).map(fromAnilistMedia);
617
+ }
618
+ return [];
619
+ }
620
+ const media = result?.data?.Page?.media;
621
+ if (!media)
622
+ return [];
623
+ return media.map(fromAnilistMedia);
624
+ }
625
+ async function getTrending(page = 1, perPage = 20, sort = 'TRENDING_DESC', status) {
626
+ const media = await fetchAnilistMedia(page, perPage, sort, status);
627
+ if (media && media.length > 0) {
628
+ return media.map(fromAnilistMedia);
629
+ }
630
+ if (anilistUnavailable()) {
631
+ return (await tryFallback(async () => {
632
+ if (status === 'RELEASING')
633
+ return (0, mal_1.malTop)((0, mal_cache_repository_1.malCacheStore)(), 'airing', page);
634
+ switch (sort) {
635
+ case 'POPULARITY_DESC':
636
+ case 'FAVOURITES_DESC':
637
+ return (0, mal_1.malTop)((0, mal_cache_repository_1.malCacheStore)(), 'bypopularity', page);
638
+ case 'SCORE_DESC':
639
+ return (0, mal_1.malTop)((0, mal_cache_repository_1.malCacheStore)(), null, page);
640
+ default:
641
+ return (0, mal_1.malTop)((0, mal_cache_repository_1.malCacheStore)(), 'airing', page);
642
+ }
643
+ }, () => (0, kitsu_1.kitsuTrending)(page, perPage))).map(fromAnilistMedia);
644
+ }
645
+ return [];
646
+ }
647
+ async function fetchAnilistMedia(page = 1, perPage = 20, sort = 'TRENDING_DESC', status) {
648
+ const statusFilter = status ? `status: ${status},` : '';
649
+ const query = `
650
+ query ($page: Int, $perPage: Int) {
651
+ Page(page: $page, perPage: $perPage) {
652
+ media(sort: ${sort}, type: ANIME, isAdult: false, ${statusFilter}) {
653
+ ${mediaFields()}
654
+ }
655
+ }
656
+ }
657
+ `;
658
+ const result = await anilistRequest(query, {
659
+ page,
660
+ perPage,
661
+ });
662
+ if (!result?.data)
663
+ return null;
664
+ return result?.data?.Page?.media ?? [];
665
+ }
666
+ async function getSpotlightBanners(page = 1, perPage = 20) {
667
+ const media = await fetchAnilistMedia(page, perPage);
668
+ const source = media ?? null;
669
+ if (!source) {
670
+ const now = new Date();
671
+ const year = now.getFullYear();
672
+ const month = now.getMonth() + 1;
673
+ const currentSeason = month <= 3 ? 'WINTER' : month <= 6 ? 'SPRING' : month <= 9 ? 'SUMMER' : 'FALL';
674
+ const kitsuMedia = await tryFallback(() => (0, mal_1.malTop)((0, mal_cache_repository_1.malCacheStore)(), 'airing', 1), () => (0, kitsu_1.kitsuSeasonal)(currentSeason, year, 'TV', 1, 20));
675
+ if (!kitsuMedia || kitsuMedia.length === 0) {
676
+ return [];
677
+ }
678
+ const shuffled = kitsuMedia.sort(() => Math.random() - 0.5);
679
+ const results = [];
680
+ for (const m of shuffled) {
681
+ if (results.length >= 6)
682
+ break;
683
+ const artwork = await (0, tmdb_1.findTmdbDefaultBackdrop)({
684
+ english: m.title?.english,
685
+ romaji: m.title?.romaji,
686
+ native: m.title?.native,
687
+ });
688
+ if (artwork) {
689
+ const show = fromAnilistMedia(m);
690
+ results.push({
691
+ ...show,
692
+ bannerImage: artwork.backdrop,
693
+ description: show.description || artwork.overview || '',
694
+ });
695
+ }
696
+ }
697
+ return results;
698
+ }
699
+ const results = [];
700
+ for (const m of source) {
701
+ if (results.length >= 6)
702
+ break;
703
+ const artwork = await (0, tmdb_1.findTmdbDefaultBackdrop)({
704
+ english: m.title?.english,
705
+ romaji: m.title?.romaji,
706
+ native: m.title?.native,
707
+ });
708
+ if (artwork) {
709
+ const show = fromAnilistMedia(m);
710
+ results.push({
711
+ ...show,
712
+ bannerImage: artwork.backdrop,
713
+ description: show.description || artwork.overview || '',
714
+ });
715
+ }
716
+ }
717
+ return results;
718
+ }
719
+ function parseMalId(id) {
720
+ if (typeof id === 'number')
721
+ return id < 0 ? Math.abs(id) : null;
722
+ const prefixed = /^mal-(\d+)$/i.exec(id.trim());
723
+ if (prefixed)
724
+ return parseInt(prefixed[1], 10);
725
+ if (/^-\d+$/.test(id.trim()))
726
+ return Math.abs(parseInt(id, 10));
727
+ return null;
728
+ }
729
+ function hasPoster(show) {
730
+ return !!show?.thumbnail && show.thumbnail.trim() !== '';
731
+ }
732
+ function posterFromMedia(m) {
733
+ const url = m?.coverImage?.extraLarge ||
734
+ m?.coverImage?.large ||
735
+ m?.coverImage?.medium ||
736
+ m?.coverImage?.original ||
737
+ m?.coverImage?.small ||
738
+ m?.coverImage?.tiny;
739
+ return url && url.trim() !== '' ? url : null;
740
+ }
741
+ function titleWords(s) {
742
+ return new Set(s
743
+ .toLowerCase()
744
+ .normalize('NFD')
745
+ .replace(/[\u0300-\u036f]/g, '')
746
+ .split(/[^a-z0-9]+/)
747
+ .filter((w) => w.length >= 2));
748
+ }
749
+ function titleSimilarity(a, b) {
750
+ if (!a.trim() || !b.trim())
751
+ return 0;
752
+ const an = a.trim().toLowerCase();
753
+ const bn = b.trim().toLowerCase();
754
+ if (an === bn || an.includes(bn) || bn.includes(an))
755
+ return 1;
756
+ const wa = titleWords(a);
757
+ const wb = titleWords(b);
758
+ if (wa.size === 0 || wb.size === 0)
759
+ return 0;
760
+ const overlap = [...wa].filter((w) => wb.has(w)).length;
761
+ return overlap / Math.min(wa.size, wb.size);
762
+ }
763
+ function titlesMatchAny(candidate, expected) {
764
+ if (!candidate)
765
+ return false;
766
+ return expected.some((t) => titleSimilarity(candidate, t) >= 0.6);
767
+ }
768
+ async function findPosterByTitle(title, expected) {
769
+ const clean = title?.trim();
770
+ if (!clean)
771
+ return null;
772
+ const expectedTitles = (expected?.titles ?? []).filter((t) => t.trim() !== '');
773
+ try {
774
+ const { entries } = await (0, mal_1.malSearchAnime)((0, mal_cache_repository_1.malCacheStore)(), clean, 1);
775
+ for (const e of entries) {
776
+ if (!e.imageUrl || e.imageUrl.trim() === '')
777
+ continue;
778
+ if (expected?.malId && e.idMal === expected.malId)
779
+ return e.imageUrl;
780
+ if (expected && titlesMatchAny(e.title, expectedTitles))
781
+ return e.imageUrl;
782
+ if (!expected)
783
+ return e.imageUrl;
784
+ }
785
+ }
786
+ catch {
787
+ // ignore, try Kitsu next
788
+ }
789
+ try {
790
+ const kitsu = await (0, kitsu_1.kitsuSearchAnime)({ query: clean, page: 1, perPage: 5 });
791
+ for (const m of kitsu) {
792
+ const url = posterFromMedia(m);
793
+ if (!url)
794
+ continue;
795
+ if (expected?.anilistId && m.id === expected.anilistId)
796
+ return url;
797
+ if (expected?.malId && m.idMal === expected.malId)
798
+ return url;
799
+ }
800
+ }
801
+ catch {
802
+ // ignore
803
+ }
804
+ return null;
805
+ }
806
+ async function withPosterBackfill(show, cacheKey, expected) {
807
+ if (!show)
808
+ return null;
809
+ if (hasPoster(show)) {
810
+ setCachedAnilist(cacheKey, show);
811
+ return show;
812
+ }
813
+ const candidates = [show.names?.english, show.englishName, show.name, show.names?.romaji]
814
+ .filter((t) => !!t && t.trim().length >= 4)
815
+ .filter((t, i, a) => a.findIndex((x) => x.toLowerCase() === t.toLowerCase()) === i);
816
+ const exp = expected ?? { titles: [] };
817
+ if (exp.titles.length === 0)
818
+ exp.titles = [...candidates];
819
+ if (show.names?.native)
820
+ exp.titles.push(show.names.native);
821
+ const seen = new Set();
822
+ for (const title of candidates) {
823
+ const key = title.toLowerCase();
824
+ if (seen.has(key))
825
+ continue;
826
+ seen.add(key);
827
+ try {
828
+ const poster = await findPosterByTitle(title, exp);
829
+ if (poster) {
830
+ show.thumbnail = poster;
831
+ setCachedAnilist(cacheKey, show);
832
+ return show;
833
+ }
834
+ }
835
+ catch {
836
+ // ignore
837
+ }
838
+ }
839
+ return show;
840
+ }
841
+ async function backfillKitsuShowFromMal(show, malId) {
842
+ try {
843
+ const d = await (0, mal_1.malAnimeDetail)((0, mal_cache_repository_1.malCacheStore)(), malId);
844
+ if (d.detail?.imageUrl)
845
+ show.thumbnail = d.detail.imageUrl;
846
+ try {
847
+ (0, mal_cache_repository_1.malCacheStore)().put(`${mal_1.EN_TITLE_CACHE_PREFIX}${malId}`, (d.detail?.titleEnglish ?? '').trim(), 7 * 24 * 3600);
848
+ }
849
+ catch {
850
+ // ignore
851
+ }
852
+ const titleEnglish = d.detail?.titleEnglish?.trim();
853
+ if (titleEnglish &&
854
+ (!show.names?.english ||
855
+ show.names.english.trim() === '' ||
856
+ show.names.english === show.names?.romaji)) {
857
+ show.names = { ...show.names, english: titleEnglish };
858
+ show.englishName = titleEnglish;
859
+ }
860
+ const titleJapanese = d.detail?.titleJapanese?.trim();
861
+ if (titleJapanese && !show.names?.native) {
862
+ show.names = { ...show.names, native: titleJapanese };
863
+ show.nativeName = titleJapanese;
864
+ }
865
+ if ((!show.name || show.name === 'Unknown') && (show.names?.english || show.names?.native)) {
866
+ show.name = show.names.english ?? show.names.native ?? show.name;
867
+ }
868
+ }
869
+ catch {
870
+ // ignore
871
+ }
872
+ }
873
+ async function getShowMetaById(id) {
874
+ const malId = parseMalId(id);
875
+ if (malId)
876
+ return getShowMetaByMalId(malId);
877
+ const numericId = parseInt(id);
878
+ if (isNaN(numericId))
879
+ return null;
880
+ if (numericId < 0)
881
+ return getShowMetaByMalId(Math.abs(numericId));
882
+ const fields = mediaFields();
883
+ const cacheKey = `meta:v2:${id}`;
884
+ const legacyCached = getCachedAnilist(`meta:${id}`);
885
+ const cached = getCachedAnilist(cacheKey) ?? legacyCached;
886
+ if (cached && hasPoster(cached)) {
887
+ setCachedAnilist(cacheKey, cached);
888
+ return cached;
889
+ }
890
+ const expected = { anilistId: numericId, titles: [] };
891
+ const query = `query ($id: Int) { Media(id: $id, type: ANIME) { ${fields} } }`;
892
+ const queryMal = `query ($id: Int) { Media(idMal: $id, type: ANIME) { ${fields} } }`;
893
+ const byId = await anilistRequest(query, { id: numericId });
894
+ if (byId?.data?.Media) {
895
+ const show = fromAnilistMedia(byId.data.Media);
896
+ return withPosterBackfill(show, cacheKey, expected);
897
+ }
898
+ const byMal = await anilistRequest(queryMal, { id: numericId });
899
+ if (byMal?.data?.Media) {
900
+ const show = fromAnilistMedia(byMal.data.Media);
901
+ return withPosterBackfill(show, cacheKey, expected);
902
+ }
903
+ const apiFailed = !byId?.data && !byMal?.data;
904
+ if (apiFailed && anilistUnavailable()) {
905
+ const fb = (await (0, kitsu_1.kitsuMetaByAnilistId)(numericId)) ?? (await (0, kitsu_1.kitsuMetaByMalId)(numericId));
906
+ if (fb) {
907
+ const show = fromAnilistMedia(fb);
908
+ if (hasPoster(show)) {
909
+ if (fb.idMal) {
910
+ await backfillKitsuShowFromMal(show, fb.idMal);
911
+ }
912
+ setCachedAnilist(cacheKey, show);
913
+ return show;
914
+ }
915
+ return withPosterBackfill(show, cacheKey, expected);
916
+ }
917
+ }
918
+ if (cached && !hasPoster(cached)) {
919
+ return withPosterBackfill(cached, cacheKey, expected);
920
+ }
921
+ return null;
922
+ }
923
+ async function getShowMetaByMalId(malId) {
924
+ const absMal = Math.abs(malId);
925
+ if (!absMal)
926
+ return null;
927
+ const cacheKey = `meta:v2:-${absMal}`;
928
+ const cached = getCachedAnilist(cacheKey) ?? getCachedAnilist(`meta:-${absMal}`);
929
+ const expected = { malId: absMal, titles: [] };
930
+ if (cached && cached.anilistId != null && cached.anilistId > 0 && hasPoster(cached)) {
931
+ setCachedAnilist(cacheKey, cached);
932
+ return cached;
933
+ }
934
+ const anilistUp = !anilistUnavailable();
935
+ if (cached && !anilistUp && hasPoster(cached)) {
936
+ setCachedAnilist(cacheKey, cached);
937
+ return cached;
938
+ }
939
+ if (anilistUp) {
940
+ try {
941
+ const byMal = await anilistRequest(`query ($id: Int) { Media(idMal: $id, type: ANIME) { ${mediaFields()} } }`, { id: absMal });
942
+ if (byMal?.data?.Media) {
943
+ const show = fromAnilistMedia(byMal.data.Media);
944
+ return withPosterBackfill(show, cacheKey, expected);
945
+ }
946
+ }
947
+ catch {
948
+ // ignore
949
+ }
950
+ }
951
+ try {
952
+ const fb = await (0, kitsu_1.kitsuMetaByMalId)(absMal);
953
+ if (fb) {
954
+ const show = fromAnilistMedia(fb);
955
+ await backfillKitsuShowFromMal(show, absMal);
956
+ if (hasPoster(show)) {
957
+ setCachedAnilist(cacheKey, show);
958
+ return show;
959
+ }
960
+ return withPosterBackfill(show, cacheKey, expected);
961
+ }
962
+ }
963
+ catch {
964
+ // ignore
965
+ }
966
+ try {
967
+ const d = await (0, mal_1.malAnimeDetail)((0, mal_cache_repository_1.malCacheStore)(), absMal);
968
+ if (d.detail) {
969
+ const show = fromAnilistMedia((0, mal_1.toAnilistDetailMedia)(d.detail));
970
+ return withPosterBackfill(show, cacheKey, expected);
971
+ }
972
+ }
973
+ catch {
974
+ // ignore
975
+ }
976
+ if (cached && !hasPoster(cached)) {
977
+ return withPosterBackfill(cached, cacheKey, expected);
978
+ }
979
+ logger_1.default.warn({ malId: absMal }, 'MAL id unresolvable on all metadata providers');
980
+ return null;
981
+ }
982
+ async function batchGetShowStatuses(ids) {
983
+ const result = new Map();
984
+ if (ids.length === 0)
985
+ return result;
986
+ if (!isAnilistRateLimited()) {
987
+ const BATCH = 10;
988
+ for (let i = 0; i < ids.length; i += BATCH) {
989
+ const batch = ids.slice(i, i + BATCH);
990
+ const aliases = batch
991
+ .map((id, idx) => `a${idx}: Media(id: $id${idx}, type: ANIME) { id status }`)
992
+ .join('\n');
993
+ const variables = {};
994
+ batch.forEach((id, idx) => {
995
+ variables[`id${idx}`] = id;
996
+ });
997
+ try {
998
+ const resp = await anilistRequest(`query (${batch.map((_, idx) => `$id${idx}: Int`).join(', ')}) { ${aliases} }`, variables);
999
+ for (let j = 0; j < batch.length; j++) {
1000
+ const alias = `a${j}`;
1001
+ const media = resp?.data?.[alias];
1002
+ if (media?.status)
1003
+ result.set(media.id, media.status);
1004
+ }
1005
+ }
1006
+ catch {
1007
+ break;
1008
+ }
1009
+ }
1010
+ if (result.size === ids.length)
1011
+ return result;
1012
+ }
1013
+ const missing = ids.filter((id) => !result.has(id));
1014
+ if (missing.length > 0) {
1015
+ try {
1016
+ const { kitsuBatchGetStatuses } = await Promise.resolve().then(() => __importStar(require('./kitsu')));
1017
+ const kitsuStatuses = await kitsuBatchGetStatuses(missing);
1018
+ for (const [id, status] of kitsuStatuses) {
1019
+ result.set(id, status);
1020
+ }
1021
+ }
1022
+ catch {
1023
+ // Kitsu fallback failed
1024
+ }
1025
+ }
1026
+ return result;
1027
+ }
1028
+ async function getAnilistEpisodes(id) {
1029
+ const malId = parseMalId(id);
1030
+ if (malId)
1031
+ return getEpisodesByMalId(malId);
1032
+ const numericId = parseInt(id);
1033
+ if (isNaN(numericId))
1034
+ return [];
1035
+ if (numericId < 0)
1036
+ return getEpisodesByMalId(Math.abs(numericId));
1037
+ const cacheKey = `eps:${id}`;
1038
+ const cached = getCachedAnilist(cacheKey);
1039
+ if (cached)
1040
+ return cached;
1041
+ const subquery = `id episodes status nextAiringEpisode { episode airingAt } airingSchedule(perPage: 100) { nodes { episode airingAt } }`;
1042
+ const query = `query ($id: Int) { Media(id: $id, type: ANIME) { ${subquery} } }`;
1043
+ const queryMal = `query ($id: Int) { Media(idMal: $id, type: ANIME) { ${subquery} } }`;
1044
+ const byId = await anilistRequest(query, { id: numericId });
1045
+ const media = byId?.data?.Media;
1046
+ if (!media) {
1047
+ const byMal = await anilistRequest(queryMal, { id: numericId });
1048
+ if (!byMal?.data) {
1049
+ if (anilistUnavailable()) {
1050
+ const episodes = await (0, kitsu_1.kitsuEpisodes)(numericId, numericId);
1051
+ if (episodes.length > 0) {
1052
+ setCachedAnilist(cacheKey, episodes);
1053
+ return episodes;
1054
+ }
1055
+ }
1056
+ return [];
1057
+ }
1058
+ if (!byMal?.data?.Media)
1059
+ return [];
1060
+ const episodes = extractEpisodes(byMal.data.Media);
1061
+ setCachedAnilist(cacheKey, episodes);
1062
+ return episodes;
1063
+ }
1064
+ const episodes = extractEpisodes(media);
1065
+ setCachedAnilist(cacheKey, episodes);
1066
+ return episodes;
1067
+ }
1068
+ async function getEpisodesByMalId(malId) {
1069
+ const cacheKey = `eps:-${malId}`;
1070
+ const cached = getCachedAnilist(cacheKey);
1071
+ if (cached)
1072
+ return cached;
1073
+ try {
1074
+ const episodes = await (0, kitsu_1.kitsuEpisodes)(0, malId);
1075
+ if (episodes.length > 0) {
1076
+ setCachedAnilist(cacheKey, episodes);
1077
+ return episodes;
1078
+ }
1079
+ }
1080
+ catch {
1081
+ // ignore
1082
+ }
1083
+ try {
1084
+ const d = await (0, mal_1.malAnimeDetail)((0, mal_cache_repository_1.malCacheStore)(), malId);
1085
+ const total = d.detail?.episodes;
1086
+ if (total && total > 0 && total <= 2000) {
1087
+ const episodes = Array.from({ length: total }, (_, i) => (i + 1).toString());
1088
+ setCachedAnilist(cacheKey, episodes);
1089
+ return episodes;
1090
+ }
1091
+ }
1092
+ catch {
1093
+ // ignore
1094
+ }
1095
+ return [];
1096
+ }
1097
+ function extractEpisodes(result) {
1098
+ const { status, episodes: total, nextAiringEpisode, airingSchedule } = result;
1099
+ if (status === 'FINISHED' && total) {
1100
+ return Array.from({ length: total }, (_, i) => (i + 1).toString());
1101
+ }
1102
+ if (status === 'RELEASING' && nextAiringEpisode?.episode) {
1103
+ const airedCount = nextAiringEpisode.episode - 1;
1104
+ if (airedCount > 0) {
1105
+ return Array.from({ length: airedCount }, (_, i) => (i + 1).toString());
1106
+ }
1107
+ }
1108
+ if (airingSchedule?.nodes) {
1109
+ const now = Date.now() / 1000;
1110
+ const aired = airingSchedule.nodes.filter((n) => n.airingAt < now);
1111
+ if (aired.length > 0) {
1112
+ const max = Math.max(...aired.map((n) => n.episode));
1113
+ return Array.from({ length: max }, (_, i) => (i + 1).toString());
1114
+ }
1115
+ }
1116
+ if (total) {
1117
+ return Array.from({ length: total }, (_, i) => (i + 1).toString());
1118
+ }
1119
+ return [];
1120
+ }
1121
+ async function searchAnilistByTitle(title) {
1122
+ const query = `
1123
+ query ($search: String) {
1124
+ Page(page: 1, perPage: 5) {
1125
+ media(search: $search, type: ANIME) {
1126
+ id
1127
+ title { romaji english native }
1128
+ }
1129
+ }
1130
+ }
1131
+ `;
1132
+ const normalizedTitle = title.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
1133
+ const words = normalizedTitle.split(/\s+/).filter((word) => word.length >= 3);
1134
+ const searchTerms = [
1135
+ title,
1136
+ normalizedTitle,
1137
+ normalizedTitle.replace(/:/g, ''),
1138
+ words.slice(0, 5).join(' '),
1139
+ words.slice(0, 4).join(' '),
1140
+ words.slice(0, 3).join(' '),
1141
+ words.slice(0, 2).join(' ').replace(/:/g, ''),
1142
+ normalizedTitle.replace(/\s*\d+(?:st|nd|rd|th)?\s*Season\s*\d*/gi, '').trim(),
1143
+ ].filter((t, i, a) => t && a.indexOf(t) === i);
1144
+ let media;
1145
+ let anyFailed = false;
1146
+ for (const searchTerm of searchTerms) {
1147
+ const result = await anilistRequest(query, { search: searchTerm });
1148
+ if (!result?.data) {
1149
+ anyFailed = true;
1150
+ break;
1151
+ }
1152
+ media = result?.data?.Page?.media;
1153
+ if (media?.length)
1154
+ break;
1155
+ }
1156
+ if (!media || media.length === 0) {
1157
+ if (anyFailed && anilistUnavailable()) {
1158
+ const fb = await (0, kitsu_1.kitsuSearchAnime)({ query: title, page: 1, perPage: 5 });
1159
+ if (fb.length > 0) {
1160
+ const best = fb[0];
1161
+ return { id: best.id, title: best.title ?? {} };
1162
+ }
1163
+ }
1164
+ return null;
1165
+ }
1166
+ const lowerTitle = title.toLowerCase();
1167
+ const exactMatch = media.find((m) => m.title?.romaji?.toLowerCase() === lowerTitle ||
1168
+ m.title?.english?.toLowerCase() === lowerTitle ||
1169
+ m.title?.native === title);
1170
+ if (exactMatch) {
1171
+ return { id: exactMatch.id, title: exactMatch.title };
1172
+ }
1173
+ const inputWords = new Set(normalizedTitle
1174
+ .toLowerCase()
1175
+ .split(/\s+/)
1176
+ .filter((w) => w.length >= 2));
1177
+ let best = media[0];
1178
+ let bestScore = 0;
1179
+ for (const m of media) {
1180
+ const text = (m.title?.romaji || m.title?.english || '').toLowerCase();
1181
+ const overlap = text.split(/\s+/).filter((w) => inputWords.has(w)).length;
1182
+ if (overlap > bestScore) {
1183
+ bestScore = overlap;
1184
+ best = m;
1185
+ }
1186
+ }
1187
+ return { id: best.id, title: best.title };
1188
+ }
1189
+ async function getAiredEpisodesForShows(ids, startDate, endDate) {
1190
+ const dayStart = Math.floor(startDate.getTime() / 1000);
1191
+ const dayEnd = Math.floor(endDate.getTime() / 1000);
1192
+ const cacheKey = `aired:${dayStart}:${dayEnd}:${Array.from(ids)
1193
+ .sort((a, b) => a - b)
1194
+ .join(',')}`;
1195
+ const cached = getCachedAiredEpisodes(cacheKey);
1196
+ if (cached)
1197
+ return cached;
1198
+ try {
1199
+ const results = await (0, anischedule_1.getAiredEpisodesFromAniScheduleFeed)(ids, dayStart, dayEnd);
1200
+ setCachedAiredEpisodes(cacheKey, results);
1201
+ return results;
1202
+ }
1203
+ catch (e) {
1204
+ logger_1.default.warn({ err: e }, 'AniSchedule episode feed failed, falling back to AniList aired episodes');
1205
+ }
1206
+ if (isAnilistRateLimited()) {
1207
+ return [];
1208
+ }
1209
+ const now = Math.floor(Date.now() / 1000);
1210
+ const results = [];
1211
+ const seen = new Set();
1212
+ const BATCH = 5;
1213
+ for (let i = 0; i < ids.length; i += BATCH) {
1214
+ const batch = ids.slice(i, i + BATCH);
1215
+ const aliases = batch
1216
+ .map((id, idx) => `a${idx}: Media(id: $id${idx}, type: ANIME) { id airingSchedule(perPage: 100) { nodes { episode airingAt } } }`)
1217
+ .join('\n');
1218
+ const variables = {};
1219
+ batch.forEach((id, idx) => {
1220
+ variables[`id${idx}`] = id;
1221
+ });
1222
+ const result = await anilistRequest(`query (${batch.map((_, idx) => `$id${idx}: Int`).join(', ')}) { ${aliases} }`, variables);
1223
+ for (let j = 0; j < batch.length; j++) {
1224
+ const alias = `a${j}`;
1225
+ const media = result?.data?.[alias];
1226
+ if (!media?.airingSchedule?.nodes)
1227
+ continue;
1228
+ for (const node of media.airingSchedule.nodes) {
1229
+ if (node.airingAt >= dayStart && node.airingAt <= dayEnd && node.airingAt <= now) {
1230
+ const key = `${media.id}:${node.episode}`;
1231
+ if (!seen.has(key)) {
1232
+ seen.add(key);
1233
+ results.push({ mediaId: media.id, episode: node.episode, airingAt: node.airingAt });
1234
+ }
1235
+ }
1236
+ }
1237
+ }
1238
+ }
1239
+ setCachedAiredEpisodes(cacheKey, results);
1240
+ return results;
1241
+ }
1242
+ async function getAiredEpisodesForShow(id, startDate, endDate) {
1243
+ const dayStart = Math.floor(startDate.getTime() / 1000);
1244
+ const dayEnd = Math.floor(endDate.getTime() / 1000);
1245
+ const query = `
1246
+ query ($id: Int, $dayStart: Int, $dayEnd: Int) {
1247
+ Media(id: $id, type: ANIME) {
1248
+ id
1249
+ airingSchedule(airingAt_greater: $dayStart, airingAt_lesser: $dayEnd) {
1250
+ episode
1251
+ airingAt
1252
+ }
1253
+ }
1254
+ }
1255
+ `;
1256
+ const result = await anilistRequest(query, { id, dayStart, dayEnd });
1257
+ const schedules = result?.data?.Media?.airingSchedule;
1258
+ if (!schedules || schedules.length === 0)
1259
+ return [];
1260
+ const now = Math.floor(Date.now() / 1000);
1261
+ return schedules.filter((s) => s.airingAt <= now).map((s) => s.episode);
1262
+ }
1263
+ async function getSchedule(date, format, adult = false) {
1264
+ if (format === 'ADULT') {
1265
+ adult = true;
1266
+ format = undefined;
1267
+ }
1268
+ try {
1269
+ const result = await (0, anischedule_1.getScheduleFromAniSchedule)(date, format, adult);
1270
+ const shows = result.entries
1271
+ .filter((e) => e.show)
1272
+ .map((e) => (0, anischedule_1.withScheduleFields)(e.show, e.episode, e.airingAt));
1273
+ if (result.missingMeta.length > 0) {
1274
+ for (const entry of result.missingMeta) {
1275
+ try {
1276
+ const meta = await getShowMetaById(String(entry.id));
1277
+ if (!meta)
1278
+ continue;
1279
+ if (adult ? !meta.isAdult : meta.isAdult)
1280
+ continue;
1281
+ shows.push((0, anischedule_1.withScheduleFields)(meta, entry.episode, entry.airingAt));
1282
+ }
1283
+ catch {
1284
+ // keep going without this show
1285
+ }
1286
+ }
1287
+ }
1288
+ return shows;
1289
+ }
1290
+ catch (e) {
1291
+ logger_1.default.warn({ err: e }, 'AniSchedule schedule failed, falling back to AniList');
1292
+ if (isAnilistRateLimited())
1293
+ return [];
1294
+ return getScheduleFromAnilist(date, format, adult);
1295
+ }
1296
+ }
1297
+ async function getScheduleFromAnilist(date, format, adult = false) {
1298
+ const dayStart = Math.floor(date.getTime() / 1000);
1299
+ const dayEnd = dayStart + 86400;
1300
+ const query = `
1301
+ query ($dayStart: Int, $dayEnd: Int) {
1302
+ Page(perPage: 50) {
1303
+ airingSchedules(airingAt_greater: $dayStart, airingAt_lesser: $dayEnd) {
1304
+ episode
1305
+ airingAt
1306
+ media {
1307
+ ${mediaFields()}
1308
+ }
1309
+ }
1310
+ }
1311
+ }
1312
+ `;
1313
+ const result = await anilistRequest(query, { dayStart, dayEnd });
1314
+ const schedules = result?.data?.Page?.airingSchedules;
1315
+ if (!schedules || schedules.length === 0)
1316
+ return [];
1317
+ const seen = new Set();
1318
+ const results = [];
1319
+ const now = Math.floor(Date.now() / 1000);
1320
+ for (const entry of schedules) {
1321
+ const media = entry.media;
1322
+ if (!media)
1323
+ continue;
1324
+ if (format && format !== 'ALL' && media.format !== format)
1325
+ continue;
1326
+ const isAdultShow = !!media.isAdult;
1327
+ if (adult ? !isAdultShow : isAdultShow)
1328
+ continue;
1329
+ const key = media.idMal ?? media.id;
1330
+ if (seen.has(key))
1331
+ continue;
1332
+ seen.add(key);
1333
+ const show = fromAnilistMedia(media);
1334
+ show.episodeNumber = entry.episode;
1335
+ show.aired = entry.airingAt <= now;
1336
+ show.nextAiring = {
1337
+ episode: entry.episode,
1338
+ timeUntilAiring: entry.airingAt - now,
1339
+ };
1340
+ show.nextEpisodeAirDate = new Date(entry.airingAt * 1000).toLocaleDateString('en-US', {
1341
+ month: 'short',
1342
+ day: 'numeric',
1343
+ year: 'numeric',
1344
+ timeZone: 'UTC',
1345
+ });
1346
+ show.airTime = new Date(entry.airingAt * 1000).toLocaleTimeString('en-US', {
1347
+ hour: '2-digit',
1348
+ minute: '2-digit',
1349
+ timeZone: 'UTC',
1350
+ });
1351
+ results.push(show);
1352
+ }
1353
+ return results;
1354
+ }
1355
+ async function searchAnilist(options = {}) {
1356
+ const { query, page = 1, perPage = 14, format, status, season, seasonYear, countryOfOrigin, genre, genre_not_in, tag_not_in, averageScore_greater, episodes_greater, isAdult, sort, } = options;
1357
+ const searchVars = {
1358
+ page,
1359
+ perPage,
1360
+ };
1361
+ if (query)
1362
+ searchVars.search = query;
1363
+ if (format && format !== 'ALL') {
1364
+ if (format === 'ADULT') {
1365
+ searchVars.isAdult = true;
1366
+ }
1367
+ else {
1368
+ searchVars.format = format.toUpperCase();
1369
+ }
1370
+ }
1371
+ if (status)
1372
+ searchVars.status = status;
1373
+ if (season && season !== 'ALL')
1374
+ searchVars.season = season.toUpperCase();
1375
+ if (seasonYear)
1376
+ searchVars.seasonYear = seasonYear;
1377
+ if (countryOfOrigin && countryOfOrigin !== 'ALL')
1378
+ searchVars.countryOfOrigin = countryOfOrigin;
1379
+ if (genre)
1380
+ searchVars.genre = genre;
1381
+ if (genre_not_in && genre_not_in.length > 0)
1382
+ searchVars.genre_not_in = genre_not_in;
1383
+ if (tag_not_in && tag_not_in.length > 0)
1384
+ searchVars.tag_not_in = tag_not_in;
1385
+ if (averageScore_greater)
1386
+ searchVars.averageScore_greater = averageScore_greater;
1387
+ if (episodes_greater)
1388
+ searchVars.episodes_greater = episodes_greater;
1389
+ if (isAdult !== undefined)
1390
+ searchVars.isAdult = isAdult;
1391
+ if (sort)
1392
+ searchVars.sort = [sort];
1393
+ const queryStr = `
1394
+ query ($page: Int, $perPage: Int, $search: String, $format: MediaFormat, $status: MediaStatus, $season: MediaSeason, $seasonYear: Int, $countryOfOrigin: CountryCode, $genre: String, $genre_not_in: [String], $tag_not_in: [String], $averageScore_greater: Int, $episodes_greater: Int, $isAdult: Boolean, $sort: [MediaSort]) {
1395
+ Page(page: $page, perPage: $perPage) {
1396
+ pageInfo { hasNextPage total }
1397
+ media(
1398
+ search: $search,
1399
+ type: ANIME,
1400
+ format: $format,
1401
+ status: $status,
1402
+ season: $season,
1403
+ seasonYear: $seasonYear,
1404
+ countryOfOrigin: $countryOfOrigin,
1405
+ genre: $genre,
1406
+ genre_not_in: $genre_not_in,
1407
+ tag_not_in: $tag_not_in,
1408
+ averageScore_greater: $averageScore_greater,
1409
+ episodes_greater: $episodes_greater,
1410
+ isAdult: $isAdult,
1411
+ sort: $sort
1412
+ ) {
1413
+ ${mediaFieldsLight()}
1414
+ }
1415
+ }
1416
+ }
1417
+ `;
1418
+ const result = await anilistRequest(queryStr, searchVars);
1419
+ if (!result?.data) {
1420
+ if (anilistUnavailable()) {
1421
+ const malSearch = async () => {
1422
+ const store = (0, mal_cache_repository_1.malCacheStore)();
1423
+ let fb = await (0, mal_1.malSearchMedia)(store, {
1424
+ query,
1425
+ page,
1426
+ perPage,
1427
+ format: format && format !== 'ALL' && format !== 'ADULT' ? format : undefined,
1428
+ status,
1429
+ season,
1430
+ seasonYear,
1431
+ genre,
1432
+ genre_not_in,
1433
+ averageScore_greater,
1434
+ episodes_greater,
1435
+ isAdult: format === 'ADULT' ? true : isAdult,
1436
+ sort,
1437
+ });
1438
+ if (fb.length > 0) {
1439
+ try {
1440
+ const hentai = await (0, mal_1.malSearchMedia)(store, {
1441
+ query,
1442
+ page,
1443
+ perPage,
1444
+ format: format && format !== 'ALL' && format !== 'ADULT' ? format : undefined,
1445
+ status,
1446
+ season,
1447
+ seasonYear,
1448
+ genre,
1449
+ averageScore_greater,
1450
+ episodes_greater,
1451
+ sort,
1452
+ isAdult: true,
1453
+ }, false);
1454
+ if (hentai.length > 0) {
1455
+ const hentaiIds = new Set(hentai.map((m) => m.id));
1456
+ fb = fb.filter((m) => !hentaiIds.has(m.id));
1457
+ }
1458
+ }
1459
+ catch {
1460
+ // ignore
1461
+ }
1462
+ }
1463
+ return fb;
1464
+ };
1465
+ const fb = await tryFallback(malSearch, () => (0, kitsu_1.kitsuSearchAnime)({
1466
+ query,
1467
+ page,
1468
+ perPage,
1469
+ format: format && format !== 'ALL' && format !== 'ADULT' ? format : undefined,
1470
+ status,
1471
+ season,
1472
+ seasonYear,
1473
+ genre,
1474
+ genre_not_in,
1475
+ averageScore_greater,
1476
+ episodes_greater,
1477
+ isAdult: format === 'ADULT' ? true : isAdult,
1478
+ sort,
1479
+ }));
1480
+ try {
1481
+ await (0, mal_1.enrichEnglishFromMalDetail)((0, mal_cache_repository_1.malCacheStore)(), fb);
1482
+ }
1483
+ catch {
1484
+ // ignore
1485
+ }
1486
+ let results = mapFallbackResults(fb);
1487
+ if (results.length === 0 && query?.trim()) {
1488
+ try {
1489
+ const mal = await (0, mal_1.malSearchAnime)((0, mal_cache_repository_1.malCacheStore)(), query, page);
1490
+ const media = mal.entries.map((e) => (0, mal_1.toAnilistSearchMedia)(e));
1491
+ try {
1492
+ await (0, mal_1.enrichEnglishFromMalDetail)((0, mal_cache_repository_1.malCacheStore)(), media);
1493
+ }
1494
+ catch {
1495
+ // ignore
1496
+ }
1497
+ results = media.map((m) => fromAnilistMedia(m));
1498
+ }
1499
+ catch {
1500
+ // ignore
1501
+ }
1502
+ }
1503
+ return results;
1504
+ }
1505
+ return [];
1506
+ }
1507
+ const media = result?.data?.Page?.media;
1508
+ if (!media || media.length === 0)
1509
+ return [];
1510
+ const seen = new Set();
1511
+ const results = [];
1512
+ for (const m of media) {
1513
+ if (!m.id || seen.has(m.id))
1514
+ continue;
1515
+ seen.add(m.id);
1516
+ results.push(fromAnilistMedia(m));
1517
+ }
1518
+ return results;
1519
+ }