@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,312 @@
|
|
|
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.syncAniList = syncAniList;
|
|
7
|
+
exports.importFromUsername = importFromUsername;
|
|
8
|
+
const anilist_tracker_1 = require("./anilist-tracker");
|
|
9
|
+
const watchlist_repository_1 = require("../../repositories/watchlist.repository");
|
|
10
|
+
const watched_episodes_repository_1 = require("../../repositories/watched-episodes.repository");
|
|
11
|
+
const shows_meta_repository_1 = require("../../repositories/shows-meta.repository");
|
|
12
|
+
const settings_repository_1 = require("../../repositories/settings.repository");
|
|
13
|
+
const notifications_repository_1 = require("../../repositories/notifications.repository");
|
|
14
|
+
const sync_1 = require("../../sync");
|
|
15
|
+
const db_utils_1 = require("../../utils/db-utils");
|
|
16
|
+
const logger_1 = __importDefault(require("../../logger"));
|
|
17
|
+
const TOKEN_KEY = 'tracker_anilist_token';
|
|
18
|
+
const USER_KEY = 'tracker_anilist_user';
|
|
19
|
+
const SYNC_STATE_KEY = 'tracker_anilist_sync_state';
|
|
20
|
+
async function readSyncState(db) {
|
|
21
|
+
const row = await settings_repository_1.SettingsRepository.getByKey(db, SYNC_STATE_KEY);
|
|
22
|
+
if (!row?.value)
|
|
23
|
+
return {};
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(row.value);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function getWatchedCount(db, showId) {
|
|
32
|
+
const row = await (0, db_utils_1.dbGet)(db, 'SELECT COUNT(DISTINCT episodeNumber) as total FROM watched_episodes WHERE showId = ?', [showId]);
|
|
33
|
+
return row?.total ?? 0;
|
|
34
|
+
}
|
|
35
|
+
function resolveStatus(localStatus, remoteStatus, remoteUpdated, localLastSync) {
|
|
36
|
+
if (remoteUpdated > localLastSync && remoteStatus !== localStatus) {
|
|
37
|
+
return { status: remoteStatus, pull: true };
|
|
38
|
+
}
|
|
39
|
+
if (remoteStatus !== localStatus) {
|
|
40
|
+
return { status: localStatus, pull: false };
|
|
41
|
+
}
|
|
42
|
+
return { status: localStatus, pull: false };
|
|
43
|
+
}
|
|
44
|
+
async function syncAniList(db) {
|
|
45
|
+
const tokenRow = await settings_repository_1.SettingsRepository.getByKey(db, TOKEN_KEY);
|
|
46
|
+
const token = tokenRow?.value;
|
|
47
|
+
if (!token)
|
|
48
|
+
throw new Error('AniList is not connected. Please log in first.');
|
|
49
|
+
const tracker = new anilist_tracker_1.AniListTracker(token);
|
|
50
|
+
const viewer = await tracker.getViewer();
|
|
51
|
+
const remoteEntries = await tracker.fetchUserAnimeList(viewer.id);
|
|
52
|
+
const localItems = await watchlist_repository_1.WatchlistRepository.getAll(db);
|
|
53
|
+
const syncState = await readSyncState(db);
|
|
54
|
+
const remoteByMediaId = new Map();
|
|
55
|
+
for (const entry of remoteEntries)
|
|
56
|
+
remoteByMediaId.set(entry.mediaId, entry);
|
|
57
|
+
const localByMediaId = new Map();
|
|
58
|
+
for (const item of localItems) {
|
|
59
|
+
const meta = (await shows_meta_repository_1.ShowsMetaRepository.getById(db, item.id));
|
|
60
|
+
const anilistId = meta?.anilistId;
|
|
61
|
+
if (anilistId == null || !Number.isFinite(anilistId) || String(anilistId) !== String(item.id)) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (!localByMediaId.has(anilistId)) {
|
|
65
|
+
localByMediaId.set(anilistId, item);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const allMediaIds = new Set([
|
|
69
|
+
...localByMediaId.keys(),
|
|
70
|
+
...remoteByMediaId.keys(),
|
|
71
|
+
...Object.keys(syncState)
|
|
72
|
+
.map((k) => Number.parseInt(k, 10))
|
|
73
|
+
.filter((n) => Number.isFinite(n)),
|
|
74
|
+
]);
|
|
75
|
+
const summary = { pushed: 0, pulled: 0, merged: 0, unchanged: 0, errors: [] };
|
|
76
|
+
const watchlistUpserts = [];
|
|
77
|
+
const metaUpserts = [];
|
|
78
|
+
const watchedInserts = [];
|
|
79
|
+
const stateUpdates = {};
|
|
80
|
+
const watchlistDeletes = [];
|
|
81
|
+
const pushUpdates = [];
|
|
82
|
+
const remoteDeleteEntryIds = [];
|
|
83
|
+
const now = Math.floor(Date.now() / 1000);
|
|
84
|
+
for (const mediaId of allMediaIds) {
|
|
85
|
+
const local = localByMediaId.get(mediaId);
|
|
86
|
+
const remote = remoteByMediaId.get(mediaId);
|
|
87
|
+
const showId = String(mediaId);
|
|
88
|
+
try {
|
|
89
|
+
if (local && !remote) {
|
|
90
|
+
if (syncState[showId]) {
|
|
91
|
+
watchlistDeletes.push(showId);
|
|
92
|
+
stateUpdates[showId] = undefined;
|
|
93
|
+
summary.pulled++;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const watchedCount = await getWatchedCount(db, showId);
|
|
97
|
+
pushUpdates.push({
|
|
98
|
+
mediaId,
|
|
99
|
+
status: DANGO_STATUS_OR_FALLBACK(local.status),
|
|
100
|
+
progress: watchedCount,
|
|
101
|
+
});
|
|
102
|
+
stateUpdates[showId] = { lastSyncedAt: now, remoteUpdatedAt: now };
|
|
103
|
+
summary.pushed++;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (!local && remote) {
|
|
107
|
+
if (syncState[showId]) {
|
|
108
|
+
if (remote.entryId) {
|
|
109
|
+
remoteDeleteEntryIds.push({ showId, entryId: remote.entryId });
|
|
110
|
+
stateUpdates[showId] = undefined;
|
|
111
|
+
summary.pushed++;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
stateUpdates[showId] = undefined;
|
|
115
|
+
summary.pushed++;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
logger_1.default.debug({ mediaId, remoteStatus: remote.status }, '[AniList Sync] Remote only -> pulling to local');
|
|
119
|
+
const title = remote.title.english || remote.title.romaji || `Anime #${mediaId}`;
|
|
120
|
+
watchlistUpserts.push({
|
|
121
|
+
id: showId,
|
|
122
|
+
name: title,
|
|
123
|
+
thumbnail: remote.coverImage ?? '',
|
|
124
|
+
status: remote.status,
|
|
125
|
+
nativeName: remote.title.native ?? '',
|
|
126
|
+
englishName: remote.title.english ?? '',
|
|
127
|
+
type: 'TV',
|
|
128
|
+
});
|
|
129
|
+
metaUpserts.push({
|
|
130
|
+
id: showId,
|
|
131
|
+
name: title,
|
|
132
|
+
thumbnail: remote.coverImage,
|
|
133
|
+
nativeName: remote.title.native,
|
|
134
|
+
englishName: remote.title.english,
|
|
135
|
+
episodeCount: remote.totalEpisodes,
|
|
136
|
+
anilistId: mediaId,
|
|
137
|
+
});
|
|
138
|
+
if (remote.progress > 0) {
|
|
139
|
+
watchedInserts.push({ showId, from: 1, to: remote.progress });
|
|
140
|
+
}
|
|
141
|
+
stateUpdates[showId] = { lastSyncedAt: now, remoteUpdatedAt: remote.updatedAt };
|
|
142
|
+
summary.pulled++;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (!local && !remote && syncState[showId]) {
|
|
146
|
+
stateUpdates[showId] = undefined;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (local && remote) {
|
|
150
|
+
const watchedCount = await getWatchedCount(db, showId);
|
|
151
|
+
const localLastSync = syncState[showId]?.lastSyncedAt ?? 0;
|
|
152
|
+
const remoteUpdated = remote.updatedAt || 0;
|
|
153
|
+
const targetProgress = Math.max(watchedCount, remote.progress);
|
|
154
|
+
const shouldPushProgress = watchedCount > remote.progress;
|
|
155
|
+
const shouldPullProgress = remote.progress > watchedCount;
|
|
156
|
+
const { status: targetStatus, pull: pullStatus } = resolveStatus(local.status, remote.status, remoteUpdated, localLastSync);
|
|
157
|
+
const statusDiffers = remote.status !== local.status;
|
|
158
|
+
if (shouldPushProgress || (!pullStatus && statusDiffers)) {
|
|
159
|
+
pushUpdates.push({
|
|
160
|
+
mediaId,
|
|
161
|
+
status: targetStatus,
|
|
162
|
+
progress: targetProgress,
|
|
163
|
+
});
|
|
164
|
+
if (targetStatus !== local.status) {
|
|
165
|
+
watchlistUpserts.push({
|
|
166
|
+
id: showId,
|
|
167
|
+
name: local.name,
|
|
168
|
+
thumbnail: local.thumbnail ?? '',
|
|
169
|
+
status: targetStatus,
|
|
170
|
+
nativeName: local.nativeName ?? '',
|
|
171
|
+
englishName: local.englishName ?? '',
|
|
172
|
+
type: local.type ?? '',
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
if (shouldPullProgress) {
|
|
176
|
+
watchedInserts.push({ showId, from: watchedCount + 1, to: targetProgress });
|
|
177
|
+
}
|
|
178
|
+
stateUpdates[showId] = { lastSyncedAt: now, remoteUpdatedAt: now };
|
|
179
|
+
summary.merged++;
|
|
180
|
+
}
|
|
181
|
+
else if (pullStatus || shouldPullProgress) {
|
|
182
|
+
if (targetStatus !== local.status) {
|
|
183
|
+
watchlistUpserts.push({
|
|
184
|
+
id: showId,
|
|
185
|
+
name: local.name,
|
|
186
|
+
thumbnail: local.thumbnail ?? '',
|
|
187
|
+
status: targetStatus,
|
|
188
|
+
nativeName: local.nativeName ?? '',
|
|
189
|
+
englishName: local.englishName ?? '',
|
|
190
|
+
type: local.type ?? '',
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
if (shouldPullProgress) {
|
|
194
|
+
watchedInserts.push({ showId, from: watchedCount + 1, to: targetProgress });
|
|
195
|
+
}
|
|
196
|
+
stateUpdates[showId] = { lastSyncedAt: now, remoteUpdatedAt: remoteUpdated };
|
|
197
|
+
summary.merged++;
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
summary.unchanged++;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
206
|
+
logger_1.default.warn({ err, mediaId }, '[AniList Sync] Entry failed');
|
|
207
|
+
summary.errors.push(`${local?.name ?? `Media ${mediaId}`}: ${message}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (pushUpdates.length > 0) {
|
|
211
|
+
const batchSize = 10;
|
|
212
|
+
for (let i = 0; i < pushUpdates.length; i += batchSize) {
|
|
213
|
+
const batch = pushUpdates.slice(i, i + batchSize);
|
|
214
|
+
await tracker.batchUpdateMediaEntries(batch);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (remoteDeleteEntryIds.length > 0) {
|
|
218
|
+
const deleteBatch = remoteDeleteEntryIds.map((r) => r.entryId);
|
|
219
|
+
const batchSize = 10;
|
|
220
|
+
for (let i = 0; i < deleteBatch.length; i += batchSize) {
|
|
221
|
+
const batch = deleteBatch.slice(i, i + batchSize);
|
|
222
|
+
await tracker.batchDeleteMediaEntries(batch);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (watchlistUpserts.length > 0 ||
|
|
226
|
+
metaUpserts.length > 0 ||
|
|
227
|
+
watchedInserts.length > 0 ||
|
|
228
|
+
watchlistDeletes.length > 0 ||
|
|
229
|
+
Object.keys(stateUpdates).length > 0) {
|
|
230
|
+
await (0, sync_1.performWriteTransaction)(db, (tx) => {
|
|
231
|
+
for (const item of watchlistUpserts) {
|
|
232
|
+
watchlist_repository_1.WatchlistRepository.upsert(tx, item);
|
|
233
|
+
}
|
|
234
|
+
for (const meta of metaUpserts) {
|
|
235
|
+
shows_meta_repository_1.ShowsMetaRepository.upsert(tx, meta);
|
|
236
|
+
}
|
|
237
|
+
for (const w of watchedInserts) {
|
|
238
|
+
for (let ep = w.from; ep <= w.to; ep++) {
|
|
239
|
+
watched_episodes_repository_1.WatchedEpisodesRepository.upsert(tx, {
|
|
240
|
+
showId: w.showId,
|
|
241
|
+
episodeNumber: String(ep),
|
|
242
|
+
currentTime: 0,
|
|
243
|
+
duration: 0,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
for (const id of watchlistDeletes) {
|
|
248
|
+
watchlist_repository_1.WatchlistRepository.delete(tx, id);
|
|
249
|
+
watched_episodes_repository_1.WatchedEpisodesRepository.deleteByShow(tx, id);
|
|
250
|
+
notifications_repository_1.NotificationsRepository.deleteByShow(tx, id);
|
|
251
|
+
}
|
|
252
|
+
const mergedState = { ...syncState };
|
|
253
|
+
for (const [k, v] of Object.entries(stateUpdates)) {
|
|
254
|
+
if (v === undefined)
|
|
255
|
+
delete mergedState[k];
|
|
256
|
+
else
|
|
257
|
+
mergedState[k] = v;
|
|
258
|
+
}
|
|
259
|
+
settings_repository_1.SettingsRepository.upsert(tx, SYNC_STATE_KEY, JSON.stringify(mergedState));
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return summary;
|
|
263
|
+
}
|
|
264
|
+
function DANGO_STATUS_OR_FALLBACK(status) {
|
|
265
|
+
const known = ['Watching', 'Completed', 'On-Hold', 'Dropped', 'Planned'];
|
|
266
|
+
return known.includes(status) ? status : undefined;
|
|
267
|
+
}
|
|
268
|
+
async function importFromUsername(db, username) {
|
|
269
|
+
const tracker = new anilist_tracker_1.AniListTracker();
|
|
270
|
+
const entries = await tracker.fetchUserAnimeList(username);
|
|
271
|
+
if (entries.length === 0)
|
|
272
|
+
return 0;
|
|
273
|
+
const now = Math.floor(Date.now() / 1000);
|
|
274
|
+
const syncState = await readSyncState(db);
|
|
275
|
+
await (0, sync_1.performWriteTransaction)(db, (tx) => {
|
|
276
|
+
for (const remote of entries) {
|
|
277
|
+
const showId = String(remote.mediaId);
|
|
278
|
+
const title = remote.title.english || remote.title.romaji || `Anime #${remote.mediaId}`;
|
|
279
|
+
watchlist_repository_1.WatchlistRepository.upsert(tx, {
|
|
280
|
+
id: showId,
|
|
281
|
+
name: title,
|
|
282
|
+
thumbnail: remote.coverImage ?? '',
|
|
283
|
+
status: remote.status,
|
|
284
|
+
nativeName: remote.title.native ?? '',
|
|
285
|
+
englishName: remote.title.english ?? '',
|
|
286
|
+
type: 'TV',
|
|
287
|
+
});
|
|
288
|
+
shows_meta_repository_1.ShowsMetaRepository.upsert(tx, {
|
|
289
|
+
id: showId,
|
|
290
|
+
name: title,
|
|
291
|
+
thumbnail: remote.coverImage,
|
|
292
|
+
nativeName: remote.title.native,
|
|
293
|
+
englishName: remote.title.english,
|
|
294
|
+
episodeCount: remote.totalEpisodes,
|
|
295
|
+
anilistId: remote.mediaId,
|
|
296
|
+
});
|
|
297
|
+
if (remote.progress > 0) {
|
|
298
|
+
for (let ep = 1; ep <= remote.progress; ep++) {
|
|
299
|
+
watched_episodes_repository_1.WatchedEpisodesRepository.upsert(tx, {
|
|
300
|
+
showId,
|
|
301
|
+
episodeNumber: String(ep),
|
|
302
|
+
currentTime: 0,
|
|
303
|
+
duration: 0,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
syncState[showId] = { lastSyncedAt: now, remoteUpdatedAt: remote.updatedAt };
|
|
308
|
+
}
|
|
309
|
+
settings_repository_1.SettingsRepository.upsert(tx, SYNC_STATE_KEY, JSON.stringify(syncState));
|
|
310
|
+
});
|
|
311
|
+
return entries.length;
|
|
312
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
const pino_1 = __importDefault(require("pino"));
|
|
7
|
+
const isDevelopment = process.argv.includes('--dev') || process.env.NODE_ENV === 'development';
|
|
8
|
+
const logger = (0, pino_1.default)({
|
|
9
|
+
level: isDevelopment ? 'debug' : 'info',
|
|
10
|
+
transport: {
|
|
11
|
+
target: 'pino-pretty',
|
|
12
|
+
options: {
|
|
13
|
+
colorize: true,
|
|
14
|
+
translateTime: 'SYS:standard',
|
|
15
|
+
ignore: 'pid,hostname',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
exports.default = logger;
|
|
@@ -0,0 +1,240 @@
|
|
|
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._123AnimeProvider = void 0;
|
|
7
|
+
const logger_1 = __importDefault(require("../logger"));
|
|
8
|
+
const title_matching_1 = require("./title-matching");
|
|
9
|
+
const base_provider_1 = require("./base-provider");
|
|
10
|
+
const BASE_URL = 'https://shirayuki-scrapper-api.onrender.com';
|
|
11
|
+
class _123AnimeProvider extends base_provider_1.BaseProvider {
|
|
12
|
+
name = '123Anime';
|
|
13
|
+
normalizeSlugForSearch(title) {
|
|
14
|
+
return title
|
|
15
|
+
.toLowerCase()
|
|
16
|
+
.replace(/[^\w\s-]/g, '')
|
|
17
|
+
.replace(/['"]/g, '')
|
|
18
|
+
.replace(/\s+/g, '-')
|
|
19
|
+
.replace(/-+/g, '-')
|
|
20
|
+
.replace(/^-+|-+$/g, '');
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Picks the best-matching show from a list of search results by comparing
|
|
24
|
+
* how closely each result's title / id matches the query.
|
|
25
|
+
* Scoring (highest wins):
|
|
26
|
+
* 3 – id/slug exact match
|
|
27
|
+
* 2 – title exact match (case-insensitive)
|
|
28
|
+
* 1 – title starts with query
|
|
29
|
+
* 0 – title contains query word (partial)
|
|
30
|
+
* -1 – no match (but still returned as last resort)
|
|
31
|
+
*/
|
|
32
|
+
bestMatch(results, query) {
|
|
33
|
+
const q = query.toLowerCase().trim();
|
|
34
|
+
const qSlug = this.normalizeSlugForSearch(q);
|
|
35
|
+
let best = results[0];
|
|
36
|
+
let bestScore = -1;
|
|
37
|
+
for (const s of results) {
|
|
38
|
+
const id = (s.id || s._id || '').toLowerCase();
|
|
39
|
+
const title = (s.name || '').toLowerCase();
|
|
40
|
+
let score = -1;
|
|
41
|
+
if (id === qSlug || id === q) {
|
|
42
|
+
score = 3;
|
|
43
|
+
}
|
|
44
|
+
else if (title === q) {
|
|
45
|
+
score = 2;
|
|
46
|
+
}
|
|
47
|
+
else if (title.startsWith(q)) {
|
|
48
|
+
score = 1;
|
|
49
|
+
}
|
|
50
|
+
else if (title.includes(q) || id.startsWith(qSlug)) {
|
|
51
|
+
score = 0;
|
|
52
|
+
}
|
|
53
|
+
if (score > bestScore) {
|
|
54
|
+
bestScore = score;
|
|
55
|
+
best = s;
|
|
56
|
+
if (score === 3)
|
|
57
|
+
break; // can't do better
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return best;
|
|
61
|
+
}
|
|
62
|
+
extractSlugFromUrl(url) {
|
|
63
|
+
if (!url)
|
|
64
|
+
return null;
|
|
65
|
+
try {
|
|
66
|
+
const parts = url.split('/');
|
|
67
|
+
const lastPart = parts[parts.length - 1];
|
|
68
|
+
if (lastPart) {
|
|
69
|
+
return lastPart.replace(/\.(jpg|jpeg|png|webp|gif)$/i, '');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
// ignore
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
async search(options) {
|
|
78
|
+
try {
|
|
79
|
+
const rawQuery = options.query || '';
|
|
80
|
+
const query = rawQuery
|
|
81
|
+
.replace(/["",':]/g, ' ')
|
|
82
|
+
.replace(/\s+/g, ' ')
|
|
83
|
+
.trim();
|
|
84
|
+
const performSearch = async (q) => {
|
|
85
|
+
const url = `${BASE_URL}/search?keyword=${encodeURIComponent(q)}`;
|
|
86
|
+
const response = await fetch(url);
|
|
87
|
+
if (!response.ok)
|
|
88
|
+
return [];
|
|
89
|
+
const data = (await response.json());
|
|
90
|
+
if (!data.success || !data.data)
|
|
91
|
+
return [];
|
|
92
|
+
return data.data.filter((anime) => anime.title !== 'Dogge');
|
|
93
|
+
};
|
|
94
|
+
let results = await performSearch(query);
|
|
95
|
+
if (results.length === 0) {
|
|
96
|
+
const words = query.split(/\s+/).filter((w) => w.length >= 3);
|
|
97
|
+
if (words.length > 1) {
|
|
98
|
+
const mid = Math.max(1, Math.floor(words.length / 2));
|
|
99
|
+
const half = words.slice(0, mid).join(' ');
|
|
100
|
+
results = await performSearch(half);
|
|
101
|
+
}
|
|
102
|
+
if (results.length === 0 && words.length > 2) {
|
|
103
|
+
const firstThree = words.slice(0, 3).join(' ');
|
|
104
|
+
results = await performSearch(firstThree);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const mapped = results.map((anime) => {
|
|
108
|
+
const imageUrl = anime.thumbnail || anime.image || anime.poster;
|
|
109
|
+
const slugFromUrl = this.extractSlugFromUrl(imageUrl);
|
|
110
|
+
const titleForSlug = anime.japanese_title || anime.title;
|
|
111
|
+
const id = anime.id || slugFromUrl || this.normalizeSlugForSearch(titleForSlug);
|
|
112
|
+
return {
|
|
113
|
+
_id: id,
|
|
114
|
+
id: id,
|
|
115
|
+
name: anime.title,
|
|
116
|
+
englishName: anime.title,
|
|
117
|
+
thumbnail: imageUrl,
|
|
118
|
+
type: anime.type,
|
|
119
|
+
availableEpisodesDetail: {
|
|
120
|
+
sub: Array.from({ length: Number(anime.episode) || 0 }, (_, i) => (i + 1).toString()),
|
|
121
|
+
dub: [],
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
return mapped;
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
logger_1.default.error({ err: error }, '123Anime search failed');
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async resolveShowId(title, romaji, mode) {
|
|
133
|
+
const targets = [title, romaji].filter((t) => !!t && t.trim().length > 0);
|
|
134
|
+
if (targets.length === 0)
|
|
135
|
+
return null;
|
|
136
|
+
for (const variant of (0, title_matching_1.buildQueryVariants)(title, romaji)) {
|
|
137
|
+
const results = await this.search({ query: variant });
|
|
138
|
+
if (results.length === 0)
|
|
139
|
+
continue;
|
|
140
|
+
const candidates = results.map((r) => ({
|
|
141
|
+
title: r.name || r.englishName || '',
|
|
142
|
+
id: r._id ?? r.id ?? '',
|
|
143
|
+
type: (r.type || '').toLowerCase(),
|
|
144
|
+
}));
|
|
145
|
+
let pool = candidates;
|
|
146
|
+
if (mode) {
|
|
147
|
+
const modeMatched = candidates.filter((c) => c.type === mode);
|
|
148
|
+
if (modeMatched.length > 0) {
|
|
149
|
+
pool = modeMatched;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const matchResult = (0, title_matching_1.pickBestMatch)(pool.map((c) => ({ title: c.title, id: c.id })), targets);
|
|
153
|
+
if (matchResult) {
|
|
154
|
+
return matchResult.item.id;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
async getEpisodes(showId, mode) {
|
|
160
|
+
try {
|
|
161
|
+
const cacheKey = `123anime_eps_${showId}_${mode || 'any'}`;
|
|
162
|
+
const cached = this.cache.get(cacheKey);
|
|
163
|
+
if (cached) {
|
|
164
|
+
return cached;
|
|
165
|
+
}
|
|
166
|
+
const results = await this.search({ query: showId.replace(/ /g, '-') });
|
|
167
|
+
const show = results.find((s) => s.id === showId || s._id === showId) ||
|
|
168
|
+
(results.length > 0 ? this.bestMatch(results, showId) : undefined);
|
|
169
|
+
if (!show || !show.availableEpisodesDetail) {
|
|
170
|
+
logger_1.default.warn({ showId, mode, matchedShow: show }, '123Anime getEpisodes no show or episodes detail found');
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
const episodes = mode === 'dub'
|
|
174
|
+
? show.availableEpisodesDetail.dub || []
|
|
175
|
+
: show.availableEpisodesDetail.sub || [];
|
|
176
|
+
const result = {
|
|
177
|
+
episodes,
|
|
178
|
+
description: '',
|
|
179
|
+
};
|
|
180
|
+
this.cache.set(cacheKey, result, 3600);
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
logger_1.default.error({ err: error, showId, mode }, '123Anime getEpisodes failed');
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async getStreamUrls(showId, episodeNumber, mode) {
|
|
189
|
+
try {
|
|
190
|
+
const query = showId.replace(/ /g, '-');
|
|
191
|
+
const searchResults = await this.search({ query });
|
|
192
|
+
if (!searchResults || searchResults.length === 0) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
const exactMatch = searchResults.find((s) => s.id === showId || s._id === showId) || undefined;
|
|
196
|
+
const modeMatch = mode
|
|
197
|
+
? searchResults.find((s) => (s.id || s._id) === showId && s.type === mode) ||
|
|
198
|
+
searchResults.find((s) => s.type === mode) ||
|
|
199
|
+
undefined
|
|
200
|
+
: undefined;
|
|
201
|
+
const match = modeMatch || exactMatch || this.bestMatch(searchResults, showId);
|
|
202
|
+
const animeId = match.id || match._id;
|
|
203
|
+
const url = `${BASE_URL}/episode-stream?id=${animeId}&ep=${episodeNumber}`;
|
|
204
|
+
const response = await fetch(url);
|
|
205
|
+
if (!response.ok) {
|
|
206
|
+
logger_1.default.warn({ url, status: response.status }, '123Anime stream request failed');
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
const data = (await response.json());
|
|
210
|
+
if (!data.success || !data.data) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
const streamingLink = data.data['streaming_link'] || data.data['stream'] || data.data['url'];
|
|
214
|
+
if (!streamingLink) {
|
|
215
|
+
logger_1.default.warn({ data }, '123Anime No streaming link found in response data');
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
const separator = streamingLink.includes('?') ? '&' : '?';
|
|
219
|
+
const finalUrl = `${streamingLink}${separator}autoplay=1`;
|
|
220
|
+
return [
|
|
221
|
+
{
|
|
222
|
+
sourceName: '123Anime',
|
|
223
|
+
links: [
|
|
224
|
+
{
|
|
225
|
+
resolutionStr: 'auto',
|
|
226
|
+
link: finalUrl,
|
|
227
|
+
hls: false,
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
type: 'iframe',
|
|
231
|
+
},
|
|
232
|
+
];
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
logger_1.default.error({ err: error, showId, episodeNumber, mode }, '123Anime getStreamUrls failed');
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
exports._123AnimeProvider = _123AnimeProvider;
|