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.
- package/LICENSE +21 -0
- package/README.md +220 -0
- package/client/dist/assets/AnimeInfo-B88ZA3gl.js +1 -0
- package/client/dist/assets/AnimeInfo-R63luGTP.css +1 -0
- package/client/dist/assets/AnimeInfoPage-xGVarrXG.js +2 -0
- package/client/dist/assets/Button-Fq9KaUOg.css +1 -0
- package/client/dist/assets/Button-lkEUHIDS.js +1 -0
- package/client/dist/assets/ErrorMessage-BVWNgHMx.css +1 -0
- package/client/dist/assets/ErrorMessage-BXKDLzn8.js +1 -0
- package/client/dist/assets/Home-O1FbN8t_.js +1 -0
- package/client/dist/assets/Home-r0eYbWNc.css +1 -0
- package/client/dist/assets/Insights-CF4K-oO5.css +1 -0
- package/client/dist/assets/Insights-opcB-aTo.js +1 -0
- package/client/dist/assets/MAL-BGM33N5c.js +1 -0
- package/client/dist/assets/MAL-DeQNXXPx.css +1 -0
- package/client/dist/assets/Player-BarbgKjI.css +1 -0
- package/client/dist/assets/Player-CSyGax33.js +9 -0
- package/client/dist/assets/PlayerSettings-BaCVQyw6.js +1 -0
- package/client/dist/assets/PlayerSettings-l6aLKrHh.css +1 -0
- package/client/dist/assets/QueueRail-Cxn5U8kE.js +1 -0
- package/client/dist/assets/QueueRail-DOM_pWkE.css +1 -0
- package/client/dist/assets/RemoveConfirmationModal-BBiogSdf.css +1 -0
- package/client/dist/assets/RemoveConfirmationModal-DatCZQKq.js +1 -0
- package/client/dist/assets/Search-BzO-aRP7.css +1 -0
- package/client/dist/assets/Search-DJxo3BYH.js +1 -0
- package/client/dist/assets/SearchableSelect-BkCrf6E8.css +1 -0
- package/client/dist/assets/SearchableSelect-BzYsMz8B.js +1 -0
- package/client/dist/assets/Settings-Bv9fX-x3.css +1 -0
- package/client/dist/assets/Settings-C5adinOe.js +1 -0
- package/client/dist/assets/SynopsisText-C3AK-aRc.js +1 -0
- package/client/dist/assets/SynopsisText-DsI3mW5v.css +1 -0
- package/client/dist/assets/ToggleSwitch-BIlQxIjg.css +1 -0
- package/client/dist/assets/ToggleSwitch-CrXim14A.js +1 -0
- package/client/dist/assets/Watchlist-CXw0vbNx.js +1 -0
- package/client/dist/assets/Watchlist-a2RHQogs.css +1 -0
- package/client/dist/assets/hls.light-DcbkToIY.js +27 -0
- package/client/dist/assets/index-BzX_xmnf.css +1 -0
- package/client/dist/assets/index-Ciivz6fh.js +178 -0
- package/client/dist/assets/useAnimeInfoData-Dqthchpa.js +1 -0
- package/client/dist/assets/useIsMobile-BviODivc.js +1 -0
- package/client/dist/assets/vendor-Bc4EraM_.js +3 -0
- package/client/dist/favicon.ico +0 -0
- package/client/dist/index.html +35 -0
- package/client/dist/logo.png +0 -0
- package/client/dist/placeholder.svg +4 -0
- package/client/dist/robots.txt +3 -0
- package/client/package.json +58 -0
- package/orchestrator.js +323 -0
- package/package.json +88 -0
- package/server/.env +1 -0
- package/server/dist/config.js +89 -0
- package/server/dist/constants.json +1359 -0
- package/server/dist/controllers/auth.controller.js +215 -0
- package/server/dist/controllers/data.controller.js +232 -0
- package/server/dist/controllers/insights.controller.js +200 -0
- package/server/dist/controllers/proxy.controller.js +353 -0
- package/server/dist/controllers/settings.controller.js +159 -0
- package/server/dist/controllers/watchlist.controller.js +749 -0
- package/server/dist/db.js +152 -0
- package/server/dist/discord-rpc.js +279 -0
- package/server/dist/github-sync.js +342 -0
- package/server/dist/google.js +310 -0
- package/server/dist/logger.js +21 -0
- package/server/dist/providers/123anime.provider.js +226 -0
- package/server/dist/providers/allanime.provider.js +736 -0
- package/server/dist/providers/animepahe.provider.js +457 -0
- package/server/dist/providers/animeya.provider.js +787 -0
- package/server/dist/providers/megaplay.provider.js +264 -0
- package/server/dist/providers/provider.interface.js +2 -0
- package/server/dist/rclone.js +126 -0
- package/server/dist/repositories/insights.repository.js +42 -0
- package/server/dist/repositories/notifications.repository.js +30 -0
- package/server/dist/repositories/queue.repository.js +38 -0
- package/server/dist/repositories/settings.repository.js +14 -0
- package/server/dist/repositories/shows-meta.repository.js +41 -0
- package/server/dist/repositories/watched-episodes.repository.js +67 -0
- package/server/dist/repositories/watchlist.repository.js +80 -0
- package/server/dist/routes/auth.routes.js +26 -0
- package/server/dist/routes/data.routes.js +42 -0
- package/server/dist/routes/insights.routes.js +12 -0
- package/server/dist/routes/proxy.routes.js +14 -0
- package/server/dist/routes/settings.routes.js +27 -0
- package/server/dist/routes/watchlist.routes.js +46 -0
- package/server/dist/server.js +229 -0
- package/server/dist/sync-config.js +28 -0
- package/server/dist/sync.js +427 -0
- package/server/dist/utils/db-utils.js +15 -0
- package/server/dist/utils/env.utils.js +79 -0
- package/server/dist/utils/machine-id.js +46 -0
- package/server/dist/utils/request-context.js +5 -0
- package/server/package.json +19 -0
|
@@ -0,0 +1,215 @@
|
|
|
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.AuthController = void 0;
|
|
40
|
+
const logger_1 = __importDefault(require("../logger"));
|
|
41
|
+
const google_1 = require("../google");
|
|
42
|
+
const github_sync_1 = require("../github-sync");
|
|
43
|
+
const sync_1 = require("../sync");
|
|
44
|
+
const config_1 = require("../config");
|
|
45
|
+
const rclone_1 = require("../rclone");
|
|
46
|
+
class AuthController {
|
|
47
|
+
runSyncSequence;
|
|
48
|
+
constructor(runSyncSequence) {
|
|
49
|
+
this.runSyncSequence = runSyncSequence;
|
|
50
|
+
}
|
|
51
|
+
getConfigStatus = (_req, res) => {
|
|
52
|
+
const hasConfig = !!process.env.GOOGLE_CLIENT_ID && !!process.env.GOOGLE_CLIENT_SECRET;
|
|
53
|
+
res.json({ hasConfig });
|
|
54
|
+
};
|
|
55
|
+
getGoogleAuthSettings = (_req, res) => {
|
|
56
|
+
res.json({
|
|
57
|
+
clientId: process.env.GOOGLE_CLIENT_ID || '',
|
|
58
|
+
hasClientSecret: !!process.env.GOOGLE_CLIENT_SECRET,
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
updateGoogleAuthSettings = async (req, res) => {
|
|
62
|
+
const { clientId, clientSecret } = req.body;
|
|
63
|
+
const { updateEnvFile } = await Promise.resolve().then(() => __importStar(require('../utils/env.utils')));
|
|
64
|
+
const updates = {};
|
|
65
|
+
if (typeof clientId === 'string') {
|
|
66
|
+
updates.GOOGLE_CLIENT_ID = clientId;
|
|
67
|
+
}
|
|
68
|
+
if (typeof clientSecret === 'string') {
|
|
69
|
+
updates.GOOGLE_CLIENT_SECRET = clientSecret;
|
|
70
|
+
}
|
|
71
|
+
await updateEnvFile(updates);
|
|
72
|
+
res.json({ success: true });
|
|
73
|
+
};
|
|
74
|
+
getRcloneSettings = async (_req, res) => {
|
|
75
|
+
const remotes = await rclone_1.rcloneService.listRemotes();
|
|
76
|
+
res.json({
|
|
77
|
+
remote: config_1.CONFIG.RCLONE_REMOTE || '',
|
|
78
|
+
availableRemotes: remotes,
|
|
79
|
+
activeRemote: rclone_1.rcloneService.isActive() ? rclone_1.rcloneService.getRemoteName() : null,
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
getSyncSettings = async (_req, res) => {
|
|
83
|
+
const { getActiveProvider } = await Promise.resolve().then(() => __importStar(require('../sync')));
|
|
84
|
+
res.json({
|
|
85
|
+
activeProvider: process.env.SYNC_PROVIDER || 'default',
|
|
86
|
+
actualActiveProvider: getActiveProvider(),
|
|
87
|
+
authenticatedProviders: {
|
|
88
|
+
github: github_sync_1.githubSyncService.isAuthenticated(),
|
|
89
|
+
google: google_1.googleDriveService.isAuthenticated(),
|
|
90
|
+
rclone: rclone_1.rcloneService.isActive(),
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
updateSyncProvider = async (req, res) => {
|
|
95
|
+
const { provider } = req.body;
|
|
96
|
+
const { updateEnvFile } = await Promise.resolve().then(() => __importStar(require('../utils/env.utils')));
|
|
97
|
+
const value = provider === 'default' ? '' : provider;
|
|
98
|
+
await updateEnvFile({ SYNC_PROVIDER: value });
|
|
99
|
+
await (0, sync_1.initSyncProvider)();
|
|
100
|
+
res.json({ success: true, activeProvider: process.env.SYNC_PROVIDER || 'default' });
|
|
101
|
+
};
|
|
102
|
+
getGitHubAuthStatus = async (_req, res) => {
|
|
103
|
+
try {
|
|
104
|
+
const user = await github_sync_1.githubSyncService.getUserProfile();
|
|
105
|
+
res.json({
|
|
106
|
+
authenticated: !!user,
|
|
107
|
+
user,
|
|
108
|
+
device: github_sync_1.githubSyncService.getDeviceState(),
|
|
109
|
+
clientId: process.env.GITHUB_CLIENT_ID || '',
|
|
110
|
+
usingDefaultClientId: !process.env.GITHUB_CLIENT_ID,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
logger_1.default.error({ err: error }, 'Failed to fetch GitHub auth status');
|
|
115
|
+
res.json({
|
|
116
|
+
authenticated: false,
|
|
117
|
+
user: null,
|
|
118
|
+
device: github_sync_1.githubSyncService.getDeviceState(),
|
|
119
|
+
clientId: process.env.GITHUB_CLIENT_ID || '',
|
|
120
|
+
usingDefaultClientId: !process.env.GITHUB_CLIENT_ID,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
startGitHubDeviceAuth = async (req, res) => {
|
|
125
|
+
const state = await github_sync_1.githubSyncService.startDeviceAuth(req.db, this.runSyncSequence);
|
|
126
|
+
res.json(state);
|
|
127
|
+
};
|
|
128
|
+
pollGitHubDeviceAuth = (_req, res) => {
|
|
129
|
+
res.json(github_sync_1.githubSyncService.getDeviceState());
|
|
130
|
+
};
|
|
131
|
+
logoutGitHub = async (_req, res) => {
|
|
132
|
+
await github_sync_1.githubSyncService.logout();
|
|
133
|
+
const { updateEnvFile } = await Promise.resolve().then(() => __importStar(require('../utils/env.utils')));
|
|
134
|
+
await updateEnvFile({ SYNC_PROVIDER: '' });
|
|
135
|
+
await (0, sync_1.initSyncProvider)();
|
|
136
|
+
res.json({ success: true });
|
|
137
|
+
};
|
|
138
|
+
updateRcloneSettings = async (req, res) => {
|
|
139
|
+
const { remote } = req.body;
|
|
140
|
+
const { updateEnvFile } = await Promise.resolve().then(() => __importStar(require('../utils/env.utils')));
|
|
141
|
+
await updateEnvFile({
|
|
142
|
+
RCLONE_REMOTE: remote,
|
|
143
|
+
SYNC_PROVIDER: 'rclone',
|
|
144
|
+
});
|
|
145
|
+
await this.runSyncSequence(req.db, 'rclone');
|
|
146
|
+
res.json({ success: true });
|
|
147
|
+
};
|
|
148
|
+
getAuthUrl = async (_req, res) => {
|
|
149
|
+
const url = google_1.googleDriveService.getAuthUrl();
|
|
150
|
+
res.json({ url });
|
|
151
|
+
};
|
|
152
|
+
loginGoogle = async (req, res) => {
|
|
153
|
+
if (google_1.googleDriveService.isAuthenticated()) {
|
|
154
|
+
const user = await google_1.googleDriveService.getUserProfile();
|
|
155
|
+
if (user) {
|
|
156
|
+
const { updateEnvFile } = await Promise.resolve().then(() => __importStar(require('../utils/env.utils')));
|
|
157
|
+
await updateEnvFile({ SYNC_PROVIDER: 'google' });
|
|
158
|
+
await this.runSyncSequence(req.db, 'google');
|
|
159
|
+
return res.json({ url: null, authenticated: true });
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
logger_1.default.warn('Google tokens found but invalid. Clearing and requesting new auth.');
|
|
163
|
+
await google_1.googleDriveService.logout();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const url = google_1.googleDriveService.getAuthUrl();
|
|
167
|
+
res.json({ url, authenticated: false });
|
|
168
|
+
};
|
|
169
|
+
handleCallback = async (req, res) => {
|
|
170
|
+
const code = req.query.code;
|
|
171
|
+
if (!code) {
|
|
172
|
+
return res.status(400).send('No code provided');
|
|
173
|
+
}
|
|
174
|
+
await google_1.googleDriveService.handleCallback(code);
|
|
175
|
+
const user = await google_1.googleDriveService.getUserProfile();
|
|
176
|
+
const { updateEnvFile } = await Promise.resolve().then(() => __importStar(require('../utils/env.utils')));
|
|
177
|
+
await updateEnvFile({ SYNC_PROVIDER: 'google' });
|
|
178
|
+
logger_1.default.info('User logged in. Syncing database (please wait)...');
|
|
179
|
+
try {
|
|
180
|
+
await this.runSyncSequence(req.db, 'google');
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
logger_1.default.error({ err }, 'Post-login sync failed');
|
|
184
|
+
}
|
|
185
|
+
const responseHtml = `
|
|
186
|
+
<html>
|
|
187
|
+
<body>
|
|
188
|
+
<h1>Authentication Successful</h1>
|
|
189
|
+
<p>Database synced. Closing window...</p>
|
|
190
|
+
<script>
|
|
191
|
+
if (window.opener) {
|
|
192
|
+
window.opener.postMessage({ type: 'GOOGLE_AUTH_SUCCESS', user: ${JSON.stringify(user)} }, '*');
|
|
193
|
+
window.close();
|
|
194
|
+
} else {
|
|
195
|
+
window.location.href = '/';
|
|
196
|
+
}
|
|
197
|
+
</script>
|
|
198
|
+
</body>
|
|
199
|
+
</html>
|
|
200
|
+
`;
|
|
201
|
+
res.send(responseHtml);
|
|
202
|
+
};
|
|
203
|
+
getUserProfile = async (_req, res) => {
|
|
204
|
+
const user = await google_1.googleDriveService.getUserProfile();
|
|
205
|
+
res.json(user);
|
|
206
|
+
};
|
|
207
|
+
logout = async (_req, res) => {
|
|
208
|
+
await google_1.googleDriveService.logout();
|
|
209
|
+
const { updateEnvFile } = await Promise.resolve().then(() => __importStar(require('../utils/env.utils')));
|
|
210
|
+
await updateEnvFile({ SYNC_PROVIDER: '' });
|
|
211
|
+
await (0, sync_1.initSyncProvider)();
|
|
212
|
+
res.json({ success: true });
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
exports.AuthController = AuthController;
|
|
@@ -0,0 +1,232 @@
|
|
|
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.DataController = void 0;
|
|
7
|
+
const constants_json_1 = require("../constants.json");
|
|
8
|
+
const logger_1 = __importDefault(require("../logger"));
|
|
9
|
+
class DataController {
|
|
10
|
+
providers;
|
|
11
|
+
constructor(providers) {
|
|
12
|
+
this.providers = providers;
|
|
13
|
+
}
|
|
14
|
+
getProvider(req) {
|
|
15
|
+
const providerName = req.query.provider || 'allanime';
|
|
16
|
+
return this.providers[providerName.toLowerCase()] || this.providers['allanime'];
|
|
17
|
+
}
|
|
18
|
+
getPopular = async (req, res) => {
|
|
19
|
+
const timeframe = req.params.timeframe.toLowerCase();
|
|
20
|
+
const page = parseInt(req.query.page) || 1;
|
|
21
|
+
const size = parseInt(req.query.size) || 10;
|
|
22
|
+
try {
|
|
23
|
+
const data = await this.getProvider(req).getPopular(timeframe, page, size);
|
|
24
|
+
res.set('Cache-Control', 'public, max-age=300').json(data);
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
if (e.message === 'AUTH_REQUIRED') {
|
|
28
|
+
return res.status(403).json({ error: 'AUTH_REQUIRED', provider: 'animepahe' });
|
|
29
|
+
}
|
|
30
|
+
throw e;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
getSchedule = async (req, res) => {
|
|
34
|
+
try {
|
|
35
|
+
const data = await this.getProvider(req).getSchedule(new Date(req.params.date + 'T00:00:00.000Z'));
|
|
36
|
+
res.set('Cache-Control', 'public, max-age=300').json(data);
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
if (e.message === 'AUTH_REQUIRED') {
|
|
40
|
+
return res.status(403).json({ error: 'AUTH_REQUIRED', provider: 'animepahe' });
|
|
41
|
+
}
|
|
42
|
+
throw e;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
getSkipTimes = async (req, res) => {
|
|
46
|
+
try {
|
|
47
|
+
const data = await this.getProvider(req).getSkipTimes(req.params.showId, req.params.episodeNumber);
|
|
48
|
+
res.json(data);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
res.json({ found: false, results: [] });
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
getVideo = async (req, res) => {
|
|
55
|
+
try {
|
|
56
|
+
const urls = await this.getProvider(req).getStreamUrls(req.query.showId, req.query.episodeNumber, req.query.mode);
|
|
57
|
+
res.json(urls || []);
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
if (e.message === 'AUTH_REQUIRED') {
|
|
61
|
+
return res.status(403).json({ error: 'AUTH_REQUIRED', provider: 'animepahe' });
|
|
62
|
+
}
|
|
63
|
+
logger_1.default.error({ err: e, provider: req.query.provider }, 'Provider video fetch failed');
|
|
64
|
+
res.json([]);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
getEpisodes = async (req, res) => {
|
|
68
|
+
try {
|
|
69
|
+
let showId = req.query.showId;
|
|
70
|
+
const provider = this.getProvider(req);
|
|
71
|
+
const providerName = (req.query.provider || 'allanime').toLowerCase();
|
|
72
|
+
if (providerName === 'animepahe') {
|
|
73
|
+
const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(showId);
|
|
74
|
+
if (!isUuid) {
|
|
75
|
+
let animeName = '';
|
|
76
|
+
try {
|
|
77
|
+
const allAnimeProvider = this.providers['allanime'];
|
|
78
|
+
if (allAnimeProvider) {
|
|
79
|
+
const meta = await allAnimeProvider.getShowMeta(showId);
|
|
80
|
+
if (meta) {
|
|
81
|
+
animeName = meta.name || meta.englishName || '';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
logger_1.default.warn({ showId, error: e.message }, 'Failed to resolve AllAnime metadata for AnimePahe name resolution');
|
|
87
|
+
}
|
|
88
|
+
const searchFor = animeName || showId;
|
|
89
|
+
const results = await provider.search({ query: searchFor });
|
|
90
|
+
if (results.length > 0) {
|
|
91
|
+
showId = results[0].session || results[0].id || results[0]._id;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const data = await provider.getEpisodes(showId, req.query.mode);
|
|
96
|
+
res.json(data);
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
if (e.message === 'AUTH_REQUIRED') {
|
|
100
|
+
return res.status(403).json({ error: 'AUTH_REQUIRED', provider: 'animepahe' });
|
|
101
|
+
}
|
|
102
|
+
throw e;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
search = async (req, res) => {
|
|
106
|
+
try {
|
|
107
|
+
const data = await this.getProvider(req).search(req.query);
|
|
108
|
+
res.json(data);
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
if (e.message === 'AUTH_REQUIRED') {
|
|
112
|
+
return res.status(403).json({ error: 'AUTH_REQUIRED', provider: 'animepahe' });
|
|
113
|
+
}
|
|
114
|
+
throw e;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
getSeasonal = async (req, res) => {
|
|
118
|
+
const page = parseInt(req.query.page) || 1;
|
|
119
|
+
try {
|
|
120
|
+
const data = await this.getProvider(req).getSeasonal(page);
|
|
121
|
+
res.json(data);
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
if (e.message === 'AUTH_REQUIRED') {
|
|
125
|
+
return res.status(403).json({ error: 'AUTH_REQUIRED', provider: 'animepahe' });
|
|
126
|
+
}
|
|
127
|
+
throw e;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
getLatestReleases = async (req, res) => {
|
|
131
|
+
const page = parseInt(req.query.page) || 1;
|
|
132
|
+
const size = parseInt(req.query.size) || 14;
|
|
133
|
+
try {
|
|
134
|
+
const data = await this.getProvider(req).getLatestReleases(page, size);
|
|
135
|
+
res.json(data);
|
|
136
|
+
}
|
|
137
|
+
catch (e) {
|
|
138
|
+
if (e.message === 'AUTH_REQUIRED') {
|
|
139
|
+
return res.status(403).json({ error: 'AUTH_REQUIRED', provider: 'animepahe' });
|
|
140
|
+
}
|
|
141
|
+
throw e;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
getShowMeta = async (req, res) => {
|
|
145
|
+
try {
|
|
146
|
+
const id = req.params.id;
|
|
147
|
+
const providerQuery = req.query.provider;
|
|
148
|
+
if (providerQuery) {
|
|
149
|
+
let meta = null;
|
|
150
|
+
const provider = this.getProvider(req);
|
|
151
|
+
const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id);
|
|
152
|
+
if (isUuid) {
|
|
153
|
+
meta = await provider.getShowMeta(id);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
if (providerQuery.toLowerCase() === 'allanime') {
|
|
157
|
+
meta = await provider.getShowMeta(id);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
let animeName = '';
|
|
161
|
+
try {
|
|
162
|
+
const allAnimeProvider = this.providers['allanime'];
|
|
163
|
+
if (allAnimeProvider) {
|
|
164
|
+
const localMeta = await allAnimeProvider.getShowMeta(id);
|
|
165
|
+
if (localMeta) {
|
|
166
|
+
animeName = localMeta.name || localMeta.englishName || '';
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch (e) {
|
|
171
|
+
logger_1.default.warn({ id, error: e.message }, 'Failed to resolve AllAnime name in getShowMeta');
|
|
172
|
+
}
|
|
173
|
+
const searchFor = animeName || id;
|
|
174
|
+
const results = await provider.search({ query: searchFor });
|
|
175
|
+
if (results.length > 0) {
|
|
176
|
+
const targetId = results[0].session || results[0].id || results[0]._id;
|
|
177
|
+
meta = await provider.getShowMeta(targetId);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return res.json(meta || {});
|
|
182
|
+
}
|
|
183
|
+
const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id);
|
|
184
|
+
let meta = null;
|
|
185
|
+
if (isUuid) {
|
|
186
|
+
try {
|
|
187
|
+
if (this.providers['animepahe']) {
|
|
188
|
+
meta = await this.providers['animepahe'].getShowMeta(id);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch (e) {
|
|
192
|
+
if (e.message === 'AUTH_REQUIRED') {
|
|
193
|
+
return res.status(403).json({ error: 'AUTH_REQUIRED', provider: 'animepahe' });
|
|
194
|
+
}
|
|
195
|
+
logger_1.default.warn({ id, error: e.message }, 'AnimePahe getShowMeta failed for UUID');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
try {
|
|
200
|
+
meta = await this.providers['allanime'].getShowMeta(id);
|
|
201
|
+
}
|
|
202
|
+
catch (e) {
|
|
203
|
+
logger_1.default.warn({ id, error: e.message }, 'AllAnime getShowMeta failed, trying fallback');
|
|
204
|
+
}
|
|
205
|
+
if (!meta || !meta.name) {
|
|
206
|
+
try {
|
|
207
|
+
if (this.providers['animepahe']) {
|
|
208
|
+
meta = await this.providers['animepahe'].getShowMeta(id);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
catch (e) {
|
|
212
|
+
if (e.message === 'AUTH_REQUIRED') {
|
|
213
|
+
return res.status(403).json({ error: 'AUTH_REQUIRED', provider: 'animepahe' });
|
|
214
|
+
}
|
|
215
|
+
logger_1.default.warn({ id, error: e.message }, 'AnimePahe fallback getShowMeta failed');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
res.json(meta || {});
|
|
220
|
+
}
|
|
221
|
+
catch (e) {
|
|
222
|
+
if (e.message === 'AUTH_REQUIRED') {
|
|
223
|
+
return res.status(403).json({ error: 'AUTH_REQUIRED', provider: 'animepahe' });
|
|
224
|
+
}
|
|
225
|
+
throw e;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
getGenresAndTags = (_req, res) => {
|
|
229
|
+
res.json({ genres: constants_json_1.genres, tags: constants_json_1.tags, studios: constants_json_1.studios });
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
exports.DataController = DataController;
|
|
@@ -0,0 +1,200 @@
|
|
|
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.InsightsController = void 0;
|
|
7
|
+
const logger_1 = __importDefault(require("../logger"));
|
|
8
|
+
const insights_repository_1 = require("../repositories/insights.repository");
|
|
9
|
+
class InsightsController {
|
|
10
|
+
provider;
|
|
11
|
+
constructor(provider) {
|
|
12
|
+
this.provider = provider;
|
|
13
|
+
}
|
|
14
|
+
getWatchInsights = async (req, res) => {
|
|
15
|
+
const db = req.db;
|
|
16
|
+
const [core, activityGrid, hourlyDist, seasonality, allWatches, watchedShows, droppedWarning, velocities,] = (await Promise.all([
|
|
17
|
+
insights_repository_1.InsightsRepository.getCoreStats(db),
|
|
18
|
+
insights_repository_1.InsightsRepository.getActivityGrid(db),
|
|
19
|
+
insights_repository_1.InsightsRepository.getHourlyDist(db),
|
|
20
|
+
insights_repository_1.InsightsRepository.getSeasonality(db),
|
|
21
|
+
insights_repository_1.InsightsRepository.getAllWatches(db),
|
|
22
|
+
insights_repository_1.InsightsRepository.getWatchedShowsMeta(db),
|
|
23
|
+
insights_repository_1.InsightsRepository.getDroppedShows(db),
|
|
24
|
+
insights_repository_1.InsightsRepository.getCompletionVelocities(db),
|
|
25
|
+
]));
|
|
26
|
+
const bingeFactor = activityGrid.length > 0 ? Math.max(...activityGrid.map((a) => a.count)) : 0;
|
|
27
|
+
const sessions = [];
|
|
28
|
+
if (allWatches.length > 0) {
|
|
29
|
+
let currentSessionSeconds = allWatches[0].currentTime;
|
|
30
|
+
for (let i = 1; i < allWatches.length; i++) {
|
|
31
|
+
const prev = new Date(allWatches[i - 1].watchedAt).getTime();
|
|
32
|
+
const curr = new Date(allWatches[i].watchedAt).getTime();
|
|
33
|
+
if (curr - prev < 3600000) {
|
|
34
|
+
currentSessionSeconds += allWatches[i].currentTime;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
sessions.push(currentSessionSeconds);
|
|
38
|
+
currentSessionSeconds = allWatches[i].currentTime;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
sessions.push(currentSessionSeconds);
|
|
42
|
+
}
|
|
43
|
+
const avgSessionMinutes = sessions.length > 0
|
|
44
|
+
? Math.round(sessions.reduce((a, b) => a + b, 0) / sessions.length / 60)
|
|
45
|
+
: 0;
|
|
46
|
+
const genreCounts = {};
|
|
47
|
+
let totalPopScore = 0;
|
|
48
|
+
let popCount = 0;
|
|
49
|
+
for (const show of watchedShows) {
|
|
50
|
+
let genres = [];
|
|
51
|
+
if (show.genres) {
|
|
52
|
+
try {
|
|
53
|
+
if (show.genres.startsWith('[')) {
|
|
54
|
+
genres = JSON.parse(show.genres);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
genres = show.genres.split(',').map((g) => g.trim());
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
logger_1.default.warn({ err: e, showId: show.id }, 'Failed to parse genres for insights');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (const g of genres) {
|
|
65
|
+
genreCounts[g] = (genreCounts[g] || 0) + 1;
|
|
66
|
+
}
|
|
67
|
+
if (show.popularityScore) {
|
|
68
|
+
totalPopScore += show.popularityScore;
|
|
69
|
+
popCount++;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const topGenre = Object.entries(genreCounts).sort((a, b) => b[1] - a[1])[0]?.[0];
|
|
73
|
+
const personaMap = {
|
|
74
|
+
Action: 'Shonen Warrior',
|
|
75
|
+
Romance: 'Hopeless Romantic',
|
|
76
|
+
Comedy: 'Chaos Enjoyer',
|
|
77
|
+
'Slice of Life': 'Vibe Seeker',
|
|
78
|
+
Horror: 'Fearless Watcher',
|
|
79
|
+
Fantasy: 'Isekai Traveller',
|
|
80
|
+
'Sci-Fi': 'Future Scientist',
|
|
81
|
+
Drama: 'Feels Collector',
|
|
82
|
+
};
|
|
83
|
+
const persona = personaMap[topGenre || ''] || 'Anime Enthusiast';
|
|
84
|
+
const avgCompletionDays = velocities.length > 0
|
|
85
|
+
? Math.round(velocities.reduce((a, b) => a + b.daysToFinish, 0) / velocities.length)
|
|
86
|
+
: 0;
|
|
87
|
+
res.json({
|
|
88
|
+
totalHours: Math.round((core?.totalSeconds || 0) / 3600),
|
|
89
|
+
totalEpisodes: core?.totalEpisodes || 0,
|
|
90
|
+
completedAnime: core?.completedCount || 0,
|
|
91
|
+
completionRate: core?.totalWatchlist > 0
|
|
92
|
+
? Math.round((core.completedCount / core.totalWatchlist) * 100)
|
|
93
|
+
: 0,
|
|
94
|
+
persona,
|
|
95
|
+
bingeFactor,
|
|
96
|
+
avgSessionMinutes,
|
|
97
|
+
avgCompletionDays,
|
|
98
|
+
popularityScore: popCount > 0 ? Math.round(totalPopScore / popCount) : 0,
|
|
99
|
+
genreSplit: Object.entries(genreCounts)
|
|
100
|
+
.map(([name, count]) => ({ name, count }))
|
|
101
|
+
.sort((a, b) => b.count - a.count)
|
|
102
|
+
.slice(0, 8) || [],
|
|
103
|
+
activityGrid: activityGrid || [],
|
|
104
|
+
hourlyDist: Array.from({ length: 24 }, (_, i) => {
|
|
105
|
+
const hour = i.toString().padStart(2, '0');
|
|
106
|
+
return { hour, count: hourlyDist?.find((d) => d.hour === hour)?.count || 0 };
|
|
107
|
+
}) || [],
|
|
108
|
+
seasonality: Array.from({ length: 12 }, (_, i) => {
|
|
109
|
+
const month = (i + 1).toString().padStart(2, '0');
|
|
110
|
+
return {
|
|
111
|
+
month,
|
|
112
|
+
seconds: seasonality?.find((s) => s.month === month)?.seconds || 0,
|
|
113
|
+
};
|
|
114
|
+
}) || [],
|
|
115
|
+
droppedShows: (droppedWarning || []).slice(0, 5),
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
formatTime(seconds) {
|
|
119
|
+
const days = Math.floor(seconds / 86400);
|
|
120
|
+
const hours = Math.floor((seconds % 86400) / 3600);
|
|
121
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
122
|
+
if (days > 0) {
|
|
123
|
+
return `${days} days ${hours > 0 ? `${hours} hours` : ''}`;
|
|
124
|
+
}
|
|
125
|
+
if (hours > 0) {
|
|
126
|
+
return `${hours} hours${minutes > 0 ? ` ${minutes} mins` : ''}`;
|
|
127
|
+
}
|
|
128
|
+
return `${minutes} mins`;
|
|
129
|
+
}
|
|
130
|
+
getGenreCards = async (req, res) => {
|
|
131
|
+
const db = req.db;
|
|
132
|
+
const rows = await insights_repository_1.InsightsRepository.getWatchedEpisodesWithMeta(db);
|
|
133
|
+
const genreData = {};
|
|
134
|
+
for (const row of rows) {
|
|
135
|
+
let genres = [];
|
|
136
|
+
if (row.genres) {
|
|
137
|
+
try {
|
|
138
|
+
if (row.genres.startsWith('[')) {
|
|
139
|
+
genres = JSON.parse(row.genres);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
genres = row.genres.split(',').map((g) => g.trim());
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
logger_1.default.warn({ err: e, showId: row.showId }, 'Failed to parse genres for genre cards');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const timeWatched = (row.currentTime || 0) + (row.duration || 0);
|
|
150
|
+
let score = row.popularityScore || 0;
|
|
151
|
+
if (score > 10)
|
|
152
|
+
score = score / 10;
|
|
153
|
+
for (const genre of genres) {
|
|
154
|
+
if (!genreData[genre]) {
|
|
155
|
+
genreData[genre] = { count: 0, totalTime: 0, scores: [], showWatches: {}, showMeta: {} };
|
|
156
|
+
}
|
|
157
|
+
genreData[genre].count++;
|
|
158
|
+
genreData[genre].totalTime += timeWatched;
|
|
159
|
+
if (score > 0)
|
|
160
|
+
genreData[genre].scores.push(score);
|
|
161
|
+
genreData[genre].showWatches[row.showId] =
|
|
162
|
+
(genreData[genre].showWatches[row.showId] || 0) + 1;
|
|
163
|
+
if (row.name && row.thumbnail) {
|
|
164
|
+
genreData[genre].showMeta[row.showId] = {
|
|
165
|
+
name: row.name,
|
|
166
|
+
nativeName: row.nativeName,
|
|
167
|
+
englishName: row.englishName,
|
|
168
|
+
thumbnail: row.thumbnail,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const genreCards = Object.entries(genreData).map(([name, data]) => {
|
|
174
|
+
const topShows = Object.entries(data.showWatches)
|
|
175
|
+
.sort((a, b) => b[1] - a[1])
|
|
176
|
+
.slice(0, 4)
|
|
177
|
+
.map(([showId]) => ({
|
|
178
|
+
id: showId,
|
|
179
|
+
name: data.showMeta[showId]?.name || '',
|
|
180
|
+
nativeName: data.showMeta[showId]?.nativeName,
|
|
181
|
+
englishName: data.showMeta[showId]?.englishName,
|
|
182
|
+
thumbnail: data.showMeta[showId]?.thumbnail || '',
|
|
183
|
+
}));
|
|
184
|
+
return {
|
|
185
|
+
rank: 0,
|
|
186
|
+
name,
|
|
187
|
+
count: data.count,
|
|
188
|
+
meanScore: data.scores.length > 0 ? data.scores.reduce((a, b) => a + b, 0) / data.scores.length : 0,
|
|
189
|
+
timeWatched: this.formatTime(data.totalTime),
|
|
190
|
+
topShows,
|
|
191
|
+
};
|
|
192
|
+
});
|
|
193
|
+
genreCards.sort((a, b) => b.count - a.count);
|
|
194
|
+
genreCards.forEach((card, i) => {
|
|
195
|
+
card.rank = i + 1;
|
|
196
|
+
});
|
|
197
|
+
res.json(genreCards);
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
exports.InsightsController = InsightsController;
|