ani-web 1.5.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.

Potentially problematic release.


This version of ani-web might be problematic. Click here for more details.

Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +174 -0
  3. package/client/dist/assets/AnimeInfo-C7DQp7Oo.js +1 -0
  4. package/client/dist/assets/AnimeInfo-Sb3YiXHJ.css +1 -0
  5. package/client/dist/assets/AnimeInfoPage-DJA7AJQ8.js +2 -0
  6. package/client/dist/assets/Button-Fq9KaUOg.css +1 -0
  7. package/client/dist/assets/Button-o0l9V_NG.js +1 -0
  8. package/client/dist/assets/ErrorMessage-Ddf2zmRx.js +1 -0
  9. package/client/dist/assets/ErrorMessage-FOxXyZC9.css +1 -0
  10. package/client/dist/assets/Home-CKHJA97j.css +1 -0
  11. package/client/dist/assets/Home-Dey0azy1.js +1 -0
  12. package/client/dist/assets/Insights-BSRcCkDs.css +1 -0
  13. package/client/dist/assets/Insights-CogjPOd_.js +1 -0
  14. package/client/dist/assets/MAL-CYArH4yf.js +1 -0
  15. package/client/dist/assets/MAL-DeQNXXPx.css +1 -0
  16. package/client/dist/assets/Player-BWFN9gud.js +9 -0
  17. package/client/dist/assets/Player-CBCYW7uG.css +1 -0
  18. package/client/dist/assets/PlayerSettings-BgStUrrP.css +1 -0
  19. package/client/dist/assets/PlayerSettings-rWZuATQf.js +1 -0
  20. package/client/dist/assets/RemoveConfirmationModal-BBiogSdf.css +1 -0
  21. package/client/dist/assets/RemoveConfirmationModal-CLYqyGOv.js +1 -0
  22. package/client/dist/assets/Search-DZAWgKwq.js +1 -0
  23. package/client/dist/assets/Search-lWsVQ0Ke.css +1 -0
  24. package/client/dist/assets/Settings-Bv9fX-x3.css +1 -0
  25. package/client/dist/assets/Settings-DyisJGeD.js +1 -0
  26. package/client/dist/assets/ToggleSwitch-CLnWnAuY.js +1 -0
  27. package/client/dist/assets/ToggleSwitch-DInRb7iM.css +1 -0
  28. package/client/dist/assets/Watchlist-2dVYksxq.css +1 -0
  29. package/client/dist/assets/Watchlist-CuqJISI3.js +1 -0
  30. package/client/dist/assets/hls.light-DcbkToIY.js +27 -0
  31. package/client/dist/assets/index-BK_Zaqaw.css +1 -0
  32. package/client/dist/assets/index-CHVF4D4L.js +178 -0
  33. package/client/dist/assets/useAnimeInfoData-Cr58brCY.js +1 -0
  34. package/client/dist/assets/useIsMobile-gHo4t6g6.js +1 -0
  35. package/client/dist/assets/vendor-DdbgYKo4.js +3 -0
  36. package/client/dist/favicon.ico +0 -0
  37. package/client/dist/index.html +35 -0
  38. package/client/dist/logo.png +0 -0
  39. package/client/dist/placeholder.svg +4 -0
  40. package/client/dist/robots.txt +3 -0
  41. package/client/package.json +54 -0
  42. package/orchestrator.js +302 -0
  43. package/package.json +69 -0
  44. package/server/dist/config.js +86 -0
  45. package/server/dist/constants.json +1359 -0
  46. package/server/dist/controllers/auth.controller.js +213 -0
  47. package/server/dist/controllers/data.controller.js +126 -0
  48. package/server/dist/controllers/insights.controller.js +125 -0
  49. package/server/dist/controllers/proxy.controller.js +235 -0
  50. package/server/dist/controllers/settings.controller.js +147 -0
  51. package/server/dist/controllers/watchlist.controller.js +499 -0
  52. package/server/dist/db.js +231 -0
  53. package/server/dist/github-sync.js +279 -0
  54. package/server/dist/google.js +274 -0
  55. package/server/dist/logger.js +21 -0
  56. package/server/dist/providers/123anime.provider.js +229 -0
  57. package/server/dist/providers/allanime.provider.js +773 -0
  58. package/server/dist/providers/animepahe.provider.js +313 -0
  59. package/server/dist/providers/animeya.provider.js +799 -0
  60. package/server/dist/providers/provider.interface.js +2 -0
  61. package/server/dist/rclone.js +126 -0
  62. package/server/dist/repositories/insights.repository.js +30 -0
  63. package/server/dist/repositories/notifications.repository.js +22 -0
  64. package/server/dist/repositories/settings.repository.js +13 -0
  65. package/server/dist/repositories/shows-meta.repository.js +39 -0
  66. package/server/dist/repositories/watched-episodes.repository.js +60 -0
  67. package/server/dist/repositories/watchlist.repository.js +49 -0
  68. package/server/dist/routes/auth.routes.js +23 -0
  69. package/server/dist/routes/data.routes.js +43 -0
  70. package/server/dist/routes/insights.routes.js +11 -0
  71. package/server/dist/routes/proxy.routes.js +13 -0
  72. package/server/dist/routes/settings.routes.js +26 -0
  73. package/server/dist/routes/watchlist.routes.js +26 -0
  74. package/server/dist/server.js +179 -0
  75. package/server/dist/sync-config.js +28 -0
  76. package/server/dist/sync.js +383 -0
  77. package/server/dist/utils/db-utils.js +36 -0
  78. package/server/dist/utils/env.utils.js +70 -0
  79. package/server/package.json +54 -0
