ani-web 2.0.7

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.
Files changed (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +220 -0
  3. package/client/dist/assets/AnimeInfo-B88ZA3gl.js +1 -0
  4. package/client/dist/assets/AnimeInfo-R63luGTP.css +1 -0
  5. package/client/dist/assets/AnimeInfoPage-xGVarrXG.js +2 -0
  6. package/client/dist/assets/Button-Fq9KaUOg.css +1 -0
  7. package/client/dist/assets/Button-lkEUHIDS.js +1 -0
  8. package/client/dist/assets/ErrorMessage-BVWNgHMx.css +1 -0
  9. package/client/dist/assets/ErrorMessage-BXKDLzn8.js +1 -0
  10. package/client/dist/assets/Home-O1FbN8t_.js +1 -0
  11. package/client/dist/assets/Home-r0eYbWNc.css +1 -0
  12. package/client/dist/assets/Insights-CF4K-oO5.css +1 -0
  13. package/client/dist/assets/Insights-opcB-aTo.js +1 -0
  14. package/client/dist/assets/MAL-BGM33N5c.js +1 -0
  15. package/client/dist/assets/MAL-DeQNXXPx.css +1 -0
  16. package/client/dist/assets/Player-BarbgKjI.css +1 -0
  17. package/client/dist/assets/Player-CSyGax33.js +9 -0
  18. package/client/dist/assets/PlayerSettings-BaCVQyw6.js +1 -0
  19. package/client/dist/assets/PlayerSettings-l6aLKrHh.css +1 -0
  20. package/client/dist/assets/QueueRail-Cxn5U8kE.js +1 -0
  21. package/client/dist/assets/QueueRail-DOM_pWkE.css +1 -0
  22. package/client/dist/assets/RemoveConfirmationModal-BBiogSdf.css +1 -0
  23. package/client/dist/assets/RemoveConfirmationModal-DatCZQKq.js +1 -0
  24. package/client/dist/assets/Search-BzO-aRP7.css +1 -0
  25. package/client/dist/assets/Search-DJxo3BYH.js +1 -0
  26. package/client/dist/assets/SearchableSelect-BkCrf6E8.css +1 -0
  27. package/client/dist/assets/SearchableSelect-BzYsMz8B.js +1 -0
  28. package/client/dist/assets/Settings-Bv9fX-x3.css +1 -0
  29. package/client/dist/assets/Settings-C5adinOe.js +1 -0
  30. package/client/dist/assets/SynopsisText-C3AK-aRc.js +1 -0
  31. package/client/dist/assets/SynopsisText-DsI3mW5v.css +1 -0
  32. package/client/dist/assets/ToggleSwitch-BIlQxIjg.css +1 -0
  33. package/client/dist/assets/ToggleSwitch-CrXim14A.js +1 -0
  34. package/client/dist/assets/Watchlist-CXw0vbNx.js +1 -0
  35. package/client/dist/assets/Watchlist-a2RHQogs.css +1 -0
  36. package/client/dist/assets/hls.light-DcbkToIY.js +27 -0
  37. package/client/dist/assets/index-BzX_xmnf.css +1 -0
  38. package/client/dist/assets/index-Ciivz6fh.js +178 -0
  39. package/client/dist/assets/useAnimeInfoData-Dqthchpa.js +1 -0
  40. package/client/dist/assets/useIsMobile-BviODivc.js +1 -0
  41. package/client/dist/assets/vendor-Bc4EraM_.js +3 -0
  42. package/client/dist/favicon.ico +0 -0
  43. package/client/dist/index.html +35 -0
  44. package/client/dist/logo.png +0 -0
  45. package/client/dist/placeholder.svg +4 -0
  46. package/client/dist/robots.txt +3 -0
  47. package/client/package.json +58 -0
  48. package/orchestrator.js +323 -0
  49. package/package.json +88 -0
  50. package/server/.env +1 -0
  51. package/server/dist/config.js +89 -0
  52. package/server/dist/constants.json +1359 -0
  53. package/server/dist/controllers/auth.controller.js +215 -0
  54. package/server/dist/controllers/data.controller.js +232 -0
  55. package/server/dist/controllers/insights.controller.js +200 -0
  56. package/server/dist/controllers/proxy.controller.js +353 -0
  57. package/server/dist/controllers/settings.controller.js +159 -0
  58. package/server/dist/controllers/watchlist.controller.js +749 -0
  59. package/server/dist/db.js +152 -0
  60. package/server/dist/discord-rpc.js +279 -0
  61. package/server/dist/github-sync.js +342 -0
  62. package/server/dist/google.js +310 -0
  63. package/server/dist/logger.js +21 -0
  64. package/server/dist/providers/123anime.provider.js +226 -0
  65. package/server/dist/providers/allanime.provider.js +736 -0
  66. package/server/dist/providers/animepahe.provider.js +457 -0
  67. package/server/dist/providers/animeya.provider.js +787 -0
  68. package/server/dist/providers/megaplay.provider.js +264 -0
  69. package/server/dist/providers/provider.interface.js +2 -0
  70. package/server/dist/rclone.js +126 -0
  71. package/server/dist/repositories/insights.repository.js +42 -0
  72. package/server/dist/repositories/notifications.repository.js +30 -0
  73. package/server/dist/repositories/queue.repository.js +38 -0
  74. package/server/dist/repositories/settings.repository.js +14 -0
  75. package/server/dist/repositories/shows-meta.repository.js +41 -0
  76. package/server/dist/repositories/watched-episodes.repository.js +67 -0
  77. package/server/dist/repositories/watchlist.repository.js +80 -0
  78. package/server/dist/routes/auth.routes.js +26 -0
  79. package/server/dist/routes/data.routes.js +42 -0
  80. package/server/dist/routes/insights.routes.js +12 -0
  81. package/server/dist/routes/proxy.routes.js +14 -0
  82. package/server/dist/routes/settings.routes.js +27 -0
  83. package/server/dist/routes/watchlist.routes.js +46 -0
  84. package/server/dist/server.js +229 -0
  85. package/server/dist/sync-config.js +28 -0
  86. package/server/dist/sync.js +427 -0
  87. package/server/dist/utils/db-utils.js +15 -0
  88. package/server/dist/utils/env.utils.js +79 -0
  89. package/server/dist/utils/machine-id.js +46 -0
  90. package/server/dist/utils/request-context.js +5 -0
  91. package/server/package.json +19 -0
@@ -0,0 +1,427 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.waitForSync = waitForSync;
40
+ exports.getActiveProvider = getActiveProvider;
41
+ exports.initSyncProvider = initSyncProvider;
42
+ exports.getLocalManifestVersion = getLocalManifestVersion;
43
+ exports.setLocalManifestVersion = setLocalManifestVersion;
44
+ exports.syncDownOnBoot = syncDownOnBoot;
45
+ exports.syncUp = syncUp;
46
+ exports.performWriteTransaction = performWriteTransaction;
47
+ exports.initializeDatabase = initializeDatabase;
48
+ const fs = __importStar(require("fs/promises"));
49
+ const fs_1 = require("fs");
50
+ const path_1 = __importDefault(require("path"));
51
+ const logger_1 = __importDefault(require("./logger"));
52
+ const google_1 = require("./google");
53
+ const rclone_1 = require("./rclone");
54
+ const github_sync_1 = require("./github-sync");
55
+ const config_1 = require("./config");
56
+ const db_1 = require("./db");
57
+ const db_utils_1 = require("./utils/db-utils");
58
+ const log = logger_1.default.child({ module: 'Sync' });
59
+ class Mutex {
60
+ _locked = false;
61
+ _waiting = [];
62
+ async lock() {
63
+ return new Promise((resolve) => {
64
+ if (!this._locked) {
65
+ this._locked = true;
66
+ resolve();
67
+ }
68
+ else {
69
+ this._waiting.push(resolve);
70
+ }
71
+ });
72
+ }
73
+ unlock() {
74
+ if (this._waiting.length > 0) {
75
+ const resolve = this._waiting.shift();
76
+ resolve();
77
+ }
78
+ else {
79
+ this._locked = false;
80
+ }
81
+ }
82
+ }
83
+ const syncMutex = new Mutex();
84
+ let isSyncing = false;
85
+ let activeProvider = 'none';
86
+ async function waitForSync() {
87
+ await syncMutex.lock();
88
+ syncMutex.unlock();
89
+ }
90
+ function getActiveProvider() {
91
+ return activeProvider;
92
+ }
93
+ async function initSyncProvider(preferred) {
94
+ const provider = preferred || process.env.SYNC_PROVIDER;
95
+ if (provider === 'github' && github_sync_1.githubSyncService.isAuthenticated()) {
96
+ activeProvider = 'github';
97
+ log.info('Sync Provider: GitHub (Selected)');
98
+ return;
99
+ }
100
+ if (provider === 'google' && google_1.googleDriveService.isAuthenticated()) {
101
+ activeProvider = 'google';
102
+ log.info('Sync Provider: Google Drive API (Selected)');
103
+ return;
104
+ }
105
+ if (provider === 'rclone') {
106
+ const rcloneAvailable = await rclone_1.rcloneService.init();
107
+ if (rcloneAvailable) {
108
+ activeProvider = 'rclone';
109
+ log.info(`Sync Provider: Rclone (${rclone_1.rcloneService.getRemoteName()}) (Selected)`);
110
+ return;
111
+ }
112
+ }
113
+ if (provider === 'none') {
114
+ activeProvider = 'none';
115
+ log.info('Sync Provider: None (Forced)');
116
+ return;
117
+ }
118
+ if (github_sync_1.githubSyncService.isAuthenticated()) {
119
+ activeProvider = 'github';
120
+ log.info('Sync Provider: GitHub (Fallback)');
121
+ return;
122
+ }
123
+ if (google_1.googleDriveService.isAuthenticated()) {
124
+ activeProvider = 'google';
125
+ log.info('Sync Provider: Google Drive API (Fallback)');
126
+ return;
127
+ }
128
+ const rcloneAvailable = await rclone_1.rcloneService.init();
129
+ if (rcloneAvailable) {
130
+ activeProvider = 'rclone';
131
+ log.info(`Sync Provider: Rclone (${rclone_1.rcloneService.getRemoteName()}) (Fallback)`);
132
+ return;
133
+ }
134
+ activeProvider = 'none';
135
+ log.info('No sync provider available.');
136
+ }
137
+ async function getLocalManifestVersion() {
138
+ if ((0, fs_1.existsSync)(config_1.CONFIG.LOCAL_MANIFEST_PATH)) {
139
+ try {
140
+ const content = await fs.readFile(config_1.CONFIG.LOCAL_MANIFEST_PATH, 'utf-8');
141
+ return JSON.parse(content).version || 0;
142
+ }
143
+ catch {
144
+ return 0;
145
+ }
146
+ }
147
+ return 0;
148
+ }
149
+ async function setLocalManifestVersion(version) {
150
+ await fs.writeFile(config_1.CONFIG.LOCAL_MANIFEST_PATH, JSON.stringify({ version }));
151
+ }
152
+ async function getRemoteManifestVersion(remoteFolder) {
153
+ try {
154
+ if (activeProvider === 'github') {
155
+ if (!github_sync_1.githubSyncService.isAuthenticated())
156
+ return { version: 0 };
157
+ return { version: await github_sync_1.githubSyncService.getRemoteVersion() };
158
+ }
159
+ else if (activeProvider === 'google') {
160
+ if (!google_1.googleDriveService.isAuthenticated())
161
+ return { version: 0 };
162
+ const folderId = await google_1.googleDriveService.ensureFolder(remoteFolder);
163
+ const file = await google_1.googleDriveService.findFile(config_1.CONFIG.MANIFEST_FILENAME, folderId);
164
+ if (!file)
165
+ return { version: 0 };
166
+ const tempPath = path_1.default.join(config_1.CONFIG.ROOT, `temp_${Date.now()}_manifest.json`);
167
+ try {
168
+ await google_1.googleDriveService.downloadFile(file.id, tempPath);
169
+ const content = await fs.readFile(tempPath, 'utf-8');
170
+ return { version: JSON.parse(content).version || 0, fileId: file.id };
171
+ }
172
+ finally {
173
+ if ((0, fs_1.existsSync)(tempPath))
174
+ await fs.unlink(tempPath);
175
+ }
176
+ }
177
+ else if (activeProvider === 'rclone') {
178
+ const exists = await rclone_1.rcloneService.fileExists(remoteFolder, config_1.CONFIG.MANIFEST_FILENAME);
179
+ if (!exists)
180
+ return { version: 0 };
181
+ const tempPath = path_1.default.join(config_1.CONFIG.ROOT, `temp_${Date.now()}_manifest.json`);
182
+ try {
183
+ await rclone_1.rcloneService.downloadFile(remoteFolder, config_1.CONFIG.MANIFEST_FILENAME, tempPath);
184
+ const content = await fs.readFile(tempPath, 'utf-8');
185
+ return { version: JSON.parse(content).version || 0 };
186
+ }
187
+ finally {
188
+ if ((0, fs_1.existsSync)(tempPath))
189
+ await fs.unlink(tempPath);
190
+ }
191
+ }
192
+ }
193
+ catch (err) {
194
+ log.warn({ err }, 'Could not read remote manifest.');
195
+ }
196
+ return { version: 0 };
197
+ }
198
+ async function syncDownOnBoot(db, dbPath, remoteFolderName, closeMainDb) {
199
+ let localVersion = await getLocalManifestVersion();
200
+ if (localVersion === 0 && db) {
201
+ const row = (0, db_utils_1.dbGet)(db, "SELECT value FROM sync_metadata WHERE key = 'db_version'");
202
+ localVersion = row?.value ?? 0;
203
+ if (localVersion > 0) {
204
+ await setLocalManifestVersion(localVersion);
205
+ }
206
+ }
207
+ if (activeProvider === 'none')
208
+ return false;
209
+ await syncMutex.lock();
210
+ if (isSyncing) {
211
+ syncMutex.unlock();
212
+ return false;
213
+ }
214
+ isSyncing = true;
215
+ try {
216
+ console.log(`[SYNC_START] Initial sync check (${activeProvider})`);
217
+ const { version: remoteVersion } = await getRemoteManifestVersion(remoteFolderName);
218
+ console.log('[SYNC_END]');
219
+ log.info(`Sync Check: Local v${localVersion} vs Remote v${remoteVersion}`);
220
+ if (remoteVersion > localVersion) {
221
+ if (activeProvider === 'github') {
222
+ if (!github_sync_1.githubSyncService.isAuthenticated())
223
+ return false;
224
+ console.log(`[SYNC_START] Importing GitHub sync data (Remote v${remoteVersion})`);
225
+ const importedVersion = await github_sync_1.githubSyncService.syncDown(db);
226
+ await setLocalManifestVersion(importedVersion || remoteVersion);
227
+ console.log('[SYNC_END]');
228
+ log.info('GitHub sync down complete.');
229
+ return false;
230
+ }
231
+ console.log(`[SYNC_START] Downloading remote database (Remote v${remoteVersion})`);
232
+ await closeMainDb();
233
+ const backupPath = `${dbPath}.bak`;
234
+ const dbName = path_1.default.basename(dbPath);
235
+ try {
236
+ if ((0, fs_1.existsSync)(dbPath)) {
237
+ await fs.copyFile(dbPath, backupPath);
238
+ }
239
+ try {
240
+ await fs.unlink(`${dbPath}-wal`);
241
+ }
242
+ catch (e) {
243
+ void e;
244
+ }
245
+ try {
246
+ await fs.unlink(`${dbPath}-shm`);
247
+ }
248
+ catch (e) {
249
+ void e;
250
+ }
251
+ if (activeProvider === 'google') {
252
+ if (!google_1.googleDriveService.isAuthenticated())
253
+ return true;
254
+ const folderId = await google_1.googleDriveService.ensureFolder(remoteFolderName);
255
+ const remoteDb = await google_1.googleDriveService.findFile(dbName, folderId);
256
+ const remoteManifest = await google_1.googleDriveService.findFile(config_1.CONFIG.MANIFEST_FILENAME, folderId);
257
+ if (remoteDb)
258
+ await google_1.googleDriveService.downloadFile(remoteDb.id, dbPath);
259
+ if (remoteManifest)
260
+ await google_1.googleDriveService.downloadFile(remoteManifest.id, config_1.CONFIG.LOCAL_MANIFEST_PATH);
261
+ }
262
+ else if (activeProvider === 'rclone') {
263
+ await rclone_1.rcloneService.downloadFile(remoteFolderName, dbName, dbPath);
264
+ await rclone_1.rcloneService.downloadFile(remoteFolderName, config_1.CONFIG.MANIFEST_FILENAME, config_1.CONFIG.LOCAL_MANIFEST_PATH);
265
+ }
266
+ if ((0, fs_1.existsSync)(backupPath)) {
267
+ await fs.unlink(backupPath);
268
+ }
269
+ console.log('[SYNC_END]');
270
+ log.info('Sync down complete.');
271
+ return true;
272
+ }
273
+ catch (err) {
274
+ console.log('[SYNC_END]');
275
+ log.error({ err }, 'Sync down failed. Restoring backup.');
276
+ if ((0, fs_1.existsSync)(backupPath)) {
277
+ try {
278
+ await fs.copyFile(backupPath, dbPath);
279
+ log.info('Backup restored successfully after failed sync down.');
280
+ }
281
+ catch (restoreErr) {
282
+ log.error({ err: restoreErr }, 'Critical: restore from backup also failed.');
283
+ throw new Error('Sync down and restore both failed. Database may be corrupt.', {
284
+ cause: restoreErr,
285
+ });
286
+ }
287
+ }
288
+ return true;
289
+ }
290
+ }
291
+ else {
292
+ log.info('Local DB is up to date.');
293
+ return false;
294
+ }
295
+ }
296
+ catch (err) {
297
+ console.log('[SYNC_END]');
298
+ log.error({ err }, 'Sync boot error.');
299
+ return false;
300
+ }
301
+ finally {
302
+ isSyncing = false;
303
+ syncMutex.unlock();
304
+ }
305
+ }
306
+ async function syncUp(db, dbPath, remoteFolderName) {
307
+ if (activeProvider === 'none')
308
+ return;
309
+ await syncMutex.lock();
310
+ if (isSyncing) {
311
+ syncMutex.unlock();
312
+ return;
313
+ }
314
+ isSyncing = true;
315
+ try {
316
+ const localVersion = await getLocalManifestVersion();
317
+ console.log(`[SYNC_START] Syncing up (Local v${localVersion})`);
318
+ const { version: remoteVersion, fileId: manifestId } = await getRemoteManifestVersion(remoteFolderName);
319
+ if (localVersion > remoteVersion) {
320
+ const dbName = path_1.default.basename(dbPath);
321
+ const syncDbPath = `${dbPath}.sync.db`;
322
+ try {
323
+ if (activeProvider === 'github') {
324
+ if (!github_sync_1.githubSyncService.isAuthenticated())
325
+ return;
326
+ await github_sync_1.githubSyncService.syncUp(db);
327
+ }
328
+ else {
329
+ db.backup(syncDbPath);
330
+ }
331
+ if (activeProvider === 'google') {
332
+ if (!google_1.googleDriveService.isAuthenticated())
333
+ return;
334
+ const folderId = await google_1.googleDriveService.ensureFolder(remoteFolderName);
335
+ const remoteDbFile = await google_1.googleDriveService.findFile(dbName, folderId);
336
+ await google_1.googleDriveService.uploadFile(syncDbPath, dbName, 'application/x-sqlite3', folderId, remoteDbFile?.id);
337
+ await google_1.googleDriveService.uploadFile(config_1.CONFIG.LOCAL_MANIFEST_PATH, config_1.CONFIG.MANIFEST_FILENAME, 'application/json', folderId, manifestId);
338
+ }
339
+ else if (activeProvider === 'rclone') {
340
+ await rclone_1.rcloneService.uploadFile(syncDbPath, remoteFolderName, dbName);
341
+ await rclone_1.rcloneService.uploadFile(config_1.CONFIG.LOCAL_MANIFEST_PATH, remoteFolderName, config_1.CONFIG.MANIFEST_FILENAME);
342
+ }
343
+ }
344
+ finally {
345
+ if ((0, fs_1.existsSync)(syncDbPath))
346
+ await fs.unlink(syncDbPath).catch(() => { });
347
+ }
348
+ console.log('[SYNC_END]');
349
+ log.info('Sync up complete.');
350
+ }
351
+ else {
352
+ console.log('[SYNC_END]');
353
+ log.info('No changes to sync up or remote is newer.');
354
+ }
355
+ }
356
+ catch (err) {
357
+ console.log('[SYNC_END]');
358
+ log.error({ err }, 'Sync up failed.');
359
+ }
360
+ finally {
361
+ isSyncing = false;
362
+ syncMutex.unlock();
363
+ }
364
+ }
365
+ async function performWriteTransaction(db, runnable) {
366
+ db.serialize(() => {
367
+ runnable(db);
368
+ db.run("UPDATE sync_metadata SET value = value + 1 WHERE key = 'db_version'");
369
+ });
370
+ const row = (0, db_utils_1.dbGet)(db, "SELECT value FROM sync_metadata WHERE key = 'db_version'");
371
+ const newVersion = row?.value ?? 1;
372
+ await setLocalManifestVersion(newVersion);
373
+ }
374
+ async function initializeDatabase(dbPath) {
375
+ try {
376
+ const db = await db_1.DatabaseWrapper.create(dbPath);
377
+ db.configure('busyTimeout', 5000);
378
+ db.run('PRAGMA journal_mode = WAL;');
379
+ db.run('PRAGMA synchronous = NORMAL;');
380
+ db.run('PRAGMA cache_size = -20000;');
381
+ db.run('PRAGMA temp_store = MEMORY;');
382
+ db.run('PRAGMA mmap_size = 268435456;');
383
+ db.run('PRAGMA foreign_keys = ON;');
384
+ 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))`);
385
+ 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))`);
386
+ 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)`);
387
+ db.run(`CREATE TABLE IF NOT EXISTS settings (key TEXT NOT NULL, value TEXT, PRIMARY KEY (key))`);
388
+ 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)`);
389
+ 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))`);
390
+ 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))`);
391
+ db.run(`CREATE TABLE IF NOT EXISTS sync_metadata (key TEXT PRIMARY KEY, value INTEGER)`);
392
+ db.run(`INSERT OR IGNORE INTO sync_metadata (key, value) VALUES ('db_version', 1)`);
393
+ db.run(`CREATE INDEX IF NOT EXISTS idx_watched_episodes_showId ON watched_episodes(showId)`);
394
+ db.run(`CREATE INDEX IF NOT EXISTS idx_watched_episodes_showId_episodeNumber ON watched_episodes(showId, episodeNumber)`);
395
+ db.run(`CREATE INDEX IF NOT EXISTS idx_watched_episodes_watchedAt ON watched_episodes(watchedAt)`);
396
+ db.run(`CREATE INDEX IF NOT EXISTS idx_watchlist_status ON watchlist(status)`);
397
+ db.run(`CREATE UNIQUE INDEX IF NOT EXISTS idx_queue_show_episode ON queue(showId, episodeNumber)`);
398
+ db.run(`CREATE INDEX IF NOT EXISTS idx_queue_order ON queue(queue_order)`);
399
+ db.run('DELETE FROM watched_episodes WHERE showId NOT IN (SELECT id FROM watchlist)');
400
+ db.run('DELETE FROM dismissed_notifications WHERE showId NOT IN (SELECT id FROM watchlist)');
401
+ db.run('DELETE FROM discovered_notifications WHERE showId NOT IN (SELECT id FROM watchlist)');
402
+ db.run('DELETE FROM shows_meta WHERE id NOT IN (SELECT id FROM watchlist) AND id NOT IN (SELECT showId FROM queue)');
403
+ 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)');
404
+ 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)');
405
+ const addCol = (tbl, col, type) => {
406
+ const columns = db.all(`PRAGMA table_info(${tbl})`);
407
+ if (!columns.some((c) => c.name === col))
408
+ db.run(`ALTER TABLE ${tbl} ADD COLUMN ${col} ${type}`);
409
+ };
410
+ addCol('watchlist', 'nativeName', 'TEXT');
411
+ addCol('watchlist', 'englishName', 'TEXT');
412
+ addCol('shows_meta', 'nativeName', 'TEXT');
413
+ addCol('shows_meta', 'englishName', 'TEXT');
414
+ addCol('shows_meta', 'episodeCount', 'INTEGER');
415
+ addCol('shows_meta', 'status', 'TEXT');
416
+ addCol('shows_meta', 'genres', 'TEXT');
417
+ addCol('shows_meta', 'popularityScore', 'INTEGER');
418
+ addCol('watchlist', 'type', 'TEXT');
419
+ addCol('shows_meta', 'type', 'TEXT');
420
+ await db.saveNow();
421
+ return db;
422
+ }
423
+ catch (err) {
424
+ log.error({ err }, 'Database opening error');
425
+ throw err;
426
+ }
427
+ }
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.dbRun = exports.dbGet = exports.dbAll = void 0;
4
+ const dbAll = (db, sql, params = []) => {
5
+ return db.all(sql, params);
6
+ };
7
+ exports.dbAll = dbAll;
8
+ const dbGet = (db, sql, params = []) => {
9
+ return db.get(sql, params);
10
+ };
11
+ exports.dbGet = dbGet;
12
+ const dbRun = (db, sql, params = []) => {
13
+ db.run(sql, params);
14
+ };
15
+ exports.dbRun = dbRun;
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.updateEnvFile = updateEnvFile;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const config_1 = require("../config");
9
+ const lockPath = `${config_1.CONFIG.ENV_PATH}.lock`;
10
+ async function acquireLock() {
11
+ let handle;
12
+ let attempts = 0;
13
+ while (!handle) {
14
+ try {
15
+ handle = await fs_1.default.promises.open(lockPath, 'wx');
16
+ }
17
+ catch {
18
+ if (++attempts >= 100) {
19
+ fs_1.default.promises.unlink(lockPath).catch(() => { });
20
+ throw new Error('Failed to acquire lock on .env file - another process may be holding it');
21
+ }
22
+ await new Promise((r) => setTimeout(r, 100));
23
+ }
24
+ }
25
+ return handle;
26
+ }
27
+ function releaseLock(handle) {
28
+ handle.close().catch(() => { });
29
+ fs_1.default.promises.unlink(lockPath).catch(() => { });
30
+ }
31
+ async function updateEnvFile(updates) {
32
+ const lockHandle = await acquireLock();
33
+ try {
34
+ const envPath = config_1.CONFIG.ENV_PATH;
35
+ let envContent = '';
36
+ if (fs_1.default.existsSync(envPath)) {
37
+ envContent = fs_1.default.readFileSync(envPath, 'utf8');
38
+ }
39
+ const lines = envContent.split('\n');
40
+ const newLines = [...lines];
41
+ Object.entries(updates).forEach(([key, value]) => {
42
+ let found = false;
43
+ for (let i = 0; i < newLines.length; i++) {
44
+ const line = newLines[i];
45
+ if (line && line.startsWith(`${key}=`)) {
46
+ if (value === '') {
47
+ newLines.splice(i, 1);
48
+ continue;
49
+ }
50
+ else {
51
+ newLines[i] = `${key}=${value}`;
52
+ }
53
+ found = true;
54
+ break;
55
+ }
56
+ }
57
+ if (!found && value !== '') {
58
+ newLines.push(`${key}=${value}`);
59
+ }
60
+ });
61
+ const finalContent = newLines
62
+ .join('\n')
63
+ .replace(/\n{2,}/g, '\n')
64
+ .trim() + '\n';
65
+ fs_1.default.writeFileSync(envPath, finalContent);
66
+ // Update process.env so the changes are reflected immediately in the running process
67
+ Object.entries(updates).forEach(([key, value]) => {
68
+ if (value === '') {
69
+ delete process.env[key];
70
+ }
71
+ else {
72
+ process.env[key] = value;
73
+ }
74
+ });
75
+ }
76
+ finally {
77
+ releaseLock(lockHandle);
78
+ }
79
+ }
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getMachineId = getMachineId;
7
+ const child_process_1 = require("child_process");
8
+ const os_1 = __importDefault(require("os"));
9
+ const crypto_1 = __importDefault(require("crypto"));
10
+ const fs_1 = __importDefault(require("fs"));
11
+ function getMachineId() {
12
+ let hardwareId = '';
13
+ try {
14
+ if (process.platform === 'win32') {
15
+ hardwareId = (0, child_process_1.execSync)('reg query "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography" /v MachineGuid')
16
+ .toString()
17
+ .split('REG_SZ')[1]
18
+ .trim();
19
+ }
20
+ else if (process.platform === 'linux') {
21
+ if (fs_1.default.existsSync('/etc/machine-id')) {
22
+ hardwareId = fs_1.default.readFileSync('/etc/machine-id', 'utf8').trim();
23
+ }
24
+ else if (fs_1.default.existsSync('/var/lib/dbus/machine-id')) {
25
+ hardwareId = fs_1.default.readFileSync('/var/lib/dbus/machine-id', 'utf8').trim();
26
+ }
27
+ }
28
+ else if (process.platform === 'darwin') {
29
+ hardwareId = (0, child_process_1.execSync)("ioreg -rd1 -c IOPlatformExpertDevice | grep -E 'IOPlatformUUID' | awk -F'\"' '{print $4}'")
30
+ .toString()
31
+ .trim();
32
+ }
33
+ }
34
+ catch (err) {
35
+ const interfaces = os_1.default.networkInterfaces();
36
+ const macs = Object.values(interfaces)
37
+ .flat()
38
+ .filter((iface) => iface && !iface.internal && iface.mac !== '00:00:00:00:00:00')
39
+ .map((iface) => iface.mac)
40
+ .sort();
41
+ hardwareId = macs.length > 0 ? macs.join('-') : os_1.default.hostname();
42
+ }
43
+ const username = os_1.default.userInfo().username;
44
+ const seed = `${hardwareId}:${username}:${process.platform}`;
45
+ return crypto_1.default.createHash('sha256').update(seed).digest('hex');
46
+ }
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.requestContext = void 0;
4
+ const node_async_hooks_1 = require("node:async_hooks");
5
+ exports.requestContext = new node_async_hooks_1.AsyncLocalStorage();
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "ani-web-server",
3
+ "version": "2.0.7",
4
+ "type": "commonjs",
5
+ "engines": {
6
+ "node": ">=22.5.0"
7
+ },
8
+ "scripts": {
9
+ "start": "node --max-old-space-size=256 dist/server.js",
10
+ "dev": "nodemon --watch \"src/**/*.ts\" --exec \"ts-node --transpile-only src/server.ts --dev\"",
11
+ "build": "tsc",
12
+ "format": "prettier --write .",
13
+ "lint": "eslint ."
14
+ },
15
+ "overrides": {
16
+ "encoding-sniffer": "^1.0.2",
17
+ "glob": "^13.0.0"
18
+ }
19
+ }