@serifpersia/dango 3.6.5 → 3.6.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/client/dist/assets/{AnimeCard-JkXG40Es.js → AnimeCard-DraA_TF9.js} +1 -1
- package/client/dist/assets/{AnimeInfo-2L0UJkbB.js → AnimeInfo-BJ7pzs-P.js} +1 -1
- package/client/dist/assets/{AnimeInfoPage-BDs03vKk.js → AnimeInfoPage-B_esUr5C.js} +2 -2
- package/client/dist/assets/{Asmr-D2mVKEl9.js → Asmr-Ckuh2xfW.js} +1 -1
- package/client/dist/assets/{AsmrResetProgressModal-MQA_bNQM.js → AsmrResetProgressModal-BS6UFJN4.js} +1 -1
- package/client/dist/assets/AvSyncCalibrator-DwHCd9yB.js +10 -0
- package/client/dist/assets/{Home-DxMBHFmc.js → Home-DuM1EuvN.js} +1 -1
- package/client/dist/assets/{Insights-hmLWsSXY.js → Insights-D3Pd9c9V.js} +1 -1
- package/client/dist/assets/{LibraryListPage-CWI7hgUh.js → LibraryListPage-Dm2yVn0-.js} +1 -1
- package/client/dist/assets/{ListeningList-NOc28KAV.js → ListeningList-D-zt6EXZ.js} +1 -1
- package/client/dist/assets/{Manga-B5qNhI7v.js → Manga-JwprMKBk.js} +1 -1
- package/client/dist/assets/{MangaInfoPage-D4wIyb-V.js → MangaInfoPage-DaeyJl2p.js} +1 -1
- package/client/dist/assets/{MangaReadPage-Ce1VX7-0.js → MangaReadPage-COg0fHI9.js} +1 -1
- package/client/dist/assets/{MangaResetProgressModal-P3oY4_14.js → MangaResetProgressModal-kguXv0_M.js} +1 -1
- package/client/dist/assets/{Mature-D8EwGYQq.js → Mature-IHDaasrs.js} +1 -1
- package/client/dist/assets/{MediaCard-Dp8DjyXB.js → MediaCard-CpwsrXo5.js} +1 -1
- package/client/dist/assets/{Music-DULtVsAM.js → Music-Dks4tPeK.js} +1 -1
- package/client/dist/assets/{OptionListMenu-fIHdE-nq.js → OptionListMenu-11CcAsy8.js} +1 -1
- package/client/dist/assets/{Player-B8XeOmso.js → Player-DQoEQkHL.js} +6 -6
- package/client/dist/assets/{PlayerSettings-BycNCiCs.js → PlayerSettings-D4Ud4kv4.js} +1 -1
- package/client/dist/assets/{QueueOptionsButton-T1J37IZT.js → QueueOptionsButton-B8PlPKjC.js} +1 -1
- package/client/dist/assets/{QueueRail-BAwI-apz.js → QueueRail-B4Dok7U3.js} +1 -1
- package/client/dist/assets/{Radio-Bv9DyKtZ.js → Radio-BHK6LSk4.js} +1 -1
- package/client/dist/assets/{ReadingList-Cl0CcVp3.js → ReadingList-BCsxIz57.js} +1 -1
- package/client/dist/assets/{ResetProgressModal--N60WWCB.js → ResetProgressModal-BXHxSfy-.js} +1 -1
- package/client/dist/assets/{Search-DvgVDlUR.js → Search-PK1ds07O.js} +1 -1
- package/client/dist/assets/{Settings-dCz_-bJn.js → Settings-Bxk_x55l.js} +1 -1
- package/client/dist/assets/{SkeletonGrid-BZHKk1I6.js → SkeletonGrid-CpJCZSRN.js} +1 -1
- package/client/dist/assets/{Trackers-C1T5VcPu.js → Trackers-RieCh_84.js} +1 -1
- package/client/dist/assets/{Tv-Dhv_cg4y.js → Tv-DLytPJUj.js} +4 -4
- package/client/dist/assets/{TvInfo-DgaLK2H6.js → TvInfo-C8djKebR.js} +1 -1
- package/client/dist/assets/{TvPopup-DyX7f8kC.js → TvPopup-40JGGJDk.js} +1 -1
- package/client/dist/assets/{TvSearch-Bp0H6kue.js → TvSearch-A3hbospf.js} +1 -1
- package/client/dist/assets/{TvWatchlist-ohMtMeKa.js → TvWatchlist-DuXjqmZM.js} +1 -1
- package/client/dist/assets/{Watchlist-BbQtzsER.js → Watchlist-B9wGBh0u.js} +1 -1
- package/client/dist/assets/{hls-Bi1cjpLZ.js → hls-CZ5X3Ya-.js} +1 -1
- package/client/dist/assets/{index-BTnhzofk.js → index-dRhc9-ol.js} +2 -2
- package/client/dist/assets/{useAnimeInfoData-Cvmso3IK.js → useAnimeInfoData-ZzDtLfvD.js} +1 -1
- package/client/dist/assets/{useAsmr-CbzGCzmv.js → useAsmr-CKOdHgol.js} +1 -1
- package/client/dist/assets/{useAsmrPopup-DaREZ9Ie.js → useAsmrPopup-SFHPhpcl.js} +1 -1
- package/client/dist/assets/{useMangaLibrary-BVQRhsFh.js → useMangaLibrary-CKATe31P.js} +1 -1
- package/client/dist/assets/{useMangaPopup-CzsCbndG.js → useMangaPopup-C75QuYgY.js} +1 -1
- package/client/dist/assets/{useProviders-UuUPzft-.js → useProviders-CMtvZbEi.js} +1 -1
- package/client/dist/assets/{useTv-CSsbWKH9.js → useTv-DUX4zYMr.js} +1 -1
- package/client/dist/assets/{useTvLibrary-a_Y0c2FS.js → useTvLibrary-By4Gt-R_.js} +1 -1
- package/client/dist/index.html +1 -1
- package/package.json +2 -2
- package/server/dist/app-auth.js +1 -157
- package/server/dist/app-hono.js +7 -557
- package/server/dist/config.js +1 -174
- package/server/dist/db/drizzle.js +1 -16
- package/server/dist/db/schema-anime.js +1 -61
- package/server/dist/db/schema-asmr.js +1 -13
- package/server/dist/db/schema-manga.js +1 -31
- package/server/dist/db/schema-tv.js +1 -18
- package/server/dist/db.js +1 -217
- package/server/dist/discord-gateway.js +1 -385
- package/server/dist/discord-rpc.js +1 -413
- package/server/dist/github-sync.js +1 -508
- package/server/dist/google.js +1 -741
- package/server/dist/hono/asmr.js +1 -10
- package/server/dist/hono/auth.js +3 -26
- package/server/dist/hono/music.js +1 -209
- package/server/dist/hono/proxy.js +36 -905
- package/server/dist/hono/tv.js +5 -169
- package/server/dist/hono/watchlist.js +2 -338
- package/server/dist/lib/anilist.js +26 -1458
- package/server/dist/lib/anischedule.js +1 -191
- package/server/dist/lib/insights.js +1 -185
- package/server/dist/lib/ipc.js +1 -23
- package/server/dist/lib/kitsu.js +1 -517
- package/server/dist/lib/mal-import.js +1 -291
- package/server/dist/lib/mal.js +1 -957
- package/server/dist/lib/migration.js +1 -310
- package/server/dist/lib/offline-db.js +4 -384
- package/server/dist/lib/potoken.js +1 -135
- package/server/dist/lib/recommendations.service.js +1 -216
- package/server/dist/lib/temp-ids.js +1 -3
- package/server/dist/lib/tmdb.js +1 -72
- package/server/dist/lib/tracker/anilist-tracker.js +8 -245
- package/server/dist/lib/tracker/manga-sync.service.js +1 -773
- package/server/dist/lib/tracker/sync-state.js +1 -21
- package/server/dist/lib/tracker/sync.service.js +1 -476
- package/server/dist/lib/translate.js +3 -79
- package/server/dist/lib/ump.js +1 -106
- package/server/dist/lib/ytmusic.js +1 -162
- package/server/dist/logger.js +1 -16
- package/server/dist/providers/manga/manga.types.js +0 -1
- package/server/dist/providers/provider.interface.js +0 -1
- package/server/dist/providers/remote-loader.js +2 -605
- package/server/dist/providers/remote-types.js +1 -124
- package/server/dist/providers/title-matching.js +1 -226
- package/server/dist/providers/tv.types.js +0 -1
- package/server/dist/rclone.js +2 -120
- package/server/dist/repositories/asmr.repository.js +11 -46
- package/server/dist/repositories/insights.repository.js +11 -25
- package/server/dist/repositories/library.repository.js +3 -94
- package/server/dist/repositories/mal-cache.repository.js +1 -52
- package/server/dist/repositories/manga.repository.js +14 -58
- package/server/dist/repositories/notifications.repository.js +6 -52
- package/server/dist/repositories/queue.repository.js +5 -58
- package/server/dist/repositories/settings.repository.js +2 -29
- package/server/dist/repositories/shows-meta.repository.js +3 -56
- package/server/dist/repositories/temp-show-ids.repository.js +3 -63
- package/server/dist/repositories/tv.repository.js +11 -54
- package/server/dist/repositories/watched-episodes.repository.js +6 -55
- package/server/dist/repositories/watchlist.repository.js +3 -54
- package/server/dist/scripts/backfill-episode-duration.js +1 -42
- package/server/dist/server.js +1 -307
- package/server/dist/shipped-defaults.js +1 -6
- package/server/dist/sync-payload.js +1 -91
- package/server/dist/sync.js +3 -647
- package/server/dist/trpc/context.js +1 -7
- package/server/dist/trpc/errors.js +1 -15
- package/server/dist/trpc/index.js +1 -18
- package/server/dist/trpc/router.js +1 -49
- package/server/dist/trpc/routers/asmr-library.js +1 -112
- package/server/dist/trpc/routers/asmr-progress.js +1 -121
- package/server/dist/trpc/routers/asmr.js +1 -87
- package/server/dist/trpc/routers/auth.js +1 -194
- package/server/dist/trpc/routers/continue-watching.js +2 -58
- package/server/dist/trpc/routers/data.js +1 -1026
- package/server/dist/trpc/routers/discord.js +1 -285
- package/server/dist/trpc/routers/discovery.js +1 -11
- package/server/dist/trpc/routers/insights.js +1 -14
- package/server/dist/trpc/routers/manga-library.js +1 -263
- package/server/dist/trpc/routers/manga-progress.js +1 -169
- package/server/dist/trpc/routers/manga.js +1 -201
- package/server/dist/trpc/routers/music.js +1 -655
- package/server/dist/trpc/routers/notifications.js +1 -80
- package/server/dist/trpc/routers/progress.js +1 -109
- package/server/dist/trpc/routers/queue.js +1 -93
- package/server/dist/trpc/routers/radio.js +1 -145
- package/server/dist/trpc/routers/settings.js +1 -96
- package/server/dist/trpc/routers/tracker.js +1 -213
- package/server/dist/trpc/routers/tv-library.js +1 -135
- package/server/dist/trpc/routers/tv-progress.js +1 -159
- package/server/dist/trpc/routers/tv.js +1 -551
- package/server/dist/trpc/routers/watchlist.js +2 -549
- package/server/dist/trpc/validation.js +1 -258
- package/server/dist/utils/cache.utils.js +1 -25
- package/server/dist/utils/cookie.utils.js +1 -13
- package/server/dist/utils/db-utils.js +5 -17
- package/server/dist/utils/env.utils.js +5 -77
- package/server/dist/utils/http.utils.js +1 -82
- package/server/dist/utils/request-context.js +1 -2
- package/server/dist/utils/security.utils.js +1 -76
- package/server/package.json +3 -2
- package/client/dist/assets/AvSyncCalibrator-DmoH5eRA.js +0 -10
- package/server/dist/utils/megaplay.utils.js +0 -40
package/server/dist/sync.js
CHANGED
|
@@ -1,657 +1,13 @@
|
|
|
1
|
-
import * as fs from 'fs/promises';
|
|
2
|
-
import { existsSync } from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import logger from './logger.js';
|
|
5
|
-
import { googleDriveService } from './google.js';
|
|
6
|
-
import { rcloneService } from './rclone.js';
|
|
7
|
-
import { githubSyncService } from './github-sync.js';
|
|
8
|
-
import { CONFIG } from './config.js';
|
|
9
|
-
import { DatabaseWrapper } from './db.js';
|
|
10
|
-
import { runTx } from './db/drizzle.js';
|
|
11
|
-
import { dbGet } from './utils/db-utils.js';
|
|
12
|
-
import { TempShowIdsRepository } from './repositories/temp-show-ids.repository.js';
|
|
13
|
-
import { malCachePruneExpired } from './repositories/mal-cache.repository.js';
|
|
14
|
-
import { notifySyncStart, notifySyncEnd } from './lib/ipc.js';
|
|
15
|
-
import { ANIME_SYNC_TABLES, exportTables, importTables, MANGA_SYNC_TABLES, TV_SYNC_TABLES, ASMR_SYNC_TABLES, normalizePayload, readPayloadVersion, } from './sync-payload.js';
|
|
16
|
-
const log = logger.child({ module: 'Sync' });
|
|
17
|
-
export const SYNC_TABLES = ANIME_SYNC_TABLES;
|
|
18
|
-
export { MANGA_SYNC_TABLES, TV_SYNC_TABLES, ASMR_SYNC_TABLES };
|
|
19
|
-
function cap(s) {
|
|
20
|
-
return s.length === 0 ? s : s[0].toUpperCase() + s.slice(1);
|
|
21
|
-
}
|
|
22
|
-
export const KIND_SYNC = {
|
|
23
|
-
anime: {
|
|
24
|
-
mid: '',
|
|
25
|
-
Head: '',
|
|
26
|
-
rcloneLabel: 'rclone',
|
|
27
|
-
tables: SYNC_TABLES,
|
|
28
|
-
libraryTables: ['watchlist', 'watched_episodes'],
|
|
29
|
-
backupName: 'pre-sync-backup.db',
|
|
30
|
-
manifestPath: CONFIG.LOCAL_MANIFEST_PATH,
|
|
31
|
-
rcloneFile: CONFIG.RCLONE_SYNC_FILENAME,
|
|
32
|
-
githubDown: (db) => githubSyncService.syncDown(db),
|
|
33
|
-
githubUp: (db) => githubSyncService.syncUp(db),
|
|
34
|
-
googleDown: (db) => googleDriveService.syncDown(db),
|
|
35
|
-
githubVersion: () => githubSyncService.getRemoteVersion(),
|
|
36
|
-
googleVersion: () => googleDriveService.getRemoteVersion(),
|
|
37
|
-
googleUp: (db) => googleDriveService.syncUp(db),
|
|
38
|
-
},
|
|
39
|
-
manga: {
|
|
40
|
-
mid: 'manga ',
|
|
41
|
-
Head: 'Manga ',
|
|
42
|
-
rcloneLabel: 'rclone manga',
|
|
43
|
-
tables: MANGA_SYNC_TABLES,
|
|
44
|
-
libraryTables: ['manga_library', 'manga_progress'],
|
|
45
|
-
backupName: 'pre-sync-manga-backup.db',
|
|
46
|
-
manifestPath: CONFIG.MANGA_LOCAL_MANIFEST_PATH,
|
|
47
|
-
rcloneFile: CONFIG.MANGA_RCLONE_SYNC_FILENAME,
|
|
48
|
-
githubDown: (db) => githubSyncService.syncMangaDown(db),
|
|
49
|
-
githubUp: (db) => githubSyncService.syncMangaUp(db),
|
|
50
|
-
googleDown: (db) => googleDriveService.syncMangaDown(db),
|
|
51
|
-
githubVersion: () => githubSyncService.getMangaRemoteVersion(),
|
|
52
|
-
googleVersion: () => googleDriveService.getMangaRemoteVersion(),
|
|
53
|
-
googleUp: (db) => googleDriveService.syncMangaUp(db),
|
|
54
|
-
},
|
|
55
|
-
tv: {
|
|
56
|
-
mid: 'TV ',
|
|
57
|
-
Head: 'TV ',
|
|
58
|
-
rcloneLabel: 'rclone tv',
|
|
59
|
-
tables: TV_SYNC_TABLES,
|
|
60
|
-
libraryTables: ['tv_library', 'tv_progress'],
|
|
61
|
-
backupName: 'pre-sync-tv-backup.db',
|
|
62
|
-
manifestPath: CONFIG.TV_LOCAL_MANIFEST_PATH,
|
|
63
|
-
rcloneFile: CONFIG.TV_RCLONE_SYNC_FILENAME,
|
|
64
|
-
githubDown: (db) => githubSyncService.syncTvDown(db),
|
|
65
|
-
githubUp: (db) => githubSyncService.syncTvUp(db),
|
|
66
|
-
googleDown: (db) => googleDriveService.syncTvDown(db),
|
|
67
|
-
githubVersion: () => githubSyncService.getTvRemoteVersion(),
|
|
68
|
-
googleVersion: () => googleDriveService.getTvRemoteVersion(),
|
|
69
|
-
googleUp: (db) => googleDriveService.syncTvUp(db),
|
|
70
|
-
},
|
|
71
|
-
asmr: {
|
|
72
|
-
mid: 'ASMR ',
|
|
73
|
-
Head: 'ASMR ',
|
|
74
|
-
rcloneLabel: 'rclone asmr',
|
|
75
|
-
tables: ASMR_SYNC_TABLES,
|
|
76
|
-
libraryTables: ['asmr_library', 'asmr_progress'],
|
|
77
|
-
backupName: 'pre-sync-asmr-backup.db',
|
|
78
|
-
manifestPath: CONFIG.ASMR_LOCAL_MANIFEST_PATH,
|
|
79
|
-
rcloneFile: CONFIG.ASMR_RCLONE_SYNC_FILENAME,
|
|
80
|
-
githubDown: (db) => githubSyncService.syncAsmrDown(db),
|
|
81
|
-
githubUp: (db) => githubSyncService.syncAsmrUp(db),
|
|
82
|
-
googleDown: (db) => googleDriveService.syncAsmrDown(db),
|
|
83
|
-
githubVersion: () => githubSyncService.getAsmrRemoteVersion(),
|
|
84
|
-
googleVersion: () => googleDriveService.getAsmrRemoteVersion(),
|
|
85
|
-
googleUp: (db) => googleDriveService.syncAsmrUp(db),
|
|
86
|
-
},
|
|
87
|
-
};
|
|
88
|
-
async function getRcloneRemotePayloadVersion(remoteFolder, fileName = CONFIG.RCLONE_SYNC_FILENAME) {
|
|
89
|
-
const exists = await rcloneService.fileExists(remoteFolder, fileName);
|
|
90
|
-
if (!exists)
|
|
91
|
-
return 0;
|
|
92
|
-
const tempPath = path.join(CONFIG.ROOT, `temp_${Date.now()}_rclone_sync.json`);
|
|
93
|
-
try {
|
|
94
|
-
await rcloneService.downloadFile(remoteFolder, fileName, tempPath);
|
|
95
|
-
const content = await fs.readFile(tempPath, 'utf-8');
|
|
96
|
-
const payload = JSON.parse(content);
|
|
97
|
-
return readPayloadVersion(payload);
|
|
98
|
-
}
|
|
99
|
-
catch {
|
|
100
|
-
return 0;
|
|
101
|
-
}
|
|
102
|
-
finally {
|
|
103
|
-
if (existsSync(tempPath))
|
|
104
|
-
await fs.unlink(tempPath);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
async function rcloneSyncUp(db, kind, remoteFolder) {
|
|
108
|
-
const def = KIND_SYNC[kind];
|
|
109
|
-
const payload = exportTables(db, def.tables);
|
|
110
|
-
const tempPath = path.join(CONFIG.ROOT, `temp_${Date.now()}_rclone_${kind}_up.json`);
|
|
111
|
-
try {
|
|
112
|
-
await fs.writeFile(tempPath, JSON.stringify(payload, null, 2));
|
|
113
|
-
await rcloneService.uploadFile(tempPath, remoteFolder, def.rcloneFile);
|
|
114
|
-
}
|
|
115
|
-
finally {
|
|
116
|
-
if (existsSync(tempPath))
|
|
117
|
-
await fs.unlink(tempPath).catch(() => { });
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
async function rcloneSyncDown(db, kind, remoteFolder) {
|
|
121
|
-
const def = KIND_SYNC[kind];
|
|
122
|
-
const tempPath = path.join(CONFIG.ROOT, `temp_${Date.now()}_rclone_${kind}_down.json`);
|
|
123
|
-
try {
|
|
124
|
-
await rcloneService.downloadFile(remoteFolder, def.rcloneFile, tempPath);
|
|
125
|
-
const content = await fs.readFile(tempPath, 'utf-8');
|
|
126
|
-
const payload = normalizePayload(JSON.parse(content), def.tables, def.rcloneLabel);
|
|
127
|
-
importTables(db, def.tables, payload, {
|
|
128
|
-
libraryTables: def.libraryTables,
|
|
129
|
-
backupName: def.backupName,
|
|
130
|
-
});
|
|
131
|
-
return readPayloadVersion(payload);
|
|
132
|
-
}
|
|
133
|
-
finally {
|
|
134
|
-
if (existsSync(tempPath))
|
|
135
|
-
await fs.unlink(tempPath).catch(() => { });
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
class Mutex {
|
|
139
|
-
_locked = false;
|
|
140
|
-
_waiting = [];
|
|
141
|
-
async lock() {
|
|
142
|
-
return new Promise((resolve) => {
|
|
143
|
-
if (!this._locked) {
|
|
144
|
-
this._locked = true;
|
|
145
|
-
resolve();
|
|
146
|
-
}
|
|
147
|
-
else {
|
|
148
|
-
this._waiting.push(resolve);
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
unlock() {
|
|
153
|
-
const resolve = this._waiting.shift();
|
|
154
|
-
if (resolve)
|
|
155
|
-
resolve();
|
|
156
|
-
else
|
|
157
|
-
this._locked = false;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
const syncMutex = new Mutex();
|
|
161
|
-
let isSyncing = false;
|
|
162
|
-
let activeProvider = 'none';
|
|
163
|
-
export async function waitForSync() {
|
|
164
|
-
await syncMutex.lock();
|
|
165
|
-
syncMutex.unlock();
|
|
166
|
-
}
|
|
167
|
-
export function getActiveProvider() {
|
|
168
|
-
return activeProvider;
|
|
169
|
-
}
|
|
170
|
-
export async function initSyncProvider(preferred) {
|
|
171
|
-
const provider = preferred || process.env.SYNC_PROVIDER;
|
|
172
|
-
if (provider === 'github' && githubSyncService.isAuthenticated()) {
|
|
173
|
-
activeProvider = 'github';
|
|
174
|
-
log.info('Sync Provider: GitHub (Selected)');
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
if (provider === 'google' && googleDriveService.isAuthenticated()) {
|
|
178
|
-
activeProvider = 'google';
|
|
179
|
-
log.info('Sync Provider: Google Drive API (Selected)');
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
if (provider === 'rclone') {
|
|
183
|
-
const rcloneAvailable = await rcloneService.init();
|
|
184
|
-
if (rcloneAvailable) {
|
|
185
|
-
activeProvider = 'rclone';
|
|
186
|
-
log.info(`Sync Provider: Rclone (${rcloneService.getRemoteName()}) (Selected)`);
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
if (provider === 'none') {
|
|
191
|
-
activeProvider = 'none';
|
|
192
|
-
log.info('Sync Provider: None (Forced)');
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
if (githubSyncService.isAuthenticated()) {
|
|
196
|
-
activeProvider = 'github';
|
|
197
|
-
log.info('Sync Provider: GitHub (Fallback)');
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
if (googleDriveService.isAuthenticated()) {
|
|
201
|
-
activeProvider = 'google';
|
|
202
|
-
log.info('Sync Provider: Google Drive API (Fallback)');
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
const rcloneAvailable = await rcloneService.init();
|
|
206
|
-
if (rcloneAvailable) {
|
|
207
|
-
activeProvider = 'rclone';
|
|
208
|
-
log.info(`Sync Provider: Rclone (${rcloneService.getRemoteName()}) (Fallback)`);
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
activeProvider = 'none';
|
|
212
|
-
log.info('No sync provider available.');
|
|
213
|
-
}
|
|
214
|
-
export async function getLocalManifestVersion(manifestPath = CONFIG.LOCAL_MANIFEST_PATH) {
|
|
215
|
-
if (existsSync(manifestPath)) {
|
|
216
|
-
try {
|
|
217
|
-
const content = await fs.readFile(manifestPath, 'utf-8');
|
|
218
|
-
return JSON.parse(content).version || 0;
|
|
219
|
-
}
|
|
220
|
-
catch {
|
|
221
|
-
return 0;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
return 0;
|
|
225
|
-
}
|
|
226
|
-
export async function setLocalManifestVersion(version, manifestPath = CONFIG.LOCAL_MANIFEST_PATH) {
|
|
227
|
-
await fs.writeFile(manifestPath, JSON.stringify({ version }));
|
|
228
|
-
}
|
|
229
|
-
export function getKindManifestVersion(kind) {
|
|
230
|
-
return getLocalManifestVersion(KIND_SYNC[kind].manifestPath);
|
|
231
|
-
}
|
|
232
|
-
export function setKindManifestVersion(kind, version) {
|
|
233
|
-
return setLocalManifestVersion(version, KIND_SYNC[kind].manifestPath);
|
|
234
|
-
}
|
|
235
|
-
async function getRemoteManifestVersion(remoteFolder, kind = 'anime') {
|
|
236
|
-
const def = KIND_SYNC[kind];
|
|
237
|
-
const label = kind === 'anime' ? 'manifest' : `${def.mid.trim()} manifest`;
|
|
238
|
-
try {
|
|
239
|
-
if (activeProvider === 'github') {
|
|
240
|
-
if (!githubSyncService.isAuthenticated())
|
|
241
|
-
return { version: 0 };
|
|
242
|
-
return { version: await def.githubVersion() };
|
|
243
|
-
}
|
|
244
|
-
else if (activeProvider === 'google') {
|
|
245
|
-
if (!googleDriveService.isAuthenticated())
|
|
246
|
-
return { version: 0 };
|
|
247
|
-
return { version: await def.googleVersion() };
|
|
248
|
-
}
|
|
249
|
-
else if (activeProvider === 'rclone') {
|
|
250
|
-
return { version: await getRcloneRemotePayloadVersion(remoteFolder, def.rcloneFile) };
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
catch (err) {
|
|
254
|
-
log.warn({ err }, `Could not read remote ${label}.`);
|
|
255
|
-
}
|
|
256
|
-
return { version: 0 };
|
|
257
|
-
}
|
|
258
|
-
export async function syncDownOnBoot(db, kind, remoteFolderName) {
|
|
259
|
-
const def = KIND_SYNC[kind];
|
|
260
|
-
let localVersion = await getKindManifestVersion(kind);
|
|
261
|
-
if (localVersion === 0 && db) {
|
|
262
|
-
const row = dbGet(db, "SELECT value FROM sync_metadata WHERE key = 'db_version'");
|
|
263
|
-
localVersion = row?.value ?? 0;
|
|
264
|
-
if (localVersion > 0) {
|
|
265
|
-
await setKindManifestVersion(kind, localVersion);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
if (activeProvider === 'none')
|
|
269
|
-
return;
|
|
270
|
-
await syncMutex.lock();
|
|
271
|
-
if (isSyncing) {
|
|
272
|
-
syncMutex.unlock();
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
isSyncing = true;
|
|
276
|
-
try {
|
|
277
|
-
notifySyncStart(`Initial ${def.mid}sync check (${activeProvider})`);
|
|
278
|
-
const { version: remoteVersion } = await getRemoteManifestVersion(remoteFolderName, kind);
|
|
279
|
-
notifySyncEnd();
|
|
280
|
-
log.info(`${def.Head}Sync Check: Local v${localVersion} vs Remote v${remoteVersion}`);
|
|
281
|
-
if (remoteVersion > localVersion) {
|
|
282
|
-
if (activeProvider === 'github') {
|
|
283
|
-
if (!githubSyncService.isAuthenticated())
|
|
284
|
-
return;
|
|
285
|
-
notifySyncStart(`Importing GitHub ${def.mid}sync data (Remote v${remoteVersion})`);
|
|
286
|
-
const importedVersion = await def.githubDown(db);
|
|
287
|
-
await setKindManifestVersion(kind, importedVersion || remoteVersion);
|
|
288
|
-
notifySyncEnd();
|
|
289
|
-
log.info(`GitHub ${def.mid}sync down complete.`);
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
if (activeProvider === 'google') {
|
|
293
|
-
if (!googleDriveService.isAuthenticated())
|
|
294
|
-
return;
|
|
295
|
-
notifySyncStart(`Importing Google ${def.mid}sync data (Remote v${remoteVersion})`);
|
|
296
|
-
const importedVersion = await def.googleDown(db);
|
|
297
|
-
await setKindManifestVersion(kind, importedVersion || remoteVersion);
|
|
298
|
-
notifySyncEnd();
|
|
299
|
-
log.info(`Google ${def.mid}sync down complete.`);
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
if (activeProvider === 'rclone') {
|
|
303
|
-
notifySyncStart(`Importing Rclone ${def.mid}sync data (Remote v${remoteVersion})`);
|
|
304
|
-
const importedVersion = await rcloneSyncDown(db, kind, remoteFolderName);
|
|
305
|
-
await setKindManifestVersion(kind, importedVersion || remoteVersion);
|
|
306
|
-
notifySyncEnd();
|
|
307
|
-
log.info(`Rclone ${def.mid}sync down complete.`);
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
else {
|
|
312
|
-
log.info(`Local ${def.mid}DB is up to date.`);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
catch (err) {
|
|
316
|
-
notifySyncEnd();
|
|
317
|
-
log.error({ err }, `${cap(`${def.mid}sync`)} boot error.`);
|
|
318
|
-
}
|
|
319
|
-
finally {
|
|
320
|
-
isSyncing = false;
|
|
321
|
-
syncMutex.unlock();
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
export async function syncUp(db, kind, remoteFolderName) {
|
|
325
|
-
const def = KIND_SYNC[kind];
|
|
326
|
-
if (activeProvider === 'none')
|
|
327
|
-
return;
|
|
328
|
-
await syncMutex.lock();
|
|
329
|
-
if (isSyncing) {
|
|
330
|
-
syncMutex.unlock();
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
isSyncing = true;
|
|
334
|
-
try {
|
|
335
|
-
const localVersion = await getKindManifestVersion(kind);
|
|
336
|
-
const { version: remoteVersion } = await getRemoteManifestVersion(remoteFolderName, kind);
|
|
337
|
-
if (localVersion > remoteVersion) {
|
|
338
|
-
notifySyncStart(`Syncing ${def.mid}up (Local v${localVersion})`);
|
|
339
|
-
if (activeProvider === 'github') {
|
|
340
|
-
if (!githubSyncService.isAuthenticated())
|
|
341
|
-
return;
|
|
342
|
-
await def.githubUp(db);
|
|
343
|
-
}
|
|
344
|
-
else if (activeProvider === 'google') {
|
|
345
|
-
if (!googleDriveService.isAuthenticated())
|
|
346
|
-
return;
|
|
347
|
-
await def.googleUp(db);
|
|
348
|
-
}
|
|
349
|
-
else if (activeProvider === 'rclone') {
|
|
350
|
-
await rcloneSyncUp(db, kind, remoteFolderName);
|
|
351
|
-
}
|
|
352
|
-
notifySyncEnd();
|
|
353
|
-
log.info(`${cap(`${def.mid}sync`)} up complete.`);
|
|
354
|
-
}
|
|
355
|
-
else {
|
|
356
|
-
log.info(`No ${def.mid}changes to sync up or remote is newer.`);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
catch (err) {
|
|
360
|
-
notifySyncEnd();
|
|
361
|
-
log.error({ err }, `${cap(`${def.mid}sync`)} up failed.`);
|
|
362
|
-
}
|
|
363
|
-
finally {
|
|
364
|
-
isSyncing = false;
|
|
365
|
-
syncMutex.unlock();
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
export async function runFullSyncSequence(dbs, opts) {
|
|
369
|
-
await initSyncProvider(opts.preferredProvider);
|
|
370
|
-
if (getActiveProvider() === 'github' && githubSyncService.isAuthenticated()) {
|
|
371
|
-
try {
|
|
372
|
-
await githubSyncService.migrateFromAniWebSync();
|
|
373
|
-
}
|
|
374
|
-
catch (err) {
|
|
375
|
-
logger.error({ err }, 'GitHub sync migration from ani-web failed');
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
await syncDownOnBoot(dbs.db, 'anime', opts.remoteFolder);
|
|
379
|
-
try {
|
|
380
|
-
await syncUp(dbs.db, 'anime', opts.remoteFolder);
|
|
381
|
-
}
|
|
382
|
-
catch (err) {
|
|
383
|
-
logger.error({ err }, 'Sync up on boot failed');
|
|
384
|
-
}
|
|
385
|
-
const kinds = [
|
|
386
|
-
{ kind: 'manga', db: dbs.mangaDb },
|
|
387
|
-
{ kind: 'tv', db: dbs.tvDb },
|
|
388
|
-
{ kind: 'asmr', db: dbs.asmrDb },
|
|
389
|
-
];
|
|
390
|
-
for (const { kind, db } of kinds) {
|
|
391
|
-
const def = KIND_SYNC[kind];
|
|
392
|
-
try {
|
|
393
|
-
await syncDownOnBoot(db, kind, opts.remoteFolder);
|
|
394
|
-
}
|
|
395
|
-
catch (err) {
|
|
396
|
-
logger.error({ err }, `${def.Head}sync down on boot failed`);
|
|
397
|
-
}
|
|
398
|
-
try {
|
|
399
|
-
await syncUp(db, kind, opts.remoteFolder);
|
|
400
|
-
}
|
|
401
|
-
catch (err) {
|
|
402
|
-
logger.error({ err }, `${def.Head}sync up on boot failed`);
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
export async function performWriteTransactionAsync(db, runnable, setVersion = (v) => setLocalManifestVersion(v)) {
|
|
407
|
-
await runTx(db, async (tx) => {
|
|
408
|
-
await runnable(tx);
|
|
409
|
-
tx.run("UPDATE sync_metadata SET value = value + 1 WHERE key = 'db_version'");
|
|
410
|
-
});
|
|
411
|
-
const row = dbGet(db, "SELECT value FROM sync_metadata WHERE key = 'db_version'");
|
|
412
|
-
const newVersion = row?.value ?? 1;
|
|
413
|
-
await setVersion(newVersion);
|
|
414
|
-
}
|
|
415
|
-
export async function performMangaWriteTransactionAsync(db, runnable) {
|
|
416
|
-
await performWriteTransactionAsync(db, runnable, (v) => setKindManifestVersion('manga', v));
|
|
417
|
-
}
|
|
418
|
-
export async function performTvWriteTransactionAsync(db, runnable) {
|
|
419
|
-
await performWriteTransactionAsync(db, runnable, (v) => setKindManifestVersion('tv', v));
|
|
420
|
-
}
|
|
421
|
-
export async function performAsmrWriteTransactionAsync(db, runnable) {
|
|
422
|
-
await performWriteTransactionAsync(db, runnable, (v) => setKindManifestVersion('asmr', v));
|
|
423
|
-
}
|
|
424
|
-
export async function initializeMangaDatabase(dbPath) {
|
|
425
|
-
try {
|
|
426
|
-
const db = await DatabaseWrapper.create(dbPath);
|
|
427
|
-
db.configure('busyTimeout', 5000);
|
|
428
|
-
db.run('PRAGMA journal_mode = WAL;');
|
|
429
|
-
db.run('PRAGMA synchronous = NORMAL;');
|
|
430
|
-
db.run('PRAGMA cache_size = -20000;');
|
|
431
|
-
db.run('PRAGMA temp_store = MEMORY;');
|
|
432
|
-
db.run('PRAGMA mmap_size = 268435456;');
|
|
433
|
-
db.run('PRAGMA foreign_keys = ON;');
|
|
434
|
-
db.run(`CREATE TABLE IF NOT EXISTS manga_library (id TEXT PRIMARY KEY, provider TEXT NOT NULL, mangaId TEXT NOT NULL, title TEXT, cover TEXT, status TEXT DEFAULT 'Reading', author TEXT, contentRating TEXT, lastChapterId TEXT, lastChapterNumber TEXT, lastPage INTEGER, updatedAt INTEGER, altTitle TEXT, anilistId INTEGER, anilistIdSource TEXT)`);
|
|
435
|
-
db.run(`CREATE TABLE IF NOT EXISTS manga_progress (mangaId TEXT NOT NULL, chapterId TEXT NOT NULL, chapterNumber TEXT, page INTEGER DEFAULT 0, pageCount INTEGER DEFAULT 0, updatedAt INTEGER, PRIMARY KEY (mangaId, chapterId))`);
|
|
436
|
-
db.run(`CREATE TABLE IF NOT EXISTS sync_metadata (key TEXT PRIMARY KEY, value INTEGER)`);
|
|
437
|
-
db.run(`INSERT OR IGNORE INTO sync_metadata (key, value) VALUES ('db_version', 1)`);
|
|
438
|
-
db.run(`CREATE UNIQUE INDEX IF NOT EXISTS idx_manga_library_provider_manga ON manga_library(provider, mangaId)`);
|
|
439
|
-
db.run(`CREATE INDEX IF NOT EXISTS idx_manga_library_status ON manga_library(status)`);
|
|
440
|
-
db.run(`CREATE INDEX IF NOT EXISTS idx_manga_progress_manga ON manga_progress(mangaId, updatedAt)`);
|
|
441
|
-
const mangaColumns = db.all(`PRAGMA table_info(manga_library)`);
|
|
442
|
-
if (!mangaColumns.some((c) => c.name === 'altTitle')) {
|
|
443
|
-
db.run(`ALTER TABLE manga_library ADD COLUMN altTitle TEXT`);
|
|
444
|
-
}
|
|
445
|
-
if (!mangaColumns.some((c) => c.name === 'anilistId')) {
|
|
446
|
-
db.run(`ALTER TABLE manga_library ADD COLUMN anilistId INTEGER`);
|
|
447
|
-
}
|
|
448
|
-
if (!mangaColumns.some((c) => c.name === 'anilistIdSource')) {
|
|
449
|
-
db.run(`ALTER TABLE manga_library ADD COLUMN anilistIdSource TEXT`);
|
|
450
|
-
}
|
|
451
|
-
db.run(`CREATE INDEX IF NOT EXISTS idx_manga_library_anilist ON manga_library(anilistId)`);
|
|
452
|
-
const mangaProgressColumns = db.all(`PRAGMA table_info(manga_progress)`);
|
|
453
|
-
if (!mangaProgressColumns.some((c) => c.name === 'title')) {
|
|
454
|
-
db.run(`ALTER TABLE manga_progress ADD COLUMN title TEXT`);
|
|
455
|
-
}
|
|
456
|
-
if (!mangaProgressColumns.some((c) => c.name === 'cover')) {
|
|
457
|
-
db.run(`ALTER TABLE manga_progress ADD COLUMN cover TEXT`);
|
|
458
|
-
}
|
|
459
|
-
if (!mangaProgressColumns.some((c) => c.name === 'provider')) {
|
|
460
|
-
db.run(`ALTER TABLE manga_progress ADD COLUMN provider TEXT`);
|
|
461
|
-
}
|
|
462
|
-
if (!mangaProgressColumns.some((c) => c.name === 'altTitle')) {
|
|
463
|
-
db.run(`ALTER TABLE manga_progress ADD COLUMN altTitle TEXT`);
|
|
464
|
-
}
|
|
465
|
-
if (!mangaProgressColumns.some((c) => c.name === 'contentRating')) {
|
|
466
|
-
db.run(`ALTER TABLE manga_progress ADD COLUMN contentRating TEXT`);
|
|
467
|
-
}
|
|
468
|
-
return db;
|
|
469
|
-
}
|
|
470
|
-
catch (err) {
|
|
471
|
-
log.error({ err }, 'Manga database opening error');
|
|
472
|
-
throw err;
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
export async function initializeTvDatabase(dbPath) {
|
|
476
|
-
try {
|
|
477
|
-
const db = await DatabaseWrapper.create(dbPath);
|
|
478
|
-
db.configure('busyTimeout', 5000);
|
|
479
|
-
db.run('PRAGMA journal_mode = WAL;');
|
|
480
|
-
db.run('PRAGMA synchronous = NORMAL;');
|
|
481
|
-
db.run('PRAGMA cache_size = -20000;');
|
|
482
|
-
db.run('PRAGMA temp_store = MEMORY;');
|
|
483
|
-
db.run('PRAGMA mmap_size = 268435456;');
|
|
484
|
-
db.run('PRAGMA foreign_keys = ON;');
|
|
485
|
-
db.run(`CREATE TABLE IF NOT EXISTS tv_library (id TEXT PRIMARY KEY, tmdbId INTEGER NOT NULL, mediaType TEXT NOT NULL, title TEXT, poster TEXT, backdrop TEXT, year TEXT, overview TEXT, status TEXT DEFAULT 'Watching', adult INTEGER DEFAULT 0, lastSeason INTEGER, lastEpisode INTEGER, updatedAt INTEGER)`);
|
|
486
|
-
db.run(`CREATE TABLE IF NOT EXISTS tv_progress (mediaId TEXT NOT NULL, season INTEGER NOT NULL, episode INTEGER NOT NULL, currentTime REAL DEFAULT 0, duration REAL DEFAULT 0, completed INTEGER NOT NULL DEFAULT 0, updatedAt INTEGER, PRIMARY KEY (mediaId, season, episode))`);
|
|
487
|
-
db.run(`CREATE TABLE IF NOT EXISTS sync_metadata (key TEXT PRIMARY KEY, value INTEGER)`);
|
|
488
|
-
db.run(`INSERT OR IGNORE INTO sync_metadata (key, value) VALUES ('db_version', 1)`);
|
|
489
|
-
db.run(`CREATE INDEX IF NOT EXISTS idx_tv_library_status ON tv_library(status)`);
|
|
490
|
-
db.run(`CREATE INDEX IF NOT EXISTS idx_tv_library_tmdb ON tv_library(tmdbId, mediaType)`);
|
|
491
|
-
db.run(`CREATE INDEX IF NOT EXISTS idx_tv_progress_media ON tv_progress(mediaId, updatedAt)`);
|
|
492
|
-
const tvProgressColumns = db.all(`PRAGMA table_info(tv_progress)`);
|
|
493
|
-
if (!tvProgressColumns.some((c) => c.name === 'title')) {
|
|
494
|
-
db.run(`ALTER TABLE tv_progress ADD COLUMN title TEXT`);
|
|
495
|
-
}
|
|
496
|
-
if (!tvProgressColumns.some((c) => c.name === 'poster')) {
|
|
497
|
-
db.run(`ALTER TABLE tv_progress ADD COLUMN poster TEXT`);
|
|
498
|
-
}
|
|
499
|
-
if (!tvProgressColumns.some((c) => c.name === 'backdrop')) {
|
|
500
|
-
db.run(`ALTER TABLE tv_progress ADD COLUMN backdrop TEXT`);
|
|
501
|
-
}
|
|
502
|
-
if (!tvProgressColumns.some((c) => c.name === 'year')) {
|
|
503
|
-
db.run(`ALTER TABLE tv_progress ADD COLUMN year TEXT`);
|
|
504
|
-
}
|
|
505
|
-
if (!tvProgressColumns.some((c) => c.name === 'overview')) {
|
|
506
|
-
db.run(`ALTER TABLE tv_progress ADD COLUMN overview TEXT`);
|
|
507
|
-
}
|
|
508
|
-
if (!tvProgressColumns.some((c) => c.name === 'tmdbId')) {
|
|
509
|
-
db.run(`ALTER TABLE tv_progress ADD COLUMN tmdbId INTEGER`);
|
|
510
|
-
}
|
|
511
|
-
if (!tvProgressColumns.some((c) => c.name === 'mediaType')) {
|
|
512
|
-
db.run(`ALTER TABLE tv_progress ADD COLUMN mediaType TEXT`);
|
|
513
|
-
}
|
|
514
|
-
if (!tvProgressColumns.some((c) => c.name === 'adult')) {
|
|
515
|
-
db.run(`ALTER TABLE tv_progress ADD COLUMN adult INTEGER`);
|
|
516
|
-
}
|
|
517
|
-
if (!tvProgressColumns.some((c) => c.name === 'completed')) {
|
|
518
|
-
db.run(`ALTER TABLE tv_progress ADD COLUMN completed INTEGER NOT NULL DEFAULT 0`);
|
|
519
|
-
}
|
|
520
|
-
db.run(`UPDATE tv_progress SET completed = 1 WHERE completed = 0 AND duration > 0 AND currentTime >= duration * 0.8`);
|
|
521
|
-
db.run(`CREATE TRIGGER IF NOT EXISTS trg_tv_progress_completed_insert
|
|
1
|
+
import*as u from"fs/promises";import{existsSync as _}from"fs";import b from"path";import m from"./logger.js";import{googleDriveService as E}from"./google.js";import{rcloneService as l}from"./rclone.js";import{githubSyncService as T}from"./github-sync.js";import{CONFIG as d}from"./config.js";import{DatabaseWrapper as L}from"./db.js";import{runTx as F}from"./db/drizzle.js";import{dbGet as S}from"./utils/db-utils.js";import{TempShowIdsRepository as G}from"./repositories/temp-show-ids.repository.js";import{malCachePruneExpired as W}from"./repositories/mal-cache.repository.js";import{notifySyncStart as I,notifySyncEnd as N}from"./lib/ipc.js";import{ANIME_SYNC_TABLES as x,exportTables as k,importTables as Y,MANGA_SYNC_TABLES as D,TV_SYNC_TABLES as O,ASMR_SYNC_TABLES as w,normalizePayload as B,readPayloadVersion as v}from"./sync-payload.js";const o=m.child({module:"Sync"}),V=x;function y(a){return a.length===0?a:a[0].toUpperCase()+a.slice(1)}const A={anime:{mid:"",Head:"",rcloneLabel:"rclone",tables:V,libraryTables:["watchlist","watched_episodes"],backupName:"pre-sync-backup.db",manifestPath:d.LOCAL_MANIFEST_PATH,rcloneFile:d.RCLONE_SYNC_FILENAME,githubDown:a=>T.syncDown(a),githubUp:a=>T.syncUp(a),googleDown:a=>E.syncDown(a),githubVersion:()=>T.getRemoteVersion(),googleVersion:()=>E.getRemoteVersion(),googleUp:a=>E.syncUp(a)},manga:{mid:"manga ",Head:"Manga ",rcloneLabel:"rclone manga",tables:D,libraryTables:["manga_library","manga_progress"],backupName:"pre-sync-manga-backup.db",manifestPath:d.MANGA_LOCAL_MANIFEST_PATH,rcloneFile:d.MANGA_RCLONE_SYNC_FILENAME,githubDown:a=>T.syncMangaDown(a),githubUp:a=>T.syncMangaUp(a),googleDown:a=>E.syncMangaDown(a),githubVersion:()=>T.getMangaRemoteVersion(),googleVersion:()=>E.getMangaRemoteVersion(),googleUp:a=>E.syncMangaUp(a)},tv:{mid:"TV ",Head:"TV ",rcloneLabel:"rclone tv",tables:O,libraryTables:["tv_library","tv_progress"],backupName:"pre-sync-tv-backup.db",manifestPath:d.TV_LOCAL_MANIFEST_PATH,rcloneFile:d.TV_RCLONE_SYNC_FILENAME,githubDown:a=>T.syncTvDown(a),githubUp:a=>T.syncTvUp(a),googleDown:a=>E.syncTvDown(a),githubVersion:()=>T.getTvRemoteVersion(),googleVersion:()=>E.getTvRemoteVersion(),googleUp:a=>E.syncTvUp(a)},asmr:{mid:"ASMR ",Head:"ASMR ",rcloneLabel:"rclone asmr",tables:w,libraryTables:["asmr_library","asmr_progress"],backupName:"pre-sync-asmr-backup.db",manifestPath:d.ASMR_LOCAL_MANIFEST_PATH,rcloneFile:d.ASMR_RCLONE_SYNC_FILENAME,githubDown:a=>T.syncAsmrDown(a),githubUp:a=>T.syncAsmrUp(a),googleDown:a=>E.syncAsmrDown(a),githubVersion:()=>T.getAsmrRemoteVersion(),googleVersion:()=>E.getAsmrRemoteVersion(),googleUp:a=>E.syncAsmrUp(a)}};async function $(a,e=d.RCLONE_SYNC_FILENAME){if(!await l.fileExists(a,e))return 0;const r=b.join(d.ROOT,`temp_${Date.now()}_rclone_sync.json`);try{await l.downloadFile(a,e,r);const n=await u.readFile(r,"utf-8"),i=JSON.parse(n);return v(i)}catch{return 0}finally{_(r)&&await u.unlink(r)}}async function H(a,e,t){const r=A[e],n=k(a,r.tables),i=b.join(d.ROOT,`temp_${Date.now()}_rclone_${e}_up.json`);try{await u.writeFile(i,JSON.stringify(n,null,2)),await l.uploadFile(i,t,r.rcloneFile)}finally{_(i)&&await u.unlink(i).catch(()=>{})}}async function K(a,e,t){const r=A[e],n=b.join(d.ROOT,`temp_${Date.now()}_rclone_${e}_down.json`);try{await l.downloadFile(t,r.rcloneFile,n);const i=await u.readFile(n,"utf-8"),c=B(JSON.parse(i),r.tables,r.rcloneLabel);return Y(a,r.tables,c,{libraryTables:r.libraryTables,backupName:r.backupName}),v(c)}finally{_(n)&&await u.unlink(n).catch(()=>{})}}class j{_locked=!1;_waiting=[];async lock(){return new Promise(e=>{this._locked?this._waiting.push(e):(this._locked=!0,e())})}unlock(){const e=this._waiting.shift();e?e():this._locked=!1}}const p=new j;let R=!1,s="none";async function me(){await p.lock(),p.unlock()}function z(){return s}async function q(a){const e=a||process.env.SYNC_PROVIDER;if(e==="github"&&T.isAuthenticated()){s="github",o.info("Sync Provider: GitHub (Selected)");return}if(e==="google"&&E.isAuthenticated()){s="google",o.info("Sync Provider: Google Drive API (Selected)");return}if(e==="rclone"&&await l.init()){s="rclone",o.info(`Sync Provider: Rclone (${l.getRemoteName()}) (Selected)`);return}if(e==="none"){s="none",o.info("Sync Provider: None (Forced)");return}if(T.isAuthenticated()){s="github",o.info("Sync Provider: GitHub (Fallback)");return}if(E.isAuthenticated()){s="google",o.info("Sync Provider: Google Drive API (Fallback)");return}if(await l.init()){s="rclone",o.info(`Sync Provider: Rclone (${l.getRemoteName()}) (Fallback)`);return}s="none",o.info("No sync provider available.")}async function J(a=d.LOCAL_MANIFEST_PATH){if(_(a))try{const e=await u.readFile(a,"utf-8");return JSON.parse(e).version||0}catch{return 0}return 0}async function h(a,e=d.LOCAL_MANIFEST_PATH){await u.writeFile(e,JSON.stringify({version:a}))}function M(a){return J(A[a].manifestPath)}function g(a,e){return h(e,A[a].manifestPath)}async function X(a,e="anime"){const t=A[e],r=e==="anime"?"manifest":`${t.mid.trim()} manifest`;try{if(s==="github")return T.isAuthenticated()?{version:await t.githubVersion()}:{version:0};if(s==="google")return E.isAuthenticated()?{version:await t.googleVersion()}:{version:0};if(s==="rclone")return{version:await $(a,t.rcloneFile)}}catch(n){o.warn({err:n},`Could not read remote ${r}.`)}return{version:0}}async function P(a,e,t){const r=A[e];let n=await M(e);if(n===0&&a&&(n=S(a,"SELECT value FROM sync_metadata WHERE key = 'db_version'")?.value??0,n>0&&await g(e,n)),s!=="none"){if(await p.lock(),R){p.unlock();return}R=!0;try{I(`Initial ${r.mid}sync check (${s})`);const{version:i}=await X(t,e);if(N(),o.info(`${r.Head}Sync Check: Local v${n} vs Remote v${i}`),i>n){if(s==="github"){if(!T.isAuthenticated())return;I(`Importing GitHub ${r.mid}sync data (Remote v${i})`);const c=await r.githubDown(a);await g(e,c||i),N(),o.info(`GitHub ${r.mid}sync down complete.`);return}if(s==="google"){if(!E.isAuthenticated())return;I(`Importing Google ${r.mid}sync data (Remote v${i})`);const c=await r.googleDown(a);await g(e,c||i),N(),o.info(`Google ${r.mid}sync down complete.`);return}if(s==="rclone"){I(`Importing Rclone ${r.mid}sync data (Remote v${i})`);const c=await K(a,e,t);await g(e,c||i),N(),o.info(`Rclone ${r.mid}sync down complete.`);return}}else o.info(`Local ${r.mid}DB is up to date.`)}catch(i){N(),o.error({err:i},`${y(`${r.mid}sync`)} boot error.`)}finally{R=!1,p.unlock()}}}async function C(a,e,t){const r=A[e];if(s!=="none"){if(await p.lock(),R){p.unlock();return}R=!0;try{const n=await M(e),{version:i}=await X(t,e);if(n>i){if(I(`Syncing ${r.mid}up (Local v${n})`),s==="github"){if(!T.isAuthenticated())return;await r.githubUp(a)}else if(s==="google"){if(!E.isAuthenticated())return;await r.googleUp(a)}else s==="rclone"&&await H(a,e,t);N(),o.info(`${y(`${r.mid}sync`)} up complete.`)}else o.info(`No ${r.mid}changes to sync up or remote is newer.`)}catch(n){N(),o.error({err:n},`${y(`${r.mid}sync`)} up failed.`)}finally{R=!1,p.unlock()}}}async function ue(a,e){if(await q(e.preferredProvider),z()==="github"&&T.isAuthenticated())try{await T.migrateFromAniWebSync()}catch(r){m.error({err:r},"GitHub sync migration from ani-web failed")}await P(a.db,"anime",e.remoteFolder);try{await C(a.db,"anime",e.remoteFolder)}catch(r){m.error({err:r},"Sync up on boot failed")}const t=[{kind:"manga",db:a.mangaDb},{kind:"tv",db:a.tvDb},{kind:"asmr",db:a.asmrDb}];for(const{kind:r,db:n}of t){const i=A[r];try{await P(n,r,e.remoteFolder)}catch(c){m.error({err:c},`${i.Head}sync down on boot failed`)}try{await C(n,r,e.remoteFolder)}catch(c){m.error({err:c},`${i.Head}sync up on boot failed`)}}}async function f(a,e,t=r=>h(r)){await F(a,async i=>{await e(i),i.run("UPDATE sync_metadata SET value = value + 1 WHERE key = 'db_version'")});const n=S(a,"SELECT value FROM sync_metadata WHERE key = 'db_version'")?.value??1;await t(n)}async function le(a,e){await f(a,e,t=>g("manga",t))}async function Ae(a,e){await f(a,e,t=>g("tv",t))}async function pe(a,e){await f(a,e,t=>g("asmr",t))}async function Ne(a){try{const e=await L.create(a);e.configure("busyTimeout",5e3),e.run("PRAGMA journal_mode = WAL;"),e.run("PRAGMA synchronous = NORMAL;"),e.run("PRAGMA cache_size = -20000;"),e.run("PRAGMA temp_store = MEMORY;"),e.run("PRAGMA mmap_size = 268435456;"),e.run("PRAGMA foreign_keys = ON;"),e.run("CREATE TABLE IF NOT EXISTS manga_library (id TEXT PRIMARY KEY, provider TEXT NOT NULL, mangaId TEXT NOT NULL, title TEXT, cover TEXT, status TEXT DEFAULT 'Reading', author TEXT, contentRating TEXT, lastChapterId TEXT, lastChapterNumber TEXT, lastPage INTEGER, updatedAt INTEGER, altTitle TEXT, anilistId INTEGER, anilistIdSource TEXT)"),e.run("CREATE TABLE IF NOT EXISTS manga_progress (mangaId TEXT NOT NULL, chapterId TEXT NOT NULL, chapterNumber TEXT, page INTEGER DEFAULT 0, pageCount INTEGER DEFAULT 0, updatedAt INTEGER, PRIMARY KEY (mangaId, chapterId))"),e.run("CREATE TABLE IF NOT EXISTS sync_metadata (key TEXT PRIMARY KEY, value INTEGER)"),e.run("INSERT OR IGNORE INTO sync_metadata (key, value) VALUES ('db_version', 1)"),e.run("CREATE UNIQUE INDEX IF NOT EXISTS idx_manga_library_provider_manga ON manga_library(provider, mangaId)"),e.run("CREATE INDEX IF NOT EXISTS idx_manga_library_status ON manga_library(status)"),e.run("CREATE INDEX IF NOT EXISTS idx_manga_progress_manga ON manga_progress(mangaId, updatedAt)");const t=e.all("PRAGMA table_info(manga_library)");t.some(n=>n.name==="altTitle")||e.run("ALTER TABLE manga_library ADD COLUMN altTitle TEXT"),t.some(n=>n.name==="anilistId")||e.run("ALTER TABLE manga_library ADD COLUMN anilistId INTEGER"),t.some(n=>n.name==="anilistIdSource")||e.run("ALTER TABLE manga_library ADD COLUMN anilistIdSource TEXT"),e.run("CREATE INDEX IF NOT EXISTS idx_manga_library_anilist ON manga_library(anilistId)");const r=e.all("PRAGMA table_info(manga_progress)");return r.some(n=>n.name==="title")||e.run("ALTER TABLE manga_progress ADD COLUMN title TEXT"),r.some(n=>n.name==="cover")||e.run("ALTER TABLE manga_progress ADD COLUMN cover TEXT"),r.some(n=>n.name==="provider")||e.run("ALTER TABLE manga_progress ADD COLUMN provider TEXT"),r.some(n=>n.name==="altTitle")||e.run("ALTER TABLE manga_progress ADD COLUMN altTitle TEXT"),r.some(n=>n.name==="contentRating")||e.run("ALTER TABLE manga_progress ADD COLUMN contentRating TEXT"),e}catch(e){throw o.error({err:e},"Manga database opening error"),e}}async function ge(a){try{const e=await L.create(a);e.configure("busyTimeout",5e3),e.run("PRAGMA journal_mode = WAL;"),e.run("PRAGMA synchronous = NORMAL;"),e.run("PRAGMA cache_size = -20000;"),e.run("PRAGMA temp_store = MEMORY;"),e.run("PRAGMA mmap_size = 268435456;"),e.run("PRAGMA foreign_keys = ON;"),e.run("CREATE TABLE IF NOT EXISTS tv_library (id TEXT PRIMARY KEY, tmdbId INTEGER NOT NULL, mediaType TEXT NOT NULL, title TEXT, poster TEXT, backdrop TEXT, year TEXT, overview TEXT, status TEXT DEFAULT 'Watching', adult INTEGER DEFAULT 0, lastSeason INTEGER, lastEpisode INTEGER, updatedAt INTEGER)"),e.run("CREATE TABLE IF NOT EXISTS tv_progress (mediaId TEXT NOT NULL, season INTEGER NOT NULL, episode INTEGER NOT NULL, currentTime REAL DEFAULT 0, duration REAL DEFAULT 0, completed INTEGER NOT NULL DEFAULT 0, updatedAt INTEGER, PRIMARY KEY (mediaId, season, episode))"),e.run("CREATE TABLE IF NOT EXISTS sync_metadata (key TEXT PRIMARY KEY, value INTEGER)"),e.run("INSERT OR IGNORE INTO sync_metadata (key, value) VALUES ('db_version', 1)"),e.run("CREATE INDEX IF NOT EXISTS idx_tv_library_status ON tv_library(status)"),e.run("CREATE INDEX IF NOT EXISTS idx_tv_library_tmdb ON tv_library(tmdbId, mediaType)"),e.run("CREATE INDEX IF NOT EXISTS idx_tv_progress_media ON tv_progress(mediaId, updatedAt)");const t=e.all("PRAGMA table_info(tv_progress)");return t.some(r=>r.name==="title")||e.run("ALTER TABLE tv_progress ADD COLUMN title TEXT"),t.some(r=>r.name==="poster")||e.run("ALTER TABLE tv_progress ADD COLUMN poster TEXT"),t.some(r=>r.name==="backdrop")||e.run("ALTER TABLE tv_progress ADD COLUMN backdrop TEXT"),t.some(r=>r.name==="year")||e.run("ALTER TABLE tv_progress ADD COLUMN year TEXT"),t.some(r=>r.name==="overview")||e.run("ALTER TABLE tv_progress ADD COLUMN overview TEXT"),t.some(r=>r.name==="tmdbId")||e.run("ALTER TABLE tv_progress ADD COLUMN tmdbId INTEGER"),t.some(r=>r.name==="mediaType")||e.run("ALTER TABLE tv_progress ADD COLUMN mediaType TEXT"),t.some(r=>r.name==="adult")||e.run("ALTER TABLE tv_progress ADD COLUMN adult INTEGER"),t.some(r=>r.name==="completed")||e.run("ALTER TABLE tv_progress ADD COLUMN completed INTEGER NOT NULL DEFAULT 0"),e.run("UPDATE tv_progress SET completed = 1 WHERE completed = 0 AND duration > 0 AND currentTime >= duration * 0.8"),e.run(`CREATE TRIGGER IF NOT EXISTS trg_tv_progress_completed_insert
|
|
522
2
|
AFTER INSERT ON tv_progress
|
|
523
3
|
WHEN NEW.duration > 0 AND NEW.currentTime >= NEW.duration * 0.8
|
|
524
4
|
BEGIN
|
|
525
5
|
UPDATE tv_progress SET completed = 1
|
|
526
6
|
WHERE mediaId = NEW.mediaId AND season = NEW.season AND episode = NEW.episode;
|
|
527
|
-
END`)
|
|
528
|
-
db.run(`DROP TRIGGER IF EXISTS trg_tv_progress_completed_update`);
|
|
529
|
-
db.run(`CREATE TRIGGER IF NOT EXISTS trg_tv_progress_completed_update
|
|
7
|
+
END`),e.run("DROP TRIGGER IF EXISTS trg_tv_progress_completed_update"),e.run(`CREATE TRIGGER IF NOT EXISTS trg_tv_progress_completed_update
|
|
530
8
|
AFTER UPDATE OF currentTime, duration ON tv_progress
|
|
531
9
|
WHEN NEW.duration > 0 AND NEW.currentTime >= NEW.duration * 0.8
|
|
532
10
|
BEGIN
|
|
533
11
|
UPDATE tv_progress SET completed = 1
|
|
534
12
|
WHERE mediaId = NEW.mediaId AND season = NEW.season AND episode = NEW.episode;
|
|
535
|
-
END`);
|
|
536
|
-
db.run(`UPDATE tv_progress SET completed = 0 WHERE completed = 1 AND NOT (duration > 0 AND currentTime >= duration * 0.8)`);
|
|
537
|
-
db.run(`UPDATE OR IGNORE tv_progress SET mediaId = REPLACE(mediaId, '-', ':') WHERE mediaId LIKE 'tv-%' OR mediaId LIKE 'movie-%'`);
|
|
538
|
-
db.run(`DELETE FROM tv_progress WHERE mediaId LIKE 'tv-%' OR mediaId LIKE 'movie-%'`);
|
|
539
|
-
return db;
|
|
540
|
-
}
|
|
541
|
-
catch (err) {
|
|
542
|
-
log.error({ err }, 'TV database opening error');
|
|
543
|
-
throw err;
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
export async function initializeAsmrDatabase(dbPath) {
|
|
547
|
-
try {
|
|
548
|
-
const db = await DatabaseWrapper.create(dbPath);
|
|
549
|
-
db.configure('busyTimeout', 5000);
|
|
550
|
-
db.run('PRAGMA journal_mode = WAL;');
|
|
551
|
-
db.run('PRAGMA synchronous = NORMAL;');
|
|
552
|
-
db.run('PRAGMA cache_size = -20000;');
|
|
553
|
-
db.run('PRAGMA temp_store = MEMORY;');
|
|
554
|
-
db.run('PRAGMA mmap_size = 268435456;');
|
|
555
|
-
db.run('PRAGMA foreign_keys = ON;');
|
|
556
|
-
db.run(`CREATE TABLE IF NOT EXISTS asmr_library (id TEXT PRIMARY KEY, rjCode TEXT NOT NULL, title TEXT, thumbnail TEXT, status TEXT DEFAULT 'Listening', isAdult INTEGER DEFAULT 0, lastTrackIndex INTEGER, lastTrackLabel TEXT, lastPosition REAL, updatedAt INTEGER)`);
|
|
557
|
-
db.run(`CREATE TABLE IF NOT EXISTS asmr_progress (workId TEXT NOT NULL, trackIndex INTEGER NOT NULL, trackLabel TEXT, currentTime REAL DEFAULT 0, duration REAL DEFAULT 0, updatedAt INTEGER, PRIMARY KEY (workId, trackIndex))`);
|
|
558
|
-
db.run(`CREATE TABLE IF NOT EXISTS sync_metadata (key TEXT PRIMARY KEY, value INTEGER)`);
|
|
559
|
-
db.run(`INSERT OR IGNORE INTO sync_metadata (key, value) VALUES ('db_version', 1)`);
|
|
560
|
-
db.run(`CREATE INDEX IF NOT EXISTS idx_asmr_library_status ON asmr_library(status)`);
|
|
561
|
-
db.run(`CREATE INDEX IF NOT EXISTS idx_asmr_library_rjcode ON asmr_library(rjCode)`);
|
|
562
|
-
db.run(`CREATE INDEX IF NOT EXISTS idx_asmr_progress_work ON asmr_progress(workId, updatedAt)`);
|
|
563
|
-
const asmrProgressColumns = db.all(`PRAGMA table_info(asmr_progress)`);
|
|
564
|
-
if (!asmrProgressColumns.some((c) => c.name === 'title')) {
|
|
565
|
-
db.run(`ALTER TABLE asmr_progress ADD COLUMN title TEXT`);
|
|
566
|
-
}
|
|
567
|
-
if (!asmrProgressColumns.some((c) => c.name === 'thumbnail')) {
|
|
568
|
-
db.run(`ALTER TABLE asmr_progress ADD COLUMN thumbnail TEXT`);
|
|
569
|
-
}
|
|
570
|
-
if (!asmrProgressColumns.some((c) => c.name === 'rjCode')) {
|
|
571
|
-
db.run(`ALTER TABLE asmr_progress ADD COLUMN rjCode TEXT`);
|
|
572
|
-
}
|
|
573
|
-
if (!asmrProgressColumns.some((c) => c.name === 'isAdult')) {
|
|
574
|
-
db.run(`ALTER TABLE asmr_progress ADD COLUMN isAdult INTEGER`);
|
|
575
|
-
}
|
|
576
|
-
return db;
|
|
577
|
-
}
|
|
578
|
-
catch (err) {
|
|
579
|
-
log.error({ err }, 'ASMR database opening error');
|
|
580
|
-
throw err;
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
export async function initializeDatabase(dbPath) {
|
|
584
|
-
try {
|
|
585
|
-
const db = await DatabaseWrapper.create(dbPath);
|
|
586
|
-
db.configure('busyTimeout', 5000);
|
|
587
|
-
db.run('PRAGMA journal_mode = WAL;');
|
|
588
|
-
db.run('PRAGMA synchronous = NORMAL;');
|
|
589
|
-
db.run('PRAGMA cache_size = -20000;');
|
|
590
|
-
db.run('PRAGMA temp_store = MEMORY;');
|
|
591
|
-
db.run('PRAGMA mmap_size = 268435456;');
|
|
592
|
-
db.run('PRAGMA foreign_keys = ON;');
|
|
593
|
-
db.run(`CREATE TABLE IF NOT EXISTS watchlist (id TEXT NOT NULL, name TEXT, thumbnail TEXT, status TEXT, nativeName TEXT, englishName TEXT, type TEXT, PRIMARY KEY (id))`);
|
|
594
|
-
db.run(`CREATE TABLE IF NOT EXISTS watched_episodes (showId TEXT NOT NULL, episodeNumber TEXT NOT NULL, watchedAt DATETIME DEFAULT CURRENT_TIMESTAMP, currentTime REAL DEFAULT 0, duration REAL DEFAULT 0, PRIMARY KEY (showId, episodeNumber))`);
|
|
595
|
-
db.run(`CREATE TABLE IF NOT EXISTS queue (id INTEGER PRIMARY KEY, showId TEXT NOT NULL, episodeNumber TEXT NOT NULL, queue_order INTEGER NOT NULL)`);
|
|
596
|
-
db.run(`CREATE TABLE IF NOT EXISTS settings (key TEXT NOT NULL, value TEXT, PRIMARY KEY (key))`);
|
|
597
|
-
db.run(`CREATE TABLE IF NOT EXISTS shows_meta (id TEXT PRIMARY KEY, name TEXT, thumbnail TEXT, nativeName TEXT, englishName TEXT, episodeCount INTEGER, status TEXT, genres TEXT, popularityScore INTEGER, type TEXT)`);
|
|
598
|
-
db.run(`CREATE TABLE IF NOT EXISTS dismissed_notifications (showId TEXT NOT NULL, episodeNumber TEXT NOT NULL, dismissedAt DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (showId, episodeNumber))`);
|
|
599
|
-
db.run(`CREATE TABLE IF NOT EXISTS discovered_notifications (showId TEXT NOT NULL, episodeNumber TEXT NOT NULL, discoveredAt DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (showId, episodeNumber))`);
|
|
600
|
-
db.run(`CREATE TABLE IF NOT EXISTS sync_metadata (key TEXT PRIMARY KEY, value INTEGER)`);
|
|
601
|
-
db.run(`INSERT OR IGNORE INTO sync_metadata (key, value) VALUES ('db_version', 1)`);
|
|
602
|
-
db.run(`CREATE TABLE IF NOT EXISTS legacy_id_mapping (legacyId TEXT PRIMARY KEY, numericId TEXT)`);
|
|
603
|
-
db.run(`CREATE TABLE IF NOT EXISTS temp_show_ids (id TEXT PRIMARY KEY, provider TEXT NOT NULL, nativeId TEXT NOT NULL, title TEXT NOT NULL, thumbnail TEXT, createdAt INTEGER NOT NULL)`);
|
|
604
|
-
db.run(`CREATE UNIQUE INDEX IF NOT EXISTS idx_temp_show_ids_provider_native ON temp_show_ids(provider, nativeId)`);
|
|
605
|
-
try {
|
|
606
|
-
const purged = await TempShowIdsRepository.purge(db);
|
|
607
|
-
if (purged > 0)
|
|
608
|
-
logger.info({ purged }, 'Purged stale temp show ids on boot');
|
|
609
|
-
}
|
|
610
|
-
catch (e) {
|
|
611
|
-
logger.warn({ err: e }, 'Temp show purge on boot failed');
|
|
612
|
-
}
|
|
613
|
-
try {
|
|
614
|
-
const pruned = malCachePruneExpired();
|
|
615
|
-
if (pruned > 0)
|
|
616
|
-
logger.info({ pruned }, 'Pruned expired mal cache rows on boot');
|
|
617
|
-
}
|
|
618
|
-
catch (e) {
|
|
619
|
-
logger.warn({ err: e }, 'Mal cache prune on boot failed');
|
|
620
|
-
}
|
|
621
|
-
db.run(`CREATE INDEX IF NOT EXISTS idx_watched_episodes_showId ON watched_episodes(showId)`);
|
|
622
|
-
db.run(`CREATE INDEX IF NOT EXISTS idx_watched_episodes_showId_episodeNumber ON watched_episodes(showId, episodeNumber)`);
|
|
623
|
-
db.run(`CREATE INDEX IF NOT EXISTS idx_watched_episodes_watchedAt ON watched_episodes(watchedAt)`);
|
|
624
|
-
db.run(`CREATE INDEX IF NOT EXISTS idx_watchlist_status ON watchlist(status)`);
|
|
625
|
-
db.run(`CREATE UNIQUE INDEX IF NOT EXISTS idx_queue_show_episode ON queue(showId, episodeNumber)`);
|
|
626
|
-
db.run(`CREATE INDEX IF NOT EXISTS idx_queue_order ON queue(queue_order)`);
|
|
627
|
-
db.run('DELETE FROM dismissed_notifications WHERE showId NOT IN (SELECT id FROM watchlist)');
|
|
628
|
-
db.run('DELETE FROM discovered_notifications WHERE showId NOT IN (SELECT id FROM watchlist)');
|
|
629
|
-
db.run('DELETE FROM legacy_id_mapping WHERE legacyId NOT IN (SELECT id FROM watchlist) AND numericId NOT IN (SELECT id FROM watchlist)');
|
|
630
|
-
db.run('DELETE FROM dismissed_notifications WHERE EXISTS (SELECT 1 FROM watched_episodes we WHERE we.showId = dismissed_notifications.showId AND we.episodeNumber = dismissed_notifications.episodeNumber)');
|
|
631
|
-
db.run('DELETE FROM discovered_notifications WHERE EXISTS (SELECT 1 FROM watched_episodes we WHERE we.showId = discovered_notifications.showId AND we.episodeNumber = discovered_notifications.episodeNumber)');
|
|
632
|
-
db.run(`UPDATE discovered_notifications SET discoveredAt = datetime('now') WHERE discoveredAt IS NULL`);
|
|
633
|
-
const addCol = (tbl, col, type) => {
|
|
634
|
-
const columns = db.all(`PRAGMA table_info(${tbl})`);
|
|
635
|
-
if (!columns.some((c) => c.name === col))
|
|
636
|
-
db.run(`ALTER TABLE ${tbl} ADD COLUMN ${col} ${type}`);
|
|
637
|
-
};
|
|
638
|
-
addCol('watchlist', 'nativeName', 'TEXT');
|
|
639
|
-
addCol('watchlist', 'englishName', 'TEXT');
|
|
640
|
-
addCol('shows_meta', 'nativeName', 'TEXT');
|
|
641
|
-
addCol('shows_meta', 'englishName', 'TEXT');
|
|
642
|
-
addCol('shows_meta', 'episodeCount', 'INTEGER');
|
|
643
|
-
addCol('shows_meta', 'status', 'TEXT');
|
|
644
|
-
addCol('shows_meta', 'genres', 'TEXT');
|
|
645
|
-
addCol('shows_meta', 'popularityScore', 'INTEGER');
|
|
646
|
-
addCol('watchlist', 'type', 'TEXT');
|
|
647
|
-
addCol('shows_meta', 'type', 'TEXT');
|
|
648
|
-
addCol('shows_meta', 'anilistId', 'INTEGER');
|
|
649
|
-
addCol('shows_meta', 'isAdult', 'INTEGER');
|
|
650
|
-
addCol('shows_meta', 'episodeDuration', 'INTEGER');
|
|
651
|
-
return db;
|
|
652
|
-
}
|
|
653
|
-
catch (err) {
|
|
654
|
-
log.error({ err }, 'Database opening error');
|
|
655
|
-
throw err;
|
|
656
|
-
}
|
|
657
|
-
}
|
|
13
|
+
END`),e.run("UPDATE tv_progress SET completed = 0 WHERE completed = 1 AND NOT (duration > 0 AND currentTime >= duration * 0.8)"),e.run("UPDATE OR IGNORE tv_progress SET mediaId = REPLACE(mediaId, '-', ':') WHERE mediaId LIKE 'tv-%' OR mediaId LIKE 'movie-%'"),e.run("DELETE FROM tv_progress WHERE mediaId LIKE 'tv-%' OR mediaId LIKE 'movie-%'"),e}catch(e){throw o.error({err:e},"TV database opening error"),e}}async function Re(a){try{const e=await L.create(a);e.configure("busyTimeout",5e3),e.run("PRAGMA journal_mode = WAL;"),e.run("PRAGMA synchronous = NORMAL;"),e.run("PRAGMA cache_size = -20000;"),e.run("PRAGMA temp_store = MEMORY;"),e.run("PRAGMA mmap_size = 268435456;"),e.run("PRAGMA foreign_keys = ON;"),e.run("CREATE TABLE IF NOT EXISTS asmr_library (id TEXT PRIMARY KEY, rjCode TEXT NOT NULL, title TEXT, thumbnail TEXT, status TEXT DEFAULT 'Listening', isAdult INTEGER DEFAULT 0, lastTrackIndex INTEGER, lastTrackLabel TEXT, lastPosition REAL, updatedAt INTEGER)"),e.run("CREATE TABLE IF NOT EXISTS asmr_progress (workId TEXT NOT NULL, trackIndex INTEGER NOT NULL, trackLabel TEXT, currentTime REAL DEFAULT 0, duration REAL DEFAULT 0, updatedAt INTEGER, PRIMARY KEY (workId, trackIndex))"),e.run("CREATE TABLE IF NOT EXISTS sync_metadata (key TEXT PRIMARY KEY, value INTEGER)"),e.run("INSERT OR IGNORE INTO sync_metadata (key, value) VALUES ('db_version', 1)"),e.run("CREATE INDEX IF NOT EXISTS idx_asmr_library_status ON asmr_library(status)"),e.run("CREATE INDEX IF NOT EXISTS idx_asmr_library_rjcode ON asmr_library(rjCode)"),e.run("CREATE INDEX IF NOT EXISTS idx_asmr_progress_work ON asmr_progress(workId, updatedAt)");const t=e.all("PRAGMA table_info(asmr_progress)");return t.some(r=>r.name==="title")||e.run("ALTER TABLE asmr_progress ADD COLUMN title TEXT"),t.some(r=>r.name==="thumbnail")||e.run("ALTER TABLE asmr_progress ADD COLUMN thumbnail TEXT"),t.some(r=>r.name==="rjCode")||e.run("ALTER TABLE asmr_progress ADD COLUMN rjCode TEXT"),t.some(r=>r.name==="isAdult")||e.run("ALTER TABLE asmr_progress ADD COLUMN isAdult INTEGER"),e}catch(e){throw o.error({err:e},"ASMR database opening error"),e}}async function Ie(a){try{const e=await L.create(a);e.configure("busyTimeout",5e3),e.run("PRAGMA journal_mode = WAL;"),e.run("PRAGMA synchronous = NORMAL;"),e.run("PRAGMA cache_size = -20000;"),e.run("PRAGMA temp_store = MEMORY;"),e.run("PRAGMA mmap_size = 268435456;"),e.run("PRAGMA foreign_keys = ON;"),e.run("CREATE TABLE IF NOT EXISTS watchlist (id TEXT NOT NULL, name TEXT, thumbnail TEXT, status TEXT, nativeName TEXT, englishName TEXT, type TEXT, PRIMARY KEY (id))"),e.run("CREATE TABLE IF NOT EXISTS watched_episodes (showId TEXT NOT NULL, episodeNumber TEXT NOT NULL, watchedAt DATETIME DEFAULT CURRENT_TIMESTAMP, currentTime REAL DEFAULT 0, duration REAL DEFAULT 0, PRIMARY KEY (showId, episodeNumber))"),e.run("CREATE TABLE IF NOT EXISTS queue (id INTEGER PRIMARY KEY, showId TEXT NOT NULL, episodeNumber TEXT NOT NULL, queue_order INTEGER NOT NULL)"),e.run("CREATE TABLE IF NOT EXISTS settings (key TEXT NOT NULL, value TEXT, PRIMARY KEY (key))"),e.run("CREATE TABLE IF NOT EXISTS shows_meta (id TEXT PRIMARY KEY, name TEXT, thumbnail TEXT, nativeName TEXT, englishName TEXT, episodeCount INTEGER, status TEXT, genres TEXT, popularityScore INTEGER, type TEXT)"),e.run("CREATE TABLE IF NOT EXISTS dismissed_notifications (showId TEXT NOT NULL, episodeNumber TEXT NOT NULL, dismissedAt DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (showId, episodeNumber))"),e.run("CREATE TABLE IF NOT EXISTS discovered_notifications (showId TEXT NOT NULL, episodeNumber TEXT NOT NULL, discoveredAt DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (showId, episodeNumber))"),e.run("CREATE TABLE IF NOT EXISTS sync_metadata (key TEXT PRIMARY KEY, value INTEGER)"),e.run("INSERT OR IGNORE INTO sync_metadata (key, value) VALUES ('db_version', 1)"),e.run("CREATE TABLE IF NOT EXISTS legacy_id_mapping (legacyId TEXT PRIMARY KEY, numericId TEXT)"),e.run("CREATE TABLE IF NOT EXISTS temp_show_ids (id TEXT PRIMARY KEY, provider TEXT NOT NULL, nativeId TEXT NOT NULL, title TEXT NOT NULL, thumbnail TEXT, createdAt INTEGER NOT NULL)"),e.run("CREATE UNIQUE INDEX IF NOT EXISTS idx_temp_show_ids_provider_native ON temp_show_ids(provider, nativeId)");try{const r=await G.purge(e);r>0&&m.info({purged:r},"Purged stale temp show ids on boot")}catch(r){m.warn({err:r},"Temp show purge on boot failed")}try{const r=W();r>0&&m.info({pruned:r},"Pruned expired mal cache rows on boot")}catch(r){m.warn({err:r},"Mal cache prune on boot failed")}e.run("CREATE INDEX IF NOT EXISTS idx_watched_episodes_showId ON watched_episodes(showId)"),e.run("CREATE INDEX IF NOT EXISTS idx_watched_episodes_showId_episodeNumber ON watched_episodes(showId, episodeNumber)"),e.run("CREATE INDEX IF NOT EXISTS idx_watched_episodes_watchedAt ON watched_episodes(watchedAt)"),e.run("CREATE INDEX IF NOT EXISTS idx_watchlist_status ON watchlist(status)"),e.run("CREATE UNIQUE INDEX IF NOT EXISTS idx_queue_show_episode ON queue(showId, episodeNumber)"),e.run("CREATE INDEX IF NOT EXISTS idx_queue_order ON queue(queue_order)"),e.run("DELETE FROM dismissed_notifications WHERE showId NOT IN (SELECT id FROM watchlist)"),e.run("DELETE FROM discovered_notifications WHERE showId NOT IN (SELECT id FROM watchlist)"),e.run("DELETE FROM legacy_id_mapping WHERE legacyId NOT IN (SELECT id FROM watchlist) AND numericId NOT IN (SELECT id FROM watchlist)"),e.run("DELETE FROM dismissed_notifications WHERE EXISTS (SELECT 1 FROM watched_episodes we WHERE we.showId = dismissed_notifications.showId AND we.episodeNumber = dismissed_notifications.episodeNumber)"),e.run("DELETE FROM discovered_notifications WHERE EXISTS (SELECT 1 FROM watched_episodes we WHERE we.showId = discovered_notifications.showId AND we.episodeNumber = discovered_notifications.episodeNumber)"),e.run("UPDATE discovered_notifications SET discoveredAt = datetime('now') WHERE discoveredAt IS NULL");const t=(r,n,i)=>{e.all(`PRAGMA table_info(${r})`).some(U=>U.name===n)||e.run(`ALTER TABLE ${r} ADD COLUMN ${n} ${i}`)};return t("watchlist","nativeName","TEXT"),t("watchlist","englishName","TEXT"),t("shows_meta","nativeName","TEXT"),t("shows_meta","englishName","TEXT"),t("shows_meta","episodeCount","INTEGER"),t("shows_meta","status","TEXT"),t("shows_meta","genres","TEXT"),t("shows_meta","popularityScore","INTEGER"),t("watchlist","type","TEXT"),t("shows_meta","type","TEXT"),t("shows_meta","anilistId","INTEGER"),t("shows_meta","isAdult","INTEGER"),t("shows_meta","episodeDuration","INTEGER"),e}catch(e){throw o.error({err:e},"Database opening error"),e}}export{w as ASMR_SYNC_TABLES,A as KIND_SYNC,D as MANGA_SYNC_TABLES,V as SYNC_TABLES,O as TV_SYNC_TABLES,z as getActiveProvider,M as getKindManifestVersion,J as getLocalManifestVersion,q as initSyncProvider,Re as initializeAsmrDatabase,Ie as initializeDatabase,Ne as initializeMangaDatabase,ge as initializeTvDatabase,pe as performAsmrWriteTransactionAsync,le as performMangaWriteTransactionAsync,Ae as performTvWriteTransactionAsync,f as performWriteTransactionAsync,ue as runFullSyncSequence,g as setKindManifestVersion,h as setLocalManifestVersion,P as syncDownOnBoot,C as syncUp,me as waitForSync};
|