@serifpersia/dango 3.6.7 → 3.6.9

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 (156) hide show
  1. package/README.md +1 -1
  2. package/client/dist/assets/{AnimeCard-C3P9zWi9.js → AnimeCard-DKDJK8Mp.js} +1 -1
  3. package/client/dist/assets/{AnimeInfo-Et6687Tl.js → AnimeInfo-CTcvfjva.js} +1 -1
  4. package/client/dist/assets/{AnimeInfoPage-DX4eNnlH.js → AnimeInfoPage-B_UiacIN.js} +2 -2
  5. package/client/dist/assets/{Asmr-CCdEWrFi.js → Asmr-DMSN2vxl.js} +2 -2
  6. package/client/dist/assets/{AsmrResetProgressModal-DgEWBJOw.js → AsmrResetProgressModal-CRwfOatX.js} +1 -1
  7. package/client/dist/assets/{AvSyncCalibrator-CQ2fjf2E.js → AvSyncCalibrator-CRJsi-vz.js} +1 -1
  8. package/client/dist/assets/{Home-DUkUTTTy.js → Home-pwmK9_Eh.js} +1 -1
  9. package/client/dist/assets/{Insights-QCh4xW12.js → Insights-m7kFzg2A.js} +1 -1
  10. package/client/dist/assets/{LibraryListPage-BoTxRqHz.js → LibraryListPage-C1wpPvu6.js} +1 -1
  11. package/client/dist/assets/ListeningList-OzfEI1Ga.js +1 -0
  12. package/client/dist/assets/{Manga-HydR-L7o.js → Manga-ChRCExhK.js} +1 -1
  13. package/client/dist/assets/{MangaInfoPage-CBakEI9a.js → MangaInfoPage-DSHJPo2A.js} +1 -1
  14. package/client/dist/assets/{MangaReadPage-DwAR3iPy.js → MangaReadPage-CzOVY1wA.js} +1 -1
  15. package/client/dist/assets/{MangaResetProgressModal-DGzl8vsO.js → MangaResetProgressModal-CJfzdUBb.js} +1 -1
  16. package/client/dist/assets/{Mature-Pi0TxKuF.js → Mature-CifQMfhs.js} +1 -1
  17. package/client/dist/assets/{MediaCard--cuFxOCS.js → MediaCard-CM10yB9V.js} +1 -1
  18. package/client/dist/assets/{OptionListMenu-B3D39dGS.js → OptionListMenu-6Kovbsqw.js} +1 -1
  19. package/client/dist/assets/{Player-CsH4x_I_.js → Player-CK0hT-u4.js} +2 -2
  20. package/client/dist/assets/{PlayerSettings-DGxBsi3F.js → PlayerSettings-Bb66uqT9.js} +1 -1
  21. package/client/dist/assets/{QueueOptionsButton-Djq4Cpex.js → QueueOptionsButton-C_nTj8_q.js} +1 -1
  22. package/client/dist/assets/{QueueRail-DOrYgNVX.js → QueueRail-DUK7URAE.js} +1 -1
  23. package/client/dist/assets/{ReadingList-DwYoBrNk.js → ReadingList-Ck9LJThU.js} +1 -1
  24. package/client/dist/assets/{ResetProgressModal-CMF86mX6.js → ResetProgressModal-BUphIv0k.js} +1 -1
  25. package/client/dist/assets/{Search-DFkhuk-x.js → Search-zJyP15h6.js} +1 -1
  26. package/client/dist/assets/{Settings-DBB2r0VQ.js → Settings-BYdoSEUh.js} +1 -1
  27. package/client/dist/assets/{SkeletonGrid-DJelJgAv.js → SkeletonGrid-CqgbOhjX.js} +1 -1
  28. package/client/dist/assets/{Trackers-DGYXFhYZ.js → Trackers-BVjyv6CM.js} +1 -1
  29. package/client/dist/assets/{Tv-C5MDGD2Z.js → Tv-L13ylswR.js} +1 -1
  30. package/client/dist/assets/{TvInfo-DrKuK13u.js → TvInfo-Cvcm5U2D.js} +1 -1
  31. package/client/dist/assets/{TvPopup-DtHilRDT.js → TvPopup-DhxfKvYm.js} +1 -1
  32. package/client/dist/assets/{TvSearch-CzNxdu_w.js → TvSearch-P8bFE6_X.js} +1 -1
  33. package/client/dist/assets/{TvWatchlist-D7p4br6S.js → TvWatchlist-D5gVPjtb.js} +1 -1
  34. package/client/dist/assets/{Watchlist-DrOzswl2.js → Watchlist-Cjq8l3rU.js} +1 -1
  35. package/client/dist/assets/{hls-BGCX7mJy.js → hls-BkzuANAg.js} +1 -1
  36. package/client/dist/assets/{index-CnfZwsds.js → index-CLVgzKLk.js} +19 -19
  37. package/client/dist/assets/{useAnimeInfoData-DYXWBPF-.js → useAnimeInfoData-DZJZu-so.js} +1 -1
  38. package/client/dist/assets/{useAsmr-BbhQ59bk.js → useAsmr-CvrMGHuo.js} +1 -1
  39. package/client/dist/assets/useAsmrPopup-Bimy8Ouw.js +1 -0
  40. package/client/dist/assets/{useMangaLibrary-DuesWQaE.js → useMangaLibrary--MiN11uR.js} +1 -1
  41. package/client/dist/assets/{useMangaPopup-D57OVGUr.js → useMangaPopup-CR5XhKKS.js} +1 -1
  42. package/client/dist/assets/{useProviders-mpQb_dix.js → useProviders-DqdT4fA5.js} +1 -1
  43. package/client/dist/assets/{useTv-DGflCsSI.js → useTv-BL0HMDyP.js} +1 -1
  44. package/client/dist/assets/{useTvLibrary-BqeSmyc4.js → useTvLibrary-D4U4iqN8.js} +1 -1
  45. package/client/dist/index.html +1 -1
  46. package/package.json +3 -5
  47. package/server/dist/config.js +1 -174
  48. package/server/dist/server.js +1068 -289
  49. package/server/dist/shipped-defaults.js +1 -6
  50. package/server/package.json +4 -5
  51. package/client/dist/assets/Asmr.module-CdWlioTY.js +0 -1
  52. package/client/dist/assets/ListeningList-DguKjI_t.js +0 -1
  53. package/client/dist/assets/Music-BGH_0voi.css +0 -1
  54. package/client/dist/assets/Music-CQpyXTLH.js +0 -1
  55. package/client/dist/assets/Radio-CCHNFN7R.css +0 -1
  56. package/client/dist/assets/Radio-DF6g6odd.js +0 -1
  57. package/client/dist/assets/Radio.module-DGSZLpKM.js +0 -1
  58. package/client/dist/assets/useAsmrPopup-SymV1J-b.js +0 -1
  59. package/server/dist/app-auth.js +0 -157
  60. package/server/dist/app-hono.js +0 -598
  61. package/server/dist/db/drizzle.js +0 -16
  62. package/server/dist/db/schema-anime.js +0 -61
  63. package/server/dist/db/schema-asmr.js +0 -13
  64. package/server/dist/db/schema-manga.js +0 -31
  65. package/server/dist/db/schema-tv.js +0 -18
  66. package/server/dist/db.js +0 -217
  67. package/server/dist/discord-gateway.js +0 -385
  68. package/server/dist/discord-rpc.js +0 -413
  69. package/server/dist/github-sync.js +0 -508
  70. package/server/dist/google.js +0 -741
  71. package/server/dist/hono/asmr.js +0 -10
  72. package/server/dist/hono/auth.js +0 -49
  73. package/server/dist/hono/music.js +0 -209
  74. package/server/dist/hono/proxy.js +0 -827
  75. package/server/dist/hono/tv.js +0 -200
  76. package/server/dist/hono/watchlist.js +0 -341
  77. package/server/dist/lib/anilist.js +0 -1583
  78. package/server/dist/lib/anischedule.js +0 -191
  79. package/server/dist/lib/insights.js +0 -185
  80. package/server/dist/lib/ipc.js +0 -23
  81. package/server/dist/lib/kitsu.js +0 -517
  82. package/server/dist/lib/mal-import.js +0 -291
  83. package/server/dist/lib/mal.js +0 -957
  84. package/server/dist/lib/migration.js +0 -310
  85. package/server/dist/lib/offline-db.js +0 -395
  86. package/server/dist/lib/potoken.js +0 -135
  87. package/server/dist/lib/recommendations.service.js +0 -216
  88. package/server/dist/lib/temp-ids.js +0 -3
  89. package/server/dist/lib/tmdb.js +0 -72
  90. package/server/dist/lib/tracker/anilist-tracker.js +0 -308
  91. package/server/dist/lib/tracker/manga-sync.service.js +0 -773
  92. package/server/dist/lib/tracker/sync-state.js +0 -21
  93. package/server/dist/lib/tracker/sync.service.js +0 -476
  94. package/server/dist/lib/translate.js +0 -79
  95. package/server/dist/lib/ump.js +0 -106
  96. package/server/dist/lib/ytmusic.js +0 -162
  97. package/server/dist/logger.js +0 -16
  98. package/server/dist/providers/manga/manga.types.js +0 -1
  99. package/server/dist/providers/provider.interface.js +0 -1
  100. package/server/dist/providers/remote-loader.js +0 -614
  101. package/server/dist/providers/remote-types.js +0 -159
  102. package/server/dist/providers/title-matching.js +0 -226
  103. package/server/dist/providers/tv.types.js +0 -1
  104. package/server/dist/rclone.js +0 -120
  105. package/server/dist/repositories/asmr.repository.js +0 -77
  106. package/server/dist/repositories/insights.repository.js +0 -106
  107. package/server/dist/repositories/library.repository.js +0 -94
  108. package/server/dist/repositories/mal-cache.repository.js +0 -52
  109. package/server/dist/repositories/manga.repository.js +0 -95
  110. package/server/dist/repositories/notifications.repository.js +0 -60
  111. package/server/dist/repositories/queue.repository.js +0 -72
  112. package/server/dist/repositories/settings.repository.js +0 -30
  113. package/server/dist/repositories/shows-meta.repository.js +0 -58
  114. package/server/dist/repositories/temp-show-ids.repository.js +0 -63
  115. package/server/dist/repositories/tv.repository.js +0 -93
  116. package/server/dist/repositories/watched-episodes.repository.js +0 -82
  117. package/server/dist/repositories/watchlist.repository.js +0 -60
  118. package/server/dist/scripts/backfill-episode-duration.js +0 -42
  119. package/server/dist/sync-payload.js +0 -91
  120. package/server/dist/sync.js +0 -657
  121. package/server/dist/trpc/context.js +0 -7
  122. package/server/dist/trpc/errors.js +0 -15
  123. package/server/dist/trpc/index.js +0 -18
  124. package/server/dist/trpc/router.js +0 -49
  125. package/server/dist/trpc/routers/asmr-library.js +0 -112
  126. package/server/dist/trpc/routers/asmr-progress.js +0 -121
  127. package/server/dist/trpc/routers/asmr.js +0 -87
  128. package/server/dist/trpc/routers/auth.js +0 -194
  129. package/server/dist/trpc/routers/continue-watching.js +0 -81
  130. package/server/dist/trpc/routers/data.js +0 -1026
  131. package/server/dist/trpc/routers/discord.js +0 -285
  132. package/server/dist/trpc/routers/discovery.js +0 -11
  133. package/server/dist/trpc/routers/insights.js +0 -14
  134. package/server/dist/trpc/routers/manga-library.js +0 -263
  135. package/server/dist/trpc/routers/manga-progress.js +0 -169
  136. package/server/dist/trpc/routers/manga.js +0 -201
  137. package/server/dist/trpc/routers/music.js +0 -655
  138. package/server/dist/trpc/routers/notifications.js +0 -80
  139. package/server/dist/trpc/routers/progress.js +0 -109
  140. package/server/dist/trpc/routers/queue.js +0 -93
  141. package/server/dist/trpc/routers/radio.js +0 -145
  142. package/server/dist/trpc/routers/settings.js +0 -96
  143. package/server/dist/trpc/routers/tracker.js +0 -213
  144. package/server/dist/trpc/routers/tv-library.js +0 -135
  145. package/server/dist/trpc/routers/tv-progress.js +0 -159
  146. package/server/dist/trpc/routers/tv.js +0 -551
  147. package/server/dist/trpc/routers/watchlist.js +0 -550
  148. package/server/dist/trpc/validation.js +0 -258
  149. package/server/dist/utils/cache.utils.js +0 -25
  150. package/server/dist/utils/cookie.utils.js +0 -13
  151. package/server/dist/utils/db-utils.js +0 -17
  152. package/server/dist/utils/env.utils.js +0 -77
  153. package/server/dist/utils/http.utils.js +0 -82
  154. package/server/dist/utils/request-context.js +0 -2
  155. package/server/dist/utils/security.utils.js +0 -76
  156. /package/client/dist/assets/{Asmr-D1KuQwQy.css → useAsmrPopup-D1KuQwQy.css} +0 -0
@@ -1,517 +0,0 @@
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
- }