@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.
- package/README.md +1 -1
- package/client/dist/assets/{AnimeCard-C3P9zWi9.js → AnimeCard-DraA_TF9.js} +1 -1
- package/client/dist/assets/{AnimeInfo-Et6687Tl.js → AnimeInfo-BJ7pzs-P.js} +1 -1
- package/client/dist/assets/{AnimeInfoPage-DX4eNnlH.js → AnimeInfoPage-B_esUr5C.js} +2 -2
- package/client/dist/assets/{Asmr-CCdEWrFi.js → Asmr-Ckuh2xfW.js} +1 -1
- package/client/dist/assets/{AsmrResetProgressModal-DgEWBJOw.js → AsmrResetProgressModal-BS6UFJN4.js} +1 -1
- package/client/dist/assets/{AvSyncCalibrator-CQ2fjf2E.js → AvSyncCalibrator-DwHCd9yB.js} +1 -1
- package/client/dist/assets/{Home-DUkUTTTy.js → Home-DuM1EuvN.js} +1 -1
- package/client/dist/assets/{Insights-QCh4xW12.js → Insights-D3Pd9c9V.js} +1 -1
- package/client/dist/assets/{LibraryListPage-BoTxRqHz.js → LibraryListPage-Dm2yVn0-.js} +1 -1
- package/client/dist/assets/{ListeningList-DguKjI_t.js → ListeningList-D-zt6EXZ.js} +1 -1
- package/client/dist/assets/{Manga-HydR-L7o.js → Manga-JwprMKBk.js} +1 -1
- package/client/dist/assets/{MangaInfoPage-CBakEI9a.js → MangaInfoPage-DaeyJl2p.js} +1 -1
- package/client/dist/assets/{MangaReadPage-DwAR3iPy.js → MangaReadPage-COg0fHI9.js} +1 -1
- package/client/dist/assets/{MangaResetProgressModal-DGzl8vsO.js → MangaResetProgressModal-kguXv0_M.js} +1 -1
- package/client/dist/assets/{Mature-Pi0TxKuF.js → Mature-IHDaasrs.js} +1 -1
- package/client/dist/assets/{MediaCard--cuFxOCS.js → MediaCard-CpwsrXo5.js} +1 -1
- package/client/dist/assets/{Music-CQpyXTLH.js → Music-Dks4tPeK.js} +1 -1
- package/client/dist/assets/{OptionListMenu-B3D39dGS.js → OptionListMenu-11CcAsy8.js} +1 -1
- package/client/dist/assets/{Player-CsH4x_I_.js → Player-DQoEQkHL.js} +2 -2
- package/client/dist/assets/{PlayerSettings-DGxBsi3F.js → PlayerSettings-D4Ud4kv4.js} +1 -1
- package/client/dist/assets/{QueueOptionsButton-Djq4Cpex.js → QueueOptionsButton-B8PlPKjC.js} +1 -1
- package/client/dist/assets/{QueueRail-DOrYgNVX.js → QueueRail-B4Dok7U3.js} +1 -1
- package/client/dist/assets/{Radio-DF6g6odd.js → Radio-BHK6LSk4.js} +1 -1
- package/client/dist/assets/{ReadingList-DwYoBrNk.js → ReadingList-BCsxIz57.js} +1 -1
- package/client/dist/assets/{ResetProgressModal-CMF86mX6.js → ResetProgressModal-BXHxSfy-.js} +1 -1
- package/client/dist/assets/{Search-DFkhuk-x.js → Search-PK1ds07O.js} +1 -1
- package/client/dist/assets/{Settings-DBB2r0VQ.js → Settings-Bxk_x55l.js} +1 -1
- package/client/dist/assets/{SkeletonGrid-DJelJgAv.js → SkeletonGrid-CpJCZSRN.js} +1 -1
- package/client/dist/assets/{Trackers-DGYXFhYZ.js → Trackers-RieCh_84.js} +1 -1
- package/client/dist/assets/{Tv-C5MDGD2Z.js → Tv-DLytPJUj.js} +1 -1
- package/client/dist/assets/{TvInfo-DrKuK13u.js → TvInfo-C8djKebR.js} +1 -1
- package/client/dist/assets/{TvPopup-DtHilRDT.js → TvPopup-40JGGJDk.js} +1 -1
- package/client/dist/assets/{TvSearch-CzNxdu_w.js → TvSearch-A3hbospf.js} +1 -1
- package/client/dist/assets/{TvWatchlist-D7p4br6S.js → TvWatchlist-DuXjqmZM.js} +1 -1
- package/client/dist/assets/{Watchlist-DrOzswl2.js → Watchlist-B9wGBh0u.js} +1 -1
- package/client/dist/assets/{hls-BGCX7mJy.js → hls-CZ5X3Ya-.js} +1 -1
- package/client/dist/assets/{index-CnfZwsds.js → index-dRhc9-ol.js} +2 -2
- package/client/dist/assets/{useAnimeInfoData-DYXWBPF-.js → useAnimeInfoData-ZzDtLfvD.js} +1 -1
- package/client/dist/assets/{useAsmr-BbhQ59bk.js → useAsmr-CKOdHgol.js} +1 -1
- package/client/dist/assets/{useAsmrPopup-SymV1J-b.js → useAsmrPopup-SFHPhpcl.js} +1 -1
- package/client/dist/assets/{useMangaLibrary-DuesWQaE.js → useMangaLibrary-CKATe31P.js} +1 -1
- package/client/dist/assets/{useMangaPopup-D57OVGUr.js → useMangaPopup-C75QuYgY.js} +1 -1
- package/client/dist/assets/{useProviders-mpQb_dix.js → useProviders-CMtvZbEi.js} +1 -1
- package/client/dist/assets/{useTv-DGflCsSI.js → useTv-DUX4zYMr.js} +1 -1
- package/client/dist/assets/{useTvLibrary-BqeSmyc4.js → useTvLibrary-By4Gt-R_.js} +1 -1
- package/client/dist/index.html +1 -1
- package/package.json +2 -2
- package/server/dist/app-auth.js +1 -157
- package/server/dist/app-hono.js +7 -557
- package/server/dist/config.js +1 -174
- package/server/dist/db/drizzle.js +1 -16
- package/server/dist/db/schema-anime.js +1 -61
- package/server/dist/db/schema-asmr.js +1 -13
- package/server/dist/db/schema-manga.js +1 -31
- package/server/dist/db/schema-tv.js +1 -18
- package/server/dist/db.js +1 -217
- package/server/dist/discord-gateway.js +1 -385
- package/server/dist/discord-rpc.js +1 -413
- package/server/dist/github-sync.js +1 -508
- package/server/dist/google.js +1 -741
- package/server/dist/hono/asmr.js +1 -10
- package/server/dist/hono/auth.js +3 -26
- package/server/dist/hono/music.js +1 -209
- package/server/dist/hono/proxy.js +36 -783
- package/server/dist/hono/tv.js +5 -200
- package/server/dist/hono/watchlist.js +2 -338
- package/server/dist/lib/anilist.js +26 -1458
- package/server/dist/lib/anischedule.js +1 -191
- package/server/dist/lib/insights.js +1 -185
- package/server/dist/lib/ipc.js +1 -23
- package/server/dist/lib/kitsu.js +1 -517
- package/server/dist/lib/mal-import.js +1 -291
- package/server/dist/lib/mal.js +1 -957
- package/server/dist/lib/migration.js +1 -310
- package/server/dist/lib/offline-db.js +4 -384
- package/server/dist/lib/potoken.js +1 -135
- package/server/dist/lib/recommendations.service.js +1 -216
- package/server/dist/lib/temp-ids.js +1 -3
- package/server/dist/lib/tmdb.js +1 -72
- package/server/dist/lib/tracker/anilist-tracker.js +8 -245
- package/server/dist/lib/tracker/manga-sync.service.js +1 -773
- package/server/dist/lib/tracker/sync-state.js +1 -21
- package/server/dist/lib/tracker/sync.service.js +1 -476
- package/server/dist/lib/translate.js +3 -79
- package/server/dist/lib/ump.js +1 -106
- package/server/dist/lib/ytmusic.js +1 -162
- package/server/dist/logger.js +1 -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 +2 -614
- package/server/dist/providers/remote-types.js +1 -159
- package/server/dist/providers/title-matching.js +1 -226
- package/server/dist/providers/tv.types.js +0 -1
- package/server/dist/rclone.js +2 -120
- package/server/dist/repositories/asmr.repository.js +11 -46
- package/server/dist/repositories/insights.repository.js +11 -25
- package/server/dist/repositories/library.repository.js +3 -94
- package/server/dist/repositories/mal-cache.repository.js +1 -52
- package/server/dist/repositories/manga.repository.js +14 -58
- package/server/dist/repositories/notifications.repository.js +6 -52
- package/server/dist/repositories/queue.repository.js +5 -58
- package/server/dist/repositories/settings.repository.js +2 -29
- package/server/dist/repositories/shows-meta.repository.js +3 -56
- package/server/dist/repositories/temp-show-ids.repository.js +3 -63
- package/server/dist/repositories/tv.repository.js +11 -54
- package/server/dist/repositories/watched-episodes.repository.js +6 -55
- package/server/dist/repositories/watchlist.repository.js +3 -54
- package/server/dist/scripts/backfill-episode-duration.js +1 -42
- package/server/dist/server.js +1 -310
- package/server/dist/shipped-defaults.js +1 -6
- package/server/dist/sync-payload.js +1 -91
- package/server/dist/sync.js +3 -647
- package/server/dist/trpc/context.js +1 -7
- package/server/dist/trpc/errors.js +1 -15
- package/server/dist/trpc/index.js +1 -18
- package/server/dist/trpc/router.js +1 -49
- package/server/dist/trpc/routers/asmr-library.js +1 -112
- package/server/dist/trpc/routers/asmr-progress.js +1 -121
- package/server/dist/trpc/routers/asmr.js +1 -87
- package/server/dist/trpc/routers/auth.js +1 -194
- package/server/dist/trpc/routers/continue-watching.js +2 -58
- package/server/dist/trpc/routers/data.js +1 -1026
- package/server/dist/trpc/routers/discord.js +1 -285
- package/server/dist/trpc/routers/discovery.js +1 -11
- package/server/dist/trpc/routers/insights.js +1 -14
- package/server/dist/trpc/routers/manga-library.js +1 -263
- package/server/dist/trpc/routers/manga-progress.js +1 -169
- package/server/dist/trpc/routers/manga.js +1 -201
- package/server/dist/trpc/routers/music.js +1 -655
- package/server/dist/trpc/routers/notifications.js +1 -80
- package/server/dist/trpc/routers/progress.js +1 -109
- package/server/dist/trpc/routers/queue.js +1 -93
- package/server/dist/trpc/routers/radio.js +1 -145
- package/server/dist/trpc/routers/settings.js +1 -96
- package/server/dist/trpc/routers/tracker.js +1 -213
- package/server/dist/trpc/routers/tv-library.js +1 -135
- package/server/dist/trpc/routers/tv-progress.js +1 -159
- package/server/dist/trpc/routers/tv.js +1 -551
- package/server/dist/trpc/routers/watchlist.js +2 -549
- package/server/dist/trpc/validation.js +1 -258
- package/server/dist/utils/cache.utils.js +1 -25
- package/server/dist/utils/cookie.utils.js +1 -13
- package/server/dist/utils/db-utils.js +5 -17
- package/server/dist/utils/env.utils.js +5 -77
- package/server/dist/utils/http.utils.js +1 -82
- package/server/dist/utils/request-context.js +1 -2
- package/server/dist/utils/security.utils.js +1 -76
- package/server/package.json +3 -2
|
@@ -1,216 +1 @@
|
|
|
1
|
-
import logger from
|
|
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
|
-
|
|
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};
|
package/server/dist/lib/tmdb.js
CHANGED
|
@@ -1,72 +1 @@
|
|
|
1
|
-
import
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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};
|