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,152 @@
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.DatabaseWrapper = void 0;
7
+ const node_sqlite_1 = require("node:sqlite");
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const logger_1 = __importDefault(require("./logger"));
11
+ class DatabaseWrapper {
12
+ db;
13
+ isClosed = false;
14
+ statementCache = new Map();
15
+ constructor(_dbPath, db) {
16
+ this.db = db;
17
+ }
18
+ static async create(dbPath) {
19
+ try {
20
+ const dir = path_1.default.dirname(dbPath);
21
+ if (!fs_1.default.existsSync(dir)) {
22
+ fs_1.default.mkdirSync(dir, { recursive: true });
23
+ }
24
+ const db = new node_sqlite_1.DatabaseSync(dbPath);
25
+ return new DatabaseWrapper(dbPath, db);
26
+ }
27
+ catch (e) {
28
+ logger_1.default.error({ err: e }, `Failed to initialize database at ${dbPath}`);
29
+ throw e;
30
+ }
31
+ }
32
+ scheduleSave() { }
33
+ async saveNow() { }
34
+ configure(option, value) {
35
+ if (option === 'busyTimeout') {
36
+ this.db.exec(`PRAGMA busy_timeout = ${value}`);
37
+ }
38
+ }
39
+ serialize(cb) {
40
+ this.db.exec('BEGIN IMMEDIATE');
41
+ try {
42
+ cb();
43
+ this.db.exec('COMMIT');
44
+ }
45
+ catch (e) {
46
+ this.db.exec('ROLLBACK');
47
+ throw e;
48
+ }
49
+ }
50
+ close(cb) {
51
+ if (this.isClosed) {
52
+ if (cb)
53
+ cb(null);
54
+ return;
55
+ }
56
+ try {
57
+ this.isClosed = true;
58
+ this.statementCache.clear();
59
+ this.db.close();
60
+ if (cb)
61
+ cb(null);
62
+ }
63
+ catch (e) {
64
+ logger_1.default.error({ err: e }, 'Error during database close');
65
+ if (cb)
66
+ cb(e);
67
+ }
68
+ }
69
+ isClosedCheck() {
70
+ return this.isClosed;
71
+ }
72
+ getPreparedStatement(query) {
73
+ let stmt = this.statementCache.get(query);
74
+ if (!stmt) {
75
+ if (this.statementCache.size > 100) {
76
+ this.statementCache.clear();
77
+ }
78
+ stmt = this.db.prepare(query);
79
+ this.statementCache.set(query, stmt);
80
+ }
81
+ return stmt;
82
+ }
83
+ run(query, params = []) {
84
+ if (this.isClosed) {
85
+ throw new Error('Database is closed');
86
+ }
87
+ const stmt = this.getPreparedStatement(query);
88
+ if (params.length > 0) {
89
+ stmt.run(...params);
90
+ }
91
+ else {
92
+ stmt.run();
93
+ }
94
+ }
95
+ get(query, params = []) {
96
+ if (this.isClosed) {
97
+ throw new Error('Database is closed');
98
+ }
99
+ const stmt = this.getPreparedStatement(query);
100
+ if (params.length > 0) {
101
+ return stmt.get(...params);
102
+ }
103
+ return stmt.get();
104
+ }
105
+ all(query, params = []) {
106
+ if (this.isClosed) {
107
+ throw new Error('Database is closed');
108
+ }
109
+ const stmt = this.getPreparedStatement(query);
110
+ if (params.length > 0) {
111
+ return stmt.all(...params);
112
+ }
113
+ return stmt.all();
114
+ }
115
+ prepare(query) {
116
+ const stmt = this.getPreparedStatement(query);
117
+ return {
118
+ run: (...args) => {
119
+ stmt.run(...args);
120
+ },
121
+ all: () => {
122
+ return stmt.all();
123
+ },
124
+ get: () => {
125
+ return stmt.get();
126
+ },
127
+ finalize: () => { },
128
+ };
129
+ }
130
+ backup(backupPath) {
131
+ try {
132
+ if (fs_1.default.existsSync(backupPath)) {
133
+ fs_1.default.rmSync(backupPath, { force: true });
134
+ }
135
+ this.db.exec(`VACUUM INTO '${backupPath}'`);
136
+ }
137
+ catch (e) {
138
+ logger_1.default.error({ err: e, backupPath }, 'Database backup failed via VACUUM INTO');
139
+ throw e;
140
+ }
141
+ }
142
+ checkpoint() {
143
+ try {
144
+ this.db.exec('PRAGMA wal_checkpoint(TRUNCATE)');
145
+ }
146
+ catch (e) {
147
+ logger_1.default.error({ err: e }, 'Database WAL checkpoint failed');
148
+ throw e;
149
+ }
150
+ }
151
+ }
152
+ exports.DatabaseWrapper = DatabaseWrapper;
@@ -0,0 +1,279 @@
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.discordRPCService = void 0;
7
+ const discord_rpc_1 = require("@xhayper/discord-rpc");
8
+ const logger_1 = __importDefault(require("./logger"));
9
+ const config_1 = require("./config");
10
+ const log = logger_1.default.child({ module: 'DiscordRPC' });
11
+ class DiscordRPCService {
12
+ client = null;
13
+ isEnabled = false;
14
+ reconnectTimeout = null;
15
+ lastActivity = null;
16
+ currentSessionId = null;
17
+ retryCount = 0;
18
+ MAX_RETRIES = 5;
19
+ INITIAL_RECONNECT_DELAY = 15000;
20
+ MAX_RECONNECT_DELAY = 300000;
21
+ setEnabled(enabled) {
22
+ if (this.isEnabled === enabled)
23
+ return;
24
+ this.isEnabled = enabled;
25
+ if (enabled) {
26
+ this.retryCount = 0;
27
+ this.connect();
28
+ }
29
+ else {
30
+ this.disconnect();
31
+ }
32
+ }
33
+ async connect() {
34
+ if (!this.isEnabled || this.client)
35
+ return;
36
+ const isTermux = !!process.env.TERMUX_VERSION;
37
+ const isAndroid = process.platform === 'android';
38
+ if (isTermux || isAndroid) {
39
+ log.debug('Discord Rich Presence is not supported on Android/Termux. Skipping connection.');
40
+ this.isEnabled = false;
41
+ return;
42
+ }
43
+ const clientId = config_1.CONFIG.DISCORD_CLIENT_ID;
44
+ if (!clientId) {
45
+ log.debug('DISCORD_CLIENT_ID is not configured. Discord Rich Presence is disabled.');
46
+ return;
47
+ }
48
+ try {
49
+ this.client = new discord_rpc_1.Client({ clientId });
50
+ this.client.on('ready', () => {
51
+ log.info('Connected to Discord client successfully');
52
+ this.retryCount = 0;
53
+ if (this.lastActivity) {
54
+ this.updatePresence(this.lastActivity);
55
+ }
56
+ else {
57
+ this.setIdleStatus('home');
58
+ }
59
+ });
60
+ this.client.on('disconnected', () => {
61
+ log.warn('Disconnected from Discord client, scheduling reconnect...');
62
+ this.cleanup();
63
+ this.scheduleReconnect();
64
+ });
65
+ this.client.on('ERROR', (err) => {
66
+ const errMsg = err instanceof Error ? err.message : String(err);
67
+ if (errMsg.includes('ENOENT') || errMsg.includes('ECONNREFUSED')) {
68
+ log.debug('Discord client not running or connection refused.');
69
+ }
70
+ else {
71
+ log.error({ err }, 'Discord RPC client error');
72
+ }
73
+ this.cleanup();
74
+ this.scheduleReconnect();
75
+ });
76
+ const loginPromise = this.client.login();
77
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Login timeout')), 5000));
78
+ await Promise.race([loginPromise, timeoutPromise]);
79
+ }
80
+ catch (err) {
81
+ this.cleanup();
82
+ this.scheduleReconnect();
83
+ }
84
+ }
85
+ scheduleReconnect() {
86
+ if (!this.isEnabled || this.reconnectTimeout)
87
+ return;
88
+ const delay = Math.min(this.INITIAL_RECONNECT_DELAY * Math.pow(1.5, this.retryCount), this.MAX_RECONNECT_DELAY);
89
+ this.retryCount++;
90
+ if (this.retryCount > this.MAX_RETRIES) {
91
+ log.debug(`Discord RPC login failed ${this.retryCount} times. Retrying in ${Math.round(delay / 1000)}s...`);
92
+ }
93
+ else {
94
+ log.info(`Discord RPC login failed. Retrying in ${Math.round(delay / 1000)}s...`);
95
+ }
96
+ this.reconnectTimeout = setTimeout(() => {
97
+ this.reconnectTimeout = null;
98
+ this.connect();
99
+ }, delay);
100
+ }
101
+ cleanup() {
102
+ if (this.client) {
103
+ try {
104
+ this.client.removeAllListeners();
105
+ this.client.destroy();
106
+ }
107
+ catch (err) {
108
+ // Ignore errors during destruction
109
+ }
110
+ this.client = null;
111
+ }
112
+ }
113
+ disconnect() {
114
+ if (this.reconnectTimeout) {
115
+ clearTimeout(this.reconnectTimeout);
116
+ this.reconnectTimeout = null;
117
+ }
118
+ this.cleanup();
119
+ this.isEnabled = false;
120
+ this.lastActivity = null;
121
+ this.currentSessionId = null;
122
+ }
123
+ formatTime(seconds) {
124
+ if (isNaN(seconds) || seconds <= 0)
125
+ return '00:00';
126
+ const h = Math.floor(seconds / 3600);
127
+ const m = Math.floor((seconds % 3600) / 60);
128
+ const s = Math.floor(seconds % 60);
129
+ const mm = String(m).padStart(2, '0');
130
+ const ss = String(s).padStart(2, '0');
131
+ if (h > 0) {
132
+ const hh = String(h).padStart(2, '0');
133
+ return `${hh}:${mm}:${ss}`;
134
+ }
135
+ return `${mm}:${ss}`;
136
+ }
137
+ async updatePresence(data) {
138
+ this.lastActivity = data;
139
+ if (data.sessionId) {
140
+ this.currentSessionId = data.sessionId;
141
+ }
142
+ if (!this.isEnabled || !this.client || !this.client.user)
143
+ return;
144
+ const isSafeUrl = (url) => {
145
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
146
+ return false;
147
+ }
148
+ if (url.includes('localhost') || url.includes('127.0.0.1')) {
149
+ return false;
150
+ }
151
+ if (url.includes('s4.anilist.co') || url.includes('anilistcdn')) {
152
+ return true;
153
+ }
154
+ const blockedDomains = [
155
+ 'youtube-anime.com',
156
+ 'allanime.day',
157
+ 'animepahe',
158
+ 'animeya.cc',
159
+ 'gogocdn.net',
160
+ ];
161
+ return !blockedDomains.some((domain) => url.includes(domain));
162
+ };
163
+ let imageKey = 'logo';
164
+ if (data.thumbnail && data.providerName !== 'AnimePahe') {
165
+ let thumbUrl = data.thumbnail;
166
+ if (thumbUrl.includes('/api/image-proxy')) {
167
+ const match = thumbUrl.match(/url=([^&]+)/);
168
+ if (match) {
169
+ thumbUrl = decodeURIComponent(match[1]);
170
+ }
171
+ }
172
+ if (isSafeUrl(thumbUrl)) {
173
+ imageKey = thumbUrl;
174
+ }
175
+ else if (data.thumbnails) {
176
+ const safeThumb = data.thumbnails.find((t) => t && (t.includes('s4.anilist.co') || t.includes('anilistcdn')));
177
+ if (safeThumb)
178
+ imageKey = safeThumb;
179
+ }
180
+ }
181
+ try {
182
+ if (!data.isPlaying) {
183
+ await this.client.user.clearActivity();
184
+ await this.client.user.setActivity({
185
+ details: data.title,
186
+ state: `Episode ${data.episode}${data.totalEpisodes ? `/${data.totalEpisodes}` : ''} (Paused)`,
187
+ largeImageKey: imageKey,
188
+ largeImageText: data.title,
189
+ smallImageKey: 'logo',
190
+ smallImageText: 'ani-web',
191
+ type: 3,
192
+ buttons: [
193
+ {
194
+ label: 'Learn More',
195
+ url: 'https://github.com/serifpersia/ani-web',
196
+ },
197
+ ],
198
+ });
199
+ return;
200
+ }
201
+ const nowSeconds = Math.round(Date.now() / 1000);
202
+ const activity = {
203
+ details: data.title,
204
+ state: `Episode ${data.episode}${data.totalEpisodes ? `/${data.totalEpisodes}` : ''}`,
205
+ largeImageKey: imageKey,
206
+ largeImageText: data.title,
207
+ smallImageKey: 'logo',
208
+ smallImageText: 'ani-web',
209
+ type: 3,
210
+ buttons: [
211
+ {
212
+ label: 'Learn More',
213
+ url: 'https://github.com/serifpersia/ani-web',
214
+ },
215
+ ],
216
+ };
217
+ if (data.currentTime && data.currentTime > 0) {
218
+ activity.startTimestamp = Math.round(nowSeconds - data.currentTime);
219
+ }
220
+ if (data.duration && data.duration > data.currentTime) {
221
+ activity.endTimestamp = Math.round(nowSeconds + (data.duration - data.currentTime));
222
+ }
223
+ await this.client.user.setActivity(activity);
224
+ }
225
+ catch (err) {
226
+ log.error({ err }, 'Failed to set Discord activity');
227
+ }
228
+ }
229
+ async setIdleStatus(page) {
230
+ if (!this.isEnabled || !this.client || !this.client.user)
231
+ return;
232
+ const pageLabels = {
233
+ home: { details: 'Browsing Anime', state: 'On the Home page' },
234
+ search: { details: 'Searching for Anime', state: 'Exploring titles...' },
235
+ watchlist: { details: 'Managing Watchlist', state: 'Reviewing anime list' },
236
+ anime: { details: 'Viewing Anime Info', state: 'Reading show details' },
237
+ insights: { details: 'Checking Insights', state: 'Reviewing stats' },
238
+ settings: { details: 'In Settings', state: 'Tweaking preferences' },
239
+ mal: { details: 'MAL Sync', state: 'Syncing with MyAnimeList' },
240
+ };
241
+ const label = pageLabels[page] ?? { details: 'Browsing Anime', state: 'Idle' };
242
+ try {
243
+ if (!this.client || !this.client.user) {
244
+ log.warn('Discord client not connected, skipping idle status update');
245
+ return;
246
+ }
247
+ await this.client.user.setActivity({
248
+ details: label.details,
249
+ state: label.state,
250
+ largeImageKey: 'logo',
251
+ largeImageText: 'ani-web',
252
+ type: 3,
253
+ buttons: [
254
+ {
255
+ label: 'Learn More',
256
+ url: 'https://github.com/serifpersia/ani-web',
257
+ },
258
+ ],
259
+ });
260
+ }
261
+ catch (err) {
262
+ if (err.message === 'Closed by Discord') {
263
+ log.warn('Discord connection ended, skipping idle status update');
264
+ }
265
+ else {
266
+ log.error({ err }, 'Failed to set Discord idle status');
267
+ }
268
+ }
269
+ }
270
+ async clearPresence(sessionId) {
271
+ if (sessionId && this.currentSessionId && sessionId !== this.currentSessionId) {
272
+ return;
273
+ }
274
+ this.lastActivity = null;
275
+ this.currentSessionId = null;
276
+ await this.setIdleStatus('home');
277
+ }
278
+ }
279
+ exports.discordRPCService = new DiscordRPCService();