@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.
- package/README.md +1 -1
- package/client/dist/assets/{AnimeCard-C3P9zWi9.js → AnimeCard-DKDJK8Mp.js} +1 -1
- package/client/dist/assets/{AnimeInfo-Et6687Tl.js → AnimeInfo-CTcvfjva.js} +1 -1
- package/client/dist/assets/{AnimeInfoPage-DX4eNnlH.js → AnimeInfoPage-B_UiacIN.js} +2 -2
- package/client/dist/assets/{Asmr-CCdEWrFi.js → Asmr-DMSN2vxl.js} +2 -2
- package/client/dist/assets/{AsmrResetProgressModal-DgEWBJOw.js → AsmrResetProgressModal-CRwfOatX.js} +1 -1
- package/client/dist/assets/{AvSyncCalibrator-CQ2fjf2E.js → AvSyncCalibrator-CRJsi-vz.js} +1 -1
- package/client/dist/assets/{Home-DUkUTTTy.js → Home-pwmK9_Eh.js} +1 -1
- package/client/dist/assets/{Insights-QCh4xW12.js → Insights-m7kFzg2A.js} +1 -1
- package/client/dist/assets/{LibraryListPage-BoTxRqHz.js → LibraryListPage-C1wpPvu6.js} +1 -1
- package/client/dist/assets/ListeningList-OzfEI1Ga.js +1 -0
- package/client/dist/assets/{Manga-HydR-L7o.js → Manga-ChRCExhK.js} +1 -1
- package/client/dist/assets/{MangaInfoPage-CBakEI9a.js → MangaInfoPage-DSHJPo2A.js} +1 -1
- package/client/dist/assets/{MangaReadPage-DwAR3iPy.js → MangaReadPage-CzOVY1wA.js} +1 -1
- package/client/dist/assets/{MangaResetProgressModal-DGzl8vsO.js → MangaResetProgressModal-CJfzdUBb.js} +1 -1
- package/client/dist/assets/{Mature-Pi0TxKuF.js → Mature-CifQMfhs.js} +1 -1
- package/client/dist/assets/{MediaCard--cuFxOCS.js → MediaCard-CM10yB9V.js} +1 -1
- package/client/dist/assets/{OptionListMenu-B3D39dGS.js → OptionListMenu-6Kovbsqw.js} +1 -1
- package/client/dist/assets/{Player-CsH4x_I_.js → Player-CK0hT-u4.js} +2 -2
- package/client/dist/assets/{PlayerSettings-DGxBsi3F.js → PlayerSettings-Bb66uqT9.js} +1 -1
- package/client/dist/assets/{QueueOptionsButton-Djq4Cpex.js → QueueOptionsButton-C_nTj8_q.js} +1 -1
- package/client/dist/assets/{QueueRail-DOrYgNVX.js → QueueRail-DUK7URAE.js} +1 -1
- package/client/dist/assets/{ReadingList-DwYoBrNk.js → ReadingList-Ck9LJThU.js} +1 -1
- package/client/dist/assets/{ResetProgressModal-CMF86mX6.js → ResetProgressModal-BUphIv0k.js} +1 -1
- package/client/dist/assets/{Search-DFkhuk-x.js → Search-zJyP15h6.js} +1 -1
- package/client/dist/assets/{Settings-DBB2r0VQ.js → Settings-BYdoSEUh.js} +1 -1
- package/client/dist/assets/{SkeletonGrid-DJelJgAv.js → SkeletonGrid-CqgbOhjX.js} +1 -1
- package/client/dist/assets/{Trackers-DGYXFhYZ.js → Trackers-BVjyv6CM.js} +1 -1
- package/client/dist/assets/{Tv-C5MDGD2Z.js → Tv-L13ylswR.js} +1 -1
- package/client/dist/assets/{TvInfo-DrKuK13u.js → TvInfo-Cvcm5U2D.js} +1 -1
- package/client/dist/assets/{TvPopup-DtHilRDT.js → TvPopup-DhxfKvYm.js} +1 -1
- package/client/dist/assets/{TvSearch-CzNxdu_w.js → TvSearch-P8bFE6_X.js} +1 -1
- package/client/dist/assets/{TvWatchlist-D7p4br6S.js → TvWatchlist-D5gVPjtb.js} +1 -1
- package/client/dist/assets/{Watchlist-DrOzswl2.js → Watchlist-Cjq8l3rU.js} +1 -1
- package/client/dist/assets/{hls-BGCX7mJy.js → hls-BkzuANAg.js} +1 -1
- package/client/dist/assets/{index-CnfZwsds.js → index-CLVgzKLk.js} +19 -19
- package/client/dist/assets/{useAnimeInfoData-DYXWBPF-.js → useAnimeInfoData-DZJZu-so.js} +1 -1
- package/client/dist/assets/{useAsmr-BbhQ59bk.js → useAsmr-CvrMGHuo.js} +1 -1
- package/client/dist/assets/useAsmrPopup-Bimy8Ouw.js +1 -0
- package/client/dist/assets/{useMangaLibrary-DuesWQaE.js → useMangaLibrary--MiN11uR.js} +1 -1
- package/client/dist/assets/{useMangaPopup-D57OVGUr.js → useMangaPopup-CR5XhKKS.js} +1 -1
- package/client/dist/assets/{useProviders-mpQb_dix.js → useProviders-DqdT4fA5.js} +1 -1
- package/client/dist/assets/{useTv-DGflCsSI.js → useTv-BL0HMDyP.js} +1 -1
- package/client/dist/assets/{useTvLibrary-BqeSmyc4.js → useTvLibrary-D4U4iqN8.js} +1 -1
- package/client/dist/index.html +1 -1
- package/package.json +3 -5
- package/server/dist/config.js +1 -174
- package/server/dist/server.js +1068 -289
- package/server/dist/shipped-defaults.js +1 -6
- package/server/package.json +4 -5
- package/client/dist/assets/Asmr.module-CdWlioTY.js +0 -1
- package/client/dist/assets/ListeningList-DguKjI_t.js +0 -1
- package/client/dist/assets/Music-BGH_0voi.css +0 -1
- package/client/dist/assets/Music-CQpyXTLH.js +0 -1
- package/client/dist/assets/Radio-CCHNFN7R.css +0 -1
- package/client/dist/assets/Radio-DF6g6odd.js +0 -1
- package/client/dist/assets/Radio.module-DGSZLpKM.js +0 -1
- package/client/dist/assets/useAsmrPopup-SymV1J-b.js +0 -1
- package/server/dist/app-auth.js +0 -157
- package/server/dist/app-hono.js +0 -598
- package/server/dist/db/drizzle.js +0 -16
- package/server/dist/db/schema-anime.js +0 -61
- package/server/dist/db/schema-asmr.js +0 -13
- package/server/dist/db/schema-manga.js +0 -31
- package/server/dist/db/schema-tv.js +0 -18
- package/server/dist/db.js +0 -217
- package/server/dist/discord-gateway.js +0 -385
- package/server/dist/discord-rpc.js +0 -413
- package/server/dist/github-sync.js +0 -508
- package/server/dist/google.js +0 -741
- package/server/dist/hono/asmr.js +0 -10
- package/server/dist/hono/auth.js +0 -49
- package/server/dist/hono/music.js +0 -209
- package/server/dist/hono/proxy.js +0 -827
- package/server/dist/hono/tv.js +0 -200
- package/server/dist/hono/watchlist.js +0 -341
- package/server/dist/lib/anilist.js +0 -1583
- package/server/dist/lib/anischedule.js +0 -191
- package/server/dist/lib/insights.js +0 -185
- package/server/dist/lib/ipc.js +0 -23
- package/server/dist/lib/kitsu.js +0 -517
- package/server/dist/lib/mal-import.js +0 -291
- package/server/dist/lib/mal.js +0 -957
- package/server/dist/lib/migration.js +0 -310
- package/server/dist/lib/offline-db.js +0 -395
- package/server/dist/lib/potoken.js +0 -135
- package/server/dist/lib/recommendations.service.js +0 -216
- package/server/dist/lib/temp-ids.js +0 -3
- package/server/dist/lib/tmdb.js +0 -72
- package/server/dist/lib/tracker/anilist-tracker.js +0 -308
- package/server/dist/lib/tracker/manga-sync.service.js +0 -773
- package/server/dist/lib/tracker/sync-state.js +0 -21
- package/server/dist/lib/tracker/sync.service.js +0 -476
- package/server/dist/lib/translate.js +0 -79
- package/server/dist/lib/ump.js +0 -106
- package/server/dist/lib/ytmusic.js +0 -162
- package/server/dist/logger.js +0 -16
- package/server/dist/providers/manga/manga.types.js +0 -1
- package/server/dist/providers/provider.interface.js +0 -1
- package/server/dist/providers/remote-loader.js +0 -614
- package/server/dist/providers/remote-types.js +0 -159
- package/server/dist/providers/title-matching.js +0 -226
- package/server/dist/providers/tv.types.js +0 -1
- package/server/dist/rclone.js +0 -120
- package/server/dist/repositories/asmr.repository.js +0 -77
- package/server/dist/repositories/insights.repository.js +0 -106
- package/server/dist/repositories/library.repository.js +0 -94
- package/server/dist/repositories/mal-cache.repository.js +0 -52
- package/server/dist/repositories/manga.repository.js +0 -95
- package/server/dist/repositories/notifications.repository.js +0 -60
- package/server/dist/repositories/queue.repository.js +0 -72
- package/server/dist/repositories/settings.repository.js +0 -30
- package/server/dist/repositories/shows-meta.repository.js +0 -58
- package/server/dist/repositories/temp-show-ids.repository.js +0 -63
- package/server/dist/repositories/tv.repository.js +0 -93
- package/server/dist/repositories/watched-episodes.repository.js +0 -82
- package/server/dist/repositories/watchlist.repository.js +0 -60
- package/server/dist/scripts/backfill-episode-duration.js +0 -42
- package/server/dist/sync-payload.js +0 -91
- package/server/dist/sync.js +0 -657
- package/server/dist/trpc/context.js +0 -7
- package/server/dist/trpc/errors.js +0 -15
- package/server/dist/trpc/index.js +0 -18
- package/server/dist/trpc/router.js +0 -49
- package/server/dist/trpc/routers/asmr-library.js +0 -112
- package/server/dist/trpc/routers/asmr-progress.js +0 -121
- package/server/dist/trpc/routers/asmr.js +0 -87
- package/server/dist/trpc/routers/auth.js +0 -194
- package/server/dist/trpc/routers/continue-watching.js +0 -81
- package/server/dist/trpc/routers/data.js +0 -1026
- package/server/dist/trpc/routers/discord.js +0 -285
- package/server/dist/trpc/routers/discovery.js +0 -11
- package/server/dist/trpc/routers/insights.js +0 -14
- package/server/dist/trpc/routers/manga-library.js +0 -263
- package/server/dist/trpc/routers/manga-progress.js +0 -169
- package/server/dist/trpc/routers/manga.js +0 -201
- package/server/dist/trpc/routers/music.js +0 -655
- package/server/dist/trpc/routers/notifications.js +0 -80
- package/server/dist/trpc/routers/progress.js +0 -109
- package/server/dist/trpc/routers/queue.js +0 -93
- package/server/dist/trpc/routers/radio.js +0 -145
- package/server/dist/trpc/routers/settings.js +0 -96
- package/server/dist/trpc/routers/tracker.js +0 -213
- package/server/dist/trpc/routers/tv-library.js +0 -135
- package/server/dist/trpc/routers/tv-progress.js +0 -159
- package/server/dist/trpc/routers/tv.js +0 -551
- package/server/dist/trpc/routers/watchlist.js +0 -550
- package/server/dist/trpc/validation.js +0 -258
- package/server/dist/utils/cache.utils.js +0 -25
- package/server/dist/utils/cookie.utils.js +0 -13
- package/server/dist/utils/db-utils.js +0 -17
- package/server/dist/utils/env.utils.js +0 -77
- package/server/dist/utils/http.utils.js +0 -82
- package/server/dist/utils/request-context.js +0 -2
- package/server/dist/utils/security.utils.js +0 -76
- /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
|
-
}
|
package/server/dist/lib/tmdb.js
DELETED
|
@@ -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
|
-
}
|