@serifpersia/dango 3.6.5 → 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-JkXG40Es.js → AnimeCard-DraA_TF9.js} +1 -1
- package/client/dist/assets/{AnimeInfo-2L0UJkbB.js → AnimeInfo-BJ7pzs-P.js} +1 -1
- package/client/dist/assets/{AnimeInfoPage-BDs03vKk.js → AnimeInfoPage-B_esUr5C.js} +2 -2
- package/client/dist/assets/{Asmr-D2mVKEl9.js → Asmr-Ckuh2xfW.js} +1 -1
- package/client/dist/assets/{AsmrResetProgressModal-MQA_bNQM.js → AsmrResetProgressModal-BS6UFJN4.js} +1 -1
- package/client/dist/assets/AvSyncCalibrator-DwHCd9yB.js +10 -0
- package/client/dist/assets/{Home-DxMBHFmc.js → Home-DuM1EuvN.js} +1 -1
- package/client/dist/assets/{Insights-hmLWsSXY.js → Insights-D3Pd9c9V.js} +1 -1
- package/client/dist/assets/{LibraryListPage-CWI7hgUh.js → LibraryListPage-Dm2yVn0-.js} +1 -1
- package/client/dist/assets/{ListeningList-NOc28KAV.js → ListeningList-D-zt6EXZ.js} +1 -1
- package/client/dist/assets/{Manga-B5qNhI7v.js → Manga-JwprMKBk.js} +1 -1
- package/client/dist/assets/{MangaInfoPage-D4wIyb-V.js → MangaInfoPage-DaeyJl2p.js} +1 -1
- package/client/dist/assets/{MangaReadPage-Ce1VX7-0.js → MangaReadPage-COg0fHI9.js} +1 -1
- package/client/dist/assets/{MangaResetProgressModal-P3oY4_14.js → MangaResetProgressModal-kguXv0_M.js} +1 -1
- package/client/dist/assets/{Mature-D8EwGYQq.js → Mature-IHDaasrs.js} +1 -1
- package/client/dist/assets/{MediaCard-Dp8DjyXB.js → MediaCard-CpwsrXo5.js} +1 -1
- package/client/dist/assets/{Music-DULtVsAM.js → Music-Dks4tPeK.js} +1 -1
- package/client/dist/assets/{OptionListMenu-fIHdE-nq.js → OptionListMenu-11CcAsy8.js} +1 -1
- package/client/dist/assets/{Player-B8XeOmso.js → Player-DQoEQkHL.js} +6 -6
- package/client/dist/assets/{PlayerSettings-BycNCiCs.js → PlayerSettings-D4Ud4kv4.js} +1 -1
- package/client/dist/assets/{QueueOptionsButton-T1J37IZT.js → QueueOptionsButton-B8PlPKjC.js} +1 -1
- package/client/dist/assets/{QueueRail-BAwI-apz.js → QueueRail-B4Dok7U3.js} +1 -1
- package/client/dist/assets/{Radio-Bv9DyKtZ.js → Radio-BHK6LSk4.js} +1 -1
- package/client/dist/assets/{ReadingList-Cl0CcVp3.js → ReadingList-BCsxIz57.js} +1 -1
- package/client/dist/assets/{ResetProgressModal--N60WWCB.js → ResetProgressModal-BXHxSfy-.js} +1 -1
- package/client/dist/assets/{Search-DvgVDlUR.js → Search-PK1ds07O.js} +1 -1
- package/client/dist/assets/{Settings-dCz_-bJn.js → Settings-Bxk_x55l.js} +1 -1
- package/client/dist/assets/{SkeletonGrid-BZHKk1I6.js → SkeletonGrid-CpJCZSRN.js} +1 -1
- package/client/dist/assets/{Trackers-C1T5VcPu.js → Trackers-RieCh_84.js} +1 -1
- package/client/dist/assets/{Tv-Dhv_cg4y.js → Tv-DLytPJUj.js} +4 -4
- package/client/dist/assets/{TvInfo-DgaLK2H6.js → TvInfo-C8djKebR.js} +1 -1
- package/client/dist/assets/{TvPopup-DyX7f8kC.js → TvPopup-40JGGJDk.js} +1 -1
- package/client/dist/assets/{TvSearch-Bp0H6kue.js → TvSearch-A3hbospf.js} +1 -1
- package/client/dist/assets/{TvWatchlist-ohMtMeKa.js → TvWatchlist-DuXjqmZM.js} +1 -1
- package/client/dist/assets/{Watchlist-BbQtzsER.js → Watchlist-B9wGBh0u.js} +1 -1
- package/client/dist/assets/{hls-Bi1cjpLZ.js → hls-CZ5X3Ya-.js} +1 -1
- package/client/dist/assets/{index-BTnhzofk.js → index-dRhc9-ol.js} +2 -2
- package/client/dist/assets/{useAnimeInfoData-Cvmso3IK.js → useAnimeInfoData-ZzDtLfvD.js} +1 -1
- package/client/dist/assets/{useAsmr-CbzGCzmv.js → useAsmr-CKOdHgol.js} +1 -1
- package/client/dist/assets/{useAsmrPopup-DaREZ9Ie.js → useAsmrPopup-SFHPhpcl.js} +1 -1
- package/client/dist/assets/{useMangaLibrary-BVQRhsFh.js → useMangaLibrary-CKATe31P.js} +1 -1
- package/client/dist/assets/{useMangaPopup-CzsCbndG.js → useMangaPopup-C75QuYgY.js} +1 -1
- package/client/dist/assets/{useProviders-UuUPzft-.js → useProviders-CMtvZbEi.js} +1 -1
- package/client/dist/assets/{useTv-CSsbWKH9.js → useTv-DUX4zYMr.js} +1 -1
- package/client/dist/assets/{useTvLibrary-a_Y0c2FS.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 -905
- package/server/dist/hono/tv.js +5 -169
- 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 -605
- package/server/dist/providers/remote-types.js +1 -124
- 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 -307
- 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
- package/client/dist/assets/AvSyncCalibrator-DmoH5eRA.js +0 -10
- package/server/dist/utils/megaplay.utils.js +0 -40
package/server/dist/lib/kitsu.js
CHANGED
|
@@ -1,517 +1 @@
|
|
|
1
|
-
import logger from '../logger.js';
|
|
2
|
-
import { parseJsonBody } from '../utils/http.utils.js';
|
|
3
|
-
const KITSU_BASE = 'https://kitsu.app/api/edge';
|
|
4
|
-
const KITSU_HEADERS = { Accept: 'application/vnd.api+json', 'User-Agent': 'dango' };
|
|
5
|
-
const REQUEST_TIMEOUT_MS = 15000;
|
|
6
|
-
const MAX_RETRIES = 3;
|
|
7
|
-
async function kitsuFetch(path) {
|
|
8
|
-
let lastError = null;
|
|
9
|
-
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
10
|
-
try {
|
|
11
|
-
const controller = new AbortController();
|
|
12
|
-
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
13
|
-
try {
|
|
14
|
-
const res = await fetch(`${KITSU_BASE}${path}`, {
|
|
15
|
-
headers: KITSU_HEADERS,
|
|
16
|
-
signal: controller.signal,
|
|
17
|
-
});
|
|
18
|
-
if (res.status === 429 || res.status >= 500) {
|
|
19
|
-
lastError = `kitsu fetch failed with status ${res.status}`;
|
|
20
|
-
await new Promise((r) => setTimeout(r, 1000 * attempt));
|
|
21
|
-
continue;
|
|
22
|
-
}
|
|
23
|
-
if (!res.ok) {
|
|
24
|
-
logger.warn({ status: res.status, path }, 'Kitsu request failed');
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
return parseJsonBody(res);
|
|
28
|
-
}
|
|
29
|
-
finally {
|
|
30
|
-
clearTimeout(timer);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
catch (err) {
|
|
34
|
-
lastError = err instanceof Error ? err.message : String(err);
|
|
35
|
-
if (attempt < MAX_RETRIES)
|
|
36
|
-
await new Promise((r) => setTimeout(r, 500 * attempt));
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
logger.warn({ lastError }, 'Kitsu request error');
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
function asArray(data) {
|
|
43
|
-
if (!data)
|
|
44
|
-
return [];
|
|
45
|
-
return Array.isArray(data) ? data : [data];
|
|
46
|
-
}
|
|
47
|
-
function includedMap(entry, included, type) {
|
|
48
|
-
if (!included || !entry.relationships)
|
|
49
|
-
return [];
|
|
50
|
-
const rel = entry.relationships[type];
|
|
51
|
-
if (!rel?.data)
|
|
52
|
-
return [];
|
|
53
|
-
const ids = new Set();
|
|
54
|
-
for (const item of Array.isArray(rel.data) ? rel.data : [rel.data]) {
|
|
55
|
-
ids.add(item.id);
|
|
56
|
-
}
|
|
57
|
-
return included.filter((i) => i.type === type && ids.has(i.id));
|
|
58
|
-
}
|
|
59
|
-
const STATUS_MAP = {
|
|
60
|
-
current: 'RELEASING',
|
|
61
|
-
finished: 'FINISHED',
|
|
62
|
-
upcoming: 'NOT_YET_RELEASED',
|
|
63
|
-
unreleased: 'NOT_YET_RELEASED',
|
|
64
|
-
tba: 'NOT_YET_RELEASED',
|
|
65
|
-
};
|
|
66
|
-
const SUBTYPE_MAP = {
|
|
67
|
-
tv: 'TV',
|
|
68
|
-
ova: 'OVA',
|
|
69
|
-
ona: 'ONA',
|
|
70
|
-
movie: 'MOVIE',
|
|
71
|
-
special: 'SPECIAL',
|
|
72
|
-
music: 'MUSIC',
|
|
73
|
-
};
|
|
74
|
-
const SEASON_BY_MONTH = [
|
|
75
|
-
'WINTER',
|
|
76
|
-
'WINTER',
|
|
77
|
-
'SPRING',
|
|
78
|
-
'SPRING',
|
|
79
|
-
'SPRING',
|
|
80
|
-
'SUMMER',
|
|
81
|
-
'SUMMER',
|
|
82
|
-
'SUMMER',
|
|
83
|
-
'FALL',
|
|
84
|
-
'FALL',
|
|
85
|
-
'FALL',
|
|
86
|
-
'WINTER',
|
|
87
|
-
];
|
|
88
|
-
function parseKitsuDate(s) {
|
|
89
|
-
if (typeof s !== 'string' || !s)
|
|
90
|
-
return undefined;
|
|
91
|
-
const m = s.match(/^(\d{4})-(\d{2})-(\d{2})/);
|
|
92
|
-
if (!m)
|
|
93
|
-
return undefined;
|
|
94
|
-
return { year: Number(m[1]), month: Number(m[2]), day: Number(m[3]) };
|
|
95
|
-
}
|
|
96
|
-
function statusToAnilist(status) {
|
|
97
|
-
if (!status)
|
|
98
|
-
return undefined;
|
|
99
|
-
return STATUS_MAP[status.toLowerCase()] ?? status.toUpperCase();
|
|
100
|
-
}
|
|
101
|
-
function formatToAnilist(subtype) {
|
|
102
|
-
if (!subtype)
|
|
103
|
-
return undefined;
|
|
104
|
-
return SUBTYPE_MAP[subtype.toLowerCase()] ?? subtype.toUpperCase();
|
|
105
|
-
}
|
|
106
|
-
function anilistSortToKitsu(sort) {
|
|
107
|
-
switch (sort) {
|
|
108
|
-
case 'SCORE_DESC':
|
|
109
|
-
return '-averageRating';
|
|
110
|
-
case 'FAVOURITES_DESC':
|
|
111
|
-
return '-favoritesCount';
|
|
112
|
-
case 'START_DATE_DESC':
|
|
113
|
-
return '-startDate';
|
|
114
|
-
case 'END_DATE_DESC':
|
|
115
|
-
return '-endDate';
|
|
116
|
-
case 'EPISODES_DESC':
|
|
117
|
-
return '-episodeCount';
|
|
118
|
-
case 'ID_DESC':
|
|
119
|
-
return '-id';
|
|
120
|
-
case 'UPDATED_AT_DESC':
|
|
121
|
-
return '-updatedAt';
|
|
122
|
-
default:
|
|
123
|
-
return '-userCount';
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
function statusToKitsuFilter(status) {
|
|
127
|
-
if (!status)
|
|
128
|
-
return undefined;
|
|
129
|
-
const s = status.toUpperCase();
|
|
130
|
-
if (s === 'RELEASING' || s === 'ONGOING')
|
|
131
|
-
return 'current';
|
|
132
|
-
if (s === 'FINISHED')
|
|
133
|
-
return 'finished';
|
|
134
|
-
if (s === 'NOT_YET_RELEASED' || s === 'UPCOMING' || s === 'HIATUS')
|
|
135
|
-
return 'upcoming';
|
|
136
|
-
return undefined;
|
|
137
|
-
}
|
|
138
|
-
function seasonToKitsu(season) {
|
|
139
|
-
if (!season)
|
|
140
|
-
return undefined;
|
|
141
|
-
return season.toLowerCase();
|
|
142
|
-
}
|
|
143
|
-
const CATEGORY_ALIASES = {
|
|
144
|
-
'Sci-Fi': 'Science Fiction',
|
|
145
|
-
'Sci Fi': 'Science Fiction',
|
|
146
|
-
Shounen: 'Shounen',
|
|
147
|
-
};
|
|
148
|
-
function normalizeCategory(cat) {
|
|
149
|
-
const c = cat.trim();
|
|
150
|
-
const alias = Object.entries(CATEGORY_ALIASES).find(([k]) => k.toLowerCase() === c.toLowerCase());
|
|
151
|
-
return alias ? alias[1] : c;
|
|
152
|
-
}
|
|
153
|
-
function mappingItem(entry, included) {
|
|
154
|
-
const rel = entry.relationships?.item?.data;
|
|
155
|
-
if (!rel)
|
|
156
|
-
return undefined;
|
|
157
|
-
if (Array.isArray(rel))
|
|
158
|
-
return undefined;
|
|
159
|
-
return included.find((i) => i.type === rel.type && i.id === rel.id);
|
|
160
|
-
}
|
|
161
|
-
export async function kitsuSearchAnime(options = {}) {
|
|
162
|
-
const { query, page = 1, perPage = 14, format, status, season, seasonYear, genre, genre_in, genre_not_in, averageScore_greater, episodes_greater, isAdult, sort, } = options;
|
|
163
|
-
const filters = [];
|
|
164
|
-
if (query)
|
|
165
|
-
filters.push(`filter[text]=${encodeURIComponent(query)}`);
|
|
166
|
-
if (format && format !== 'ALL' && format !== 'ADULT') {
|
|
167
|
-
filters.push(`filter[subtype]=${format.toLowerCase()}`);
|
|
168
|
-
}
|
|
169
|
-
const statusFilter = statusToKitsuFilter(status);
|
|
170
|
-
if (statusFilter)
|
|
171
|
-
filters.push(`filter[status]=${statusFilter}`);
|
|
172
|
-
const seasonFilter = seasonToKitsu(season);
|
|
173
|
-
if (seasonFilter && seasonFilter !== 'all')
|
|
174
|
-
filters.push(`filter[season]=${seasonFilter}`);
|
|
175
|
-
if (seasonYear)
|
|
176
|
-
filters.push(`filter[seasonYear]=${seasonYear}`);
|
|
177
|
-
const genreCombined = [
|
|
178
|
-
...(Array.isArray(genre) ? genre : typeof genre === 'string' ? genre.split(',') : []),
|
|
179
|
-
...(genre_in ?? []),
|
|
180
|
-
]
|
|
181
|
-
.map((g) => g.trim())
|
|
182
|
-
.filter(Boolean)
|
|
183
|
-
.join(',');
|
|
184
|
-
if (genreCombined)
|
|
185
|
-
filters.push(`filter[categories]=${encodeURIComponent(genreCombined)}`);
|
|
186
|
-
const offset = (page - 1) * perPage;
|
|
187
|
-
const path = `/anime?${filters.join('&')}${filters.length ? '&' : ''}sort=${anilistSortToKitsu(sort)}&page[limit]=${perPage}&page[offset]=${offset}&include=mappings,categories`;
|
|
188
|
-
const json = await kitsuFetch(path);
|
|
189
|
-
if (!json)
|
|
190
|
-
return [];
|
|
191
|
-
const entries = asArray(json.data);
|
|
192
|
-
const included = json.included ?? [];
|
|
193
|
-
const results = [];
|
|
194
|
-
for (const entry of entries) {
|
|
195
|
-
const media = normalizeKitsuEntry(entry, included);
|
|
196
|
-
if (!media)
|
|
197
|
-
continue;
|
|
198
|
-
if (genre_not_in?.length) {
|
|
199
|
-
const hasExcluded = (media.genres ?? []).some((g) => genre_not_in.some((ng) => ng.toLowerCase() === g.toLowerCase()));
|
|
200
|
-
if (hasExcluded)
|
|
201
|
-
continue;
|
|
202
|
-
}
|
|
203
|
-
if (averageScore_greater != null && (media.averageScore ?? 0) < averageScore_greater)
|
|
204
|
-
continue;
|
|
205
|
-
if (episodes_greater != null && (media.episodes ?? 0) < episodes_greater)
|
|
206
|
-
continue;
|
|
207
|
-
if (isAdult === true && !media.isAdult)
|
|
208
|
-
continue;
|
|
209
|
-
if (isAdult === false && media.isAdult)
|
|
210
|
-
continue;
|
|
211
|
-
results.push(media);
|
|
212
|
-
}
|
|
213
|
-
return results;
|
|
214
|
-
}
|
|
215
|
-
export function dedupeKitsuMedia(list) {
|
|
216
|
-
const seen = new Set();
|
|
217
|
-
const out = [];
|
|
218
|
-
for (const m of list) {
|
|
219
|
-
const key = m.id > 0 ? m.id : (m.idMal ?? m.id);
|
|
220
|
-
if (seen.has(key))
|
|
221
|
-
continue;
|
|
222
|
-
seen.add(key);
|
|
223
|
-
out.push(m);
|
|
224
|
-
}
|
|
225
|
-
return out;
|
|
226
|
-
}
|
|
227
|
-
function normalizeKitsuEntry(entry, included, knownIds) {
|
|
228
|
-
const a = entry.attributes;
|
|
229
|
-
const mappings = includedMap(entry, included, 'mappings');
|
|
230
|
-
const anilistMapping = mappings.find((m) => m.attributes?.externalSite === 'anilist/anime');
|
|
231
|
-
const malMapping = mappings.find((m) => m.attributes?.externalSite === 'myanimelist/anime');
|
|
232
|
-
const anilistId = anilistMapping
|
|
233
|
-
? Number(anilistMapping.attributes.externalId)
|
|
234
|
-
: (knownIds?.anilistId ?? null);
|
|
235
|
-
const malId = malMapping ? Number(malMapping.attributes.externalId) : (knownIds?.malId ?? null);
|
|
236
|
-
if (!anilistId && !malId)
|
|
237
|
-
return null;
|
|
238
|
-
const id = anilistId ?? -Math.abs(malId);
|
|
239
|
-
const title = {};
|
|
240
|
-
const titles = a.titles ?? {};
|
|
241
|
-
title.romaji =
|
|
242
|
-
titles.en_jp || titles.en || entry.attributes.canonicalTitle;
|
|
243
|
-
title.english = titles.en || titles.en_us || title.romaji;
|
|
244
|
-
title.native = titles.ja_jp || titles.ja || title.romaji;
|
|
245
|
-
const startDate = parseKitsuDate(a.startDate);
|
|
246
|
-
const endDate = parseKitsuDate(a.endDate);
|
|
247
|
-
const poster = a.posterImage ?? {};
|
|
248
|
-
const cover = a.coverImage ?? {};
|
|
249
|
-
const posterUrl = poster.large || poster.original || poster.medium || poster.small || poster.tiny || null;
|
|
250
|
-
const coverUrl = cover.large || cover.original;
|
|
251
|
-
const genres = includedMap(entry, included, 'categories')
|
|
252
|
-
.map((c) => c.attributes?.title)
|
|
253
|
-
.filter(Boolean)
|
|
254
|
-
.map((g) => normalizeCategory(g));
|
|
255
|
-
const nextAiringAt = a.nextRelease != null ? Math.floor(new Date(a.nextRelease).getTime() / 1000) : null;
|
|
256
|
-
return {
|
|
257
|
-
id,
|
|
258
|
-
idMal: malId,
|
|
259
|
-
title,
|
|
260
|
-
bannerImage: coverUrl || null,
|
|
261
|
-
coverImage: posterUrl
|
|
262
|
-
? { extraLarge: posterUrl, large: posterUrl, medium: posterUrl }
|
|
263
|
-
: undefined,
|
|
264
|
-
description: a.synopsis || a.description || null,
|
|
265
|
-
genres: genres.length > 0 ? genres : undefined,
|
|
266
|
-
averageScore: a.averageRating != null ? Math.round(Number(a.averageRating)) : null,
|
|
267
|
-
format: formatToAnilist(a.subtype),
|
|
268
|
-
status: statusToAnilist(a.status),
|
|
269
|
-
episodes: a.episodeCount ?? null,
|
|
270
|
-
duration: a.episodeLength ?? null,
|
|
271
|
-
season: startDate?.month ? SEASON_BY_MONTH[startDate.month - 1] : undefined,
|
|
272
|
-
seasonYear: startDate?.year ?? undefined,
|
|
273
|
-
startDate,
|
|
274
|
-
endDate,
|
|
275
|
-
isAdult: !!a.nsfw,
|
|
276
|
-
synonyms: a.abbreviatedTitles ?? undefined,
|
|
277
|
-
popularity: a.userCount ?? undefined,
|
|
278
|
-
favourites: a.favoritesCount ?? undefined,
|
|
279
|
-
nextAiringEpisode: nextAiringAt != null
|
|
280
|
-
? {
|
|
281
|
-
episode: 0,
|
|
282
|
-
timeUntilAiring: nextAiringAt - Math.floor(Date.now() / 1000),
|
|
283
|
-
airingAt: nextAiringAt,
|
|
284
|
-
}
|
|
285
|
-
: null,
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
async function kitsuIdByAnilistId(anilistId) {
|
|
289
|
-
const json = await kitsuFetch(`/mappings?filter[externalSite]=anilist%2Fanime&filter[externalId]=${anilistId}&include=item`);
|
|
290
|
-
const entries = asArray(json?.data);
|
|
291
|
-
if (entries.length === 0)
|
|
292
|
-
return null;
|
|
293
|
-
return mappingItem(entries[0], json?.included ?? [])?.id ?? null;
|
|
294
|
-
}
|
|
295
|
-
const KITSU_STATUS_MAP = {
|
|
296
|
-
current: 'RELEASING',
|
|
297
|
-
finished: 'FINISHED',
|
|
298
|
-
upcoming: 'NOT_YET_RELEASED',
|
|
299
|
-
unreleased: 'NOT_YET_RELEASED',
|
|
300
|
-
tba: 'NOT_YET_RELEASED',
|
|
301
|
-
};
|
|
302
|
-
export async function kitsuBatchGetStatuses(anilistIds) {
|
|
303
|
-
const result = new Map();
|
|
304
|
-
for (const id of anilistIds) {
|
|
305
|
-
try {
|
|
306
|
-
const json = await kitsuFetch(`/mappings?filter[externalSite]=anilist%2Fanime&filter[externalId]=${id}&include=item`);
|
|
307
|
-
const entries = asArray(json?.data);
|
|
308
|
-
if (entries.length === 0)
|
|
309
|
-
continue;
|
|
310
|
-
const included = json?.included ?? [];
|
|
311
|
-
const entry = entries[0];
|
|
312
|
-
const rel = entry.relationships?.item?.data;
|
|
313
|
-
if (!rel)
|
|
314
|
-
continue;
|
|
315
|
-
const relSingle = Array.isArray(rel) ? rel[0] : rel;
|
|
316
|
-
if (!relSingle)
|
|
317
|
-
continue;
|
|
318
|
-
const anime = included.find((i) => i.id === relSingle.id && i.type === 'anime');
|
|
319
|
-
if (!anime)
|
|
320
|
-
continue;
|
|
321
|
-
const raw = anime.attributes?.status?.toLowerCase();
|
|
322
|
-
const status = raw ? (KITSU_STATUS_MAP[raw] ?? raw.toUpperCase()) : undefined;
|
|
323
|
-
if (status)
|
|
324
|
-
result.set(id, status);
|
|
325
|
-
}
|
|
326
|
-
catch {
|
|
327
|
-
// ignore
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
return result;
|
|
331
|
-
}
|
|
332
|
-
async function kitsuDetail(kitsuId, knownIds) {
|
|
333
|
-
const [animeJson, studiosJson] = await Promise.all([
|
|
334
|
-
kitsuFetch(`/anime/${kitsuId}?include=categories,mappings`),
|
|
335
|
-
kitsuFetch(`/anime/${kitsuId}/anime-productions?include=producer&page[limit]=10`),
|
|
336
|
-
]);
|
|
337
|
-
if (!animeJson)
|
|
338
|
-
return null;
|
|
339
|
-
const entry = asArray(animeJson.data)[0];
|
|
340
|
-
if (!entry)
|
|
341
|
-
return null;
|
|
342
|
-
const media = normalizeKitsuEntry(entry, animeJson.included ?? [], knownIds);
|
|
343
|
-
if (!media)
|
|
344
|
-
return null;
|
|
345
|
-
const producers = (studiosJson?.included ?? [])
|
|
346
|
-
.filter((i) => i.type === 'producers')
|
|
347
|
-
.map((i) => ({ id: 0, name: i.attributes?.name ?? '' }))
|
|
348
|
-
.filter((p) => p.name.length > 0);
|
|
349
|
-
if (producers.length > 0) {
|
|
350
|
-
media.studios = { nodes: producers };
|
|
351
|
-
}
|
|
352
|
-
return media;
|
|
353
|
-
}
|
|
354
|
-
export async function kitsuMetaByAnilistId(anilistId) {
|
|
355
|
-
const kitsuId = await kitsuIdByAnilistId(anilistId);
|
|
356
|
-
if (!kitsuId)
|
|
357
|
-
return null;
|
|
358
|
-
return kitsuDetail(kitsuId, { anilistId });
|
|
359
|
-
}
|
|
360
|
-
export async function kitsuMetaByMalId(malId) {
|
|
361
|
-
const json = await kitsuFetch(`/mappings?filter[externalSite]=myanimelist%2Fanime&filter[externalId]=${malId}&include=item`);
|
|
362
|
-
const entries = asArray(json?.data);
|
|
363
|
-
if (entries.length === 0)
|
|
364
|
-
return null;
|
|
365
|
-
const item = mappingItem(entries[0], json?.included ?? []);
|
|
366
|
-
if (!item)
|
|
367
|
-
return null;
|
|
368
|
-
return kitsuDetail(item.id, { malId });
|
|
369
|
-
}
|
|
370
|
-
export async function kitsuTitlesByMalIds(malIds) {
|
|
371
|
-
const result = new Map();
|
|
372
|
-
const unique = [...new Set(malIds.filter((n) => Number.isFinite(n) && n > 0))];
|
|
373
|
-
const CHUNK = 20;
|
|
374
|
-
const chunks = [];
|
|
375
|
-
for (let i = 0; i < unique.length; i += CHUNK)
|
|
376
|
-
chunks.push(unique.slice(i, i + CHUNK));
|
|
377
|
-
await Promise.all(chunks.map(async (ids) => {
|
|
378
|
-
try {
|
|
379
|
-
const json = await kitsuFetch(`/mappings?filter[externalSite]=myanimelist%2Fanime&filter[externalId]=${ids.join(',')}&include=item&page[limit]=${CHUNK}`);
|
|
380
|
-
if (!json)
|
|
381
|
-
return;
|
|
382
|
-
const animeById = new Map();
|
|
383
|
-
for (const inc of json.included ?? []) {
|
|
384
|
-
if (inc.type === 'anime')
|
|
385
|
-
animeById.set(inc.id, inc);
|
|
386
|
-
}
|
|
387
|
-
for (const entry of asArray(json.data)) {
|
|
388
|
-
const rel = entry.relationships?.item?.data;
|
|
389
|
-
const single = Array.isArray(rel) ? rel[0] : rel;
|
|
390
|
-
if (!single)
|
|
391
|
-
continue;
|
|
392
|
-
const titles = (animeById.get(single.id)?.attributes?.titles ?? {});
|
|
393
|
-
const english = titles.en || titles.en_us || undefined;
|
|
394
|
-
const native = titles.ja_jp || undefined;
|
|
395
|
-
if (!english && !native)
|
|
396
|
-
continue;
|
|
397
|
-
const malId = Number(entry.attributes?.externalId);
|
|
398
|
-
if (!Number.isFinite(malId) || malId <= 0)
|
|
399
|
-
continue;
|
|
400
|
-
result.set(malId, { english, native });
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
catch {
|
|
404
|
-
// ignore
|
|
405
|
-
}
|
|
406
|
-
}));
|
|
407
|
-
return result;
|
|
408
|
-
}
|
|
409
|
-
export async function kitsuEpisodes(anilistId, idMal) {
|
|
410
|
-
let kitsuId = null;
|
|
411
|
-
if (anilistId > 0)
|
|
412
|
-
kitsuId = await kitsuIdByAnilistId(anilistId);
|
|
413
|
-
if (!kitsuId && idMal != null) {
|
|
414
|
-
const json = await kitsuFetch(`/mappings?filter[externalSite]=myanimelist%2Fanime&filter[externalId]=${idMal}&include=item`);
|
|
415
|
-
const entries = asArray(json?.data);
|
|
416
|
-
if (entries.length > 0) {
|
|
417
|
-
kitsuId = mappingItem(entries[0], json?.included ?? [])?.id ?? null;
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
if (!kitsuId)
|
|
421
|
-
return [];
|
|
422
|
-
const all = [];
|
|
423
|
-
const LIMIT = 20;
|
|
424
|
-
let offset = 0;
|
|
425
|
-
while (offset < 1000) {
|
|
426
|
-
const json = await kitsuFetch(`/anime/${kitsuId}/episodes?page[limit]=${LIMIT}&page[offset]=${offset}&sort=number`);
|
|
427
|
-
if (!json)
|
|
428
|
-
break;
|
|
429
|
-
const entries = asArray(json.data);
|
|
430
|
-
if (entries.length === 0)
|
|
431
|
-
break;
|
|
432
|
-
for (const e of entries) {
|
|
433
|
-
const n = e.attributes?.number;
|
|
434
|
-
if (typeof n === 'number' && Number.isFinite(n) && n > 0)
|
|
435
|
-
all.push(n);
|
|
436
|
-
}
|
|
437
|
-
if (entries.length < LIMIT)
|
|
438
|
-
break;
|
|
439
|
-
offset += LIMIT;
|
|
440
|
-
}
|
|
441
|
-
if (all.length === 0)
|
|
442
|
-
return [];
|
|
443
|
-
const max = Math.max(...all);
|
|
444
|
-
return Array.from({ length: max }, (_, i) => (i + 1).toString());
|
|
445
|
-
}
|
|
446
|
-
export async function kitsuTrending(page = 1, perPage = 20, status) {
|
|
447
|
-
const statusFilter = statusToKitsuFilter(status);
|
|
448
|
-
const filters = statusFilter ? `filter[status]=${statusFilter}` : '';
|
|
449
|
-
const offset = (page - 1) * perPage;
|
|
450
|
-
const json = await kitsuFetch(`/anime?${filters}${filters ? '&' : ''}sort=-userCount&page[limit]=${perPage}&page[offset]=${offset}&include=mappings,categories`);
|
|
451
|
-
if (!json)
|
|
452
|
-
return [];
|
|
453
|
-
return dedupeKitsuMedia(asArray(json.data)
|
|
454
|
-
.map((e) => normalizeKitsuEntry(e, json.included ?? []))
|
|
455
|
-
.filter((m) => m !== null));
|
|
456
|
-
}
|
|
457
|
-
export async function kitsuSpotlight(page = 1, perPage = 20) {
|
|
458
|
-
const offset = (page - 1) * perPage;
|
|
459
|
-
const json = await kitsuFetch(`/anime?sort=-userCount&page[limit]=${perPage}&page[offset]=${offset}&include=mappings,categories`);
|
|
460
|
-
if (!json)
|
|
461
|
-
return [];
|
|
462
|
-
return dedupeKitsuMedia(asArray(json.data)
|
|
463
|
-
.map((e) => normalizeKitsuEntry(e, json.included ?? []))
|
|
464
|
-
.filter((m) => m !== null)
|
|
465
|
-
.filter((m) => !!m.bannerImage));
|
|
466
|
-
}
|
|
467
|
-
export async function kitsuLatestReleases(format = 'TV', page = 1, size = 12) {
|
|
468
|
-
const needed = page * size;
|
|
469
|
-
const formatFilter = format && format !== 'ALL' ? `filter[subtype]=${format.toLowerCase()}` : '';
|
|
470
|
-
const all = [];
|
|
471
|
-
let offset = 0;
|
|
472
|
-
const LIMIT = 20;
|
|
473
|
-
while (all.length < needed && offset < 500) {
|
|
474
|
-
const json = await kitsuFetch(`/anime?filter[status]=current${formatFilter ? `&${formatFilter}` : ''}&sort=-startDate&page[limit]=${LIMIT}&page[offset]=${offset}&include=mappings,categories`);
|
|
475
|
-
if (!json)
|
|
476
|
-
break;
|
|
477
|
-
const entries = asArray(json.data);
|
|
478
|
-
if (entries.length === 0)
|
|
479
|
-
break;
|
|
480
|
-
for (const e of entries) {
|
|
481
|
-
const m = normalizeKitsuEntry(e, json.included ?? []);
|
|
482
|
-
if (!m)
|
|
483
|
-
continue;
|
|
484
|
-
all.push(m);
|
|
485
|
-
}
|
|
486
|
-
offset += LIMIT;
|
|
487
|
-
}
|
|
488
|
-
all.sort((a, b) => {
|
|
489
|
-
const aNext = a.nextAiringEpisode?.airingAt ?? Number.MAX_SAFE_INTEGER;
|
|
490
|
-
const bNext = b.nextAiringEpisode?.airingAt ?? Number.MAX_SAFE_INTEGER;
|
|
491
|
-
if (aNext !== bNext)
|
|
492
|
-
return aNext - bNext;
|
|
493
|
-
const aStart = fallbackStartRank(a);
|
|
494
|
-
const bStart = fallbackStartRank(b);
|
|
495
|
-
if (aStart !== bStart)
|
|
496
|
-
return bStart - aStart;
|
|
497
|
-
return (b.popularity ?? 0) - (a.popularity ?? 0);
|
|
498
|
-
});
|
|
499
|
-
const start = (page - 1) * size;
|
|
500
|
-
return dedupeKitsuMedia(all.slice(start, start + size));
|
|
501
|
-
}
|
|
502
|
-
function fallbackStartRank(m) {
|
|
503
|
-
return ((m.startDate?.year ?? 0) * 10000 + (m.startDate?.month ?? 0) * 100 + (m.startDate?.day ?? 0));
|
|
504
|
-
}
|
|
505
|
-
export async function kitsuSeasonal(season, year, format, page = 1, size = 14) {
|
|
506
|
-
const filters = [`filter[season]=${seasonToKitsu(season)}`, `filter[seasonYear]=${year}`];
|
|
507
|
-
if (format && format !== 'ALL' && format !== 'ADULT') {
|
|
508
|
-
filters.push(`filter[subtype]=${format.toLowerCase()}`);
|
|
509
|
-
}
|
|
510
|
-
const offset = (page - 1) * size;
|
|
511
|
-
const json = await kitsuFetch(`/anime?${filters.join('&')}&sort=-userCount&page[limit]=${size}&page[offset]=${offset}&include=mappings,categories`);
|
|
512
|
-
if (!json)
|
|
513
|
-
return [];
|
|
514
|
-
return dedupeKitsuMedia(asArray(json.data)
|
|
515
|
-
.map((e) => normalizeKitsuEntry(e, json.included ?? []))
|
|
516
|
-
.filter((m) => m !== null));
|
|
517
|
-
}
|
|
1
|
+
import x from"../logger.js";import{parseJsonBody as G}from"../utils/http.utils.js";const B="https://kitsu.app/api/edge",H={Accept:"application/vnd.api+json","User-Agent":"dango"},q=15e3,C=3;async function g(e){let t=null;for(let i=1;i<=C;i++)try{const n=new AbortController,s=setTimeout(()=>n.abort(),q);try{const r=await fetch(`${B}${e}`,{headers:H,signal:n.signal});if(r.status===429||r.status>=500){t=`kitsu fetch failed with status ${r.status}`,await new Promise(l=>setTimeout(l,1e3*i));continue}return r.ok?G(r):(x.warn({status:r.status,path:e},"Kitsu request failed"),null)}finally{clearTimeout(s)}}catch(n){t=n instanceof Error?n.message:String(n),i<C&&await new Promise(s=>setTimeout(s,500*i))}return x.warn({lastError:t},"Kitsu request error"),null}function p(e){return e?Array.isArray(e)?e:[e]:[]}function L(e,t,i){if(!t||!e.relationships)return[];const n=e.relationships[i];if(!n?.data)return[];const s=new Set;for(const r of Array.isArray(n.data)?n.data:[n.data])s.add(r.id);return t.filter(r=>r.type===i&&s.has(r.id))}const V={current:"RELEASING",finished:"FINISHED",upcoming:"NOT_YET_RELEASED",unreleased:"NOT_YET_RELEASED",tba:"NOT_YET_RELEASED"},J={tv:"TV",ova:"OVA",ona:"ONA",movie:"MOVIE",special:"SPECIAL",music:"MUSIC"},W=["WINTER","WINTER","SPRING","SPRING","SPRING","SUMMER","SUMMER","SUMMER","FALL","FALL","FALL","WINTER"];function D(e){if(typeof e!="string"||!e)return;const t=e.match(/^(\d{4})-(\d{2})-(\d{2})/);if(t)return{year:Number(t[1]),month:Number(t[2]),day:Number(t[3])}}function X(e){if(e)return V[e.toLowerCase()]??e.toUpperCase()}function Q(e){if(e)return J[e.toLowerCase()]??e.toUpperCase()}function Z(e){switch(e){case"SCORE_DESC":return"-averageRating";case"FAVOURITES_DESC":return"-favoritesCount";case"START_DATE_DESC":return"-startDate";case"END_DATE_DESC":return"-endDate";case"EPISODES_DESC":return"-episodeCount";case"ID_DESC":return"-id";case"UPDATED_AT_DESC":return"-updatedAt";default:return"-userCount"}}function K(e){if(!e)return;const t=e.toUpperCase();if(t==="RELEASING"||t==="ONGOING")return"current";if(t==="FINISHED")return"finished";if(t==="NOT_YET_RELEASED"||t==="UPCOMING"||t==="HIATUS")return"upcoming"}function U(e){if(e)return e.toLowerCase()}const z={"Sci-Fi":"Science Fiction","Sci Fi":"Science Fiction",Shounen:"Shounen"};function ee(e){const t=e.trim(),i=Object.entries(z).find(([n])=>n.toLowerCase()===t.toLowerCase());return i?i[1]:t}function N(e,t){const i=e.relationships?.item?.data;if(i&&!Array.isArray(i))return t.find(n=>n.type===i.type&&n.id===i.id)}async function se(e={}){const{query:t,page:i=1,perPage:n=14,format:s,status:r,season:l,seasonYear:a,genre:o,genre_in:c,genre_not_in:u,averageScore_greater:d,episodes_greater:f,isAdult:A,sort:m}=e,E=[];t&&E.push(`filter[text]=${encodeURIComponent(t)}`),s&&s!=="ALL"&&s!=="ADULT"&&E.push(`filter[subtype]=${s.toLowerCase()}`);const b=K(r);b&&E.push(`filter[status]=${b}`);const I=U(l);I&&I!=="all"&&E.push(`filter[season]=${I}`),a&&E.push(`filter[seasonYear]=${a}`);const T=[...Array.isArray(o)?o:typeof o=="string"?o.split(","):[],...c??[]].map(w=>w.trim()).filter(Boolean).join(",");T&&E.push(`filter[categories]=${encodeURIComponent(T)}`);const M=(i-1)*n,y=`/anime?${E.join("&")}${E.length?"&":""}sort=${Z(m)}&page[limit]=${n}&page[offset]=${M}&include=mappings,categories`,R=await g(y);if(!R)return[];const F=p(R.data),v=R.included??[],$=[];for(const w of F){const S=h(w,v);S&&(u?.length&&(S.genres??[]).some(O=>u.some(Y=>Y.toLowerCase()===O.toLowerCase()))||d!=null&&(S.averageScore??0)<d||f!=null&&(S.episodes??0)<f||A===!0&&!S.isAdult||A===!1&&S.isAdult||$.push(S))}return $}function _(e){const t=new Set,i=[];for(const n of e){const s=n.id>0?n.id:n.idMal??n.id;t.has(s)||(t.add(s),i.push(n))}return i}function h(e,t,i){const n=e.attributes,s=L(e,t,"mappings"),r=s.find(y=>y.attributes?.externalSite==="anilist/anime"),l=s.find(y=>y.attributes?.externalSite==="myanimelist/anime"),a=r?Number(r.attributes.externalId):i?.anilistId??null,o=l?Number(l.attributes.externalId):i?.malId??null;if(!a&&!o)return null;const c=a??-Math.abs(o),u={},d=n.titles??{};u.romaji=d.en_jp||d.en||e.attributes.canonicalTitle,u.english=d.en||d.en_us||u.romaji,u.native=d.ja_jp||d.ja||u.romaji;const f=D(n.startDate),A=D(n.endDate),m=n.posterImage??{},E=n.coverImage??{},b=m.large||m.original||m.medium||m.small||m.tiny||null,I=E.large||E.original,T=L(e,t,"categories").map(y=>y.attributes?.title).filter(Boolean).map(y=>ee(y)),M=n.nextRelease!=null?Math.floor(new Date(n.nextRelease).getTime()/1e3):null;return{id:c,idMal:o,title:u,bannerImage:I||null,coverImage:b?{extraLarge:b,large:b,medium:b}:void 0,description:n.synopsis||n.description||null,genres:T.length>0?T:void 0,averageScore:n.averageRating!=null?Math.round(Number(n.averageRating)):null,format:Q(n.subtype),status:X(n.status),episodes:n.episodeCount??null,duration:n.episodeLength??null,season:f?.month?W[f.month-1]:void 0,seasonYear:f?.year??void 0,startDate:f,endDate:A,isAdult:!!n.nsfw,synonyms:n.abbreviatedTitles??void 0,popularity:n.userCount??void 0,favourites:n.favoritesCount??void 0,nextAiringEpisode:M!=null?{episode:0,timeUntilAiring:M-Math.floor(Date.now()/1e3),airingAt:M}:null}}async function j(e){const t=await g(`/mappings?filter[externalSite]=anilist%2Fanime&filter[externalId]=${e}&include=item`),i=p(t?.data);return i.length===0?null:N(i[0],t?.included??[])?.id??null}const ne={current:"RELEASING",finished:"FINISHED",upcoming:"NOT_YET_RELEASED",unreleased:"NOT_YET_RELEASED",tba:"NOT_YET_RELEASED"};async function ae(e){const t=new Map;for(const i of e)try{const n=await g(`/mappings?filter[externalSite]=anilist%2Fanime&filter[externalId]=${i}&include=item`),s=p(n?.data);if(s.length===0)continue;const r=n?.included??[],a=s[0].relationships?.item?.data;if(!a)continue;const o=Array.isArray(a)?a[0]:a;if(!o)continue;const c=r.find(f=>f.id===o.id&&f.type==="anime");if(!c)continue;const u=c.attributes?.status?.toLowerCase(),d=u?ne[u]??u.toUpperCase():void 0;d&&t.set(i,d)}catch{}return t}async function P(e,t){const[i,n]=await Promise.all([g(`/anime/${e}?include=categories,mappings`),g(`/anime/${e}/anime-productions?include=producer&page[limit]=10`)]);if(!i)return null;const s=p(i.data)[0];if(!s)return null;const r=h(s,i.included??[],t);if(!r)return null;const l=(n?.included??[]).filter(a=>a.type==="producers").map(a=>({id:0,name:a.attributes?.name??""})).filter(a=>a.name.length>0);return l.length>0&&(r.studios={nodes:l}),r}async function oe(e){const t=await j(e);return t?P(t,{anilistId:e}):null}async function ue(e){const t=await g(`/mappings?filter[externalSite]=myanimelist%2Fanime&filter[externalId]=${e}&include=item`),i=p(t?.data);if(i.length===0)return null;const n=N(i[0],t?.included??[]);return n?P(n.id,{malId:e}):null}async function le(e){const t=new Map,i=[...new Set(e.filter(r=>Number.isFinite(r)&&r>0))],n=20,s=[];for(let r=0;r<i.length;r+=n)s.push(i.slice(r,r+n));return await Promise.all(s.map(async r=>{try{const l=await g(`/mappings?filter[externalSite]=myanimelist%2Fanime&filter[externalId]=${r.join(",")}&include=item&page[limit]=${n}`);if(!l)return;const a=new Map;for(const o of l.included??[])o.type==="anime"&&a.set(o.id,o);for(const o of p(l.data)){const c=o.relationships?.item?.data,u=Array.isArray(c)?c[0]:c;if(!u)continue;const d=a.get(u.id)?.attributes?.titles??{},f=d.en||d.en_us||void 0,A=d.ja_jp||void 0;if(!f&&!A)continue;const m=Number(o.attributes?.externalId);!Number.isFinite(m)||m<=0||t.set(m,{english:f,native:A})}}catch{}})),t}async function de(e,t){let i=null;if(e>0&&(i=await j(e)),!i&&t!=null){const a=await g(`/mappings?filter[externalSite]=myanimelist%2Fanime&filter[externalId]=${t}&include=item`),o=p(a?.data);o.length>0&&(i=N(o[0],a?.included??[])?.id??null)}if(!i)return[];const n=[],s=20;let r=0;for(;r<1e3;){const a=await g(`/anime/${i}/episodes?page[limit]=${s}&page[offset]=${r}&sort=number`);if(!a)break;const o=p(a.data);if(o.length===0)break;for(const c of o){const u=c.attributes?.number;typeof u=="number"&&Number.isFinite(u)&&u>0&&n.push(u)}if(o.length<s)break;r+=s}if(n.length===0)return[];const l=Math.max(...n);return Array.from({length:l},(a,o)=>(o+1).toString())}async function ce(e=1,t=20,i){const n=K(i),s=n?`filter[status]=${n}`:"",r=(e-1)*t,l=await g(`/anime?${s}${s?"&":""}sort=-userCount&page[limit]=${t}&page[offset]=${r}&include=mappings,categories`);return l?_(p(l.data).map(a=>h(a,l.included??[])).filter(a=>a!==null)):[]}async function fe(e=1,t=20){const i=(e-1)*t,n=await g(`/anime?sort=-userCount&page[limit]=${t}&page[offset]=${i}&include=mappings,categories`);return n?_(p(n.data).map(s=>h(s,n.included??[])).filter(s=>s!==null).filter(s=>!!s.bannerImage)):[]}async function me(e="TV",t=1,i=12){const n=t*i,s=e&&e!=="ALL"?`filter[subtype]=${e.toLowerCase()}`:"",r=[];let l=0;const a=20;for(;r.length<n&&l<500;){const c=await g(`/anime?filter[status]=current${s?`&${s}`:""}&sort=-startDate&page[limit]=${a}&page[offset]=${l}&include=mappings,categories`);if(!c)break;const u=p(c.data);if(u.length===0)break;for(const d of u){const f=h(d,c.included??[]);f&&r.push(f)}l+=a}r.sort((c,u)=>{const d=c.nextAiringEpisode?.airingAt??Number.MAX_SAFE_INTEGER,f=u.nextAiringEpisode?.airingAt??Number.MAX_SAFE_INTEGER;if(d!==f)return d-f;const A=k(c),m=k(u);return A!==m?m-A:(u.popularity??0)-(c.popularity??0)});const o=(t-1)*i;return _(r.slice(o,o+i))}function k(e){return(e.startDate?.year??0)*1e4+(e.startDate?.month??0)*100+(e.startDate?.day??0)}async function ge(e,t,i,n=1,s=14){const r=[`filter[season]=${U(e)}`,`filter[seasonYear]=${t}`];i&&i!=="ALL"&&i!=="ADULT"&&r.push(`filter[subtype]=${i.toLowerCase()}`);const l=(n-1)*s,a=await g(`/anime?${r.join("&")}&sort=-userCount&page[limit]=${s}&page[offset]=${l}&include=mappings,categories`);return a?_(p(a.data).map(o=>h(o,a.included??[])).filter(o=>o!==null)):[]}export{_ as dedupeKitsuMedia,ae as kitsuBatchGetStatuses,de as kitsuEpisodes,me as kitsuLatestReleases,oe as kitsuMetaByAnilistId,ue as kitsuMetaByMalId,se as kitsuSearchAnime,ge as kitsuSeasonal,fe as kitsuSpotlight,le as kitsuTitlesByMalIds,ce as kitsuTrending};
|