@@ -0,0 +1,235 @@
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
+ handleProxy = async (req, res) => {
28
+ const { url, referer } = req.query;
29
+ if (!url)
30
+ return res.status(400).send('URL required');
31
+ const urlStr = url;
32
+ const refererStr = referer || '';
33
+ const cacheKey = `m3u8-${urlStr}-${refererStr}`;
34
+ const abortController = new AbortController();
35
+ req.on('close', () => {
36
+ abortController.abort();
37
+ });
38
+ try {
39
+ const headers = {
40
+ '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',
41
+ };
42
+ if (referer)
43
+ headers['Referer'] = refererStr;
44
+ if (req.headers.range)
45
+ headers['Range'] = req.headers.range;
46
+ if (urlStr.includes('.m3u8')) {
47
+ const cached = proxyCache.get(cacheKey);
48
+ if (cached) {
49
+ return res
50
+ .set('Content-Type', 'application/vnd.apple.mpegurl')
51
+ .set('Access-Control-Allow-Origin', '*')
52
+ .send(cached);
53
+ }
54
+ const resp = await exports.axiosInstance.get(urlStr, {
55
+ headers,
56
+ responseType: 'text',
57
+ signal: abortController.signal,
58
+ });
59
+ const baseUrl = new URL(urlStr);
60
+ const rewritten = resp.data
61
+ .split('\n')
62
+ .map((l) => {
63
+ const line = l.trim();
64
+ if (!line)
65
+ return l;
66
+ if (line.startsWith('#')) {
67
+ return line.replace(/URI="([^"]+)"/g, (match, uri) => {
68
+ const fullUri = new URL(uri, baseUrl).href;
69
+ if (fullUri.includes('.m3u8')) {
70
+ return `URI="/api/proxy?url=${encodeURIComponent(fullUri)}&referer=${encodeURIComponent(refererStr)}"`;
71
+ }
72
+ return `URI="${fullUri}"`;
73
+ });
74
+ }
75
+ const fullUrl = new URL(line, baseUrl).href;
76
+ if (fullUrl.includes('.m3u8')) {
77
+ return `/api/proxy?url=${encodeURIComponent(fullUrl)}&referer=${encodeURIComponent(refererStr)}`;
78
+ }
79
+ return fullUrl;
80
+ })
81
+ .join('\n');
82
+ proxyCache.set(cacheKey, rewritten);
83
+ res
84
+ .set('Content-Type', 'application/vnd.apple.mpegurl')
85
+ .set('Access-Control-Allow-Origin', '*')
86
+ .send(rewritten);
87
+ }
88
+ else {
89
+ const resp = await (0, exports.axiosInstance)({
90
+ method: 'get',
91
+ url: urlStr,
92
+ responseType: 'stream',
93
+ headers,
94
+ signal: abortController.signal,
95
+ });
96
+ res.status(resp.status);
97
+ const forwardHeaders = [
98
+ 'content-type',
99
+ 'content-length',
100
+ 'content-range',
101
+ 'accept-ranges',
102
+ 'cache-control',
103
+ 'last-modified',
104
+ 'etag',
105
+ ];
106
+ Object.keys(resp.headers).forEach((k) => {
107
+ if (forwardHeaders.includes(k.toLowerCase())) {
108
+ res.set(k, resp.headers[k]);
109
+ }
110
+ });
111
+ res.set('Access-Control-Allow-Origin', '*');
112
+ resp.data.on('error', () => {
113
+ abortController.abort();
114
+ if (!res.headersSent)
115
+ res.status(502).send('Upstream error');
116
+ else
117
+ res.destroy();
118
+ });
119
+ res.on('close', () => {
120
+ if (!resp.data.destroyed) {
121
+ resp.data.destroy();
122
+ }
123
+ });
124
+ resp.data.pipe(res);
125
+ }
126
+ }
127
+ catch (e) {
128
+ if (axios_1.default.isCancel(e)) {
129
+ return;
130
+ }
131
+ if (!res.headersSent)
132
+ res.status(500).send('Proxy error');
133
+ }
134
+ };
135
+ handleSubtitleProxy = async (req, res) => {
136
+ const { url, referer } = req.query;
137
+ if (!url)
138
+ return res.status(400).send('URL required');
139
+ const abortController = new AbortController();
140
+ req.on('close', () => {
141
+ abortController.abort();
142
+ });
143
+ try {
144
+ const headers = {
145
+ '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',
146
+ };
147
+ if (referer)
148
+ headers['Referer'] = referer;
149
+ const response = await exports.axiosInstance.get(url, {
150
+ headers,
151
+ responseType: 'text',
152
+ signal: abortController.signal,
153
+ });
154
+ res.set('Content-Type', 'text/vtt; charset=utf-8').send(response.data);
155
+ }
156
+ catch (e) {
157
+ if (axios_1.default.isCancel(e))
158
+ return;
159
+ res.status(500).send('Proxy error');
160
+ }
161
+ };
162
+ handleImageProxy = async (req, res) => {
163
+ const { url } = req.query;
164
+ if (!url)
165
+ return res.status(400).send('URL required');
166
+ const abortController = new AbortController();
167
+ req.on('close', () => {
168
+ abortController.abort();
169
+ });
170
+ try {
171
+ const targetUrl = url;
172
+ let refererValue = 'https://allanime.day';
173
+ if (targetUrl.includes('anilist.co')) {
174
+ refererValue = 'https://anilist.co/';
175
+ }
176
+ else if (targetUrl.includes('gogocdn.net')) {
177
+ refererValue = 'https://gogoanime.lu/';
178
+ }
179
+ else if (targetUrl.includes('youtube-anime.com') || targetUrl.includes('allanime.day')) {
180
+ refererValue = 'https://allanime.day/';
181
+ }
182
+ else if (targetUrl.includes('animeya.cc')) {
183
+ refererValue = 'https://animeya.cc/';
184
+ }
185
+ const imageResponse = await (0, exports.axiosInstance)({
186
+ method: 'get',
187
+ url: targetUrl,
188
+ responseType: 'stream',
189
+ headers: {
190
+ Referer: refererValue,
191
+ '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',
192
+ },
193
+ timeout: 30000,
194
+ signal: abortController.signal,
195
+ });
196
+ if (imageResponse.status === 200) {
197
+ res.set('Cache-Control', 'public, max-age=604800, immutable');
198
+ }
199
+ res.set('Content-Type', String(imageResponse.headers['content-type'] ?? ''));
200
+ imageResponse.data.on('error', () => {
201
+ if (!res.headersSent) {
202
+ this.sendPlaceholder(res);
203
+ }
204
+ });
205
+ res.on('close', () => {
206
+ if (!imageResponse.data.destroyed) {
207
+ imageResponse.data.destroy();
208
+ }
209
+ });
210
+ imageResponse.data.pipe(res);
211
+ }
212
+ catch (e) {
213
+ if (axios_1.default.isCancel(e)) {
214
+ return;
215
+ }
216
+ if (!res.headersSent) {
217
+ this.sendPlaceholder(res);
218
+ }
219
+ }
220
+ };
221
+ sendPlaceholder(res) {
222
+ const possiblePaths = [
223
+ path_1.default.join(config_1.CONFIG.PACKAGE_ROOT, 'client/public/placeholder.svg'),
224
+ path_1.default.join(config_1.CONFIG.PACKAGE_ROOT, 'client/dist/placeholder.svg'),
225
+ path_1.default.join(config_1.CONFIG.SERVER_ROOT, '..', 'client/public/placeholder.svg'),
226
+ ];
227
+ for (const p of possiblePaths) {
228
+ if (fs_1.default.existsSync(p)) {
229
+ return res.status(200).sendFile(p);
230
+ }
231
+ }
232
+ res.status(404).send('Not Found');
233
+ }
234
+ }
235
+ exports.ProxyController = ProxyController;
@@ -0,0 +1,147 @@
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
+ class SettingsController {
15
+ provider;
16
+ constructor(provider) {
17
+ this.provider = provider;
18
+ }
19
+ getSettings = async (req, res) => {
20
+ try {
21
+ const row = await settings_repository_1.SettingsRepository.getByKey(req.db, req.query.key);
22
+ res.json({ value: row ? row.value : null });
23
+ }
24
+ catch {
25
+ res.status(500).json({ error: 'DB error' });
26
+ }
27
+ };
28
+ updateSettings = async (req, res) => {
29
+ try {
30
+ await (0, sync_1.performWriteTransaction)(req.db, (tx) => {
31
+ settings_repository_1.SettingsRepository.upsert(tx, req.body.key, String(req.body.value));
32
+ });
33
+ res.json({ success: true });
34
+ }
35
+ catch {
36
+ res.status(500).json({ error: 'DB error' });
37
+ }
38
+ };
39
+ backupDatabase = (req, res) => {
40
+ const backupPath = path_1.default.join(config_1.CONFIG.ROOT, 'ani-web-backup.db');
41
+ try {
42
+ req.db.backup(backupPath);
43
+ res.download(backupPath, 'ani-web-backup.db', () => {
44
+ fs_1.default.unlink(backupPath, () => { });
45
+ });
46
+ }
47
+ catch (err) {
48
+ logger_1.default.error({ err }, 'Manual backup failed');
49
+ return res.status(500).json({ error: 'Backup failed' });
50
+ }
51
+ };
52
+ restoreDatabase = (req, res, db, initializeDatabase, setDb) => {
53
+ if (!req.file)
54
+ return res.status(400).json({ error: 'No file uploaded.' });
55
+ const dbName = config_1.CONFIG.IS_DEV ? config_1.CONFIG.DB_NAME_DEV : config_1.CONFIG.DB_NAME_PROD;
56
+ const tempPath = path_1.default.join(config_1.CONFIG.ROOT, `restore_temp.db`);
57
+ const dbPath = path_1.default.join(config_1.CONFIG.ROOT, dbName);
58
+ db.close((closeErr) => {
59
+ if (closeErr)
60
+ return res.status(500).json({ error: 'Failed to close database.' });
61
+ try {
62
+ req.db.checkpoint();
63
+ }
64
+ catch (checkpointErr) {
65
+ logger_1.default.warn({ err: checkpointErr }, 'WAL checkpoint failed');
66
+ }
67
+ try {
68
+ if (fs_1.default.existsSync(`${dbPath}-wal`))
69
+ fs_1.default.unlinkSync(`${dbPath}-wal`);
70
+ if (fs_1.default.existsSync(`${dbPath}-shm`))
71
+ fs_1.default.unlinkSync(`${dbPath}-shm`);
72
+ }
73
+ catch (cleanupErr) {
74
+ logger_1.default.warn({ err: cleanupErr }, 'Failed to clean up WAL files');
75
+ }
76
+ fs_1.default.rename(tempPath, dbPath, async (renameErr) => {
77
+ if (renameErr) {
78
+ try {
79
+ const reopenedDb = await initializeDatabase(dbPath);
80
+ setDb(reopenedDb);
81
+ req.db = reopenedDb;
82
+ }
83
+ catch (e) {
84
+ logger_1.default.error({ err: e }, 'Failed to reopen DB after rename failure');
85
+ }
86
+ return res.status(500).json({ error: 'Failed to replace database file.' });
87
+ }
88
+ try {
89
+ const newDb = await initializeDatabase(dbPath);
90
+ setDb(newDb);
91
+ req.db = newDb;
92
+ res.json({ success: true, message: 'Database restored.' });
93
+ }
94
+ catch (e) {
95
+ logger_1.default.error({ err: e }, 'Failed to initialize restored database');
96
+ res.status(500).json({ error: 'Failed to initialize restored database.' });
97
+ }
98
+ });
99
+ });
100
+ };
101
+ importMalXml = async (req, res) => {
102
+ if (!req.file)
103
+ return res.status(400).json({ error: 'No file' });
104
+ const { erase } = req.body;
105
+ let result;
106
+ try {
107
+ result = await (0, xml2js_1.parseStringPromise)(req.file.buffer.toString());
108
+ }
109
+ catch {
110
+ return res.status(400).json({ error: 'Invalid XML' });
111
+ }
112
+ const animeList = result?.myanimelist?.anime || [];
113
+ let skippedCount = 0;
114
+ const showsToInsert = [];
115
+ const BATCH_SIZE = 5;
116
+ for (let i = 0; i < animeList.length; i += BATCH_SIZE) {
117
+ const batch = animeList.slice(i, i + BATCH_SIZE);
118
+ const batchResults = await Promise.allSettled(batch.map((item) => this.provider.search({ query: item.series_title[0] })));
119
+ batchResults.forEach((r, idx) => {
120
+ if (r.status === 'fulfilled' && r.value.length > 0) {
121
+ showsToInsert.push({
122
+ id: r.value[0]._id,
123
+ name: r.value[0].name,
124
+ thumbnail: r.value[0].thumbnail,
125
+ status: batch[idx].my_status[0],
126
+ });
127
+ }
128
+ else {
129
+ skippedCount++;
130
+ }
131
+ });
132
+ }
133
+ try {
134
+ await (0, sync_1.performWriteTransaction)(req.db, (tx) => {
135
+ if (erase)
136
+ settings_repository_1.SettingsRepository.clearWatchlist(tx);
137
+ settings_repository_1.SettingsRepository.upsertWatchlistBatch(tx, showsToInsert);
138
+ });
139
+ res.json({ imported: showsToInsert.length, skipped: skippedCount });
140
+ }
141
+ catch (dbError) {
142
+ logger_1.default.error({ err: dbError }, 'Import DB error');
143
+ res.status(500).json({ error: 'DB error' });
144
+ }
145
+ };
146
+ }
147
+ exports.SettingsController = SettingsController;