@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,216 +1 @@
1
- import logger from '../logger.js';
2
- import { InsightsRepository } from '../repositories/insights.repository.js';
3
- import { anilistRequest } from '../lib/anilist.js';
4
- function parseGenres(raw) {
5
- if (!raw)
6
- return [];
7
- try {
8
- if (raw.startsWith('[')) {
9
- const arr = JSON.parse(raw);
10
- return Array.isArray(arr) ? arr.map(String) : [];
11
- }
12
- return raw
13
- .split(',')
14
- .map((g) => g.trim())
15
- .filter(Boolean);
16
- }
17
- catch {
18
- return [];
19
- }
20
- }
21
- export async function computeTasteProfile(db) {
22
- const [rows, anilistRows, epRows] = (await Promise.all([
23
- InsightsRepository.getLibraryShowsWithGenres(db),
24
- InsightsRepository.getAllAnilistIds(db),
25
- InsightsRepository.getWatchedEpisodesWithMeta(db),
26
- ]));
27
- const genreComp = {};
28
- const genreDrop = {};
29
- for (const row of rows) {
30
- const genres = parseGenres(row.genres);
31
- if (row.status === 'Completed') {
32
- for (const g of genres)
33
- genreComp[g] = (genreComp[g] || 0) + 1;
34
- }
35
- else if (row.status === 'Dropped') {
36
- for (const g of genres)
37
- genreDrop[g] = (genreDrop[g] || 0) + 1;
38
- }
39
- }
40
- const watchesByGenre = {};
41
- for (const ep of epRows) {
42
- const title = ep.englishName || ep.name || ep.showId;
43
- for (const g of parseGenres(ep.genres)) {
44
- if (!watchesByGenre[g])
45
- watchesByGenre[g] = new Map();
46
- const entry = watchesByGenre[g].get(ep.showId) || { title, count: 0 };
47
- entry.count += 1;
48
- watchesByGenre[g].set(ep.showId, entry);
49
- }
50
- }
51
- const exemplars = {};
52
- const topGenres = Object.entries(genreComp)
53
- .sort((a, b) => b[1] - a[1])
54
- .slice(0, 6);
55
- for (const [genre] of topGenres) {
56
- exemplars[genre] = [...(watchesByGenre[genre]?.values() || [])]
57
- .sort((a, b) => b.count - a.count)
58
- .slice(0, 2)
59
- .map((e) => e.title);
60
- }
61
- const knownIds = new Set();
62
- for (const row of rows) {
63
- if (/^\d+$/.test(row.id))
64
- knownIds.add(Number(row.id));
65
- }
66
- for (const r of anilistRows) {
67
- if (r.anilistId)
68
- knownIds.add(r.anilistId);
69
- }
70
- return { genreComp, genreDrop, exemplars, knownIds: [...knownIds] };
71
- }
72
- const REC_FIELDS = `id title { romaji english } coverImage { large } averageScore popularity episodes seasonYear format genres isAdult status`;
73
- function tasteWeights(genreComp, genreDrop) {
74
- const max = Math.max(1, ...Object.values(genreComp));
75
- const W = {};
76
- for (const [g, c] of Object.entries(genreComp)) {
77
- const d = genreDrop[g] || 0;
78
- W[g] = (c / max) * (c / (c + d));
79
- }
80
- return W;
81
- }
82
- function recCombos(W) {
83
- const top = Object.entries(W)
84
- .sort((a, b) => b[1] - a[1])
85
- .slice(0, 4)
86
- .map(([g]) => g);
87
- while (top.length < 2)
88
- top.push('Fantasy');
89
- const pairs = [
90
- [top[0], top[1]],
91
- [top[0], top[2] || top[1]],
92
- [top[1], top[2] || top[0]],
93
- [top[0], top[3] || top[1]],
94
- ];
95
- const seen = new Set();
96
- return pairs.filter((p) => {
97
- const key = [...p].sort().join('|');
98
- if (seen.has(key))
99
- return false;
100
- seen.add(key);
101
- return true;
102
- });
103
- }
104
- function exemplarsForCandidate(exemplars, candidateGenres, n = 2) {
105
- const votes = new Map();
106
- for (const g of candidateGenres) {
107
- for (const t of exemplars[g] || [])
108
- votes.set(t, (votes.get(t) || 0) + 1);
109
- }
110
- return [...votes.entries()]
111
- .sort((a, b) => b[1] - a[1])
112
- .slice(0, n)
113
- .map(([t]) => t);
114
- }
115
- export async function computeRecCandidates(taste) {
116
- const knownIds = new Set(taste.knownIds || []);
117
- if (knownIds.size === 0)
118
- return [];
119
- const W = tasteWeights(taste.genreComp, taste.genreDrop);
120
- const DEFAULT_W = 0.15;
121
- const wsum = Object.values(W).reduce((a, b) => a + b, 0) || 1;
122
- const seen = new Map();
123
- for (const combo of recCombos(W)) {
124
- try {
125
- const res = await anilistRequest(`query ($g: [String]) { Page(page: 1, perPage: 20) { media(genre_in: $g, type: ANIME, status: FINISHED, isAdult: false, averageScore_greater: 68, sort: SCORE_DESC) { ${REC_FIELDS} } } }`, { g: combo });
126
- for (const m of res?.data?.Page?.media || []) {
127
- if (!seen.has(m.id))
128
- seen.set(m.id, m);
129
- }
130
- }
131
- catch {
132
- // ignore
133
- }
134
- }
135
- if (seen.size === 0)
136
- return [];
137
- const scored = [];
138
- for (const m of seen.values()) {
139
- if (knownIds.has(m.id) || m.isAdult)
140
- continue;
141
- if (m.format === 'MUSIC' || m.format === 'SPECIAL')
142
- continue;
143
- const gs = m.genres || [];
144
- const fit = gs.reduce((a, g) => a + (W[g] ?? DEFAULT_W), 0) / wsum;
145
- const score = 0.65 * fit +
146
- 0.25 * ((m.averageScore || 70) / 100) +
147
- 0.1 * Math.min(1, (m.popularity || 0) / 200000);
148
- scored.push({ m, score, overlap: gs.filter((g) => (W[g] ?? 0) > 0.3) });
149
- }
150
- scored.sort((a, b) => b.score - a.score);
151
- const top = scored.slice(0, 15);
152
- const relMap = new Map();
153
- try {
154
- const res = await anilistRequest(`query ($ids: [Int]) { Page(page: 1, perPage: 50) { media(id_in: $ids, type: ANIME) { id relations { edges { relationType node { id title { romaji english } type } } } } } }`, { ids: top.map((t) => t.m.id) });
155
- for (const m of res?.data?.Page?.media || [])
156
- relMap.set(m.id, m);
157
- }
158
- catch {
159
- // ignore
160
- }
161
- return top.map((t) => {
162
- const rel = relMap.get(t.m.id);
163
- const pre = rel?.relations?.edges?.find((e) => e.relationType === 'PREQUEL' && e.node?.type === 'ANIME')?.node;
164
- let adj = t.score;
165
- let kind = 'entry';
166
- let prequelTitle = null;
167
- if (pre && pre.id && !knownIds.has(pre.id)) {
168
- kind = 'sequel';
169
- prequelTitle = pre.title?.english || pre.title?.romaji || null;
170
- adj -= 0.3;
171
- }
172
- else if (pre) {
173
- kind = 'continue';
174
- adj += 0.05;
175
- }
176
- const gs = t.m.genres || [];
177
- const title = t.m.title?.english || t.m.title?.romaji || 'Unknown';
178
- return {
179
- id: t.m.id,
180
- title,
181
- url: `https://anilist.co/anime/${t.m.id}`,
182
- cover: t.m.coverImage?.large || null,
183
- year: t.m.seasonYear ?? null,
184
- format: t.m.format || 'TV',
185
- episodes: t.m.episodes ?? null,
186
- averageScore: t.m.averageScore ?? null,
187
- genres: gs,
188
- matchPct: Math.round(adj * 100),
189
- sharedGenres: t.overlap,
190
- exemplars: exemplarsForCandidate(taste.exemplars, gs),
191
- kind,
192
- prequelTitle,
193
- };
194
- });
195
- }
196
- let recCache = null;
197
- function tasteHashOf(taste) {
198
- return JSON.stringify([taste.genreComp, taste.genreDrop, taste.knownIds.length]);
199
- }
200
- async function refreshCandidatesIfNeeded(taste, hash) {
201
- if (!recCache || recCache.hash !== hash) {
202
- try {
203
- const fresh = await computeRecCandidates(taste);
204
- if (fresh.length > 0)
205
- recCache = { hash, candidates: fresh };
206
- }
207
- catch (err) {
208
- logger.warn({ err: err?.message }, 'Recommendation candidates failed, reusing previous');
209
- }
210
- }
211
- return recCache?.candidates || [];
212
- }
213
- export async function getCachedRecommendations(db) {
214
- const taste = await computeTasteProfile(db);
215
- return refreshCandidatesIfNeeded(taste, tasteHashOf(taste));
216
- }
1
+ import P from"../logger.js";import{InsightsRepository as h}from"../repositories/insights.repository.js";import{anilistRequest as w}from"../lib/anilist.js";function R(n){if(!n)return[];try{if(n.startsWith("[")){const r=JSON.parse(n);return Array.isArray(r)?r.map(String):[]}return n.split(",").map(r=>r.trim()).filter(Boolean)}catch{return[]}}async function S(n){const[r,c,d]=await Promise.all([h.getLibraryShowsWithGenres(n),h.getAllAnilistIds(n),h.getWatchedEpisodesWithMeta(n)]),o={},s={};for(const e of r){const i=R(e.genres);if(e.status==="Completed")for(const a of i)o[a]=(o[a]||0)+1;else if(e.status==="Dropped")for(const a of i)s[a]=(s[a]||0)+1}const g={};for(const e of d){const i=e.englishName||e.name||e.showId;for(const a of R(e.genres)){g[a]||(g[a]=new Map);const l=g[a].get(e.showId)||{title:i,count:0};l.count+=1,g[a].set(e.showId,l)}}const m={},u=Object.entries(o).sort((e,i)=>i[1]-e[1]).slice(0,6);for(const[e]of u)m[e]=[...g[e]?.values()||[]].sort((i,a)=>a.count-i.count).slice(0,2).map(i=>i.title);const t=new Set;for(const e of r)/^\d+$/.test(e.id)&&t.add(Number(e.id));for(const e of c)e.anilistId&&t.add(e.anilistId);return{genreComp:o,genreDrop:s,exemplars:m,knownIds:[...t]}}const C="id title { romaji english } coverImage { large } averageScore popularity episodes seasonYear format genres isAdult status";function v(n,r){const c=Math.max(1,...Object.values(n)),d={};for(const[o,s]of Object.entries(n)){const g=r[o]||0;d[o]=s/c*(s/(s+g))}return d}function M(n){const r=Object.entries(n).sort((o,s)=>s[1]-o[1]).slice(0,4).map(([o])=>o);for(;r.length<2;)r.push("Fantasy");const c=[[r[0],r[1]],[r[0],r[2]||r[1]],[r[1],r[2]||r[0]],[r[0],r[3]||r[1]]],d=new Set;return c.filter(o=>{const s=[...o].sort().join("|");return d.has(s)?!1:(d.add(s),!0)})}function A(n,r,c=2){const d=new Map;for(const o of r)for(const s of n[o]||[])d.set(s,(d.get(s)||0)+1);return[...d.entries()].sort((o,s)=>s[1]-o[1]).slice(0,c).map(([o])=>o)}async function E(n){const r=new Set(n.knownIds||[]);if(r.size===0)return[];const c=v(n.genreComp,n.genreDrop),d=.15,o=Object.values(c).reduce((t,e)=>t+e,0)||1,s=new Map;for(const t of M(c))try{const e=await w(`query ($g: [String]) { Page(page: 1, perPage: 20) { media(genre_in: $g, type: ANIME, status: FINISHED, isAdult: false, averageScore_greater: 68, sort: SCORE_DESC) { ${C} } } }`,{g:t});for(const i of e?.data?.Page?.media||[])s.has(i.id)||s.set(i.id,i)}catch{}if(s.size===0)return[];const g=[];for(const t of s.values()){if(r.has(t.id)||t.isAdult||t.format==="MUSIC"||t.format==="SPECIAL")continue;const e=t.genres||[],a=.65*(e.reduce((l,p)=>l+(c[p]??d),0)/o)+.25*((t.averageScore||70)/100)+.1*Math.min(1,(t.popularity||0)/2e5);g.push({m:t,score:a,overlap:e.filter(l=>(c[l]??0)>.3)})}g.sort((t,e)=>e.score-t.score);const m=g.slice(0,15),u=new Map;try{const t=await w("query ($ids: [Int]) { Page(page: 1, perPage: 50) { media(id_in: $ids, type: ANIME) { id relations { edges { relationType node { id title { romaji english } type } } } } } }",{ids:m.map(e=>e.m.id)});for(const e of t?.data?.Page?.media||[])u.set(e.id,e)}catch{}return m.map(t=>{const i=u.get(t.m.id)?.relations?.edges?.find(y=>y.relationType==="PREQUEL"&&y.node?.type==="ANIME")?.node;let a=t.score,l="entry",p=null;i&&i.id&&!r.has(i.id)?(l="sequel",p=i.title?.english||i.title?.romaji||null,a-=.3):i&&(l="continue",a+=.05);const b=t.m.genres||[],I=t.m.title?.english||t.m.title?.romaji||"Unknown";return{id:t.m.id,title:I,url:`https://anilist.co/anime/${t.m.id}`,cover:t.m.coverImage?.large||null,year:t.m.seasonYear??null,format:t.m.format||"TV",episodes:t.m.episodes??null,averageScore:t.m.averageScore??null,genres:b,matchPct:Math.round(a*100),sharedGenres:t.overlap,exemplars:A(n.exemplars,b),kind:l,prequelTitle:p}})}let f=null;function T(n){return JSON.stringify([n.genreComp,n.genreDrop,n.knownIds.length])}async function j(n,r){if(!f||f.hash!==r)try{const c=await E(n);c.length>0&&(f={hash:r,candidates:c})}catch(c){P.warn({err:c?.message},"Recommendation candidates failed, reusing previous")}return f?.candidates||[]}async function q(n){const r=await S(n);return j(r,T(r))}export{E as computeRecCandidates,S as computeTasteProfile,q as getCachedRecommendations};
@@ -1,3 +1 @@
1
- export const TEMP_SHOW_ID_PREFIX = 'dango-mt-';
2
- export const isTempShowId = (id) => !!id && /^dango-mt-\d+$/.test(id);
3
- export const isTempSyncRow = (row) => Object.values(row).some((v) => typeof v === 'string' && v.startsWith(TEMP_SHOW_ID_PREFIX));
1
+ const n="dango-mt-",e=o=>!!o&&/^dango-mt-\d+$/.test(o),s=o=>Object.values(o).some(t=>typeof t=="string"&&t.startsWith(n));export{n as TEMP_SHOW_ID_PREFIX,e as isTempShowId,s as isTempSyncRow};
@@ -1,72 +1 @@
1
- import { parseJsonBody } from '../utils/http.utils.js';
2
- const TMDB_BASE = 'https://api.themoviedb.org/3';
3
- const TMDB_IMAGE = 'https://image.tmdb.org/t/p';
4
- const FALLBACK_TMDB_KEY = '9e7096a7575623aa30c66e9cc987e411';
5
- const TMDB_KEYS = [
6
- 'fb7bb23f03b6994dafc674c074d01761',
7
- 'e55425032d3d0f371fc776f302e7c09b',
8
- '8301a21598f8b45668d5711a814f01f6',
9
- '8cf43ad9c085135b9479ad5cf6bbcbda',
10
- 'da63548086e399ffc910fbc08526df05',
11
- '13e53ff644a8bd4ba37b3e1044ad24f3',
12
- '269890f657dddf4635473cf4cf456576',
13
- 'a2f888b27315e62e471b2d587048f32e',
14
- '8476a7ab80ad76f0936744df0430e67c',
15
- '5622cafbfe8f8cfe358a29c53e19bba0',
16
- 'ae4bd1b6fce2a5648671bfc171d15ba4',
17
- '257654f35e3dff105574f97fb4b97035',
18
- '2f4038e83265214a0dcd6ec2eb3276f5',
19
- '9e43f45f94705cc8e1d5a0400d19a7b7',
20
- 'af6887753365e14160254ac7f4345dd2',
21
- '06f10fc8741a672af455421c239a1ffc',
22
- '09ad8ace66eec34302943272db0e8d2c',
23
- ];
24
- let cachedKey = null;
25
- export { TMDB_BASE, TMDB_IMAGE };
26
- export async function getTmdbKey() {
27
- if (!cachedKey) {
28
- cachedKey =
29
- process.env.TMDB_API_KEY ||
30
- TMDB_KEYS[Math.floor(Math.random() * TMDB_KEYS.length)] ||
31
- FALLBACK_TMDB_KEY;
32
- }
33
- return cachedKey;
34
- }
35
- export async function tmdbSearch(query) {
36
- const key = await getTmdbKey();
37
- const url = `${TMDB_BASE}/search/multi?api_key=${key}&query=${encodeURIComponent(query)}&include_adult=false`;
38
- const res = await fetch(url);
39
- if (!res.ok)
40
- return null;
41
- const json = await parseJsonBody(res);
42
- return json.results ?? null;
43
- }
44
- export async function tmdbTvDetails(tmdbId) {
45
- const key = await getTmdbKey();
46
- const url = `${TMDB_BASE}/tv/${tmdbId}?api_key=${key}`;
47
- const res = await fetch(url);
48
- if (!res.ok)
49
- return null;
50
- const json = await parseJsonBody(res);
51
- return json;
52
- }
53
- export async function findTmdbDefaultBackdrop(titleParts) {
54
- const searchNames = [titleParts.english, titleParts.romaji, titleParts.native].filter(Boolean);
55
- for (const name of searchNames) {
56
- const results = await tmdbSearch(name);
57
- if (!results)
58
- continue;
59
- const tvResults = results.filter((r) => r.media_type === 'tv');
60
- if (tvResults.length === 0)
61
- continue;
62
- const bestMatch = tvResults.sort((a, b) => (b.vote_count || 0) - (a.vote_count || 0))[0];
63
- const details = await tmdbTvDetails(bestMatch.id);
64
- if (!details?.backdrop_path)
65
- continue;
66
- return {
67
- backdrop: `${TMDB_IMAGE}/original${details.backdrop_path}`,
68
- overview: details.overview || bestMatch.overview || undefined,
69
- };
70
- }
71
- return null;
72
- }
1
+ import{parseJsonBody as d}from"../utils/http.utils.js";const i="https://api.themoviedb.org/3",b="https://image.tmdb.org/t/p",p="9e7096a7575623aa30c66e9cc987e411",l=["fb7bb23f03b6994dafc674c074d01761","e55425032d3d0f371fc776f302e7c09b","8301a21598f8b45668d5711a814f01f6","8cf43ad9c085135b9479ad5cf6bbcbda","da63548086e399ffc910fbc08526df05","13e53ff644a8bd4ba37b3e1044ad24f3","269890f657dddf4635473cf4cf456576","a2f888b27315e62e471b2d587048f32e","8476a7ab80ad76f0936744df0430e67c","5622cafbfe8f8cfe358a29c53e19bba0","ae4bd1b6fce2a5648671bfc171d15ba4","257654f35e3dff105574f97fb4b97035","2f4038e83265214a0dcd6ec2eb3276f5","9e43f45f94705cc8e1d5a0400d19a7b7","af6887753365e14160254ac7f4345dd2","06f10fc8741a672af455421c239a1ffc","09ad8ace66eec34302943272db0e8d2c"];let s=null;async function u(){return s||(s=process.env.TMDB_API_KEY||l[Math.floor(Math.random()*l.length)]||p),s}async function v(t){const n=await u(),a=`${i}/search/multi?api_key=${n}&query=${encodeURIComponent(t)}&include_adult=false`,e=await fetch(a);return e.ok?(await d(e)).results??null:null}async function g(t){const n=await u(),a=`${i}/tv/${t}?api_key=${n}`,e=await fetch(a);return e.ok?await d(e):null}async function T(t){const n=[t.english,t.romaji,t.native].filter(Boolean);for(const a of n){const e=await v(a);if(!e)continue;const c=e.filter(o=>o.media_type==="tv");if(c.length===0)continue;const f=c.sort((o,m)=>(m.vote_count||0)-(o.vote_count||0))[0],r=await g(f.id);if(r?.backdrop_path)return{backdrop:`${b}/original${r.backdrop_path}`,overview:r.overview||f.overview||void 0}}return null}export{i as TMDB_BASE,b as TMDB_IMAGE,T as findTmdbDefaultBackdrop,u as getTmdbKey,v as tmdbSearch,g as tmdbTvDetails};
@@ -1,82 +1,4 @@
1
- import logger from '../../logger.js';
2
- import { waitForAnilistSlot, applyRateLimitHeaders } from '../anilist.js';
3
- import { parseJsonBody } from '../../utils/http.utils.js';
4
- const ANILIST_GRAPHQL_ENDPOINT = 'https://graphql.anilist.co';
5
- export const ANILIST_TO_DANGO_STATUS = {
6
- CURRENT: 'Watching',
7
- COMPLETED: 'Completed',
8
- PAUSED: 'On-Hold',
9
- DROPPED: 'Dropped',
10
- PLANNING: 'Planned',
11
- };
12
- export const DANGO_TO_ANILIST_STATUS = {
13
- Watching: 'CURRENT',
14
- Reading: 'CURRENT',
15
- Completed: 'COMPLETED',
16
- 'On-Hold': 'PAUSED',
17
- Dropped: 'DROPPED',
18
- Planned: 'PLANNING',
19
- };
20
- export const ANILIST_TO_MANGA_STATUS = {
21
- CURRENT: 'Reading',
22
- COMPLETED: 'Completed',
23
- PAUSED: 'On-Hold',
24
- DROPPED: 'Dropped',
25
- PLANNING: 'Planned',
26
- };
27
- export class AniListTracker {
28
- token;
29
- constructor(token) {
30
- this.token = token;
31
- }
32
- sleep(ms) {
33
- return new Promise((resolve) => setTimeout(resolve, ms));
34
- }
35
- async request(query, variables = {}, options = {}) {
36
- const retries = options.retries ?? 3;
37
- await waitForAnilistSlot();
38
- const headers = {
39
- 'Content-Type': 'application/json',
40
- Accept: 'application/json',
41
- };
42
- if (this.token)
43
- headers.Authorization = `Bearer ${this.token}`;
44
- let response;
45
- try {
46
- response = await fetch(ANILIST_GRAPHQL_ENDPOINT, {
47
- method: 'POST',
48
- headers,
49
- body: JSON.stringify({ query, variables }),
50
- });
51
- }
52
- catch (err) {
53
- logger.error({ err }, '[AniList Tracker] Network error');
54
- throw new Error('Could not reach AniList. Please try again later.', { cause: err });
55
- }
56
- applyRateLimitHeaders(response.headers);
57
- if (response.status === 429) {
58
- if (retries <= 0)
59
- throw new Error('AniList rate limit reached, try again later');
60
- const retryHeader = response.headers.get('retry-after');
61
- const waitSeconds = retryHeader ? Number.parseInt(retryHeader, 10) || 60 : 60;
62
- logger.warn({ waitSeconds }, '[AniList Tracker] Rate limited, waiting before retry');
63
- await this.sleep(Math.min(waitSeconds, 60) * 1000);
64
- return this.request(query, variables, { retries: retries - 1 });
65
- }
66
- if (!response.ok && response.status === 401) {
67
- throw new Error('AniList token is invalid or expired. Please reconnect your account.');
68
- }
69
- if (!response.ok) {
70
- throw new Error(`AniList request failed with status ${response.status}`);
71
- }
72
- const json = await parseJsonBody(response);
73
- if (json.errors && !json.data) {
74
- throw new Error(json.errors.map((e) => e.message).join(', '));
75
- }
76
- return json.data;
77
- }
78
- async getViewer() {
79
- const query = `
1
+ import f from"../../logger.js";import{waitForAnilistSlot as b,applyRateLimitHeaders as I}from"../anilist.js";import{parseJsonBody as A}from"../../utils/http.utils.js";const E="https://graphql.anilist.co",$={CURRENT:"Watching",COMPLETED:"Completed",PAUSED:"On-Hold",DROPPED:"Dropped",PLANNING:"Planned"},h={Watching:"CURRENT",Reading:"CURRENT",Completed:"COMPLETED","On-Hold":"PAUSED",Dropped:"DROPPED",Planned:"PLANNING"},L={CURRENT:"Reading",COMPLETED:"Completed",PAUSED:"On-Hold",DROPPED:"Dropped",PLANNING:"Planned"};class P{constructor(t){this.token=t}token;sleep(t){return new Promise(s=>setTimeout(s,t))}async request(t,s={},o={}){const i=o.retries??3;await b();const d={"Content-Type":"application/json",Accept:"application/json"};this.token&&(d.Authorization=`Bearer ${this.token}`);let r;try{r=await fetch(E,{method:"POST",headers:d,body:JSON.stringify({query:t,variables:s})})}catch(e){throw f.error({err:e},"[AniList Tracker] Network error"),new Error("Could not reach AniList. Please try again later.",{cause:e})}if(I(r.headers),r.status===429){if(i<=0)throw new Error("AniList rate limit reached, try again later");const e=r.headers.get("retry-after"),m=e&&Number.parseInt(e,10)||60;return f.warn({waitSeconds:m},"[AniList Tracker] Rate limited, waiting before retry"),await this.sleep(Math.min(m,60)*1e3),this.request(t,s,{retries:i-1})}if(!r.ok&&r.status===401)throw new Error("AniList token is invalid or expired. Please reconnect your account.");if(!r.ok)throw new Error(`AniList request failed with status ${r.status}`);const n=await A(r);if(n.errors&&!n.data)throw new Error(n.errors.map(e=>e.message).join(", "));return n.data}async getViewer(){const s=await this.request(`
80
2
  query {
81
3
  Viewer {
82
4
  id
@@ -84,16 +6,7 @@ export class AniListTracker {
84
6
  avatar { medium }
85
7
  }
86
8
  }
87
- `;
88
- const data = await this.request(query);
89
- return {
90
- id: data.Viewer.id,
91
- name: data.Viewer.name,
92
- avatar: data.Viewer.avatar?.medium,
93
- };
94
- }
95
- async fetchUserAnimeList(userIdOrName) {
96
- const query = `
9
+ `);return{id:s.Viewer.id,name:s.Viewer.name,avatar:s.Viewer.avatar?.medium}}async fetchUserAnimeList(t){const s=`
97
10
  query ($userId: Int, $userName: String) {
98
11
  MediaListCollection(userId: $userId, userName: $userName, type: ANIME) {
99
12
  lists {
@@ -116,40 +29,7 @@ export class AniListTracker {
116
29
  }
117
30
  }
118
31
  }
119
- `;
120
- const variables = typeof userIdOrName === 'number'
121
- ? { userId: userIdOrName }
122
- : typeof userIdOrName === 'string'
123
- ? { userName: userIdOrName }
124
- : {};
125
- const data = await this.request(query, variables);
126
- const lists = data.MediaListCollection?.lists ?? [];
127
- const entries = [];
128
- for (const list of lists) {
129
- for (const entry of list.entries ?? []) {
130
- if (!entry.media)
131
- continue;
132
- if (entry.status === 'REPEATING')
133
- continue;
134
- entries.push({
135
- mediaId: entry.mediaId,
136
- entryId: entry.id,
137
- idMal: entry.media.idMal ?? undefined,
138
- status: ANILIST_TO_DANGO_STATUS[entry.status] ?? 'Planned',
139
- progress: entry.progress ?? 0,
140
- score: entry.score ?? undefined,
141
- updatedAt: entry.updatedAt ?? 0,
142
- title: entry.media.title ?? {},
143
- coverImage: entry.media.coverImage?.large,
144
- totalEpisodes: entry.media.episodes ?? undefined,
145
- episodeDuration: entry.media.duration ?? undefined,
146
- });
147
- }
148
- }
149
- return entries;
150
- }
151
- async fetchUserMangaList(userIdOrName) {
152
- const query = `
32
+ `,o=typeof t=="number"?{userId:t}:typeof t=="string"?{userName:t}:{},d=(await this.request(s,o)).MediaListCollection?.lists??[],r=[];for(const n of d)for(const e of n.entries??[])e.media&&e.status!=="REPEATING"&&r.push({mediaId:e.mediaId,entryId:e.id,idMal:e.media.idMal??void 0,status:$[e.status]??"Planned",progress:e.progress??0,score:e.score??void 0,updatedAt:e.updatedAt??0,title:e.media.title??{},coverImage:e.media.coverImage?.large,totalEpisodes:e.media.episodes??void 0,episodeDuration:e.media.duration??void 0});return r}async fetchUserMangaList(t){const s=`
153
33
  query ($userId: Int, $userName: String) {
154
34
  MediaListCollection(userId: $userId, userName: $userName, type: MANGA) {
155
35
  lists {
@@ -173,41 +53,7 @@ export class AniListTracker {
173
53
  }
174
54
  }
175
55
  }
176
- `;
177
- const variables = typeof userIdOrName === 'number'
178
- ? { userId: userIdOrName }
179
- : typeof userIdOrName === 'string'
180
- ? { userName: userIdOrName }
181
- : {};
182
- const data = await this.request(query, variables);
183
- const lists = data.MediaListCollection?.lists ?? [];
184
- const entries = [];
185
- for (const list of lists) {
186
- for (const entry of list.entries ?? []) {
187
- if (!entry.media)
188
- continue;
189
- if (entry.status === 'REPEATING')
190
- continue;
191
- entries.push({
192
- mediaId: entry.mediaId,
193
- entryId: entry.id,
194
- idMal: entry.media.idMal ?? undefined,
195
- status: ANILIST_TO_MANGA_STATUS[entry.status] ?? 'Planned',
196
- progress: entry.progress ?? 0,
197
- progressVolumes: entry.progressVolumes ?? undefined,
198
- score: entry.score ?? undefined,
199
- updatedAt: entry.updatedAt ?? 0,
200
- title: entry.media.title ?? {},
201
- coverImage: entry.media.coverImage?.large,
202
- totalChapters: entry.media.chapters ?? undefined,
203
- totalVolumes: entry.media.volumes ?? undefined,
204
- });
205
- }
206
- }
207
- return entries;
208
- }
209
- async updateMediaEntry(params) {
210
- const mutation = `
56
+ `,o=typeof t=="number"?{userId:t}:typeof t=="string"?{userName:t}:{},d=(await this.request(s,o)).MediaListCollection?.lists??[],r=[];for(const n of d)for(const e of n.entries??[])e.media&&e.status!=="REPEATING"&&r.push({mediaId:e.mediaId,entryId:e.id,idMal:e.media.idMal??void 0,status:L[e.status]??"Planned",progress:e.progress??0,progressVolumes:e.progressVolumes??void 0,score:e.score??void 0,updatedAt:e.updatedAt??0,title:e.media.title??{},coverImage:e.media.coverImage?.large,totalChapters:e.media.chapters??void 0,totalVolumes:e.media.volumes??void 0});return r}async updateMediaEntry(t){const s=`
211
57
  mutation ($mediaId: Int, $status: MediaListStatus, $progress: Int, $score: Float) {
212
58
  SaveMediaListEntry(mediaId: $mediaId, status: $status, progress: $progress, score: $score) {
213
59
  id
@@ -216,93 +62,10 @@ export class AniListTracker {
216
62
  progress
217
63
  }
218
64
  }
219
- `;
220
- const variables = { mediaId: params.mediaId };
221
- const status = params.status ? DANGO_TO_ANILIST_STATUS[params.status] : undefined;
222
- if (status)
223
- variables.status = status;
224
- if (params.progress !== undefined)
225
- variables.progress = params.progress;
226
- if (params.score !== undefined)
227
- variables.score = params.score;
228
- return this.request(mutation, variables);
229
- }
230
- async batchUpdateMediaEntries(entries) {
231
- if (entries.length === 0)
232
- return {};
233
- if (entries.length === 1) {
234
- return this.updateMediaEntry(entries[0]);
235
- }
236
- const BATCH_SIZE = 10;
237
- const results = {};
238
- for (let i = 0; i < entries.length; i += BATCH_SIZE) {
239
- const batch = entries.slice(i, i + BATCH_SIZE);
240
- const aliases = [];
241
- const variables = {};
242
- const varDecls = [];
243
- batch.forEach((entry, idx) => {
244
- const prefix = `a${i + idx}`;
245
- const args = [`mediaId: $${prefix}mediaId`];
246
- variables[`${prefix}mediaId`] = entry.mediaId;
247
- varDecls.push(`$${prefix}mediaId: Int!`);
248
- if (entry.status) {
249
- const anilistStatus = DANGO_TO_ANILIST_STATUS[entry.status];
250
- if (anilistStatus) {
251
- args.push(`status: $${prefix}status`);
252
- variables[`${prefix}status`] = anilistStatus;
253
- varDecls.push(`$${prefix}status: MediaListStatus`);
254
- }
255
- }
256
- if (entry.progress !== undefined) {
257
- args.push(`progress: $${prefix}progress`);
258
- variables[`${prefix}progress`] = entry.progress;
259
- varDecls.push(`$${prefix}progress: Int`);
260
- }
261
- if (entry.score !== undefined) {
262
- args.push(`score: $${prefix}score`);
263
- variables[`${prefix}score`] = entry.score;
264
- varDecls.push(`$${prefix}score: Float`);
265
- }
266
- aliases.push(`${prefix}: SaveMediaListEntry(${args.join(', ')}) { id mediaId status progress }`);
267
- });
268
- const mutation = `mutation (${varDecls.join(', ')}) { ${aliases.join('\n')} }`;
269
- const data = await this.request(mutation, variables);
270
- if (data)
271
- Object.assign(results, data);
272
- }
273
- return results;
274
- }
275
- async deleteMediaEntry(entryId) {
276
- const mutation = `
65
+ `,o={mediaId:t.mediaId},i=t.status?h[t.status]:void 0;return i&&(o.status=i),t.progress!==void 0&&(o.progress=t.progress),t.score!==void 0&&(o.score=t.score),this.request(s,o)}async batchUpdateMediaEntries(t){if(t.length===0)return{};if(t.length===1)return this.updateMediaEntry(t[0]);const s=10,o={};for(let i=0;i<t.length;i+=s){const d=t.slice(i,i+s),r=[],n={},e=[];d.forEach((u,l)=>{const a=`a${i+l}`,g=[`mediaId: $${a}mediaId`];if(n[`${a}mediaId`]=u.mediaId,e.push(`$${a}mediaId: Int!`),u.status){const p=h[u.status];p&&(g.push(`status: $${a}status`),n[`${a}status`]=p,e.push(`$${a}status: MediaListStatus`))}u.progress!==void 0&&(g.push(`progress: $${a}progress`),n[`${a}progress`]=u.progress,e.push(`$${a}progress: Int`)),u.score!==void 0&&(g.push(`score: $${a}score`),n[`${a}score`]=u.score,e.push(`$${a}score: Float`)),r.push(`${a}: SaveMediaListEntry(${g.join(", ")}) { id mediaId status progress }`)});const m=`mutation (${e.join(", ")}) { ${r.join(`
66
+ `)} }`,c=await this.request(m,n);c&&Object.assign(o,c)}return o}async deleteMediaEntry(t){return this.request(`
277
67
  mutation ($id: Int) {
278
68
  DeleteMediaListEntry(id: $id) { deleted }
279
69
  }
280
- `;
281
- return this.request(mutation, { id: entryId });
282
- }
283
- async batchDeleteMediaEntries(entryIds) {
284
- if (entryIds.length === 0)
285
- return {};
286
- if (entryIds.length === 1) {
287
- return this.deleteMediaEntry(entryIds[0]);
288
- }
289
- const BATCH_SIZE = 10;
290
- const results = {};
291
- for (let i = 0; i < entryIds.length; i += BATCH_SIZE) {
292
- const batch = entryIds.slice(i, i + BATCH_SIZE);
293
- const aliases = [];
294
- const variables = {};
295
- batch.forEach((entryId, idx) => {
296
- const prefix = `d${i + idx}`;
297
- aliases.push(`${prefix}: DeleteMediaListEntry(id: $${prefix}id) { deleted }`);
298
- variables[`${prefix}id`] = entryId;
299
- });
300
- const varDecls = batch.map((_, idx) => `$d${i + idx}id: Int`).join(', ');
301
- const mutation = `mutation (${varDecls}) { ${aliases.join('\n')} }`;
302
- const data = await this.request(mutation, variables);
303
- if (data)
304
- Object.assign(results, data);
305
- }
306
- return results;
307
- }
308
- }
70
+ `,{id:t})}async batchDeleteMediaEntries(t){if(t.length===0)return{};if(t.length===1)return this.deleteMediaEntry(t[0]);const s=10,o={};for(let i=0;i<t.length;i+=s){const d=t.slice(i,i+s),r=[],n={};d.forEach((u,l)=>{const a=`d${i+l}`;r.push(`${a}: DeleteMediaListEntry(id: $${a}id) { deleted }`),n[`${a}id`]=u});const m=`mutation (${d.map((u,l)=>`$d${i+l}id: Int`).join(", ")}) { ${r.join(`
71
+ `)} }`,c=await this.request(m,n);c&&Object.assign(o,c)}return o}}export{$ as ANILIST_TO_DANGO_STATUS,L as ANILIST_TO_MANGA_STATUS,P as AniListTracker,h as DANGO_TO_ANILIST_STATUS};