@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,216 +0,0 @@
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,3 +0,0 @@
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,72 +0,0 @@
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,308 +0,0 @@
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 = `
80
- query {
81
- Viewer {
82
- id
83
- name
84
- avatar { medium }
85
- }
86
- }
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 = `
97
- query ($userId: Int, $userName: String) {
98
- MediaListCollection(userId: $userId, userName: $userName, type: ANIME) {
99
- lists {
100
- entries {
101
- id
102
- mediaId
103
- status
104
- progress
105
- score(format: POINT_10)
106
- updatedAt
107
- media {
108
- id
109
- idMal
110
- episodes
111
- duration
112
- title { romaji english native }
113
- coverImage { large }
114
- }
115
- }
116
- }
117
- }
118
- }
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 = `
153
- query ($userId: Int, $userName: String) {
154
- MediaListCollection(userId: $userId, userName: $userName, type: MANGA) {
155
- lists {
156
- entries {
157
- id
158
- mediaId
159
- status
160
- progress
161
- progressVolumes
162
- score(format: POINT_10)
163
- updatedAt
164
- media {
165
- id
166
- idMal
167
- chapters
168
- volumes
169
- title { romaji english native }
170
- coverImage { large }
171
- }
172
- }
173
- }
174
- }
175
- }
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 = `
211
- mutation ($mediaId: Int, $status: MediaListStatus, $progress: Int, $score: Float) {
212
- SaveMediaListEntry(mediaId: $mediaId, status: $status, progress: $progress, score: $score) {
213
- id
214
- mediaId
215
- status
216
- progress
217
- }
218
- }
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 = `
277
- mutation ($id: Int) {
278
- DeleteMediaListEntry(id: $id) { deleted }
279
- }
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
- }