@serifpersia/dango 3.6.7 → 3.6.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/README.md +1 -1
  2. package/client/dist/assets/{AnimeCard-C3P9zWi9.js → AnimeCard-DraA_TF9.js} +1 -1
  3. package/client/dist/assets/{AnimeInfo-Et6687Tl.js → AnimeInfo-BJ7pzs-P.js} +1 -1
  4. package/client/dist/assets/{AnimeInfoPage-DX4eNnlH.js → AnimeInfoPage-B_esUr5C.js} +2 -2
  5. package/client/dist/assets/{Asmr-CCdEWrFi.js → Asmr-Ckuh2xfW.js} +1 -1
  6. package/client/dist/assets/{AsmrResetProgressModal-DgEWBJOw.js → AsmrResetProgressModal-BS6UFJN4.js} +1 -1
  7. package/client/dist/assets/{AvSyncCalibrator-CQ2fjf2E.js → AvSyncCalibrator-DwHCd9yB.js} +1 -1
  8. package/client/dist/assets/{Home-DUkUTTTy.js → Home-DuM1EuvN.js} +1 -1
  9. package/client/dist/assets/{Insights-QCh4xW12.js → Insights-D3Pd9c9V.js} +1 -1
  10. package/client/dist/assets/{LibraryListPage-BoTxRqHz.js → LibraryListPage-Dm2yVn0-.js} +1 -1
  11. package/client/dist/assets/{ListeningList-DguKjI_t.js → ListeningList-D-zt6EXZ.js} +1 -1
  12. package/client/dist/assets/{Manga-HydR-L7o.js → Manga-JwprMKBk.js} +1 -1
  13. package/client/dist/assets/{MangaInfoPage-CBakEI9a.js → MangaInfoPage-DaeyJl2p.js} +1 -1
  14. package/client/dist/assets/{MangaReadPage-DwAR3iPy.js → MangaReadPage-COg0fHI9.js} +1 -1
  15. package/client/dist/assets/{MangaResetProgressModal-DGzl8vsO.js → MangaResetProgressModal-kguXv0_M.js} +1 -1
  16. package/client/dist/assets/{Mature-Pi0TxKuF.js → Mature-IHDaasrs.js} +1 -1
  17. package/client/dist/assets/{MediaCard--cuFxOCS.js → MediaCard-CpwsrXo5.js} +1 -1
  18. package/client/dist/assets/{Music-CQpyXTLH.js → Music-Dks4tPeK.js} +1 -1
  19. package/client/dist/assets/{OptionListMenu-B3D39dGS.js → OptionListMenu-11CcAsy8.js} +1 -1
  20. package/client/dist/assets/{Player-CsH4x_I_.js → Player-DQoEQkHL.js} +2 -2
  21. package/client/dist/assets/{PlayerSettings-DGxBsi3F.js → PlayerSettings-D4Ud4kv4.js} +1 -1
  22. package/client/dist/assets/{QueueOptionsButton-Djq4Cpex.js → QueueOptionsButton-B8PlPKjC.js} +1 -1
  23. package/client/dist/assets/{QueueRail-DOrYgNVX.js → QueueRail-B4Dok7U3.js} +1 -1
  24. package/client/dist/assets/{Radio-DF6g6odd.js → Radio-BHK6LSk4.js} +1 -1
  25. package/client/dist/assets/{ReadingList-DwYoBrNk.js → ReadingList-BCsxIz57.js} +1 -1
  26. package/client/dist/assets/{ResetProgressModal-CMF86mX6.js → ResetProgressModal-BXHxSfy-.js} +1 -1
  27. package/client/dist/assets/{Search-DFkhuk-x.js → Search-PK1ds07O.js} +1 -1
  28. package/client/dist/assets/{Settings-DBB2r0VQ.js → Settings-Bxk_x55l.js} +1 -1
  29. package/client/dist/assets/{SkeletonGrid-DJelJgAv.js → SkeletonGrid-CpJCZSRN.js} +1 -1
  30. package/client/dist/assets/{Trackers-DGYXFhYZ.js → Trackers-RieCh_84.js} +1 -1
  31. package/client/dist/assets/{Tv-C5MDGD2Z.js → Tv-DLytPJUj.js} +1 -1
  32. package/client/dist/assets/{TvInfo-DrKuK13u.js → TvInfo-C8djKebR.js} +1 -1
  33. package/client/dist/assets/{TvPopup-DtHilRDT.js → TvPopup-40JGGJDk.js} +1 -1
  34. package/client/dist/assets/{TvSearch-CzNxdu_w.js → TvSearch-A3hbospf.js} +1 -1
  35. package/client/dist/assets/{TvWatchlist-D7p4br6S.js → TvWatchlist-DuXjqmZM.js} +1 -1
  36. package/client/dist/assets/{Watchlist-DrOzswl2.js → Watchlist-B9wGBh0u.js} +1 -1
  37. package/client/dist/assets/{hls-BGCX7mJy.js → hls-CZ5X3Ya-.js} +1 -1
  38. package/client/dist/assets/{index-CnfZwsds.js → index-dRhc9-ol.js} +2 -2
  39. package/client/dist/assets/{useAnimeInfoData-DYXWBPF-.js → useAnimeInfoData-ZzDtLfvD.js} +1 -1
  40. package/client/dist/assets/{useAsmr-BbhQ59bk.js → useAsmr-CKOdHgol.js} +1 -1
  41. package/client/dist/assets/{useAsmrPopup-SymV1J-b.js → useAsmrPopup-SFHPhpcl.js} +1 -1
  42. package/client/dist/assets/{useMangaLibrary-DuesWQaE.js → useMangaLibrary-CKATe31P.js} +1 -1
  43. package/client/dist/assets/{useMangaPopup-D57OVGUr.js → useMangaPopup-C75QuYgY.js} +1 -1
  44. package/client/dist/assets/{useProviders-mpQb_dix.js → useProviders-CMtvZbEi.js} +1 -1
  45. package/client/dist/assets/{useTv-DGflCsSI.js → useTv-DUX4zYMr.js} +1 -1
  46. package/client/dist/assets/{useTvLibrary-BqeSmyc4.js → useTvLibrary-By4Gt-R_.js} +1 -1
  47. package/client/dist/index.html +1 -1
  48. package/package.json +2 -2
  49. package/server/dist/app-auth.js +1 -157
  50. package/server/dist/app-hono.js +7 -557
  51. package/server/dist/config.js +1 -174
  52. package/server/dist/db/drizzle.js +1 -16
  53. package/server/dist/db/schema-anime.js +1 -61
  54. package/server/dist/db/schema-asmr.js +1 -13
  55. package/server/dist/db/schema-manga.js +1 -31
  56. package/server/dist/db/schema-tv.js +1 -18
  57. package/server/dist/db.js +1 -217
  58. package/server/dist/discord-gateway.js +1 -385
  59. package/server/dist/discord-rpc.js +1 -413
  60. package/server/dist/github-sync.js +1 -508
  61. package/server/dist/google.js +1 -741
  62. package/server/dist/hono/asmr.js +1 -10
  63. package/server/dist/hono/auth.js +3 -26
  64. package/server/dist/hono/music.js +1 -209
  65. package/server/dist/hono/proxy.js +36 -783
  66. package/server/dist/hono/tv.js +5 -200
  67. package/server/dist/hono/watchlist.js +2 -338
  68. package/server/dist/lib/anilist.js +26 -1458
  69. package/server/dist/lib/anischedule.js +1 -191
  70. package/server/dist/lib/insights.js +1 -185
  71. package/server/dist/lib/ipc.js +1 -23
  72. package/server/dist/lib/kitsu.js +1 -517
  73. package/server/dist/lib/mal-import.js +1 -291
  74. package/server/dist/lib/mal.js +1 -957
  75. package/server/dist/lib/migration.js +1 -310
  76. package/server/dist/lib/offline-db.js +4 -384
  77. package/server/dist/lib/potoken.js +1 -135
  78. package/server/dist/lib/recommendations.service.js +1 -216
  79. package/server/dist/lib/temp-ids.js +1 -3
  80. package/server/dist/lib/tmdb.js +1 -72
  81. package/server/dist/lib/tracker/anilist-tracker.js +8 -245
  82. package/server/dist/lib/tracker/manga-sync.service.js +1 -773
  83. package/server/dist/lib/tracker/sync-state.js +1 -21
  84. package/server/dist/lib/tracker/sync.service.js +1 -476
  85. package/server/dist/lib/translate.js +3 -79
  86. package/server/dist/lib/ump.js +1 -106
  87. package/server/dist/lib/ytmusic.js +1 -162
  88. package/server/dist/logger.js +1 -16
  89. package/server/dist/providers/manga/manga.types.js +0 -1
  90. package/server/dist/providers/provider.interface.js +0 -1
  91. package/server/dist/providers/remote-loader.js +2 -614
  92. package/server/dist/providers/remote-types.js +1 -159
  93. package/server/dist/providers/title-matching.js +1 -226
  94. package/server/dist/providers/tv.types.js +0 -1
  95. package/server/dist/rclone.js +2 -120
  96. package/server/dist/repositories/asmr.repository.js +11 -46
  97. package/server/dist/repositories/insights.repository.js +11 -25
  98. package/server/dist/repositories/library.repository.js +3 -94
  99. package/server/dist/repositories/mal-cache.repository.js +1 -52
  100. package/server/dist/repositories/manga.repository.js +14 -58
  101. package/server/dist/repositories/notifications.repository.js +6 -52
  102. package/server/dist/repositories/queue.repository.js +5 -58
  103. package/server/dist/repositories/settings.repository.js +2 -29
  104. package/server/dist/repositories/shows-meta.repository.js +3 -56
  105. package/server/dist/repositories/temp-show-ids.repository.js +3 -63
  106. package/server/dist/repositories/tv.repository.js +11 -54
  107. package/server/dist/repositories/watched-episodes.repository.js +6 -55
  108. package/server/dist/repositories/watchlist.repository.js +3 -54
  109. package/server/dist/scripts/backfill-episode-duration.js +1 -42
  110. package/server/dist/server.js +1 -310
  111. package/server/dist/shipped-defaults.js +1 -6
  112. package/server/dist/sync-payload.js +1 -91
  113. package/server/dist/sync.js +3 -647
  114. package/server/dist/trpc/context.js +1 -7
  115. package/server/dist/trpc/errors.js +1 -15
  116. package/server/dist/trpc/index.js +1 -18
  117. package/server/dist/trpc/router.js +1 -49
  118. package/server/dist/trpc/routers/asmr-library.js +1 -112
  119. package/server/dist/trpc/routers/asmr-progress.js +1 -121
  120. package/server/dist/trpc/routers/asmr.js +1 -87
  121. package/server/dist/trpc/routers/auth.js +1 -194
  122. package/server/dist/trpc/routers/continue-watching.js +2 -58
  123. package/server/dist/trpc/routers/data.js +1 -1026
  124. package/server/dist/trpc/routers/discord.js +1 -285
  125. package/server/dist/trpc/routers/discovery.js +1 -11
  126. package/server/dist/trpc/routers/insights.js +1 -14
  127. package/server/dist/trpc/routers/manga-library.js +1 -263
  128. package/server/dist/trpc/routers/manga-progress.js +1 -169
  129. package/server/dist/trpc/routers/manga.js +1 -201
  130. package/server/dist/trpc/routers/music.js +1 -655
  131. package/server/dist/trpc/routers/notifications.js +1 -80
  132. package/server/dist/trpc/routers/progress.js +1 -109
  133. package/server/dist/trpc/routers/queue.js +1 -93
  134. package/server/dist/trpc/routers/radio.js +1 -145
  135. package/server/dist/trpc/routers/settings.js +1 -96
  136. package/server/dist/trpc/routers/tracker.js +1 -213
  137. package/server/dist/trpc/routers/tv-library.js +1 -135
  138. package/server/dist/trpc/routers/tv-progress.js +1 -159
  139. package/server/dist/trpc/routers/tv.js +1 -551
  140. package/server/dist/trpc/routers/watchlist.js +2 -549
  141. package/server/dist/trpc/validation.js +1 -258
  142. package/server/dist/utils/cache.utils.js +1 -25
  143. package/server/dist/utils/cookie.utils.js +1 -13
  144. package/server/dist/utils/db-utils.js +5 -17
  145. package/server/dist/utils/env.utils.js +5 -77
  146. package/server/dist/utils/http.utils.js +1 -82
  147. package/server/dist/utils/request-context.js +1 -2
  148. package/server/dist/utils/security.utils.js +1 -76
  149. package/server/package.json +3 -2
@@ -1,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};