@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.
- package/README.md +1 -1
- package/client/dist/assets/{AnimeCard-C3P9zWi9.js → AnimeCard-DraA_TF9.js} +1 -1
- package/client/dist/assets/{AnimeInfo-Et6687Tl.js → AnimeInfo-BJ7pzs-P.js} +1 -1
- package/client/dist/assets/{AnimeInfoPage-DX4eNnlH.js → AnimeInfoPage-B_esUr5C.js} +2 -2
- package/client/dist/assets/{Asmr-CCdEWrFi.js → Asmr-Ckuh2xfW.js} +1 -1
- package/client/dist/assets/{AsmrResetProgressModal-DgEWBJOw.js → AsmrResetProgressModal-BS6UFJN4.js} +1 -1
- package/client/dist/assets/{AvSyncCalibrator-CQ2fjf2E.js → AvSyncCalibrator-DwHCd9yB.js} +1 -1
- package/client/dist/assets/{Home-DUkUTTTy.js → Home-DuM1EuvN.js} +1 -1
- package/client/dist/assets/{Insights-QCh4xW12.js → Insights-D3Pd9c9V.js} +1 -1
- package/client/dist/assets/{LibraryListPage-BoTxRqHz.js → LibraryListPage-Dm2yVn0-.js} +1 -1
- package/client/dist/assets/{ListeningList-DguKjI_t.js → ListeningList-D-zt6EXZ.js} +1 -1
- package/client/dist/assets/{Manga-HydR-L7o.js → Manga-JwprMKBk.js} +1 -1
- package/client/dist/assets/{MangaInfoPage-CBakEI9a.js → MangaInfoPage-DaeyJl2p.js} +1 -1
- package/client/dist/assets/{MangaReadPage-DwAR3iPy.js → MangaReadPage-COg0fHI9.js} +1 -1
- package/client/dist/assets/{MangaResetProgressModal-DGzl8vsO.js → MangaResetProgressModal-kguXv0_M.js} +1 -1
- package/client/dist/assets/{Mature-Pi0TxKuF.js → Mature-IHDaasrs.js} +1 -1
- package/client/dist/assets/{MediaCard--cuFxOCS.js → MediaCard-CpwsrXo5.js} +1 -1
- package/client/dist/assets/{Music-CQpyXTLH.js → Music-Dks4tPeK.js} +1 -1
- package/client/dist/assets/{OptionListMenu-B3D39dGS.js → OptionListMenu-11CcAsy8.js} +1 -1
- package/client/dist/assets/{Player-CsH4x_I_.js → Player-DQoEQkHL.js} +2 -2
- package/client/dist/assets/{PlayerSettings-DGxBsi3F.js → PlayerSettings-D4Ud4kv4.js} +1 -1
- package/client/dist/assets/{QueueOptionsButton-Djq4Cpex.js → QueueOptionsButton-B8PlPKjC.js} +1 -1
- package/client/dist/assets/{QueueRail-DOrYgNVX.js → QueueRail-B4Dok7U3.js} +1 -1
- package/client/dist/assets/{Radio-DF6g6odd.js → Radio-BHK6LSk4.js} +1 -1
- package/client/dist/assets/{ReadingList-DwYoBrNk.js → ReadingList-BCsxIz57.js} +1 -1
- package/client/dist/assets/{ResetProgressModal-CMF86mX6.js → ResetProgressModal-BXHxSfy-.js} +1 -1
- package/client/dist/assets/{Search-DFkhuk-x.js → Search-PK1ds07O.js} +1 -1
- package/client/dist/assets/{Settings-DBB2r0VQ.js → Settings-Bxk_x55l.js} +1 -1
- package/client/dist/assets/{SkeletonGrid-DJelJgAv.js → SkeletonGrid-CpJCZSRN.js} +1 -1
- package/client/dist/assets/{Trackers-DGYXFhYZ.js → Trackers-RieCh_84.js} +1 -1
- package/client/dist/assets/{Tv-C5MDGD2Z.js → Tv-DLytPJUj.js} +1 -1
- package/client/dist/assets/{TvInfo-DrKuK13u.js → TvInfo-C8djKebR.js} +1 -1
- package/client/dist/assets/{TvPopup-DtHilRDT.js → TvPopup-40JGGJDk.js} +1 -1
- package/client/dist/assets/{TvSearch-CzNxdu_w.js → TvSearch-A3hbospf.js} +1 -1
- package/client/dist/assets/{TvWatchlist-D7p4br6S.js → TvWatchlist-DuXjqmZM.js} +1 -1
- package/client/dist/assets/{Watchlist-DrOzswl2.js → Watchlist-B9wGBh0u.js} +1 -1
- package/client/dist/assets/{hls-BGCX7mJy.js → hls-CZ5X3Ya-.js} +1 -1
- package/client/dist/assets/{index-CnfZwsds.js → index-dRhc9-ol.js} +2 -2
- package/client/dist/assets/{useAnimeInfoData-DYXWBPF-.js → useAnimeInfoData-ZzDtLfvD.js} +1 -1
- package/client/dist/assets/{useAsmr-BbhQ59bk.js → useAsmr-CKOdHgol.js} +1 -1
- package/client/dist/assets/{useAsmrPopup-SymV1J-b.js → useAsmrPopup-SFHPhpcl.js} +1 -1
- package/client/dist/assets/{useMangaLibrary-DuesWQaE.js → useMangaLibrary-CKATe31P.js} +1 -1
- package/client/dist/assets/{useMangaPopup-D57OVGUr.js → useMangaPopup-C75QuYgY.js} +1 -1
- package/client/dist/assets/{useProviders-mpQb_dix.js → useProviders-CMtvZbEi.js} +1 -1
- package/client/dist/assets/{useTv-DGflCsSI.js → useTv-DUX4zYMr.js} +1 -1
- package/client/dist/assets/{useTvLibrary-BqeSmyc4.js → useTvLibrary-By4Gt-R_.js} +1 -1
- package/client/dist/index.html +1 -1
- package/package.json +2 -2
- package/server/dist/app-auth.js +1 -157
- package/server/dist/app-hono.js +7 -557
- package/server/dist/config.js +1 -174
- package/server/dist/db/drizzle.js +1 -16
- package/server/dist/db/schema-anime.js +1 -61
- package/server/dist/db/schema-asmr.js +1 -13
- package/server/dist/db/schema-manga.js +1 -31
- package/server/dist/db/schema-tv.js +1 -18
- package/server/dist/db.js +1 -217
- package/server/dist/discord-gateway.js +1 -385
- package/server/dist/discord-rpc.js +1 -413
- package/server/dist/github-sync.js +1 -508
- package/server/dist/google.js +1 -741
- package/server/dist/hono/asmr.js +1 -10
- package/server/dist/hono/auth.js +3 -26
- package/server/dist/hono/music.js +1 -209
- package/server/dist/hono/proxy.js +36 -783
- package/server/dist/hono/tv.js +5 -200
- package/server/dist/hono/watchlist.js +2 -338
- package/server/dist/lib/anilist.js +26 -1458
- package/server/dist/lib/anischedule.js +1 -191
- package/server/dist/lib/insights.js +1 -185
- package/server/dist/lib/ipc.js +1 -23
- package/server/dist/lib/kitsu.js +1 -517
- package/server/dist/lib/mal-import.js +1 -291
- package/server/dist/lib/mal.js +1 -957
- package/server/dist/lib/migration.js +1 -310
- package/server/dist/lib/offline-db.js +4 -384
- package/server/dist/lib/potoken.js +1 -135
- package/server/dist/lib/recommendations.service.js +1 -216
- package/server/dist/lib/temp-ids.js +1 -3
- package/server/dist/lib/tmdb.js +1 -72
- package/server/dist/lib/tracker/anilist-tracker.js +8 -245
- package/server/dist/lib/tracker/manga-sync.service.js +1 -773
- package/server/dist/lib/tracker/sync-state.js +1 -21
- package/server/dist/lib/tracker/sync.service.js +1 -476
- package/server/dist/lib/translate.js +3 -79
- package/server/dist/lib/ump.js +1 -106
- package/server/dist/lib/ytmusic.js +1 -162
- package/server/dist/logger.js +1 -16
- package/server/dist/providers/manga/manga.types.js +0 -1
- package/server/dist/providers/provider.interface.js +0 -1
- package/server/dist/providers/remote-loader.js +2 -614
- package/server/dist/providers/remote-types.js +1 -159
- package/server/dist/providers/title-matching.js +1 -226
- package/server/dist/providers/tv.types.js +0 -1
- package/server/dist/rclone.js +2 -120
- package/server/dist/repositories/asmr.repository.js +11 -46
- package/server/dist/repositories/insights.repository.js +11 -25
- package/server/dist/repositories/library.repository.js +3 -94
- package/server/dist/repositories/mal-cache.repository.js +1 -52
- package/server/dist/repositories/manga.repository.js +14 -58
- package/server/dist/repositories/notifications.repository.js +6 -52
- package/server/dist/repositories/queue.repository.js +5 -58
- package/server/dist/repositories/settings.repository.js +2 -29
- package/server/dist/repositories/shows-meta.repository.js +3 -56
- package/server/dist/repositories/temp-show-ids.repository.js +3 -63
- package/server/dist/repositories/tv.repository.js +11 -54
- package/server/dist/repositories/watched-episodes.repository.js +6 -55
- package/server/dist/repositories/watchlist.repository.js +3 -54
- package/server/dist/scripts/backfill-episode-duration.js +1 -42
- package/server/dist/server.js +1 -310
- package/server/dist/shipped-defaults.js +1 -6
- package/server/dist/sync-payload.js +1 -91
- package/server/dist/sync.js +3 -647
- package/server/dist/trpc/context.js +1 -7
- package/server/dist/trpc/errors.js +1 -15
- package/server/dist/trpc/index.js +1 -18
- package/server/dist/trpc/router.js +1 -49
- package/server/dist/trpc/routers/asmr-library.js +1 -112
- package/server/dist/trpc/routers/asmr-progress.js +1 -121
- package/server/dist/trpc/routers/asmr.js +1 -87
- package/server/dist/trpc/routers/auth.js +1 -194
- package/server/dist/trpc/routers/continue-watching.js +2 -58
- package/server/dist/trpc/routers/data.js +1 -1026
- package/server/dist/trpc/routers/discord.js +1 -285
- package/server/dist/trpc/routers/discovery.js +1 -11
- package/server/dist/trpc/routers/insights.js +1 -14
- package/server/dist/trpc/routers/manga-library.js +1 -263
- package/server/dist/trpc/routers/manga-progress.js +1 -169
- package/server/dist/trpc/routers/manga.js +1 -201
- package/server/dist/trpc/routers/music.js +1 -655
- package/server/dist/trpc/routers/notifications.js +1 -80
- package/server/dist/trpc/routers/progress.js +1 -109
- package/server/dist/trpc/routers/queue.js +1 -93
- package/server/dist/trpc/routers/radio.js +1 -145
- package/server/dist/trpc/routers/settings.js +1 -96
- package/server/dist/trpc/routers/tracker.js +1 -213
- package/server/dist/trpc/routers/tv-library.js +1 -135
- package/server/dist/trpc/routers/tv-progress.js +1 -159
- package/server/dist/trpc/routers/tv.js +1 -551
- package/server/dist/trpc/routers/watchlist.js +2 -549
- package/server/dist/trpc/validation.js +1 -258
- package/server/dist/utils/cache.utils.js +1 -25
- package/server/dist/utils/cookie.utils.js +1 -13
- package/server/dist/utils/db-utils.js +5 -17
- package/server/dist/utils/env.utils.js +5 -77
- package/server/dist/utils/http.utils.js +1 -82
- package/server/dist/utils/request-context.js +1 -2
- package/server/dist/utils/security.utils.js +1 -76
- package/server/package.json +3 -2
|
@@ -1,117 +1,4 @@
|
|
|
1
|
-
import
|
|
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
|
-
${
|
|
48
|
+
${q()}
|
|
198
49
|
}
|
|
199
50
|
}
|
|
200
51
|
seasonal: Page(perPage: 14) {
|
|
201
|
-
media(season: $season, seasonYear: $seasonYear, type: ANIME, isAdult: false, ${
|
|
202
|
-
${
|
|
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
|
-
${
|
|
58
|
+
${Y()}
|
|
208
59
|
}
|
|
209
60
|
}
|
|
210
61
|
}
|
|
211
|
-
|
|
212
|
-
|
|
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(/&/g, '&')
|
|
375
|
-
.replace(/"/g, '"')
|
|
376
|
-
.replace(/�?39;|'/g, "'")
|
|
377
|
-
.replace(/</g, '<')
|
|
378
|
-
.replace(/>/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(/&/g,"&").replace(/"/g,'"').replace(/�?39;|'/g,"'").replace(/</g,"<").replace(/>/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
|
-
${
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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: ${
|
|
610
|
-
${
|
|
96
|
+
media(sort: ${n}, type: ANIME, isAdult: false, ${a}) {
|
|
97
|
+
${Y()}
|
|
611
98
|
}
|
|
612
99
|
}
|
|
613
100
|
}
|
|
614
|
-
|
|
615
|
-
|
|
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: ${
|
|
105
|
+
media(search: $search, type: ${t}) {
|
|
1131
106
|
id
|
|
1132
107
|
title { romaji english native }
|
|
1133
108
|
}
|
|
1134
109
|
}
|
|
1135
110
|
}
|
|
1136
|
-
|
|
1137
|
-
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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};
|