@serifpersia/dango 3.0.3
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/LICENSE +21 -0
- package/README.md +247 -0
- package/client/dist/assets/AnimeInfo-CMxs0nRH.css +1 -0
- package/client/dist/assets/AnimeInfo-DVdaocCl.js +1 -0
- package/client/dist/assets/AnimeInfoPage-Bx5EvGt-.js +2 -0
- package/client/dist/assets/Asmr-C2i9GKD4.css +1 -0
- package/client/dist/assets/Asmr-CIjmFtRH.js +2 -0
- package/client/dist/assets/Asmr.module-DPG2JhDJ.js +1 -0
- package/client/dist/assets/AvSyncCalibrator-CIYcyon5.css +1 -0
- package/client/dist/assets/AvSyncCalibrator-vqErH7Cm.js +10 -0
- package/client/dist/assets/ErrorMessage-BwSjzblw.js +1 -0
- package/client/dist/assets/ErrorMessage-CXoRbrLN.css +1 -0
- package/client/dist/assets/Home-D7YfGBUk.js +1 -0
- package/client/dist/assets/Home-DKCfEiwl.css +1 -0
- package/client/dist/assets/Insights-BljI64Ua.css +1 -0
- package/client/dist/assets/Insights-CLQGrlpX.js +1 -0
- package/client/dist/assets/Manga-DQ9HYnpk.js +1 -0
- package/client/dist/assets/Manga-H8Wb04w3.css +1 -0
- package/client/dist/assets/Map-CXFqXmsZ.css +1 -0
- package/client/dist/assets/Map-L_ryVBTq.js +1 -0
- package/client/dist/assets/Mature-BeWzAJ7B.js +1 -0
- package/client/dist/assets/Mature-DJmUUtlF.css +1 -0
- package/client/dist/assets/Player-CYpd__FP.js +30 -0
- package/client/dist/assets/Player-DreAhybc.css +1 -0
- package/client/dist/assets/PlayerRedirect-fxUAp9JV.js +1 -0
- package/client/dist/assets/PlayerSettings-BcsotxP9.js +1 -0
- package/client/dist/assets/PlayerSettings-Cqua1CDD.css +1 -0
- package/client/dist/assets/QueueRail-B9Pgbqc3.js +1 -0
- package/client/dist/assets/QueueRail-BnaltOP8.css +1 -0
- package/client/dist/assets/Radio-BvdY5twD.js +1 -0
- package/client/dist/assets/Radio-CUuYy3DY.css +1 -0
- package/client/dist/assets/Search-BzO-aRP7.css +1 -0
- package/client/dist/assets/Search-R9YLAXS1.js +1 -0
- package/client/dist/assets/Search.module-D8ZbQAiK.js +1 -0
- package/client/dist/assets/Settings-9kYcXdTR.js +21 -0
- package/client/dist/assets/Settings-DF8JThUL.css +1 -0
- package/client/dist/assets/SkeletonGrid-D5vRvX5q.js +1 -0
- package/client/dist/assets/SkeletonGrid-DEbYtO4H.css +1 -0
- package/client/dist/assets/SynopsisText-BlFw0R-0.js +1 -0
- package/client/dist/assets/SynopsisText-CUgOjOxt.css +1 -0
- package/client/dist/assets/Trackers-DfZlnnVq.js +2 -0
- package/client/dist/assets/Trackers-Dt9lR_uP.css +1 -0
- package/client/dist/assets/Tv-CVKC2YBs.css +1 -0
- package/client/dist/assets/Tv-Db1Wx_b7.js +15 -0
- package/client/dist/assets/Watchlist-B2nkNZqM.css +1 -0
- package/client/dist/assets/Watchlist-a77CYtzu.js +1 -0
- package/client/dist/assets/hls-6Owgw2Aj.js +1 -0
- package/client/dist/assets/hls-BOLXTnjL.js +42 -0
- package/client/dist/assets/index-B6qdXFtl.css +1 -0
- package/client/dist/assets/index-B8TPVaIS.js +30354 -0
- package/client/dist/assets/index-CpjgU8ou.js +178 -0
- package/client/dist/assets/useAnimeInfoData-BOfyi9UJ.js +1 -0
- package/client/dist/assets/useIsMobile-CM15uQn8.css +1 -0
- package/client/dist/assets/useIsMobile-Cb8HcKRj.js +1 -0
- package/client/dist/assets/useMatureConsent-CZMYBVXd.js +1 -0
- package/client/dist/assets/vendor-rLNfZm7Y.js +1 -0
- package/client/dist/av-sync-test.mp4 +0 -0
- package/client/dist/favicon.ico +0 -0
- package/client/dist/index.html +31 -0
- package/client/dist/logo-mochi.png +0 -0
- package/client/dist/logo-sakura.png +0 -0
- package/client/dist/logo.png +0 -0
- package/client/dist/placeholder.svg +4 -0
- package/client/dist/robots.txt +3 -0
- package/orchestrator.js +416 -0
- package/package.json +89 -0
- package/server/dist/app-auth.js +231 -0
- package/server/dist/config.js +133 -0
- package/server/dist/controllers/auth.controller.js +247 -0
- package/server/dist/controllers/data.controller.js +785 -0
- package/server/dist/controllers/insights.controller.js +197 -0
- package/server/dist/controllers/proxy.controller.js +665 -0
- package/server/dist/controllers/settings.controller.js +267 -0
- package/server/dist/controllers/watchlist.controller.js +967 -0
- package/server/dist/db.js +152 -0
- package/server/dist/discord-gateway.js +381 -0
- package/server/dist/discord-rpc.js +424 -0
- package/server/dist/github-sync.js +403 -0
- package/server/dist/google.js +549 -0
- package/server/dist/lib/anilist.js +1519 -0
- package/server/dist/lib/anischedule.js +202 -0
- package/server/dist/lib/ipc.js +32 -0
- package/server/dist/lib/kitsu.js +552 -0
- package/server/dist/lib/mal.js +886 -0
- package/server/dist/lib/migration.js +335 -0
- package/server/dist/lib/temp-ids.js +11 -0
- package/server/dist/lib/tmdb.js +71 -0
- package/server/dist/lib/tracker/anilist-tracker.js +250 -0
- package/server/dist/lib/tracker/sync.service.js +312 -0
- package/server/dist/logger.js +19 -0
- package/server/dist/providers/123anime.provider.js +240 -0
- package/server/dist/providers/anilight.provider.js +366 -0
- package/server/dist/providers/animepahe.provider.js +531 -0
- package/server/dist/providers/animeya.provider.js +746 -0
- package/server/dist/providers/base-provider.js +45 -0
- package/server/dist/providers/hn.provider.js +473 -0
- package/server/dist/providers/ht.provider.js +439 -0
- package/server/dist/providers/jasmr.provider.js +504 -0
- package/server/dist/providers/kaa.provider.js +388 -0
- package/server/dist/providers/manga/manga.types.js +10 -0
- package/server/dist/providers/manga/mangadex.provider.js +227 -0
- package/server/dist/providers/manga/mangapill.provider.js +232 -0
- package/server/dist/providers/megaplay.provider.js +583 -0
- package/server/dist/providers/op.provider.js +440 -0
- package/server/dist/providers/provider.interface.js +2 -0
- package/server/dist/providers/title-matching.js +234 -0
- package/server/dist/providers/wh.provider.js +600 -0
- package/server/dist/rclone.js +126 -0
- package/server/dist/repositories/insights.repository.js +43 -0
- package/server/dist/repositories/mal-cache.repository.js +66 -0
- package/server/dist/repositories/notifications.repository.js +40 -0
- package/server/dist/repositories/queue.repository.js +64 -0
- package/server/dist/repositories/settings.repository.js +15 -0
- package/server/dist/repositories/shows-meta.repository.js +43 -0
- package/server/dist/repositories/temp-show-ids.repository.js +54 -0
- package/server/dist/repositories/watched-episodes.repository.js +54 -0
- package/server/dist/repositories/watchlist.repository.js +74 -0
- package/server/dist/routes/asmr.routes.js +72 -0
- package/server/dist/routes/auth.routes.js +28 -0
- package/server/dist/routes/data.routes.js +50 -0
- package/server/dist/routes/discord-gateway.routes.js +57 -0
- package/server/dist/routes/insights.routes.js +12 -0
- package/server/dist/routes/lan-auth.routes.js +49 -0
- package/server/dist/routes/manga.routes.js +115 -0
- package/server/dist/routes/proxy.routes.js +15 -0
- package/server/dist/routes/radio.routes.js +143 -0
- package/server/dist/routes/settings.routes.js +27 -0
- package/server/dist/routes/tracker.routes.js +143 -0
- package/server/dist/routes/translate.routes.js +90 -0
- package/server/dist/routes/tv.routes.js +843 -0
- package/server/dist/routes/watchlist.routes.js +312 -0
- package/server/dist/server.js +310 -0
- package/server/dist/shipped-defaults.js +8 -0
- package/server/dist/sync-config.js +32 -0
- package/server/dist/sync.js +506 -0
- package/server/dist/utils/cookie.utils.js +17 -0
- package/server/dist/utils/db-utils.js +15 -0
- package/server/dist/utils/env.utils.js +83 -0
- package/server/dist/utils/machine-id.js +46 -0
- package/server/dist/utils/request-context.js +5 -0
- package/server/dist/utils/security.utils.js +99 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getMigratedId = getMigratedId;
|
|
7
|
+
const sync_1 = require("../sync");
|
|
8
|
+
const anilist_1 = require("./anilist");
|
|
9
|
+
const temp_ids_1 = require("./temp-ids");
|
|
10
|
+
const watchlist_repository_1 = require("../repositories/watchlist.repository");
|
|
11
|
+
const shows_meta_repository_1 = require("../repositories/shows-meta.repository");
|
|
12
|
+
const db_utils_1 = require("../utils/db-utils");
|
|
13
|
+
const logger_1 = __importDefault(require("../logger"));
|
|
14
|
+
const inFlightMigrations = new Map();
|
|
15
|
+
async function consolidateFromNumeric(db, numericId) {
|
|
16
|
+
// Check if this numeric ID might be a MAL alias that should redirect to the canonical AniList ID
|
|
17
|
+
const metaRow = (0, db_utils_1.dbGet)(db, 'SELECT anilistId FROM shows_meta WHERE id = ? AND anilistId IS NOT NULL', [numericId]);
|
|
18
|
+
// Determine the true anilistId for this numeric ID
|
|
19
|
+
let trueAnilistId;
|
|
20
|
+
if (metaRow?.anilistId != null && String(metaRow.anilistId) !== numericId) {
|
|
21
|
+
// shows_meta already has a different anilistId — this is clearly a MAL alias
|
|
22
|
+
trueAnilistId = metaRow.anilistId;
|
|
23
|
+
}
|
|
24
|
+
else if (!metaRow) {
|
|
25
|
+
// No local shows_meta — only verify via AniList if there's existing data that could be a MAL alias
|
|
26
|
+
if (watchlist_repository_1.WatchlistRepository.exists(db, numericId)) {
|
|
27
|
+
const meta = await (0, anilist_1.getShowMetaById)(numericId);
|
|
28
|
+
if (meta?.anilistId && meta.anilistId !== parseInt(numericId)) {
|
|
29
|
+
trueAnilistId = meta.anilistId;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (trueAnilistId) {
|
|
34
|
+
const canonicalId = String(trueAnilistId);
|
|
35
|
+
if (watchlist_repository_1.WatchlistRepository.exists(db, numericId)) {
|
|
36
|
+
// MAL alias is in the watchlist — migrate all entries to the canonical AniList ID
|
|
37
|
+
logger_1.default.info({ aliasId: numericId, canonicalId }, 'Migrating watchlist from MAL alias to canonical ID');
|
|
38
|
+
await (0, sync_1.performWriteTransaction)(db, (tx) => {
|
|
39
|
+
const aliasWatchlist = watchlist_repository_1.WatchlistRepository.getById(tx, numericId);
|
|
40
|
+
const aliasShowsMeta = shows_meta_repository_1.ShowsMetaRepository.getById(tx, numericId);
|
|
41
|
+
tx.run('UPDATE OR IGNORE watchlist SET id = ?, thumbnail = ? WHERE id = ?', [
|
|
42
|
+
canonicalId,
|
|
43
|
+
aliasWatchlist?.thumbnail || '',
|
|
44
|
+
numericId,
|
|
45
|
+
]);
|
|
46
|
+
tx.run('DELETE FROM watchlist WHERE id = ?', [numericId]);
|
|
47
|
+
tx.run('UPDATE OR IGNORE shows_meta SET id = ?, anilistId = ?, thumbnail = ? WHERE id = ?', [
|
|
48
|
+
canonicalId,
|
|
49
|
+
trueAnilistId,
|
|
50
|
+
aliasShowsMeta?.thumbnail || '',
|
|
51
|
+
numericId,
|
|
52
|
+
]);
|
|
53
|
+
tx.run('DELETE FROM shows_meta WHERE id = ?', [numericId]);
|
|
54
|
+
tx.run('UPDATE OR IGNORE watched_episodes SET showId = ? WHERE showId = ?', [
|
|
55
|
+
canonicalId,
|
|
56
|
+
numericId,
|
|
57
|
+
]);
|
|
58
|
+
tx.run('DELETE FROM watched_episodes WHERE showId = ?', [numericId]);
|
|
59
|
+
tx.run('UPDATE OR IGNORE queue SET showId = ? WHERE showId = ?', [canonicalId, numericId]);
|
|
60
|
+
tx.run('DELETE FROM queue WHERE showId = ?', [numericId]);
|
|
61
|
+
tx.run('UPDATE OR IGNORE dismissed_notifications SET showId = ? WHERE showId = ?', [
|
|
62
|
+
canonicalId,
|
|
63
|
+
numericId,
|
|
64
|
+
]);
|
|
65
|
+
tx.run('DELETE FROM dismissed_notifications WHERE showId = ?', [numericId]);
|
|
66
|
+
tx.run('UPDATE OR IGNORE discovered_notifications SET showId = ? WHERE showId = ?', [
|
|
67
|
+
canonicalId,
|
|
68
|
+
numericId,
|
|
69
|
+
]);
|
|
70
|
+
tx.run('DELETE FROM discovered_notifications WHERE showId = ?', [numericId]);
|
|
71
|
+
tx.run('INSERT OR REPLACE INTO legacy_id_mapping (legacyId, numericId) VALUES (?, ?)', [
|
|
72
|
+
numericId,
|
|
73
|
+
canonicalId,
|
|
74
|
+
]);
|
|
75
|
+
});
|
|
76
|
+
db.scheduleSave();
|
|
77
|
+
return canonicalId;
|
|
78
|
+
}
|
|
79
|
+
// Not in watchlist — check if canonical ID is in the watchlist and save a mapping
|
|
80
|
+
if (watchlist_repository_1.WatchlistRepository.exists(db, canonicalId)) {
|
|
81
|
+
const existing = (0, db_utils_1.dbGet)(db, 'SELECT numericId FROM legacy_id_mapping WHERE legacyId = ?', [numericId]);
|
|
82
|
+
if (!existing || existing.numericId !== canonicalId) {
|
|
83
|
+
(0, db_utils_1.dbRun)(db, 'INSERT OR REPLACE INTO legacy_id_mapping (legacyId, numericId) VALUES (?, ?)', [
|
|
84
|
+
numericId,
|
|
85
|
+
canonicalId,
|
|
86
|
+
]);
|
|
87
|
+
db.scheduleSave();
|
|
88
|
+
logger_1.default.info({ aliasId: numericId, canonicalId }, 'Mapped numeric alias to canonical ID');
|
|
89
|
+
}
|
|
90
|
+
return canonicalId;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (watchlist_repository_1.WatchlistRepository.exists(db, numericId))
|
|
94
|
+
return numericId;
|
|
95
|
+
let legacyId;
|
|
96
|
+
// 1. Check legacy_id_mapping for a legacy ID in the watchlist that maps to this numeric ID
|
|
97
|
+
const legacyRow = (0, db_utils_1.dbGet)(db, 'SELECT legacyId FROM legacy_id_mapping WHERE numericId = ? AND legacyId IN (SELECT id FROM watchlist)', [numericId]);
|
|
98
|
+
if (legacyRow) {
|
|
99
|
+
legacyId = legacyRow.legacyId;
|
|
100
|
+
}
|
|
101
|
+
// 2. If not found, check shows_meta by anilistId where the entry's id IS in the watchlist
|
|
102
|
+
if (!legacyId && trueAnilistId) {
|
|
103
|
+
const metaByAnilist = (0, db_utils_1.dbGet)(db, 'SELECT id FROM shows_meta WHERE anilistId = ? AND id IN (SELECT id FROM watchlist)', [trueAnilistId]);
|
|
104
|
+
if (metaByAnilist) {
|
|
105
|
+
legacyId = metaByAnilist.id;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (!legacyId)
|
|
109
|
+
return numericId;
|
|
110
|
+
logger_1.default.info({ legacyId, numericId }, 'Consolidating watchlist entry from legacy to numeric ID');
|
|
111
|
+
const aniListId = parseInt(numericId);
|
|
112
|
+
await (0, sync_1.performWriteTransaction)(db, (tx) => {
|
|
113
|
+
const legacyWatchlist = watchlist_repository_1.WatchlistRepository.getById(tx, legacyId);
|
|
114
|
+
const legacyShowsMeta = shows_meta_repository_1.ShowsMetaRepository.getById(tx, legacyId);
|
|
115
|
+
tx.run('UPDATE OR IGNORE watchlist SET id = ?, thumbnail = ? WHERE id = ?', [
|
|
116
|
+
numericId,
|
|
117
|
+
legacyWatchlist?.thumbnail || '',
|
|
118
|
+
legacyId,
|
|
119
|
+
]);
|
|
120
|
+
tx.run('DELETE FROM watchlist WHERE id = ?', [legacyId]);
|
|
121
|
+
tx.run('UPDATE OR IGNORE shows_meta SET id = ?, anilistId = ?, thumbnail = ? WHERE id = ?', [
|
|
122
|
+
numericId,
|
|
123
|
+
aniListId,
|
|
124
|
+
legacyShowsMeta?.thumbnail || '',
|
|
125
|
+
legacyId,
|
|
126
|
+
]);
|
|
127
|
+
tx.run('DELETE FROM shows_meta WHERE id = ?', [legacyId]);
|
|
128
|
+
tx.run('UPDATE OR IGNORE watched_episodes SET showId = ? WHERE showId = ?', [
|
|
129
|
+
numericId,
|
|
130
|
+
legacyId,
|
|
131
|
+
]);
|
|
132
|
+
tx.run('DELETE FROM watched_episodes WHERE showId = ?', [legacyId]);
|
|
133
|
+
tx.run('UPDATE OR IGNORE queue SET showId = ? WHERE showId = ?', [numericId, legacyId]);
|
|
134
|
+
tx.run('DELETE FROM queue WHERE showId = ?', [legacyId]);
|
|
135
|
+
tx.run('UPDATE OR IGNORE dismissed_notifications SET showId = ? WHERE showId = ?', [
|
|
136
|
+
numericId,
|
|
137
|
+
legacyId,
|
|
138
|
+
]);
|
|
139
|
+
tx.run('DELETE FROM dismissed_notifications WHERE showId = ?', [legacyId]);
|
|
140
|
+
tx.run('UPDATE OR IGNORE discovered_notifications SET showId = ? WHERE showId = ?', [
|
|
141
|
+
numericId,
|
|
142
|
+
legacyId,
|
|
143
|
+
]);
|
|
144
|
+
tx.run('DELETE FROM discovered_notifications WHERE showId = ?', [legacyId]);
|
|
145
|
+
tx.run('INSERT OR REPLACE INTO legacy_id_mapping (legacyId, numericId) VALUES (?, ?)', [
|
|
146
|
+
legacyId,
|
|
147
|
+
numericId,
|
|
148
|
+
]);
|
|
149
|
+
});
|
|
150
|
+
db.scheduleSave();
|
|
151
|
+
return numericId;
|
|
152
|
+
}
|
|
153
|
+
async function migrateId(db, legacyId) {
|
|
154
|
+
const isNumeric = /^\d+$/.test(legacyId);
|
|
155
|
+
if (isNumeric) {
|
|
156
|
+
return consolidateFromNumeric(db, legacyId);
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
// 1. Check if mapping already exists
|
|
160
|
+
const mapping = (0, db_utils_1.dbGet)(db, 'SELECT numericId FROM legacy_id_mapping WHERE legacyId = ?', [legacyId]);
|
|
161
|
+
if (mapping) {
|
|
162
|
+
const mappedMeta = (await shows_meta_repository_1.ShowsMetaRepository.getById(db, mapping.numericId));
|
|
163
|
+
const canonicalId = mappedMeta?.anilistId ? String(mappedMeta.anilistId) : undefined;
|
|
164
|
+
if (!canonicalId || canonicalId === mapping.numericId) {
|
|
165
|
+
return consolidateFromNumeric(db, mapping.numericId);
|
|
166
|
+
}
|
|
167
|
+
await (0, sync_1.performWriteTransaction)(db, (tx) => {
|
|
168
|
+
const legacyWatchlist = watchlist_repository_1.WatchlistRepository.getById(tx, mapping.numericId);
|
|
169
|
+
const legacyShowsMeta = shows_meta_repository_1.ShowsMetaRepository.getById(tx, mapping.numericId);
|
|
170
|
+
tx.run('UPDATE OR IGNORE watchlist SET id = ?, thumbnail = ? WHERE id = ?', [
|
|
171
|
+
canonicalId,
|
|
172
|
+
legacyWatchlist?.thumbnail || '',
|
|
173
|
+
mapping.numericId,
|
|
174
|
+
]);
|
|
175
|
+
tx.run('DELETE FROM watchlist WHERE id = ?', [mapping.numericId]);
|
|
176
|
+
tx.run('UPDATE OR IGNORE shows_meta SET id = ?, anilistId = ?, thumbnail = ? WHERE id = ?', [
|
|
177
|
+
canonicalId,
|
|
178
|
+
parseInt(canonicalId),
|
|
179
|
+
legacyShowsMeta?.thumbnail || '',
|
|
180
|
+
mapping.numericId,
|
|
181
|
+
]);
|
|
182
|
+
tx.run('DELETE FROM shows_meta WHERE id = ?', [mapping.numericId]);
|
|
183
|
+
tx.run('UPDATE OR IGNORE watched_episodes SET showId = ? WHERE showId = ?', [
|
|
184
|
+
canonicalId,
|
|
185
|
+
mapping.numericId,
|
|
186
|
+
]);
|
|
187
|
+
tx.run('DELETE FROM watched_episodes WHERE showId = ?', [mapping.numericId]);
|
|
188
|
+
tx.run('UPDATE OR IGNORE queue SET showId = ? WHERE showId = ?', [
|
|
189
|
+
canonicalId,
|
|
190
|
+
mapping.numericId,
|
|
191
|
+
]);
|
|
192
|
+
tx.run('DELETE FROM queue WHERE showId = ?', [mapping.numericId]);
|
|
193
|
+
tx.run('UPDATE OR IGNORE dismissed_notifications SET showId = ? WHERE showId = ?', [
|
|
194
|
+
canonicalId,
|
|
195
|
+
mapping.numericId,
|
|
196
|
+
]);
|
|
197
|
+
tx.run('DELETE FROM dismissed_notifications WHERE showId = ?', [mapping.numericId]);
|
|
198
|
+
tx.run('UPDATE OR IGNORE discovered_notifications SET showId = ? WHERE showId = ?', [
|
|
199
|
+
canonicalId,
|
|
200
|
+
mapping.numericId,
|
|
201
|
+
]);
|
|
202
|
+
tx.run('DELETE FROM discovered_notifications WHERE showId = ?', [mapping.numericId]);
|
|
203
|
+
tx.run('UPDATE legacy_id_mapping SET numericId = ? WHERE legacyId = ?', [
|
|
204
|
+
canonicalId,
|
|
205
|
+
legacyId,
|
|
206
|
+
]);
|
|
207
|
+
});
|
|
208
|
+
db.scheduleSave();
|
|
209
|
+
logger_1.default.info({ legacyId, previousId: mapping.numericId, newId: canonicalId }, 'Canonicalized migrated show ID');
|
|
210
|
+
return canonicalId;
|
|
211
|
+
}
|
|
212
|
+
// 2. We need to find the title/name of the show
|
|
213
|
+
let showName;
|
|
214
|
+
// Try shows_meta by direct ID or anilistId
|
|
215
|
+
const localMeta = (await shows_meta_repository_1.ShowsMetaRepository.getById(db, legacyId));
|
|
216
|
+
if (localMeta && localMeta.name) {
|
|
217
|
+
showName = localMeta.name;
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
const metaByAnilist = (0, db_utils_1.dbGet)(db, 'SELECT id, name FROM shows_meta WHERE anilistId = ?', [parseInt(legacyId) || 0]);
|
|
221
|
+
if (metaByAnilist && metaByAnilist.name) {
|
|
222
|
+
showName = metaByAnilist.name;
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
// Try watchlist
|
|
226
|
+
const watchlistEntry = await watchlist_repository_1.WatchlistRepository.getById(db, legacyId);
|
|
227
|
+
if (watchlistEntry && watchlistEntry.name) {
|
|
228
|
+
showName = watchlistEntry.name;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (!showName) {
|
|
233
|
+
logger_1.default.warn({ id: legacyId }, 'No show name found for legacy ID migration');
|
|
234
|
+
return legacyId;
|
|
235
|
+
}
|
|
236
|
+
// 3. Search AniList by title
|
|
237
|
+
const aniListShow = await (0, anilist_1.searchAnilistByTitle)(showName);
|
|
238
|
+
if (!aniListShow) {
|
|
239
|
+
logger_1.default.warn({ id: legacyId, showName }, 'No AniList match found for legacy ID migration');
|
|
240
|
+
return legacyId;
|
|
241
|
+
}
|
|
242
|
+
const newId = String(aniListShow.id);
|
|
243
|
+
let newThumbnail;
|
|
244
|
+
try {
|
|
245
|
+
const freshMeta = await (0, anilist_1.getShowMetaById)(newId);
|
|
246
|
+
newThumbnail = freshMeta?.thumbnail;
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
newThumbnail = undefined;
|
|
250
|
+
}
|
|
251
|
+
// 4. Perform the DB updates across all tables atomically
|
|
252
|
+
await (0, sync_1.performWriteTransaction)(db, (tx) => {
|
|
253
|
+
tx.run('INSERT OR REPLACE INTO legacy_id_mapping (legacyId, numericId) VALUES (?, ?)', [
|
|
254
|
+
legacyId,
|
|
255
|
+
newId,
|
|
256
|
+
]);
|
|
257
|
+
const legacyWatchlist = watchlist_repository_1.WatchlistRepository.getById(tx, legacyId);
|
|
258
|
+
if (legacyWatchlist) {
|
|
259
|
+
const newWatchlistExists = watchlist_repository_1.WatchlistRepository.getById(tx, newId);
|
|
260
|
+
if (newWatchlistExists) {
|
|
261
|
+
watchlist_repository_1.WatchlistRepository.delete(tx, legacyId);
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
tx.run('UPDATE watchlist SET id = ?, thumbnail = ? WHERE id = ?', [
|
|
265
|
+
newId,
|
|
266
|
+
newThumbnail || legacyWatchlist.thumbnail || '',
|
|
267
|
+
legacyId,
|
|
268
|
+
]);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const legacyShowsMeta = shows_meta_repository_1.ShowsMetaRepository.getById(tx, legacyId);
|
|
272
|
+
if (legacyShowsMeta) {
|
|
273
|
+
const newShowsMetaExists = shows_meta_repository_1.ShowsMetaRepository.getById(tx, newId);
|
|
274
|
+
if (newShowsMetaExists) {
|
|
275
|
+
tx.run('DELETE FROM shows_meta WHERE id = ?', [legacyId]);
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
tx.run('UPDATE shows_meta SET id = ?, anilistId = ?, thumbnail = ? WHERE id = ?', [
|
|
279
|
+
newId,
|
|
280
|
+
aniListShow.id,
|
|
281
|
+
newThumbnail || legacyShowsMeta.thumbnail || '',
|
|
282
|
+
legacyId,
|
|
283
|
+
]);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
tx.run('UPDATE OR IGNORE watched_episodes SET showId = ? WHERE showId = ?', [newId, legacyId]);
|
|
287
|
+
tx.run('DELETE FROM watched_episodes WHERE showId = ?', [legacyId]);
|
|
288
|
+
tx.run('UPDATE OR IGNORE queue SET showId = ? WHERE showId = ?', [newId, legacyId]);
|
|
289
|
+
tx.run('DELETE FROM queue WHERE showId = ?', [legacyId]);
|
|
290
|
+
tx.run('UPDATE OR IGNORE dismissed_notifications SET showId = ? WHERE showId = ?', [
|
|
291
|
+
newId,
|
|
292
|
+
legacyId,
|
|
293
|
+
]);
|
|
294
|
+
tx.run('DELETE FROM dismissed_notifications WHERE showId = ?', [legacyId]);
|
|
295
|
+
tx.run('UPDATE OR IGNORE discovered_notifications SET showId = ? WHERE showId = ?', [
|
|
296
|
+
newId,
|
|
297
|
+
legacyId,
|
|
298
|
+
]);
|
|
299
|
+
tx.run('DELETE FROM discovered_notifications WHERE showId = ?', [legacyId]);
|
|
300
|
+
});
|
|
301
|
+
db.scheduleSave();
|
|
302
|
+
logger_1.default.info({ legacyId, newId, showName }, 'Successfully migrated legacy show ID to numeric ID');
|
|
303
|
+
return newId;
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
logger_1.default.error({ err, legacyId }, 'Error migrating legacy show ID');
|
|
307
|
+
return legacyId;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function getMigratedId(db, legacyId) {
|
|
311
|
+
if ((0, temp_ids_1.isTempShowId)(legacyId))
|
|
312
|
+
return Promise.resolve(legacyId);
|
|
313
|
+
if (/^(mal-\d+|-\d+)$/i.test(legacyId)) {
|
|
314
|
+
const mapping = (0, db_utils_1.dbGet)(db, 'SELECT numericId FROM legacy_id_mapping WHERE legacyId = ?', [legacyId]);
|
|
315
|
+
if (mapping)
|
|
316
|
+
return consolidateFromNumeric(db, mapping.numericId);
|
|
317
|
+
return Promise.resolve(legacyId);
|
|
318
|
+
}
|
|
319
|
+
if (/^\d+$/.test(legacyId)) {
|
|
320
|
+
const existing = inFlightMigrations.get(legacyId);
|
|
321
|
+
if (existing)
|
|
322
|
+
return existing;
|
|
323
|
+
const migration = migrateId(db, legacyId);
|
|
324
|
+
inFlightMigrations.set(legacyId, migration);
|
|
325
|
+
migration.then(() => inFlightMigrations.delete(legacyId), () => inFlightMigrations.delete(legacyId));
|
|
326
|
+
return migration;
|
|
327
|
+
}
|
|
328
|
+
const existing = inFlightMigrations.get(legacyId);
|
|
329
|
+
if (existing)
|
|
330
|
+
return existing;
|
|
331
|
+
const migration = migrateId(db, legacyId);
|
|
332
|
+
inFlightMigrations.set(legacyId, migration);
|
|
333
|
+
migration.then(() => inFlightMigrations.delete(legacyId), () => inFlightMigrations.delete(legacyId));
|
|
334
|
+
return migration;
|
|
335
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isTempSyncRow = exports.isTempMatureProvider = exports.isTempShowId = exports.TEMP_MATURE_PROVIDERS = exports.TEMP_SHOW_ID_PREFIX = void 0;
|
|
4
|
+
exports.TEMP_SHOW_ID_PREFIX = 'dango-mt-';
|
|
5
|
+
exports.TEMP_MATURE_PROVIDERS = ['wh', 'op', 'ht', 'hn'];
|
|
6
|
+
const isTempShowId = (id) => !!id && /^dango-mt-\d+$/.test(id);
|
|
7
|
+
exports.isTempShowId = isTempShowId;
|
|
8
|
+
const isTempMatureProvider = (provider) => !!provider && exports.TEMP_MATURE_PROVIDERS.includes(provider.toLowerCase());
|
|
9
|
+
exports.isTempMatureProvider = isTempMatureProvider;
|
|
10
|
+
const isTempSyncRow = (row) => Object.values(row).some((v) => typeof v === 'string' && v.startsWith(exports.TEMP_SHOW_ID_PREFIX));
|
|
11
|
+
exports.isTempSyncRow = isTempSyncRow;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.TMDB_IMAGE = exports.TMDB_BASE = void 0;
|
|
7
|
+
exports.getTmdbKey = getTmdbKey;
|
|
8
|
+
exports.tmdbSearch = tmdbSearch;
|
|
9
|
+
exports.tmdbTvDetails = tmdbTvDetails;
|
|
10
|
+
exports.findTmdbDefaultBackdrop = findTmdbDefaultBackdrop;
|
|
11
|
+
const freekeys_1 = __importDefault(require("freekeys"));
|
|
12
|
+
const TMDB_BASE = 'https://api.themoviedb.org/3';
|
|
13
|
+
exports.TMDB_BASE = TMDB_BASE;
|
|
14
|
+
const TMDB_IMAGE = 'https://image.tmdb.org/t/p';
|
|
15
|
+
exports.TMDB_IMAGE = TMDB_IMAGE;
|
|
16
|
+
let cachedKey = null;
|
|
17
|
+
async function getTmdbKey() {
|
|
18
|
+
if (cachedKey)
|
|
19
|
+
return cachedKey;
|
|
20
|
+
try {
|
|
21
|
+
const keys = await freekeys_1.default.getKeys();
|
|
22
|
+
const tmdbKey = keys.tmdb_key;
|
|
23
|
+
if (tmdbKey)
|
|
24
|
+
cachedKey = tmdbKey;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// silently ignore freekeys failure and use fallback key
|
|
28
|
+
}
|
|
29
|
+
if (!cachedKey) {
|
|
30
|
+
cachedKey = '9e7096a7575623aa30c66e9cc987e411';
|
|
31
|
+
}
|
|
32
|
+
return cachedKey;
|
|
33
|
+
}
|
|
34
|
+
async function tmdbSearch(query) {
|
|
35
|
+
const key = await getTmdbKey();
|
|
36
|
+
const url = `${TMDB_BASE}/search/multi?api_key=${key}&query=${encodeURIComponent(query)}&include_adult=false`;
|
|
37
|
+
const res = await fetch(url);
|
|
38
|
+
if (!res.ok)
|
|
39
|
+
return null;
|
|
40
|
+
const json = (await res.json());
|
|
41
|
+
return json.results ?? null;
|
|
42
|
+
}
|
|
43
|
+
async function tmdbTvDetails(tmdbId) {
|
|
44
|
+
const key = await getTmdbKey();
|
|
45
|
+
const url = `${TMDB_BASE}/tv/${tmdbId}?api_key=${key}`;
|
|
46
|
+
const res = await fetch(url);
|
|
47
|
+
if (!res.ok)
|
|
48
|
+
return null;
|
|
49
|
+
const json = (await res.json());
|
|
50
|
+
return json;
|
|
51
|
+
}
|
|
52
|
+
async function findTmdbDefaultBackdrop(titleParts) {
|
|
53
|
+
const searchNames = [titleParts.english, titleParts.romaji, titleParts.native].filter(Boolean);
|
|
54
|
+
for (const name of searchNames) {
|
|
55
|
+
const results = await tmdbSearch(name);
|
|
56
|
+
if (!results)
|
|
57
|
+
continue;
|
|
58
|
+
const tvResults = results.filter((r) => r.media_type === 'tv');
|
|
59
|
+
if (tvResults.length === 0)
|
|
60
|
+
continue;
|
|
61
|
+
const bestMatch = tvResults.sort((a, b) => (b.vote_count || 0) - (a.vote_count || 0))[0];
|
|
62
|
+
const details = await tmdbTvDetails(bestMatch.id);
|
|
63
|
+
if (!details?.backdrop_path)
|
|
64
|
+
continue;
|
|
65
|
+
return {
|
|
66
|
+
backdrop: `${TMDB_IMAGE}/original${details.backdrop_path}`,
|
|
67
|
+
overview: details.overview || bestMatch.overview || undefined,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.AniListTracker = exports.DANGO_TO_ANILIST_STATUS = exports.ANILIST_TO_DANGO_STATUS = void 0;
|
|
7
|
+
exports.exchangeAuthorizationCode = exchangeAuthorizationCode;
|
|
8
|
+
const logger_1 = __importDefault(require("../../logger"));
|
|
9
|
+
const anilist_1 = require("../anilist");
|
|
10
|
+
const ANILIST_GRAPHQL_ENDPOINT = 'https://graphql.anilist.co';
|
|
11
|
+
exports.ANILIST_TO_DANGO_STATUS = {
|
|
12
|
+
CURRENT: 'Watching',
|
|
13
|
+
COMPLETED: 'Completed',
|
|
14
|
+
PAUSED: 'On-Hold',
|
|
15
|
+
DROPPED: 'Dropped',
|
|
16
|
+
PLANNING: 'Planned',
|
|
17
|
+
};
|
|
18
|
+
exports.DANGO_TO_ANILIST_STATUS = {
|
|
19
|
+
Watching: 'CURRENT',
|
|
20
|
+
Completed: 'COMPLETED',
|
|
21
|
+
'On-Hold': 'PAUSED',
|
|
22
|
+
Dropped: 'DROPPED',
|
|
23
|
+
Planned: 'PLANNING',
|
|
24
|
+
};
|
|
25
|
+
async function exchangeAuthorizationCode() {
|
|
26
|
+
throw new Error('Authorization Code flow removed — use Implicit Grant (response_type=token) instead.');
|
|
27
|
+
}
|
|
28
|
+
class AniListTracker {
|
|
29
|
+
token;
|
|
30
|
+
constructor(token) {
|
|
31
|
+
this.token = token;
|
|
32
|
+
}
|
|
33
|
+
sleep(ms) {
|
|
34
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
35
|
+
}
|
|
36
|
+
async request(query, variables = {}, options = {}) {
|
|
37
|
+
const retries = options.retries ?? 3;
|
|
38
|
+
await (0, anilist_1.waitForAnilistSlot)();
|
|
39
|
+
const headers = {
|
|
40
|
+
'Content-Type': 'application/json',
|
|
41
|
+
Accept: 'application/json',
|
|
42
|
+
};
|
|
43
|
+
if (this.token)
|
|
44
|
+
headers.Authorization = `Bearer ${this.token}`;
|
|
45
|
+
let response;
|
|
46
|
+
try {
|
|
47
|
+
response = await fetch(ANILIST_GRAPHQL_ENDPOINT, {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers,
|
|
50
|
+
body: JSON.stringify({ query, variables }),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
logger_1.default.error({ err }, '[AniList Tracker] Network error');
|
|
55
|
+
throw new Error('Could not reach AniList. Please try again later.', { cause: err });
|
|
56
|
+
}
|
|
57
|
+
(0, anilist_1.applyRateLimitHeaders)(response.headers);
|
|
58
|
+
if (response.status === 429) {
|
|
59
|
+
if (retries <= 0)
|
|
60
|
+
throw new Error('AniList rate limit reached, try again later');
|
|
61
|
+
const retryHeader = response.headers.get('retry-after');
|
|
62
|
+
const waitSeconds = retryHeader ? Number.parseInt(retryHeader, 10) || 60 : 60;
|
|
63
|
+
logger_1.default.warn({ waitSeconds }, '[AniList Tracker] Rate limited, waiting before retry');
|
|
64
|
+
await this.sleep(Math.min(waitSeconds, 60) * 1000);
|
|
65
|
+
return this.request(query, variables, { retries: retries - 1 });
|
|
66
|
+
}
|
|
67
|
+
if (!response.ok && response.status === 401) {
|
|
68
|
+
throw new Error('AniList token is invalid or expired. Please reconnect your account.');
|
|
69
|
+
}
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
throw new Error(`AniList request failed with status ${response.status}`);
|
|
72
|
+
}
|
|
73
|
+
const json = (await response.json());
|
|
74
|
+
if (json.errors && !json.data) {
|
|
75
|
+
throw new Error(json.errors.map((e) => e.message).join(', '));
|
|
76
|
+
}
|
|
77
|
+
return json.data;
|
|
78
|
+
}
|
|
79
|
+
async getViewer() {
|
|
80
|
+
const query = `
|
|
81
|
+
query {
|
|
82
|
+
Viewer {
|
|
83
|
+
id
|
|
84
|
+
name
|
|
85
|
+
avatar { medium }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
`;
|
|
89
|
+
const data = await this.request(query);
|
|
90
|
+
return {
|
|
91
|
+
id: data.Viewer.id,
|
|
92
|
+
name: data.Viewer.name,
|
|
93
|
+
avatar: data.Viewer.avatar?.medium,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
async fetchUserAnimeList(userIdOrName) {
|
|
97
|
+
const query = `
|
|
98
|
+
query ($userId: Int, $userName: String) {
|
|
99
|
+
MediaListCollection(userId: $userId, userName: $userName, type: ANIME) {
|
|
100
|
+
lists {
|
|
101
|
+
entries {
|
|
102
|
+
id
|
|
103
|
+
mediaId
|
|
104
|
+
status
|
|
105
|
+
progress
|
|
106
|
+
score(format: POINT_10)
|
|
107
|
+
updatedAt
|
|
108
|
+
media {
|
|
109
|
+
id
|
|
110
|
+
idMal
|
|
111
|
+
episodes
|
|
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: exports.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
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return entries;
|
|
149
|
+
}
|
|
150
|
+
async updateMediaEntry(params) {
|
|
151
|
+
const mutation = `
|
|
152
|
+
mutation ($mediaId: Int, $status: MediaListStatus, $progress: Int, $score: Float) {
|
|
153
|
+
SaveMediaListEntry(mediaId: $mediaId, status: $status, progress: $progress, score: $score) {
|
|
154
|
+
id
|
|
155
|
+
mediaId
|
|
156
|
+
status
|
|
157
|
+
progress
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
`;
|
|
161
|
+
const variables = { mediaId: params.mediaId };
|
|
162
|
+
const status = params.status ? exports.DANGO_TO_ANILIST_STATUS[params.status] : undefined;
|
|
163
|
+
if (status)
|
|
164
|
+
variables.status = status;
|
|
165
|
+
if (params.progress !== undefined)
|
|
166
|
+
variables.progress = params.progress;
|
|
167
|
+
if (params.score !== undefined)
|
|
168
|
+
variables.score = params.score;
|
|
169
|
+
return this.request(mutation, variables);
|
|
170
|
+
}
|
|
171
|
+
async batchUpdateMediaEntries(entries) {
|
|
172
|
+
if (entries.length === 0)
|
|
173
|
+
return {};
|
|
174
|
+
if (entries.length === 1) {
|
|
175
|
+
return this.updateMediaEntry(entries[0]);
|
|
176
|
+
}
|
|
177
|
+
const BATCH_SIZE = 10;
|
|
178
|
+
const results = {};
|
|
179
|
+
for (let i = 0; i < entries.length; i += BATCH_SIZE) {
|
|
180
|
+
const batch = entries.slice(i, i + BATCH_SIZE);
|
|
181
|
+
const aliases = [];
|
|
182
|
+
const variables = {};
|
|
183
|
+
const varDecls = [];
|
|
184
|
+
batch.forEach((entry, idx) => {
|
|
185
|
+
const prefix = `a${i + idx}`;
|
|
186
|
+
const args = [`mediaId: $${prefix}mediaId`];
|
|
187
|
+
variables[`${prefix}mediaId`] = entry.mediaId;
|
|
188
|
+
varDecls.push(`$${prefix}mediaId: Int!`);
|
|
189
|
+
if (entry.status) {
|
|
190
|
+
const anilistStatus = exports.DANGO_TO_ANILIST_STATUS[entry.status];
|
|
191
|
+
if (anilistStatus) {
|
|
192
|
+
args.push(`status: $${prefix}status`);
|
|
193
|
+
variables[`${prefix}status`] = anilistStatus;
|
|
194
|
+
varDecls.push(`$${prefix}status: MediaListStatus`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (entry.progress !== undefined) {
|
|
198
|
+
args.push(`progress: $${prefix}progress`);
|
|
199
|
+
variables[`${prefix}progress`] = entry.progress;
|
|
200
|
+
varDecls.push(`$${prefix}progress: Int`);
|
|
201
|
+
}
|
|
202
|
+
if (entry.score !== undefined) {
|
|
203
|
+
args.push(`score: $${prefix}score`);
|
|
204
|
+
variables[`${prefix}score`] = entry.score;
|
|
205
|
+
varDecls.push(`$${prefix}score: Float`);
|
|
206
|
+
}
|
|
207
|
+
aliases.push(`${prefix}: SaveMediaListEntry(${args.join(', ')}) { id mediaId status progress }`);
|
|
208
|
+
});
|
|
209
|
+
const mutation = `mutation (${varDecls.join(', ')}) { ${aliases.join('\n')} }`;
|
|
210
|
+
const data = await this.request(mutation, variables);
|
|
211
|
+
if (data)
|
|
212
|
+
Object.assign(results, data);
|
|
213
|
+
}
|
|
214
|
+
return results;
|
|
215
|
+
}
|
|
216
|
+
async deleteMediaEntry(entryId) {
|
|
217
|
+
const mutation = `
|
|
218
|
+
mutation ($id: Int) {
|
|
219
|
+
DeleteMediaListEntry(id: $id) { deleted }
|
|
220
|
+
}
|
|
221
|
+
`;
|
|
222
|
+
return this.request(mutation, { id: entryId });
|
|
223
|
+
}
|
|
224
|
+
async batchDeleteMediaEntries(entryIds) {
|
|
225
|
+
if (entryIds.length === 0)
|
|
226
|
+
return {};
|
|
227
|
+
if (entryIds.length === 1) {
|
|
228
|
+
return this.deleteMediaEntry(entryIds[0]);
|
|
229
|
+
}
|
|
230
|
+
const BATCH_SIZE = 10;
|
|
231
|
+
const results = {};
|
|
232
|
+
for (let i = 0; i < entryIds.length; i += BATCH_SIZE) {
|
|
233
|
+
const batch = entryIds.slice(i, i + BATCH_SIZE);
|
|
234
|
+
const aliases = [];
|
|
235
|
+
const variables = {};
|
|
236
|
+
batch.forEach((entryId, idx) => {
|
|
237
|
+
const prefix = `d${i + idx}`;
|
|
238
|
+
aliases.push(`${prefix}: DeleteMediaListEntry(id: $${prefix}id) { deleted }`);
|
|
239
|
+
variables[`${prefix}id`] = entryId;
|
|
240
|
+
});
|
|
241
|
+
const varDecls = batch.map((_, idx) => `$d${i + idx}id: Int`).join(', ');
|
|
242
|
+
const mutation = `mutation (${varDecls}) { ${aliases.join('\n')} }`;
|
|
243
|
+
const data = await this.request(mutation, variables);
|
|
244
|
+
if (data)
|
|
245
|
+
Object.assign(results, data);
|
|
246
|
+
}
|
|
247
|
+
return results;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
exports.AniListTracker = AniListTracker;
|