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,353 @@
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.ProxyController = exports.axiosInstance = void 0;
7
+ const axios_1 = __importDefault(require("axios"));
8
+ const axios_retry_1 = __importDefault(require("axios-retry"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const http_1 = __importDefault(require("http"));
11
+ const https_1 = __importDefault(require("https"));
12
+ const node_cache_1 = __importDefault(require("node-cache"));
13
+ const config_1 = require("../config");
14
+ const fs_1 = __importDefault(require("fs"));
15
+ const proxyCache = new node_cache_1.default({ stdTTL: 30, checkperiod: 60 });
16
+ const httpAgent = new http_1.default.Agent({ keepAlive: true, maxSockets: 100 });
17
+ const httpsAgent = new https_1.default.Agent({ keepAlive: true, maxSockets: 100 });
18
+ httpAgent.setMaxListeners(100);
19
+ httpsAgent.setMaxListeners(100);
20
+ exports.axiosInstance = axios_1.default.create({
21
+ httpAgent,
22
+ httpsAgent,
23
+ timeout: 30000,
24
+ });
25
+ (0, axios_retry_1.default)(exports.axiosInstance, { retries: 3, retryDelay: axios_retry_1.default.exponentialDelay });
26
+ class ProxyController {
27
+ static KWIK_DOMAINS = new Set(['kwik.cx', 'kwik.si', 'kwik.pro']);
28
+ static ANIMEPAHE_URL = 'https://animepahe.pw/';
29
+ abortWhenClientLeaves(res, abortController) {
30
+ res.on('close', () => {
31
+ if (!res.writableEnded) {
32
+ abortController.abort();
33
+ }
34
+ });
35
+ }
36
+ validateKwikUrl(value) {
37
+ if (typeof value !== 'string')
38
+ return null;
39
+ try {
40
+ const url = new URL(value);
41
+ const isSecure = url.protocol === 'https:';
42
+ const isKwik = ProxyController.KWIK_DOMAINS.has(url.hostname.toLowerCase());
43
+ const isEmbedPath = /^\/e\/[A-Za-z0-9_-]+$/.test(url.pathname);
44
+ const noAuthOrQuery = !url.username && !url.password && !url.search && !url.hash;
45
+ return isSecure && isKwik && isEmbedPath && noAuthOrQuery ? url : null;
46
+ }
47
+ catch {
48
+ return null;
49
+ }
50
+ }
51
+ handleProxy = async (req, res) => {
52
+ const { url, referer } = req.query;
53
+ if (!url)
54
+ return res.status(400).send('URL required');
55
+ const urlStr = url;
56
+ const refererStr = referer || '';
57
+ const cacheKey = `m3u8-${urlStr}-${refererStr}`;
58
+ const abortController = new AbortController();
59
+ this.abortWhenClientLeaves(res, abortController);
60
+ try {
61
+ const headers = {
62
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
63
+ };
64
+ if (referer)
65
+ headers['Referer'] = refererStr;
66
+ if (req.headers.range)
67
+ headers['Range'] = req.headers.range;
68
+ if (urlStr.includes('.m3u8')) {
69
+ const cached = proxyCache.get(cacheKey);
70
+ if (cached) {
71
+ return res
72
+ .set('Content-Type', 'application/vnd.apple.mpegurl')
73
+ .set('Access-Control-Allow-Origin', '*')
74
+ .send(cached);
75
+ }
76
+ const resp = await exports.axiosInstance.get(urlStr, {
77
+ headers,
78
+ responseType: 'text',
79
+ signal: abortController.signal,
80
+ });
81
+ const baseUrl = new URL(urlStr);
82
+ const proxiedMediaUrl = (targetUrl) => `/api/proxy?url=${encodeURIComponent(targetUrl)}&referer=${encodeURIComponent(refererStr)}`;
83
+ const needsProxy = Boolean(refererStr);
84
+ const rewritten = resp.data
85
+ .split('\n')
86
+ .map((line) => {
87
+ const trimmed = line.trim();
88
+ if (!trimmed)
89
+ return line;
90
+ if (trimmed.startsWith('#')) {
91
+ return trimmed.replace(/URI="([^"]+)"/g, (_, uri) => {
92
+ const absolute = new URL(uri, baseUrl).href;
93
+ return `URI="${needsProxy || absolute.includes('.m3u8') ? proxiedMediaUrl(absolute) : absolute}"`;
94
+ });
95
+ }
96
+ const absolute = new URL(trimmed, baseUrl).href;
97
+ return needsProxy || absolute.includes('.m3u8') ? proxiedMediaUrl(absolute) : absolute;
98
+ })
99
+ .join('\n');
100
+ proxyCache.set(cacheKey, rewritten);
101
+ res
102
+ .set('Content-Type', 'application/vnd.apple.mpegurl')
103
+ .set('Access-Control-Allow-Origin', '*')
104
+ .send(rewritten);
105
+ }
106
+ else {
107
+ const resp = await (0, exports.axiosInstance)({
108
+ method: 'get',
109
+ url: urlStr,
110
+ responseType: 'stream',
111
+ headers,
112
+ signal: abortController.signal,
113
+ });
114
+ res.status(resp.status);
115
+ const forwardHeaders = [
116
+ 'content-type',
117
+ 'content-length',
118
+ 'content-range',
119
+ 'accept-ranges',
120
+ 'cache-control',
121
+ 'last-modified',
122
+ 'etag',
123
+ ];
124
+ Object.keys(resp.headers).forEach((k) => {
125
+ if (forwardHeaders.includes(k.toLowerCase())) {
126
+ res.set(k, resp.headers[k]);
127
+ }
128
+ });
129
+ res.set('Access-Control-Allow-Origin', '*');
130
+ resp.data.on('error', () => {
131
+ abortController.abort();
132
+ if (!res.headersSent)
133
+ res.status(502).send('Upstream error');
134
+ else
135
+ res.destroy();
136
+ });
137
+ res.on('close', () => {
138
+ if (!resp.data.destroyed) {
139
+ resp.data.destroy();
140
+ }
141
+ });
142
+ resp.data.pipe(res);
143
+ }
144
+ }
145
+ catch (e) {
146
+ if (axios_1.default.isCancel(e))
147
+ return;
148
+ if (!res.headersSent)
149
+ res.status(500).send('Proxy error');
150
+ }
151
+ };
152
+ handleEmbedProxy = async (req, res) => {
153
+ const kwikUrl = this.validateKwikUrl(req.query.url);
154
+ if (!kwikUrl)
155
+ return res.status(400).send('Invalid or unsupported gateway URL');
156
+ const abortController = new AbortController();
157
+ this.abortWhenClientLeaves(res, abortController);
158
+ try {
159
+ const { data: originalHtml } = await exports.axiosInstance.get(kwikUrl.href, {
160
+ headers: {
161
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0',
162
+ Referer: ProxyController.ANIMEPAHE_URL,
163
+ Origin: 'https://animepahe.pw',
164
+ },
165
+ responseType: 'text',
166
+ signal: abortController.signal,
167
+ });
168
+ const patched = this.applyKwikPatches(originalHtml, kwikUrl);
169
+ if (!patched)
170
+ return res.status(502).send('Failed to patch video gateway');
171
+ return res
172
+ .status(200)
173
+ .set('Content-Type', 'text/html; charset=utf-8')
174
+ .set('Cache-Control', 'private, max-age=120')
175
+ .send(patched);
176
+ }
177
+ catch (e) {
178
+ if (axios_1.default.isCancel(e))
179
+ return;
180
+ if (!res.headersSent)
181
+ res.status(502).send('Gateway proxy error');
182
+ }
183
+ };
184
+ applyKwikPatches(html, kwikUrl) {
185
+ const safeReferer = JSON.stringify(kwikUrl.href).replace(/</g, '\\u003c');
186
+ const patched = html.replace(/\b(src|href|url)\s*[=:]\s*(["']?)(\/\/[^"'>)]+|\/(?!\/)[^"'>)]*)\2/gi, (match, attr, quote, path) => {
187
+ const full = path.startsWith('//') ? `https:${path}` : `${kwikUrl.origin}${path}`;
188
+ const prefix = match.startsWith('url') ? `url(` : `${attr}=`;
189
+ const suffix = match.startsWith('url') ? `)` : '';
190
+ return `${prefix}${quote}${full}${quote}${suffix}`;
191
+ });
192
+ const iconProxy = `/api/proxy?url=${encodeURIComponent(`${kwikUrl.origin}/app/js/vendor/plyr.svg`)}&referer=${encodeURIComponent(kwikUrl.href)}`;
193
+ const plyrPatch = `<script>if(window.Plyr) Plyr.defaults.iconUrl=${JSON.stringify(iconProxy).replace(/</g, '\\u003c')};</script>`;
194
+ const hlsPatch = `<script>
195
+ (function() {
196
+ var hook = function() {
197
+ if (!window.Hls) return;
198
+ var original = Hls.prototype.loadSource;
199
+ Hls.prototype.loadSource = function(src) {
200
+ if (typeof src === 'string' && src.includes('.m3u8')) {
201
+ src = window.location.origin + '/api/proxy?url=' + encodeURIComponent(src) + '&referer=' + encodeURIComponent(${safeReferer});
202
+ }
203
+ return original.call(this, src);
204
+ };
205
+ };
206
+ if (window.Hls) hook();
207
+ else {
208
+ var observer = new MutationObserver(function() {
209
+ if (window.Hls) { hook(); observer.disconnect(); }
210
+ });
211
+ observer.observe(document.documentElement, { childList: true, subtree: true });
212
+ }
213
+ })();
214
+ </script>`;
215
+ const endBridgePatch = `<script>
216
+ (function() {
217
+ var notified = false;
218
+ var notifyEnded = function() {
219
+ if (notified) return;
220
+ notified = true;
221
+ window.parent.postMessage({ type: 'ANI_WEB_MEDIA_ENDED' }, window.location.origin);
222
+ };
223
+
224
+ var attachToVideos = function(root) {
225
+ var scope = root || document;
226
+ var videos = scope.querySelectorAll ? scope.querySelectorAll('video') : [];
227
+ Array.prototype.forEach.call(videos, function(video) {
228
+ if (video.dataset && video.dataset.aniWebEndedBridge === 'true') return;
229
+ if (video.dataset) video.dataset.aniWebEndedBridge = 'true';
230
+ video.addEventListener('ended', notifyEnded, { once: true });
231
+ });
232
+ };
233
+
234
+ attachToVideos(document);
235
+
236
+ var observer = new MutationObserver(function() {
237
+ attachToVideos(document);
238
+ });
239
+
240
+ observer.observe(document.documentElement, { childList: true, subtree: true });
241
+ })();
242
+ </script>`;
243
+ const beforePlyr = patched.replace(/(<script[^>]+\/plyr\.min\.js[^>]*><\/script>)/i, `$1${plyrPatch}`);
244
+ const final = beforePlyr
245
+ .replace(/(<script[^>]+hls(?:\.min)?\.js[^>]*><\/script>)/i, `$1${hlsPatch}`)
246
+ .replace(/<\/body>/i, `${endBridgePatch}</body>`);
247
+ return final === html ? null : final;
248
+ }
249
+ handleSubtitleProxy = async (req, res) => {
250
+ const { url, referer } = req.query;
251
+ if (!url)
252
+ return res.status(400).send('URL required');
253
+ const abortController = new AbortController();
254
+ this.abortWhenClientLeaves(res, abortController);
255
+ try {
256
+ const headers = {
257
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
258
+ };
259
+ if (referer)
260
+ headers['Referer'] = referer;
261
+ const response = await exports.axiosInstance.get(url, {
262
+ headers,
263
+ responseType: 'text',
264
+ signal: abortController.signal,
265
+ });
266
+ res.set('Content-Type', 'text/vtt; charset=utf-8').send(response.data);
267
+ }
268
+ catch (e) {
269
+ if (axios_1.default.isCancel(e))
270
+ return;
271
+ res.status(500).send('Proxy error');
272
+ }
273
+ };
274
+ handleImageProxy = async (req, res) => {
275
+ const { url, cookie, ua } = req.query;
276
+ if (!url)
277
+ return res.status(400).send('URL required');
278
+ const targetUrl = url;
279
+ const abortController = new AbortController();
280
+ this.abortWhenClientLeaves(res, abortController);
281
+ let refererValue = 'https://allanime.day';
282
+ const headers = {
283
+ 'User-Agent': ua ||
284
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
285
+ };
286
+ try {
287
+ if (targetUrl.includes('animepahe')) {
288
+ refererValue = 'https://animepahe.pw/';
289
+ const rawCookie = cookie || '';
290
+ let sanitized = rawCookie.trim();
291
+ sanitized = sanitized.replace(/^cf_clearance/i, '');
292
+ sanitized = sanitized.replace(/^[:=]\s*/, '');
293
+ sanitized = sanitized.replace(/["']/g, '').trim();
294
+ headers['Cookie'] = `cf_clearance=${sanitized}`;
295
+ }
296
+ else if (targetUrl.includes('anilist.co')) {
297
+ refererValue = 'https://anilist.co/';
298
+ }
299
+ else if (targetUrl.includes('gogocdn.net')) {
300
+ refererValue = 'https://gogoanime.lu/';
301
+ }
302
+ else if (targetUrl.includes('youtube-anime.com') || targetUrl.includes('allanime.day')) {
303
+ refererValue = 'https://allanime.day/';
304
+ }
305
+ else if (targetUrl.includes('animeya.cc')) {
306
+ refererValue = 'https://animeya.cc/';
307
+ }
308
+ headers['Referer'] = refererValue;
309
+ const imageResponse = await (0, exports.axiosInstance)({
310
+ method: 'get',
311
+ url: targetUrl,
312
+ responseType: 'stream',
313
+ headers,
314
+ timeout: 30000,
315
+ signal: abortController.signal,
316
+ });
317
+ if (imageResponse.status === 200) {
318
+ res.set('Cache-Control', 'public, max-age=604800, immutable');
319
+ res.set('Content-Type', String(imageResponse.headers['content-type'] ?? 'image/webp'));
320
+ imageResponse.data.pipe(res);
321
+ }
322
+ else {
323
+ this.sendPlaceholder(res);
324
+ }
325
+ }
326
+ catch (e) {
327
+ if (axios_1.default.isCancel(e)) {
328
+ return;
329
+ }
330
+ if (!res.headersSent) {
331
+ this.sendPlaceholder(res);
332
+ }
333
+ }
334
+ };
335
+ sendPlaceholder(res) {
336
+ const possiblePaths = [
337
+ path_1.default.join(config_1.CONFIG.PACKAGE_ROOT, 'client/public/placeholder.svg'),
338
+ path_1.default.join(config_1.CONFIG.PACKAGE_ROOT, 'client/dist/placeholder.svg'),
339
+ path_1.default.join(config_1.CONFIG.SERVER_ROOT, '..', 'client/public/placeholder.svg'),
340
+ ];
341
+ for (const p of possiblePaths) {
342
+ if (fs_1.default.existsSync(p)) {
343
+ return res.sendFile(p, (err) => {
344
+ if (err && !res.headersSent) {
345
+ res.status(404).send('Not Found');
346
+ }
347
+ });
348
+ }
349
+ }
350
+ res.status(404).send('Not Found');
351
+ }
352
+ }
353
+ exports.ProxyController = ProxyController;
@@ -0,0 +1,159 @@
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.SettingsController = void 0;
7
+ const sync_1 = require("../sync");
8
+ const xml2js_1 = require("xml2js");
9
+ const logger_1 = __importDefault(require("../logger"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const fs_1 = __importDefault(require("fs"));
12
+ const config_1 = require("../config");
13
+ const settings_repository_1 = require("../repositories/settings.repository");
14
+ const machine_id_1 = require("../utils/machine-id");
15
+ const discord_rpc_1 = require("../discord-rpc");
16
+ class SettingsController {
17
+ provider;
18
+ constructor(provider) {
19
+ this.provider = provider;
20
+ }
21
+ getSettings = async (req, res) => {
22
+ try {
23
+ const row = await settings_repository_1.SettingsRepository.getByKey(req.db, req.query.key);
24
+ let value = row ? row.value : null;
25
+ if (value === null && req.query.key === 'discordRPCEnabled') {
26
+ value = 'true';
27
+ }
28
+ res.json({ value: value });
29
+ }
30
+ catch {
31
+ res.status(500).json({ error: 'DB error' });
32
+ }
33
+ };
34
+ updateSettings = async (req, res) => {
35
+ try {
36
+ await (0, sync_1.performWriteTransaction)(req.db, (tx) => {
37
+ settings_repository_1.SettingsRepository.upsert(tx, req.body.key, String(req.body.value));
38
+ });
39
+ if (req.body.key === 'discordRPCEnabled') {
40
+ discord_rpc_1.discordRPCService.setEnabled(req.body.value === 'true' || req.body.value === true);
41
+ }
42
+ res.json({ success: true });
43
+ }
44
+ catch {
45
+ res.status(500).json({ error: 'DB error' });
46
+ }
47
+ };
48
+ backupDatabase = (req, res) => {
49
+ const backupPath = path_1.default.join(config_1.CONFIG.ROOT, 'ani-web-backup.db');
50
+ try {
51
+ req.db.backup(backupPath);
52
+ res.download(backupPath, 'ani-web-backup.db', () => {
53
+ fs_1.default.unlink(backupPath, () => { });
54
+ });
55
+ }
56
+ catch (err) {
57
+ logger_1.default.error({ err }, 'Manual backup failed');
58
+ return res.status(500).json({ error: 'Backup failed' });
59
+ }
60
+ };
61
+ restoreDatabase = (req, res, db, initializeDatabase, setDb) => {
62
+ if (!req.file)
63
+ return res.status(400).json({ error: 'No file uploaded.' });
64
+ const dbName = config_1.CONFIG.IS_DEV ? config_1.CONFIG.DB_NAME_DEV : config_1.CONFIG.DB_NAME_PROD;
65
+ const tempPath = path_1.default.join(config_1.CONFIG.ROOT, `restore_temp.db`);
66
+ const dbPath = path_1.default.join(config_1.CONFIG.ROOT, dbName);
67
+ db.close((closeErr) => {
68
+ if (closeErr)
69
+ return res.status(500).json({ error: 'Failed to close database.' });
70
+ try {
71
+ req.db.checkpoint();
72
+ }
73
+ catch (checkpointErr) {
74
+ logger_1.default.warn({ err: checkpointErr }, 'WAL checkpoint failed');
75
+ }
76
+ try {
77
+ if (fs_1.default.existsSync(`${dbPath}-wal`))
78
+ fs_1.default.unlinkSync(`${dbPath}-wal`);
79
+ if (fs_1.default.existsSync(`${dbPath}-shm`))
80
+ fs_1.default.unlinkSync(`${dbPath}-shm`);
81
+ }
82
+ catch (cleanupErr) {
83
+ logger_1.default.warn({ err: cleanupErr }, 'Failed to clean up WAL files');
84
+ }
85
+ fs_1.default.rename(tempPath, dbPath, async (renameErr) => {
86
+ if (renameErr) {
87
+ try {
88
+ const reopenedDb = await initializeDatabase(dbPath);
89
+ setDb(reopenedDb);
90
+ req.db = reopenedDb;
91
+ }
92
+ catch (e) {
93
+ logger_1.default.error({ err: e }, 'Failed to reopen DB after rename failure');
94
+ }
95
+ return res.status(500).json({ error: 'Failed to replace database file.' });
96
+ }
97
+ try {
98
+ const newDb = await initializeDatabase(dbPath);
99
+ setDb(newDb);
100
+ req.db = newDb;
101
+ res.json({ success: true, message: 'Database restored.' });
102
+ }
103
+ catch (e) {
104
+ logger_1.default.error({ err: e }, 'Failed to initialize restored database');
105
+ res.status(500).json({ error: 'Failed to initialize restored database.' });
106
+ }
107
+ });
108
+ });
109
+ };
110
+ importMalXml = async (req, res) => {
111
+ if (!req.file)
112
+ return res.status(400).json({ error: 'No file' });
113
+ const { erase } = req.body;
114
+ let result;
115
+ try {
116
+ result = await (0, xml2js_1.parseStringPromise)(req.file.buffer.toString());
117
+ }
118
+ catch {
119
+ return res.status(400).json({ error: 'Invalid XML' });
120
+ }
121
+ const animeList = result?.myanimelist?.anime || [];
122
+ let skippedCount = 0;
123
+ const showsToInsert = [];
124
+ const BATCH_SIZE = 5;
125
+ for (let i = 0; i < animeList.length; i += BATCH_SIZE) {
126
+ const batch = animeList.slice(i, i + BATCH_SIZE);
127
+ const batchResults = await Promise.allSettled(batch.map((item) => this.provider.search({ query: item.series_title[0] })));
128
+ batchResults.forEach((r, idx) => {
129
+ if (r.status === 'fulfilled' && r.value.length > 0) {
130
+ showsToInsert.push({
131
+ id: r.value[0]._id,
132
+ name: r.value[0].name,
133
+ thumbnail: r.value[0].thumbnail,
134
+ status: batch[idx].my_status[0],
135
+ });
136
+ }
137
+ else {
138
+ skippedCount++;
139
+ }
140
+ });
141
+ }
142
+ await (0, sync_1.performWriteTransaction)(req.db, (tx) => {
143
+ if (erase)
144
+ settings_repository_1.SettingsRepository.clearWatchlist(tx);
145
+ settings_repository_1.SettingsRepository.upsertWatchlistBatch(tx, showsToInsert);
146
+ });
147
+ res.json({ imported: showsToInsert.length, skipped: skippedCount });
148
+ };
149
+ getInstallationId = (_req, res) => {
150
+ try {
151
+ res.json({ id: (0, machine_id_1.getMachineId)() });
152
+ }
153
+ catch (err) {
154
+ logger_1.default.error({ err }, 'Failed to get machine ID');
155
+ res.status(500).json({ error: 'Failed to get machine ID' });
156
+ }
157
+ };
158
+ }
159
+ exports.SettingsController = SettingsController;