@serifpersia/dango 3.3.8 → 3.4.0
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-D4qo4289.js → AnimeCard-CJy1SXH3.js} +1 -1
- package/client/dist/assets/{AnimeInfo-BdRRm4So.js → AnimeInfo-BoXlVvZ-.js} +1 -1
- package/client/dist/assets/{AnimeInfoPage-DeJNAKMR.js → AnimeInfoPage-BxbA_7AV.js} +2 -2
- package/client/dist/assets/Asmr--Zc8G9zv.js +2 -0
- package/client/dist/assets/AsmrResetProgressModal-CKYm-8cp.js +1 -0
- package/client/dist/assets/AvSyncCalibrator-3yjZS7s6.js +10 -0
- package/client/dist/assets/Home-rrhOGQcg.js +1 -0
- package/client/dist/assets/Insights-Bay90Yeg.js +1 -0
- package/client/dist/assets/{LibraryListPage-D3hTqTRU.js → LibraryListPage-HzuDavbb.js} +1 -1
- package/client/dist/assets/{ListeningList-BgWsfu40.js → ListeningList-DYcQnnsG.js} +1 -1
- package/client/dist/assets/{Manga-Dn-CYins.js → Manga-D8CT9aO0.js} +1 -1
- package/client/dist/assets/{MangaInfoPage-EmQoK7Bv.js → MangaInfoPage-DGK6CYp1.js} +1 -1
- package/client/dist/assets/MangaReadPage-B9XoDKF9.js +1 -0
- package/client/dist/assets/{MangaResetProgressModal-Dian37PQ.js → MangaResetProgressModal-QjZyIAjl.js} +1 -1
- package/client/dist/assets/Mature-CHt_-Hz5.js +1 -0
- package/client/dist/assets/MediaCard-B5584Cq9.js +1 -0
- package/client/dist/assets/Music-HeLrEZ2d.js +1 -0
- package/client/dist/assets/OptionListMenu-CJ90CdwI.js +1 -0
- package/client/dist/assets/Player-BYtEax_z.js +56 -0
- package/client/dist/assets/PlayerSettings-4-cUAY4B.js +1 -0
- package/client/dist/assets/QueueOptionsButton-Ba0dg_82.js +1 -0
- package/client/dist/assets/{QueueRail-B7DeSFoA.js → QueueRail-O7f4RqjB.js} +1 -1
- package/client/dist/assets/Radio-DPZxQnyD.js +1 -0
- package/client/dist/assets/{ReadingList-BT8kUjCB.js → ReadingList-n6rr8ygy.js} +1 -1
- package/client/dist/assets/{ResetProgressModal-DWeiO-hs.js → ResetProgressModal-CcStlbhP.js} +1 -1
- package/client/dist/assets/Search-C2wrWOdy.js +1 -0
- package/client/dist/assets/Settings-CluUYs8U.js +21 -0
- package/client/dist/assets/{SkeletonGrid-B5loz7ii.js → SkeletonGrid-CLEPY9nP.js} +1 -1
- package/client/dist/assets/Trackers-CTGPxs0V.js +2 -0
- package/client/dist/assets/Tv-BwHlDdi8.js +4 -0
- package/client/dist/assets/Tv-ZEA45gXK.css +1 -0
- package/client/dist/assets/TvInfo-CXnrLekr.js +1 -0
- package/client/dist/assets/TvPopup-BekfiNcX.js +1 -0
- package/client/dist/assets/{TvSearch-BYwCf5kO.js → TvSearch-DuqR-z3v.js} +1 -1
- package/client/dist/assets/{TvWatchlist-BiJU6pKT.js → TvWatchlist-CBvgFrA0.js} +1 -1
- package/client/dist/assets/Watchlist-mj8mTOam.js +1 -0
- package/client/dist/assets/{hls-C11FOWHj.js → hls-Boje3IOE.js} +1 -1
- package/client/dist/assets/index-BXwhFWB_.js +178 -0
- package/client/dist/assets/useAnimeInfoData-DJ7A4B4T.js +1 -0
- package/client/dist/assets/useAsmr-DIwIsJXF.js +1 -0
- package/client/dist/assets/{useAsmrPopup-cT_Uu41e.js → useAsmrPopup-e4zNN9ig.js} +1 -1
- package/client/dist/assets/useMangaLibrary-C9Lci5N4.js +1 -0
- package/client/dist/assets/{useMangaPopup-PnmSctHu.js → useMangaPopup-B8nnpvyi.js} +1 -1
- package/client/dist/assets/useProviders-B5Qr4I1y.js +1 -0
- package/client/dist/assets/useTv-Dm8MCYlL.js +1 -0
- package/client/dist/assets/useTvLibrary-B7jEk2Q7.js +1 -0
- package/client/dist/assets/{vendor-query-BXRECSI6.js → vendor-query-BUTye89G.js} +1 -1
- package/client/dist/index.html +2 -2
- package/orchestrator.js +1 -2
- package/package.json +2 -6
- package/server/dist/app-auth.js +9 -29
- package/server/dist/app-hono.js +621 -0
- package/server/dist/config.js +0 -1
- package/server/dist/db/drizzle.js +32 -0
- package/server/dist/db/schema-anime.js +64 -0
- package/server/dist/db/schema-asmr.js +25 -0
- package/server/dist/db/schema-manga.js +31 -0
- package/server/dist/db/schema-tv.js +33 -0
- package/server/dist/db.js +75 -4
- package/server/dist/discord-gateway.js +1 -1
- package/server/dist/google.js +0 -7
- package/server/dist/hono/asmr.js +1 -0
- package/server/dist/hono/auth.js +56 -0
- package/server/dist/hono/music.js +419 -0
- package/server/dist/{controllers/proxy.controller.js → hono/proxy.js} +390 -394
- package/server/dist/hono/tv.js +197 -0
- package/server/dist/hono/watchlist.js +733 -0
- package/server/dist/lib/anilist.js +3 -3
- package/server/dist/lib/insights.js +185 -0
- package/server/dist/lib/ipc.js +1 -2
- package/server/dist/lib/kitsu.js +1 -28
- package/server/dist/lib/mal-import.js +291 -0
- package/server/dist/lib/migration.js +20 -34
- package/server/dist/lib/offline-db.js +36 -36
- package/server/dist/lib/tracker/manga-sync.service.js +21 -21
- package/server/dist/lib/tracker/sync.service.js +20 -20
- package/server/dist/lib/translate.js +79 -0
- package/server/dist/lib/ytmusic.js +15 -4
- package/server/dist/providers/title-matching.js +0 -1
- package/server/dist/repositories/asmr.repository.js +90 -87
- package/server/dist/repositories/insights.repository.js +24 -13
- package/server/dist/repositories/library.repository.js +33 -93
- package/server/dist/repositories/manga.repository.js +108 -116
- package/server/dist/repositories/notifications.repository.js +50 -31
- package/server/dist/repositories/queue.repository.js +36 -25
- package/server/dist/repositories/settings.repository.js +18 -8
- package/server/dist/repositories/shows-meta.repository.js +54 -41
- package/server/dist/repositories/temp-show-ids.repository.js +43 -31
- package/server/dist/repositories/tv.repository.js +98 -115
- package/server/dist/repositories/watched-episodes.repository.js +52 -20
- package/server/dist/repositories/watchlist.repository.js +36 -48
- package/server/dist/server.js +59 -151
- package/server/dist/sync.js +41 -4
- package/server/dist/trpc/context.js +7 -0
- package/server/dist/trpc/errors.js +21 -0
- package/server/dist/trpc/index.js +19 -0
- package/server/dist/trpc/router.js +49 -0
- package/server/dist/trpc/routers/asmr-library.js +122 -0
- package/server/dist/trpc/routers/asmr-progress.js +133 -0
- package/server/dist/trpc/routers/asmr.js +87 -0
- package/server/dist/trpc/routers/auth.js +247 -0
- package/server/dist/trpc/routers/continue-watching.js +83 -0
- package/server/dist/trpc/routers/data.js +1039 -0
- package/server/dist/trpc/routers/discord.js +288 -0
- package/server/dist/trpc/routers/discovery.js +15 -0
- package/server/dist/trpc/routers/insights.js +45 -0
- package/server/dist/trpc/routers/manga-library.js +280 -0
- package/server/dist/trpc/routers/manga-progress.js +181 -0
- package/server/dist/trpc/routers/manga.js +201 -0
- package/server/dist/{controllers/music.controller.js → trpc/routers/music.js} +235 -295
- package/server/dist/trpc/routers/notifications.js +80 -0
- package/server/dist/trpc/routers/progress.js +15 -0
- package/server/dist/trpc/routers/queue.js +95 -0
- package/server/dist/{routes/radio.routes.js → trpc/routers/radio.js} +30 -23
- package/server/dist/trpc/routers/settings.js +105 -0
- package/server/dist/trpc/routers/tracker.js +219 -0
- package/server/dist/trpc/routers/tv-library.js +145 -0
- package/server/dist/trpc/routers/tv-progress.js +171 -0
- package/server/dist/trpc/routers/tv.js +565 -0
- package/server/dist/trpc/routers/watchlist.js +564 -0
- package/server/dist/trpc/validation.js +159 -0
- package/server/dist/utils/security.utils.js +0 -18
- package/server/package.json +5 -10
- package/client/dist/assets/Asmr-D0EGRYsv.js +0 -2
- package/client/dist/assets/AsmrResetProgressModal-Gs0GmVaT.js +0 -1
- package/client/dist/assets/AvSyncCalibrator-D8hqarVB.js +0 -10
- package/client/dist/assets/Home-BnBzjFkW.js +0 -1
- package/client/dist/assets/Insights-i9QgY3nE.js +0 -1
- package/client/dist/assets/MangaReadPage-xJB8K2YU.js +0 -1
- package/client/dist/assets/Mature-DQtXKOXc.js +0 -1
- package/client/dist/assets/MediaCard-DpKoY-1j.js +0 -1
- package/client/dist/assets/Music-BP43Zqmq.js +0 -1
- package/client/dist/assets/OptionListMenu-Bj9ebsW3.js +0 -1
- package/client/dist/assets/Player-O2Cy2GF1.js +0 -56
- package/client/dist/assets/PlayerSettings-DYZ272Yp.js +0 -1
- package/client/dist/assets/QueueOptionsButton-lv26-6jp.js +0 -1
- package/client/dist/assets/Radio-qQBYnVfY.js +0 -1
- package/client/dist/assets/Search-BkvPYN33.js +0 -1
- package/client/dist/assets/Settings-CyZEBF6S.js +0 -21
- package/client/dist/assets/Trackers-JK-aXBYC.js +0 -2
- package/client/dist/assets/Tv-BwXWJxMy.js +0 -4
- package/client/dist/assets/Tv-C2SWcVzN.css +0 -1
- package/client/dist/assets/TvInfo-Dzyq6ucH.js +0 -1
- package/client/dist/assets/TvPopup-PlCU30FO.js +0 -1
- package/client/dist/assets/Watchlist-BOEeGXH9.js +0 -1
- package/client/dist/assets/index-CtoKRNLc.js +0 -178
- package/client/dist/assets/useAnimeInfoData-kr3WOIWH.js +0 -1
- package/client/dist/assets/useAsmr-CB9THf0m.js +0 -1
- package/client/dist/assets/useMangaLibrary-rM6z3_0E.js +0 -1
- package/client/dist/assets/useProviders-B3VB7A9M.js +0 -1
- package/client/dist/assets/useTv-BTUKMH7Q.js +0 -1
- package/client/dist/assets/useTvLibrary-C0sX-ilZ.js +0 -1
- package/server/dist/controllers/asmr-library.controller.js +0 -288
- package/server/dist/controllers/auth.controller.js +0 -225
- package/server/dist/controllers/data.controller.js +0 -767
- package/server/dist/controllers/insights.controller.js +0 -203
- package/server/dist/controllers/manga-library.controller.js +0 -350
- package/server/dist/controllers/settings.controller.js +0 -609
- package/server/dist/controllers/tv-library.controller.js +0 -314
- package/server/dist/controllers/watchlist.controller.js +0 -1069
- package/server/dist/request-logger.js +0 -16
- package/server/dist/routes/asmr-library.routes.js +0 -23
- package/server/dist/routes/asmr.routes.js +0 -81
- package/server/dist/routes/auth.routes.js +0 -25
- package/server/dist/routes/data.routes.js +0 -47
- package/server/dist/routes/discord-gateway.routes.js +0 -51
- package/server/dist/routes/insights.routes.js +0 -35
- package/server/dist/routes/lan-auth.routes.js +0 -46
- package/server/dist/routes/manga-library.routes.js +0 -25
- package/server/dist/routes/manga.routes.js +0 -141
- package/server/dist/routes/music.routes.js +0 -20
- package/server/dist/routes/providers.routes.js +0 -17
- package/server/dist/routes/proxy.routes.js +0 -12
- package/server/dist/routes/settings.routes.js +0 -24
- package/server/dist/routes/tracker.routes.js +0 -287
- package/server/dist/routes/translate.routes.js +0 -88
- package/server/dist/routes/tv-library.routes.js +0 -23
- package/server/dist/routes/tv.routes.js +0 -629
- package/server/dist/routes/watchlist.routes.js +0 -341
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { performWriteTransactionAsync } from '../sync.js';
|
|
2
2
|
import { searchAnilistByTitle, getShowMetaById } from './anilist.js';
|
|
3
3
|
import { isTempShowId } from './temp-ids.js';
|
|
4
4
|
import { WatchlistRepository } from '../repositories/watchlist.repository.js';
|
|
@@ -7,17 +7,13 @@ import { dbGet, dbRun } from '../utils/db-utils.js';
|
|
|
7
7
|
import logger from '../logger.js';
|
|
8
8
|
const inFlightMigrations = new Map();
|
|
9
9
|
async function consolidateFromNumeric(db, numericId) {
|
|
10
|
-
// Check if this numeric ID might be a MAL alias that should redirect to the canonical AniList ID
|
|
11
10
|
const metaRow = dbGet(db, 'SELECT anilistId FROM shows_meta WHERE id = ? AND anilistId IS NOT NULL', [numericId]);
|
|
12
|
-
// Determine the true anilistId for this numeric ID
|
|
13
11
|
let trueAnilistId;
|
|
14
12
|
if (metaRow?.anilistId != null && String(metaRow.anilistId) !== numericId) {
|
|
15
|
-
// shows_meta already has a different anilistId — this is clearly a MAL alias
|
|
16
13
|
trueAnilistId = metaRow.anilistId;
|
|
17
14
|
}
|
|
18
15
|
else if (!metaRow) {
|
|
19
|
-
|
|
20
|
-
if (WatchlistRepository.exists(db, numericId)) {
|
|
16
|
+
if (await WatchlistRepository.exists(db, numericId)) {
|
|
21
17
|
const meta = await getShowMetaById(numericId);
|
|
22
18
|
if (meta?.anilistId && meta.anilistId !== parseInt(numericId)) {
|
|
23
19
|
trueAnilistId = meta.anilistId;
|
|
@@ -26,12 +22,11 @@ async function consolidateFromNumeric(db, numericId) {
|
|
|
26
22
|
}
|
|
27
23
|
if (trueAnilistId) {
|
|
28
24
|
const canonicalId = String(trueAnilistId);
|
|
29
|
-
if (WatchlistRepository.exists(db, numericId)) {
|
|
30
|
-
// MAL alias is in the watchlist — migrate all entries to the canonical AniList ID
|
|
25
|
+
if (await WatchlistRepository.exists(db, numericId)) {
|
|
31
26
|
logger.info({ aliasId: numericId, canonicalId }, 'Migrating watchlist from MAL alias to canonical ID');
|
|
32
|
-
await
|
|
33
|
-
const aliasWatchlist = WatchlistRepository.getById(tx, numericId);
|
|
34
|
-
const aliasShowsMeta = ShowsMetaRepository.getById(tx, numericId);
|
|
27
|
+
await performWriteTransactionAsync(db, async (tx) => {
|
|
28
|
+
const aliasWatchlist = await WatchlistRepository.getById(tx, numericId);
|
|
29
|
+
const aliasShowsMeta = await ShowsMetaRepository.getById(tx, numericId);
|
|
35
30
|
tx.run('UPDATE OR IGNORE watchlist SET id = ?, thumbnail = ? WHERE id = ?', [
|
|
36
31
|
canonicalId,
|
|
37
32
|
aliasWatchlist?.thumbnail || '',
|
|
@@ -70,8 +65,7 @@ async function consolidateFromNumeric(db, numericId) {
|
|
|
70
65
|
db.scheduleSave();
|
|
71
66
|
return canonicalId;
|
|
72
67
|
}
|
|
73
|
-
|
|
74
|
-
if (WatchlistRepository.exists(db, canonicalId)) {
|
|
68
|
+
if (await WatchlistRepository.exists(db, canonicalId)) {
|
|
75
69
|
const existing = dbGet(db, 'SELECT numericId FROM legacy_id_mapping WHERE legacyId = ?', [numericId]);
|
|
76
70
|
if (!existing || existing.numericId !== canonicalId) {
|
|
77
71
|
dbRun(db, 'INSERT OR REPLACE INTO legacy_id_mapping (legacyId, numericId) VALUES (?, ?)', [
|
|
@@ -84,15 +78,13 @@ async function consolidateFromNumeric(db, numericId) {
|
|
|
84
78
|
return canonicalId;
|
|
85
79
|
}
|
|
86
80
|
}
|
|
87
|
-
if (WatchlistRepository.exists(db, numericId))
|
|
81
|
+
if (await WatchlistRepository.exists(db, numericId))
|
|
88
82
|
return numericId;
|
|
89
83
|
let legacyId;
|
|
90
|
-
// 1. Check legacy_id_mapping for a legacy ID in the watchlist that maps to this numeric ID
|
|
91
84
|
const legacyRow = dbGet(db, 'SELECT legacyId FROM legacy_id_mapping WHERE numericId = ? AND legacyId IN (SELECT id FROM watchlist)', [numericId]);
|
|
92
85
|
if (legacyRow) {
|
|
93
86
|
legacyId = legacyRow.legacyId;
|
|
94
87
|
}
|
|
95
|
-
// 2. If not found, check shows_meta by anilistId where the entry's id IS in the watchlist
|
|
96
88
|
if (!legacyId && trueAnilistId) {
|
|
97
89
|
const metaByAnilist = dbGet(db, 'SELECT id FROM shows_meta WHERE anilistId = ? AND id IN (SELECT id FROM watchlist)', [trueAnilistId]);
|
|
98
90
|
if (metaByAnilist) {
|
|
@@ -103,9 +95,9 @@ async function consolidateFromNumeric(db, numericId) {
|
|
|
103
95
|
return numericId;
|
|
104
96
|
logger.info({ legacyId, numericId }, 'Consolidating watchlist entry from legacy to numeric ID');
|
|
105
97
|
const aniListId = parseInt(numericId);
|
|
106
|
-
await
|
|
107
|
-
const legacyWatchlist = WatchlistRepository.getById(tx, legacyId);
|
|
108
|
-
const legacyShowsMeta = ShowsMetaRepository.getById(tx, legacyId);
|
|
98
|
+
await performWriteTransactionAsync(db, async (tx) => {
|
|
99
|
+
const legacyWatchlist = await WatchlistRepository.getById(tx, legacyId);
|
|
100
|
+
const legacyShowsMeta = await ShowsMetaRepository.getById(tx, legacyId);
|
|
109
101
|
tx.run('UPDATE OR IGNORE watchlist SET id = ?, thumbnail = ? WHERE id = ?', [
|
|
110
102
|
numericId,
|
|
111
103
|
legacyWatchlist?.thumbnail || '',
|
|
@@ -150,7 +142,6 @@ async function migrateId(db, legacyId) {
|
|
|
150
142
|
return consolidateFromNumeric(db, legacyId);
|
|
151
143
|
}
|
|
152
144
|
try {
|
|
153
|
-
// 1. Check if mapping already exists
|
|
154
145
|
const mapping = dbGet(db, 'SELECT numericId FROM legacy_id_mapping WHERE legacyId = ?', [legacyId]);
|
|
155
146
|
if (mapping) {
|
|
156
147
|
const mappedMeta = (await ShowsMetaRepository.getById(db, mapping.numericId));
|
|
@@ -158,9 +149,9 @@ async function migrateId(db, legacyId) {
|
|
|
158
149
|
if (!canonicalId || canonicalId === mapping.numericId) {
|
|
159
150
|
return consolidateFromNumeric(db, mapping.numericId);
|
|
160
151
|
}
|
|
161
|
-
await
|
|
162
|
-
const legacyWatchlist = WatchlistRepository.getById(tx, mapping.numericId);
|
|
163
|
-
const legacyShowsMeta = ShowsMetaRepository.getById(tx, mapping.numericId);
|
|
152
|
+
await performWriteTransactionAsync(db, async (tx) => {
|
|
153
|
+
const legacyWatchlist = await WatchlistRepository.getById(tx, mapping.numericId);
|
|
154
|
+
const legacyShowsMeta = await ShowsMetaRepository.getById(tx, mapping.numericId);
|
|
164
155
|
tx.run('UPDATE OR IGNORE watchlist SET id = ?, thumbnail = ? WHERE id = ?', [
|
|
165
156
|
canonicalId,
|
|
166
157
|
legacyWatchlist?.thumbnail || '',
|
|
@@ -203,9 +194,7 @@ async function migrateId(db, legacyId) {
|
|
|
203
194
|
logger.info({ legacyId, previousId: mapping.numericId, newId: canonicalId }, 'Canonicalized migrated show ID');
|
|
204
195
|
return canonicalId;
|
|
205
196
|
}
|
|
206
|
-
// 2. We need to find the title/name of the show
|
|
207
197
|
let showName;
|
|
208
|
-
// Try shows_meta by direct ID or anilistId
|
|
209
198
|
const localMeta = (await ShowsMetaRepository.getById(db, legacyId));
|
|
210
199
|
if (localMeta && localMeta.name) {
|
|
211
200
|
showName = localMeta.name;
|
|
@@ -216,7 +205,6 @@ async function migrateId(db, legacyId) {
|
|
|
216
205
|
showName = metaByAnilist.name;
|
|
217
206
|
}
|
|
218
207
|
else {
|
|
219
|
-
// Try watchlist
|
|
220
208
|
const watchlistEntry = await WatchlistRepository.getById(db, legacyId);
|
|
221
209
|
if (watchlistEntry && watchlistEntry.name) {
|
|
222
210
|
showName = watchlistEntry.name;
|
|
@@ -227,7 +215,6 @@ async function migrateId(db, legacyId) {
|
|
|
227
215
|
logger.warn({ id: legacyId }, 'No show name found for legacy ID migration');
|
|
228
216
|
return legacyId;
|
|
229
217
|
}
|
|
230
|
-
// 3. Search AniList by title
|
|
231
218
|
const aniListShow = await searchAnilistByTitle(showName);
|
|
232
219
|
if (!aniListShow) {
|
|
233
220
|
logger.warn({ id: legacyId, showName }, 'No AniList match found for legacy ID migration');
|
|
@@ -242,17 +229,16 @@ async function migrateId(db, legacyId) {
|
|
|
242
229
|
catch {
|
|
243
230
|
newThumbnail = undefined;
|
|
244
231
|
}
|
|
245
|
-
|
|
246
|
-
await performWriteTransaction(db, (tx) => {
|
|
232
|
+
await performWriteTransactionAsync(db, async (tx) => {
|
|
247
233
|
tx.run('INSERT OR REPLACE INTO legacy_id_mapping (legacyId, numericId) VALUES (?, ?)', [
|
|
248
234
|
legacyId,
|
|
249
235
|
newId,
|
|
250
236
|
]);
|
|
251
|
-
const legacyWatchlist = WatchlistRepository.getById(tx, legacyId);
|
|
237
|
+
const legacyWatchlist = await WatchlistRepository.getById(tx, legacyId);
|
|
252
238
|
if (legacyWatchlist) {
|
|
253
|
-
const newWatchlistExists = WatchlistRepository.getById(tx, newId);
|
|
239
|
+
const newWatchlistExists = await WatchlistRepository.getById(tx, newId);
|
|
254
240
|
if (newWatchlistExists) {
|
|
255
|
-
WatchlistRepository.delete(tx, legacyId);
|
|
241
|
+
await WatchlistRepository.delete(tx, legacyId);
|
|
256
242
|
}
|
|
257
243
|
else {
|
|
258
244
|
tx.run('UPDATE watchlist SET id = ?, thumbnail = ? WHERE id = ?', [
|
|
@@ -262,9 +248,9 @@ async function migrateId(db, legacyId) {
|
|
|
262
248
|
]);
|
|
263
249
|
}
|
|
264
250
|
}
|
|
265
|
-
const legacyShowsMeta = ShowsMetaRepository.getById(tx, legacyId);
|
|
251
|
+
const legacyShowsMeta = await ShowsMetaRepository.getById(tx, legacyId);
|
|
266
252
|
if (legacyShowsMeta) {
|
|
267
|
-
const newShowsMetaExists = ShowsMetaRepository.getById(tx, newId);
|
|
253
|
+
const newShowsMetaExists = await ShowsMetaRepository.getById(tx, newId);
|
|
268
254
|
if (newShowsMetaExists) {
|
|
269
255
|
tx.run('DELETE FROM shows_meta WHERE id = ?', [legacyId]);
|
|
270
256
|
}
|
|
@@ -122,8 +122,8 @@ class OfflineDb {
|
|
|
122
122
|
ensureTable(db);
|
|
123
123
|
this.loadCache(db);
|
|
124
124
|
log.info(`OfflineDb loaded ${this.malToEntry.size} MAL mappings from SQLite`);
|
|
125
|
-
const wasInterrupted = SettingsRepository.getByKey(db, 'offlineDbLastStatus')?.value === 'updating';
|
|
126
|
-
const autoUpdateSetting = SettingsRepository.getByKey(db, 'offlineDbAutoUpdateEnabled');
|
|
125
|
+
const wasInterrupted = (await SettingsRepository.getByKey(db, 'offlineDbLastStatus'))?.value === 'updating';
|
|
126
|
+
const autoUpdateSetting = await SettingsRepository.getByKey(db, 'offlineDbAutoUpdateEnabled');
|
|
127
127
|
const autoUpdateEnabled = !autoUpdateSetting || autoUpdateSetting.value !== 'false';
|
|
128
128
|
this.backfillShowsMetaGenres(db);
|
|
129
129
|
if (this.malToEntry.size === 0) {
|
|
@@ -132,17 +132,17 @@ class OfflineDb {
|
|
|
132
132
|
log.warn({ err: err?.message }, 'Background offline database download failed');
|
|
133
133
|
});
|
|
134
134
|
}
|
|
135
|
-
else if (wasInterrupted && autoUpdateEnabled && this.interruptedRetryDue(db)) {
|
|
135
|
+
else if (wasInterrupted && autoUpdateEnabled && (await this.interruptedRetryDue(db))) {
|
|
136
136
|
log.info('Previous offline database refresh was interrupted. Retrying in background...');
|
|
137
|
-
SettingsRepository.upsert(db, 'offlineDbLastStatus', 'idle');
|
|
138
|
-
SettingsRepository.upsert(db, 'offlineDbLastMessage', 'Previous refresh was interrupted by a restart. Retrying automatically...');
|
|
137
|
+
await SettingsRepository.upsert(db, 'offlineDbLastStatus', 'idle');
|
|
138
|
+
await SettingsRepository.upsert(db, 'offlineDbLastMessage', 'Previous refresh was interrupted by a restart. Retrying automatically...');
|
|
139
139
|
this.refreshDatabase(db).catch((err) => {
|
|
140
140
|
log.warn({ err: err?.message }, 'Interrupted offline database retry failed');
|
|
141
141
|
});
|
|
142
142
|
}
|
|
143
143
|
else if (wasInterrupted) {
|
|
144
|
-
SettingsRepository.upsert(db, 'offlineDbLastStatus', 'idle');
|
|
145
|
-
SettingsRepository.upsert(db, 'offlineDbLastMessage', 'Previous refresh was interrupted by a restart. Trigger an update to retry.');
|
|
144
|
+
await SettingsRepository.upsert(db, 'offlineDbLastStatus', 'idle');
|
|
145
|
+
await SettingsRepository.upsert(db, 'offlineDbLastMessage', 'Previous refresh was interrupted by a restart. Trigger an update to retry.');
|
|
146
146
|
}
|
|
147
147
|
this.isInitialized = true;
|
|
148
148
|
}
|
|
@@ -214,34 +214,34 @@ class OfflineDb {
|
|
|
214
214
|
getMalIdByAnilist(anilistId) {
|
|
215
215
|
return this.anilistToMal.get(anilistId) ?? null;
|
|
216
216
|
}
|
|
217
|
-
getOfflineDbInfo(db) {
|
|
218
|
-
const get = (key) => SettingsRepository.getByKey(db, key)?.value ?? null;
|
|
219
|
-
const autoUpdate = get('offlineDbAutoUpdateEnabled');
|
|
220
|
-
const lastStatus = get('offlineDbLastStatus');
|
|
221
|
-
const entryCount = Number(get('offlineDbEntryCount') ?? this.malToEntry.size) || 0;
|
|
217
|
+
async getOfflineDbInfo(db) {
|
|
218
|
+
const get = async (key) => (await SettingsRepository.getByKey(db, key))?.value ?? null;
|
|
219
|
+
const autoUpdate = await get('offlineDbAutoUpdateEnabled');
|
|
220
|
+
const lastStatus = (await get('offlineDbLastStatus'));
|
|
221
|
+
const entryCount = Number((await get('offlineDbEntryCount')) ?? this.malToEntry.size) || 0;
|
|
222
222
|
return {
|
|
223
223
|
totalMapped: this.malToEntry.size,
|
|
224
224
|
totalMalMapped: this.malToEntry.size,
|
|
225
225
|
isInitialized: this.isInitialized,
|
|
226
226
|
isRefreshing: this.isRefreshing,
|
|
227
227
|
autoUpdateEnabled: autoUpdate ? autoUpdate !== 'false' : true,
|
|
228
|
-
lastCheckedAt: get('offlineDbLastCheckedAt'),
|
|
229
|
-
lastUpdatedAt: get('offlineDbLastUpdatedAt'),
|
|
228
|
+
lastCheckedAt: await get('offlineDbLastCheckedAt'),
|
|
229
|
+
lastUpdatedAt: await get('offlineDbLastUpdatedAt'),
|
|
230
230
|
lastStatus: lastStatus || (this.isRefreshing ? 'updating' : 'idle'),
|
|
231
|
-
lastMessage: get('offlineDbLastMessage'),
|
|
232
|
-
sourceUrl: get('offlineDbSourceUrl'),
|
|
233
|
-
releaseTag: get('offlineDbReleaseTag'),
|
|
234
|
-
contentSha256: get('offlineDbContentSha256'),
|
|
231
|
+
lastMessage: await get('offlineDbLastMessage'),
|
|
232
|
+
sourceUrl: await get('offlineDbSourceUrl'),
|
|
233
|
+
releaseTag: await get('offlineDbReleaseTag'),
|
|
234
|
+
contentSha256: await get('offlineDbContentSha256'),
|
|
235
235
|
entryCount,
|
|
236
236
|
schemaVersion: SCHEMA_VERSION,
|
|
237
237
|
nextDueAt: latestSaturdayUtc().toISOString(),
|
|
238
238
|
};
|
|
239
239
|
}
|
|
240
|
-
checkWeeklyUpdateDue(db) {
|
|
241
|
-
const autoUpdate = SettingsRepository.getByKey(db, 'offlineDbAutoUpdateEnabled');
|
|
240
|
+
async checkWeeklyUpdateDue(db) {
|
|
241
|
+
const autoUpdate = await SettingsRepository.getByKey(db, 'offlineDbAutoUpdateEnabled');
|
|
242
242
|
if (autoUpdate && autoUpdate.value === 'false')
|
|
243
243
|
return false;
|
|
244
|
-
const lastChecked = SettingsRepository.getByKey(db, 'offlineDbLastCheckedAt');
|
|
244
|
+
const lastChecked = await SettingsRepository.getByKey(db, 'offlineDbLastCheckedAt');
|
|
245
245
|
if (!lastChecked?.value)
|
|
246
246
|
return true;
|
|
247
247
|
const lastCheckedTime = new Date(lastChecked.value).getTime();
|
|
@@ -249,8 +249,8 @@ class OfflineDb {
|
|
|
249
249
|
return true;
|
|
250
250
|
return lastCheckedTime < latestSaturdayUtc().getTime();
|
|
251
251
|
}
|
|
252
|
-
interruptedRetryDue(db) {
|
|
253
|
-
const lastChecked = SettingsRepository.getByKey(db, 'offlineDbLastCheckedAt');
|
|
252
|
+
async interruptedRetryDue(db) {
|
|
253
|
+
const lastChecked = await SettingsRepository.getByKey(db, 'offlineDbLastCheckedAt');
|
|
254
254
|
if (!lastChecked?.value)
|
|
255
255
|
return true;
|
|
256
256
|
const lastCheckedTime = new Date(lastChecked.value).getTime();
|
|
@@ -276,9 +276,9 @@ class OfflineDb {
|
|
|
276
276
|
}
|
|
277
277
|
this.isRefreshing = true;
|
|
278
278
|
const startedAt = Date.now();
|
|
279
|
-
SettingsRepository.upsert(db, 'offlineDbLastStatus', 'updating');
|
|
280
|
-
SettingsRepository.upsert(db, 'offlineDbLastCheckedAt', new Date().toISOString());
|
|
281
|
-
SettingsRepository.upsert(db, 'offlineDbLastMessage', 'Downloading and parsing database...');
|
|
279
|
+
await SettingsRepository.upsert(db, 'offlineDbLastStatus', 'updating');
|
|
280
|
+
await SettingsRepository.upsert(db, 'offlineDbLastCheckedAt', new Date().toISOString());
|
|
281
|
+
await SettingsRepository.upsert(db, 'offlineDbLastMessage', 'Downloading and parsing database...');
|
|
282
282
|
try {
|
|
283
283
|
let text = null;
|
|
284
284
|
let sourceUrl = null;
|
|
@@ -302,7 +302,7 @@ class OfflineDb {
|
|
|
302
302
|
if (!text)
|
|
303
303
|
throw lastErr || new Error('Could not download anime-offline-database from any source');
|
|
304
304
|
const sha256 = crypto.createHash('sha256').update(text).digest('hex');
|
|
305
|
-
const storedSha = SettingsRepository.getByKey(db, 'offlineDbContentSha256')?.value;
|
|
305
|
+
const storedSha = (await SettingsRepository.getByKey(db, 'offlineDbContentSha256'))?.value;
|
|
306
306
|
const data = JSON.parse(text).data;
|
|
307
307
|
log.info(`Parsing ${data.length} entries from anime-offline-database...`);
|
|
308
308
|
const anilistRegex = /anilist\.co\/anime\/(\d+)/i;
|
|
@@ -375,22 +375,22 @@ class OfflineDb {
|
|
|
375
375
|
this.loadCache(db);
|
|
376
376
|
this.backfillShowsMetaGenres(db);
|
|
377
377
|
const releaseTag = sourceUrl?.includes('/releases/') ? 'latest' : 'raw';
|
|
378
|
-
SettingsRepository.upsert(db, 'offlineDbLastStatus', 'success');
|
|
379
|
-
SettingsRepository.upsert(db, 'offlineDbLastUpdatedAt', new Date().toISOString());
|
|
380
|
-
SettingsRepository.upsert(db, 'offlineDbSourceUrl', sourceUrl ?? '');
|
|
381
|
-
SettingsRepository.upsert(db, 'offlineDbReleaseTag', releaseTag);
|
|
382
|
-
SettingsRepository.upsert(db, 'offlineDbContentSha256', sha256);
|
|
383
|
-
SettingsRepository.upsert(db, 'offlineDbEntryCount', String(parsedEntries.length));
|
|
378
|
+
await SettingsRepository.upsert(db, 'offlineDbLastStatus', 'success');
|
|
379
|
+
await SettingsRepository.upsert(db, 'offlineDbLastUpdatedAt', new Date().toISOString());
|
|
380
|
+
await SettingsRepository.upsert(db, 'offlineDbSourceUrl', sourceUrl ?? '');
|
|
381
|
+
await SettingsRepository.upsert(db, 'offlineDbReleaseTag', releaseTag);
|
|
382
|
+
await SettingsRepository.upsert(db, 'offlineDbContentSha256', sha256);
|
|
383
|
+
await SettingsRepository.upsert(db, 'offlineDbEntryCount', String(parsedEntries.length));
|
|
384
384
|
const skipped = storedSha === sha256;
|
|
385
|
-
SettingsRepository.upsert(db, 'offlineDbLastMessage', `Updated ${parsedEntries.length} entries in ${Math.round((Date.now() - startedAt) / 1000)}s${skipped ? ' (unchanged upstream hash)' : ''}.`);
|
|
385
|
+
await SettingsRepository.upsert(db, 'offlineDbLastMessage', `Updated ${parsedEntries.length} entries in ${Math.round((Date.now() - startedAt) / 1000)}s${skipped ? ' (unchanged upstream hash)' : ''}.`);
|
|
386
386
|
log.info(`OfflineDb refresh completed: ${parsedEntries.length} entries from ${sourceUrl}`);
|
|
387
387
|
return { success: true, count: parsedEntries.length };
|
|
388
388
|
}
|
|
389
389
|
catch (err) {
|
|
390
390
|
const errMsg = err.message || 'Failed to refresh offline database';
|
|
391
391
|
log.error({ err }, 'Failed to refresh anime-offline-database');
|
|
392
|
-
SettingsRepository.upsert(db, 'offlineDbLastStatus', 'failed');
|
|
393
|
-
SettingsRepository.upsert(db, 'offlineDbLastMessage', errMsg);
|
|
392
|
+
await SettingsRepository.upsert(db, 'offlineDbLastStatus', 'failed');
|
|
393
|
+
await SettingsRepository.upsert(db, 'offlineDbLastMessage', errMsg);
|
|
394
394
|
return { success: false, count: this.malToEntry.size, error: errMsg };
|
|
395
395
|
}
|
|
396
396
|
finally {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AniListTracker } from './anilist-tracker.js';
|
|
2
2
|
import { MangaLibraryRepository, MangaProgressRepository, } from '../../repositories/manga.repository.js';
|
|
3
3
|
import { SettingsRepository } from '../../repositories/settings.repository.js';
|
|
4
|
-
import {
|
|
4
|
+
import { performMangaWriteTransactionAsync, performWriteTransactionAsync } from '../../sync.js';
|
|
5
5
|
import logger from '../../logger.js';
|
|
6
6
|
import { searchAnilistMangaByTitle, getMangaMetaById, getMangaMetaByMalId } from '../anilist.js';
|
|
7
7
|
import { fetchMalMangaList, mapMalMangaStatusCode } from '../mal.js';
|
|
@@ -557,19 +557,19 @@ export async function syncAniListManga(db, mangaDb, opts = {}) {
|
|
|
557
557
|
progressRows.length > 0 ||
|
|
558
558
|
libraryDeletes.length > 0 ||
|
|
559
559
|
newLinks.length > 0) {
|
|
560
|
-
await
|
|
560
|
+
await performMangaWriteTransactionAsync(mangaDb, async (tx) => {
|
|
561
561
|
for (const item of libraryUpserts) {
|
|
562
|
-
MangaLibraryRepository.upsert(tx, item);
|
|
562
|
+
await MangaLibraryRepository.upsert(tx, item);
|
|
563
563
|
}
|
|
564
564
|
for (const link of newLinks) {
|
|
565
|
-
MangaLibraryRepository.setAnilistId(tx, link.id, link.anilistId, 'search');
|
|
565
|
+
await MangaLibraryRepository.setAnilistId(tx, link.id, link.anilistId, 'search');
|
|
566
566
|
}
|
|
567
567
|
for (const p of progressPointers) {
|
|
568
|
-
MangaLibraryRepository.setProgressPointer(tx, p.id, p.chapterNumber);
|
|
568
|
+
await MangaLibraryRepository.setProgressPointer(tx, p.id, p.chapterNumber);
|
|
569
569
|
}
|
|
570
570
|
for (const p of progressRows) {
|
|
571
|
-
MangaProgressRepository.deleteSyntheticChapters(tx, p.mangaId, p.chapterId);
|
|
572
|
-
MangaProgressRepository.upsert(tx, {
|
|
571
|
+
await MangaProgressRepository.deleteSyntheticChapters(tx, p.mangaId, p.chapterId);
|
|
572
|
+
await MangaProgressRepository.upsert(tx, {
|
|
573
573
|
mangaId: p.mangaId,
|
|
574
574
|
chapterId: p.chapterId,
|
|
575
575
|
chapterNumber: p.chapterNumber,
|
|
@@ -581,8 +581,8 @@ export async function syncAniListManga(db, mangaDb, opts = {}) {
|
|
|
581
581
|
});
|
|
582
582
|
}
|
|
583
583
|
for (const id of libraryDeletes) {
|
|
584
|
-
MangaLibraryRepository.delete(tx, id);
|
|
585
|
-
MangaProgressRepository.deleteByManga(tx, id);
|
|
584
|
+
await MangaLibraryRepository.delete(tx, id);
|
|
585
|
+
await MangaProgressRepository.deleteByManga(tx, id);
|
|
586
586
|
}
|
|
587
587
|
});
|
|
588
588
|
}
|
|
@@ -596,8 +596,8 @@ export async function syncAniListManga(db, mangaDb, opts = {}) {
|
|
|
596
596
|
mergedState[k] = v;
|
|
597
597
|
}
|
|
598
598
|
const snapshot = JSON.stringify(mergedState);
|
|
599
|
-
await
|
|
600
|
-
SettingsRepository.upsert(tx, MANGA_SYNC_STATE_KEY, snapshot);
|
|
599
|
+
await performWriteTransactionAsync(db, async (tx) => {
|
|
600
|
+
await SettingsRepository.upsert(tx, MANGA_SYNC_STATE_KEY, snapshot);
|
|
601
601
|
});
|
|
602
602
|
}
|
|
603
603
|
catch (err) {
|
|
@@ -619,19 +619,19 @@ export async function insertMangaImportEntries(db, mangaDb, items, eraseAnilistR
|
|
|
619
619
|
return 0;
|
|
620
620
|
const now = Math.floor(Date.now() / 1000);
|
|
621
621
|
const syncState = await readMangaSyncState(db);
|
|
622
|
-
await
|
|
622
|
+
await performMangaWriteTransactionAsync(mangaDb, async (tx) => {
|
|
623
623
|
if (eraseAnilistRows) {
|
|
624
|
-
for (const row of MangaLibraryRepository.getAll(tx)) {
|
|
624
|
+
for (const row of await MangaLibraryRepository.getAll(tx)) {
|
|
625
625
|
if (String(row.provider).toLowerCase() === MANGA_PROVIDER) {
|
|
626
|
-
MangaLibraryRepository.delete(tx, row.id);
|
|
627
|
-
MangaProgressRepository.deleteByManga(tx, row.id);
|
|
626
|
+
await MangaLibraryRepository.delete(tx, row.id);
|
|
627
|
+
await MangaProgressRepository.deleteByManga(tx, row.id);
|
|
628
628
|
}
|
|
629
629
|
}
|
|
630
630
|
}
|
|
631
631
|
for (const item of items) {
|
|
632
632
|
const id = buildAnilistMangaId(item.mediaId);
|
|
633
633
|
const status = mangaImportStatusOrDefault(item.status);
|
|
634
|
-
MangaLibraryRepository.upsert(tx, {
|
|
634
|
+
await MangaLibraryRepository.upsert(tx, {
|
|
635
635
|
id,
|
|
636
636
|
provider: MANGA_PROVIDER,
|
|
637
637
|
mangaId: String(item.mediaId),
|
|
@@ -642,10 +642,10 @@ export async function insertMangaImportEntries(db, mangaDb, items, eraseAnilistR
|
|
|
642
642
|
anilistIdSource: 'anilist',
|
|
643
643
|
});
|
|
644
644
|
if (item.progress > 0) {
|
|
645
|
-
MangaLibraryRepository.setProgressPointer(tx, id, String(item.progress));
|
|
645
|
+
await MangaLibraryRepository.setProgressPointer(tx, id, String(item.progress));
|
|
646
646
|
const chapterId = buildSyntheticChapterId(item.progress);
|
|
647
|
-
MangaProgressRepository.deleteSyntheticChapters(tx, id, chapterId);
|
|
648
|
-
MangaProgressRepository.upsert(tx, {
|
|
647
|
+
await MangaProgressRepository.deleteSyntheticChapters(tx, id, chapterId);
|
|
648
|
+
await MangaProgressRepository.upsert(tx, {
|
|
649
649
|
mangaId: id,
|
|
650
650
|
chapterId,
|
|
651
651
|
chapterNumber: String(item.progress),
|
|
@@ -664,8 +664,8 @@ export async function insertMangaImportEntries(db, mangaDb, items, eraseAnilistR
|
|
|
664
664
|
});
|
|
665
665
|
try {
|
|
666
666
|
const snapshot = JSON.stringify(syncState);
|
|
667
|
-
await
|
|
668
|
-
SettingsRepository.upsert(tx, MANGA_SYNC_STATE_KEY, snapshot);
|
|
667
|
+
await performWriteTransactionAsync(db, async (tx) => {
|
|
668
|
+
await SettingsRepository.upsert(tx, MANGA_SYNC_STATE_KEY, snapshot);
|
|
669
669
|
});
|
|
670
670
|
}
|
|
671
671
|
catch (err) {
|
|
@@ -4,7 +4,7 @@ import { WatchedEpisodesRepository } from '../../repositories/watched-episodes.r
|
|
|
4
4
|
import { ShowsMetaRepository } from '../../repositories/shows-meta.repository.js';
|
|
5
5
|
import { SettingsRepository } from '../../repositories/settings.repository.js';
|
|
6
6
|
import { NotificationsRepository } from '../../repositories/notifications.repository.js';
|
|
7
|
-
import {
|
|
7
|
+
import { performWriteTransactionAsync } from '../../sync.js';
|
|
8
8
|
import { dbGet } from '../../utils/db-utils.js';
|
|
9
9
|
import logger from '../../logger.js';
|
|
10
10
|
import { fetchMalUserList } from '../mal.js';
|
|
@@ -242,16 +242,16 @@ export async function syncAniList(db) {
|
|
|
242
242
|
watchedInserts.length > 0 ||
|
|
243
243
|
watchlistDeletes.length > 0 ||
|
|
244
244
|
Object.keys(stateUpdates).length > 0) {
|
|
245
|
-
await
|
|
245
|
+
await performWriteTransactionAsync(db, async (tx) => {
|
|
246
246
|
for (const item of watchlistUpserts) {
|
|
247
|
-
WatchlistRepository.upsert(tx, item);
|
|
247
|
+
await WatchlistRepository.upsert(tx, item);
|
|
248
248
|
}
|
|
249
249
|
for (const meta of metaUpserts) {
|
|
250
|
-
ShowsMetaRepository.upsert(tx, meta);
|
|
250
|
+
await ShowsMetaRepository.upsert(tx, meta);
|
|
251
251
|
}
|
|
252
252
|
for (const w of watchedInserts) {
|
|
253
253
|
for (let ep = w.from; ep <= w.to; ep++) {
|
|
254
|
-
WatchedEpisodesRepository.insertIfMissing(tx, {
|
|
254
|
+
await WatchedEpisodesRepository.insertIfMissing(tx, {
|
|
255
255
|
showId: w.showId,
|
|
256
256
|
episodeNumber: String(ep),
|
|
257
257
|
watchedAt: toSqliteDatetime(w.updatedAt),
|
|
@@ -259,9 +259,9 @@ export async function syncAniList(db) {
|
|
|
259
259
|
}
|
|
260
260
|
}
|
|
261
261
|
for (const id of watchlistDeletes) {
|
|
262
|
-
WatchlistRepository.delete(tx, id);
|
|
263
|
-
WatchedEpisodesRepository.deleteByShow(tx, id);
|
|
264
|
-
NotificationsRepository.deleteByShow(tx, id);
|
|
262
|
+
await WatchlistRepository.delete(tx, id);
|
|
263
|
+
await WatchedEpisodesRepository.deleteByShow(tx, id);
|
|
264
|
+
await NotificationsRepository.deleteByShow(tx, id);
|
|
265
265
|
}
|
|
266
266
|
const mergedState = { ...syncState };
|
|
267
267
|
for (const [k, v] of Object.entries(stateUpdates)) {
|
|
@@ -270,7 +270,7 @@ export async function syncAniList(db) {
|
|
|
270
270
|
else
|
|
271
271
|
mergedState[k] = v;
|
|
272
272
|
}
|
|
273
|
-
SettingsRepository.upsert(tx, SYNC_STATE_KEY, JSON.stringify(mergedState));
|
|
273
|
+
await SettingsRepository.upsert(tx, SYNC_STATE_KEY, JSON.stringify(mergedState));
|
|
274
274
|
});
|
|
275
275
|
}
|
|
276
276
|
return summary;
|
|
@@ -300,13 +300,13 @@ export async function importFromUsername(db, username, erase = false) {
|
|
|
300
300
|
return 0;
|
|
301
301
|
const now = Math.floor(Date.now() / 1000);
|
|
302
302
|
const syncState = await readSyncState(db);
|
|
303
|
-
await
|
|
303
|
+
await performWriteTransactionAsync(db, async (tx) => {
|
|
304
304
|
if (erase)
|
|
305
|
-
SettingsRepository.clearWatchlist(tx);
|
|
305
|
+
await SettingsRepository.clearWatchlist(tx);
|
|
306
306
|
for (const remote of entries) {
|
|
307
307
|
const showId = String(remote.mediaId);
|
|
308
308
|
const title = remote.title.english || remote.title.romaji || `Anime #${remote.mediaId}`;
|
|
309
|
-
WatchlistRepository.upsert(tx, {
|
|
309
|
+
await WatchlistRepository.upsert(tx, {
|
|
310
310
|
id: showId,
|
|
311
311
|
name: title,
|
|
312
312
|
thumbnail: remote.coverImage ?? '',
|
|
@@ -315,7 +315,7 @@ export async function importFromUsername(db, username, erase = false) {
|
|
|
315
315
|
englishName: remote.title.english ?? '',
|
|
316
316
|
type: 'TV',
|
|
317
317
|
});
|
|
318
|
-
ShowsMetaRepository.upsert(tx, {
|
|
318
|
+
await ShowsMetaRepository.upsert(tx, {
|
|
319
319
|
id: showId,
|
|
320
320
|
name: title,
|
|
321
321
|
thumbnail: remote.coverImage,
|
|
@@ -326,7 +326,7 @@ export async function importFromUsername(db, username, erase = false) {
|
|
|
326
326
|
});
|
|
327
327
|
if (remote.progress > 0) {
|
|
328
328
|
for (let ep = 1; ep <= remote.progress; ep++) {
|
|
329
|
-
WatchedEpisodesRepository.insertIfMissing(tx, {
|
|
329
|
+
await WatchedEpisodesRepository.insertIfMissing(tx, {
|
|
330
330
|
showId,
|
|
331
331
|
episodeNumber: String(ep),
|
|
332
332
|
watchedAt: toSqliteDatetime(remote.updatedAt),
|
|
@@ -335,7 +335,7 @@ export async function importFromUsername(db, username, erase = false) {
|
|
|
335
335
|
}
|
|
336
336
|
syncState[showId] = { lastSyncedAt: now, remoteUpdatedAt: remote.updatedAt };
|
|
337
337
|
}
|
|
338
|
-
SettingsRepository.upsert(tx, SYNC_STATE_KEY, JSON.stringify(syncState));
|
|
338
|
+
await SettingsRepository.upsert(tx, SYNC_STATE_KEY, JSON.stringify(syncState));
|
|
339
339
|
});
|
|
340
340
|
return entries.length;
|
|
341
341
|
}
|
|
@@ -458,11 +458,11 @@ export async function importFromMalUsername(db, username, options = {}) {
|
|
|
458
458
|
}
|
|
459
459
|
}
|
|
460
460
|
if (shows.length > 0 || erase) {
|
|
461
|
-
await
|
|
461
|
+
await performWriteTransactionAsync(db, async (tx) => {
|
|
462
462
|
if (erase)
|
|
463
|
-
SettingsRepository.clearWatchlist(tx);
|
|
463
|
+
await SettingsRepository.clearWatchlist(tx);
|
|
464
464
|
for (const show of shows) {
|
|
465
|
-
WatchlistRepository.upsert(tx, {
|
|
465
|
+
await WatchlistRepository.upsert(tx, {
|
|
466
466
|
id: show.id,
|
|
467
467
|
name: show.name,
|
|
468
468
|
thumbnail: show.thumbnail ?? '',
|
|
@@ -474,7 +474,7 @@ export async function importFromMalUsername(db, username, options = {}) {
|
|
|
474
474
|
}
|
|
475
475
|
for (const meta of metas) {
|
|
476
476
|
if (meta.thumbnail || meta.type || meta.genres) {
|
|
477
|
-
ShowsMetaRepository.upsert(tx, {
|
|
477
|
+
await ShowsMetaRepository.upsert(tx, {
|
|
478
478
|
id: meta.id,
|
|
479
479
|
thumbnail: meta.thumbnail,
|
|
480
480
|
type: meta.type,
|
|
@@ -484,7 +484,7 @@ export async function importFromMalUsername(db, username, options = {}) {
|
|
|
484
484
|
}
|
|
485
485
|
for (const w of watched) {
|
|
486
486
|
for (let ep = 1; ep <= w.progress; ep++) {
|
|
487
|
-
WatchedEpisodesRepository.insertIfMissing(tx, {
|
|
487
|
+
await WatchedEpisodesRepository.insertIfMissing(tx, {
|
|
488
488
|
showId: w.showId,
|
|
489
489
|
episodeNumber: String(ep),
|
|
490
490
|
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { AppCache } from '../utils/cache.utils.js';
|
|
2
|
+
import { parseJsonBody } from '../utils/http.utils.js';
|
|
3
|
+
const cache = new AppCache({
|
|
4
|
+
ttlSeconds: 7 * 24 * 60 * 60,
|
|
5
|
+
maxKeys: 5000,
|
|
6
|
+
});
|
|
7
|
+
function toTitleCase(s) {
|
|
8
|
+
return s
|
|
9
|
+
.split(' ')
|
|
10
|
+
.map((w, i) => {
|
|
11
|
+
if (i !== 0 &&
|
|
12
|
+
['x', 'and', 'or', 'the', 'a', 'an', 'of', 'in', 'on', 'with'].includes(w.toLowerCase())) {
|
|
13
|
+
return w.toLowerCase();
|
|
14
|
+
}
|
|
15
|
+
return w.charAt(0).toUpperCase() + w.slice(1);
|
|
16
|
+
})
|
|
17
|
+
.join(' ')
|
|
18
|
+
.replace(/\bX\b/g, 'x');
|
|
19
|
+
}
|
|
20
|
+
function normalize(s) {
|
|
21
|
+
const t = s.trim();
|
|
22
|
+
if (!t)
|
|
23
|
+
return t;
|
|
24
|
+
return toTitleCase(t);
|
|
25
|
+
}
|
|
26
|
+
export async function translateTexts(texts, source, target) {
|
|
27
|
+
const jobs = texts.map((t) => String(t).trim()).filter(Boolean);
|
|
28
|
+
const result = {};
|
|
29
|
+
const toFetch = [];
|
|
30
|
+
for (const t of jobs) {
|
|
31
|
+
const key = `${source}:${target}:${t}`;
|
|
32
|
+
const cached = cache.get(key);
|
|
33
|
+
if (cached)
|
|
34
|
+
result[t] = cached;
|
|
35
|
+
else
|
|
36
|
+
toFetch.push(t);
|
|
37
|
+
}
|
|
38
|
+
if (toFetch.length > 0) {
|
|
39
|
+
const chunks = [];
|
|
40
|
+
for (let i = 0; i < toFetch.length; i += 15)
|
|
41
|
+
chunks.push(toFetch.slice(i, i + 15));
|
|
42
|
+
for (const chunk of chunks) {
|
|
43
|
+
const q = chunk.join('\n');
|
|
44
|
+
const url = `https://clients5.google.com/translate_a/t?client=dict-chrome-ex&sl=${encodeURIComponent(source)}&tl=${encodeURIComponent(target)}&dt=t&q=${encodeURIComponent(q)}`;
|
|
45
|
+
try {
|
|
46
|
+
const r = await fetch(url, {
|
|
47
|
+
headers: { 'User-Agent': 'Mozilla/5.0' },
|
|
48
|
+
signal: AbortSignal.timeout(5000),
|
|
49
|
+
});
|
|
50
|
+
if (!r.ok)
|
|
51
|
+
throw new Error(`translate ${r.status}`);
|
|
52
|
+
const data = (await parseJsonBody(r));
|
|
53
|
+
let translated = '';
|
|
54
|
+
if (Array.isArray(data) && typeof data[0] === 'string') {
|
|
55
|
+
translated = data[0];
|
|
56
|
+
}
|
|
57
|
+
else if (Array.isArray(data) && Array.isArray(data[0])) {
|
|
58
|
+
const outer = data;
|
|
59
|
+
translated = outer[0]?.map((p) => p[0]).join('') || '';
|
|
60
|
+
}
|
|
61
|
+
const parts = translated.split('\n');
|
|
62
|
+
chunk.forEach((orig, i) => {
|
|
63
|
+
const trans = (parts[i] ?? '').trim();
|
|
64
|
+
const norm = trans ? normalize(trans) : orig;
|
|
65
|
+
result[orig] = norm;
|
|
66
|
+
cache.set(`${source}:${target}:${orig}`, norm);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
for (const orig of chunk) {
|
|
71
|
+
if (!result[orig])
|
|
72
|
+
result[orig] = orig;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|