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