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.
- package/LICENSE +21 -0
- package/README.md +174 -0
- package/client/dist/assets/AnimeInfo-C7DQp7Oo.js +1 -0
- package/client/dist/assets/AnimeInfo-Sb3YiXHJ.css +1 -0
- package/client/dist/assets/AnimeInfoPage-DJA7AJQ8.js +2 -0
- package/client/dist/assets/Button-Fq9KaUOg.css +1 -0
- package/client/dist/assets/Button-o0l9V_NG.js +1 -0
- package/client/dist/assets/ErrorMessage-Ddf2zmRx.js +1 -0
- package/client/dist/assets/ErrorMessage-FOxXyZC9.css +1 -0
- package/client/dist/assets/Home-CKHJA97j.css +1 -0
- package/client/dist/assets/Home-Dey0azy1.js +1 -0
- package/client/dist/assets/Insights-BSRcCkDs.css +1 -0
- package/client/dist/assets/Insights-CogjPOd_.js +1 -0
- package/client/dist/assets/MAL-CYArH4yf.js +1 -0
- package/client/dist/assets/MAL-DeQNXXPx.css +1 -0
- package/client/dist/assets/Player-BWFN9gud.js +9 -0
- package/client/dist/assets/Player-CBCYW7uG.css +1 -0
- package/client/dist/assets/PlayerSettings-BgStUrrP.css +1 -0
- package/client/dist/assets/PlayerSettings-rWZuATQf.js +1 -0
- package/client/dist/assets/RemoveConfirmationModal-BBiogSdf.css +1 -0
- package/client/dist/assets/RemoveConfirmationModal-CLYqyGOv.js +1 -0
- package/client/dist/assets/Search-DZAWgKwq.js +1 -0
- package/client/dist/assets/Search-lWsVQ0Ke.css +1 -0
- package/client/dist/assets/Settings-Bv9fX-x3.css +1 -0
- package/client/dist/assets/Settings-DyisJGeD.js +1 -0
- package/client/dist/assets/ToggleSwitch-CLnWnAuY.js +1 -0
- package/client/dist/assets/ToggleSwitch-DInRb7iM.css +1 -0
- package/client/dist/assets/Watchlist-2dVYksxq.css +1 -0
- package/client/dist/assets/Watchlist-CuqJISI3.js +1 -0
- package/client/dist/assets/hls.light-DcbkToIY.js +27 -0
- package/client/dist/assets/index-BK_Zaqaw.css +1 -0
- package/client/dist/assets/index-CHVF4D4L.js +178 -0
- package/client/dist/assets/useAnimeInfoData-Cr58brCY.js +1 -0
- package/client/dist/assets/useIsMobile-gHo4t6g6.js +1 -0
- package/client/dist/assets/vendor-DdbgYKo4.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 +54 -0
- package/orchestrator.js +302 -0
- package/package.json +69 -0
- package/server/dist/config.js +86 -0
- package/server/dist/constants.json +1359 -0
- package/server/dist/controllers/auth.controller.js +213 -0
- package/server/dist/controllers/data.controller.js +126 -0
- package/server/dist/controllers/insights.controller.js +125 -0
- package/server/dist/controllers/proxy.controller.js +235 -0
- package/server/dist/controllers/settings.controller.js +147 -0
- package/server/dist/controllers/watchlist.controller.js +499 -0
- package/server/dist/db.js +231 -0
- package/server/dist/github-sync.js +279 -0
- package/server/dist/google.js +274 -0
- package/server/dist/logger.js +21 -0
- package/server/dist/providers/123anime.provider.js +229 -0
- package/server/dist/providers/allanime.provider.js +773 -0
- package/server/dist/providers/animepahe.provider.js +313 -0
- package/server/dist/providers/animeya.provider.js +799 -0
- package/server/dist/providers/provider.interface.js +2 -0
- package/server/dist/rclone.js +126 -0
- package/server/dist/repositories/insights.repository.js +30 -0
- package/server/dist/repositories/notifications.repository.js +22 -0
- package/server/dist/repositories/settings.repository.js +13 -0
- package/server/dist/repositories/shows-meta.repository.js +39 -0
- package/server/dist/repositories/watched-episodes.repository.js +60 -0
- package/server/dist/repositories/watchlist.repository.js +49 -0
- package/server/dist/routes/auth.routes.js +23 -0
- package/server/dist/routes/data.routes.js +43 -0
- package/server/dist/routes/insights.routes.js +11 -0
- package/server/dist/routes/proxy.routes.js +13 -0
- package/server/dist/routes/settings.routes.js +26 -0
- package/server/dist/routes/watchlist.routes.js +26 -0
- package/server/dist/server.js +179 -0
- package/server/dist/sync-config.js +28 -0
- package/server/dist/sync.js +383 -0
- package/server/dist/utils/db-utils.js +36 -0
- package/server/dist/utils/env.utils.js +70 -0
- package/server/package.json +54 -0
|
@@ -0,0 +1,274 @@
|
|
|
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.googleDriveService = exports.GoogleDriveService = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const http_1 = __importDefault(require("http"));
|
|
9
|
+
const https_1 = __importDefault(require("https"));
|
|
10
|
+
const axios_1 = __importDefault(require("axios"));
|
|
11
|
+
const promises_1 = require("stream/promises");
|
|
12
|
+
const logger_1 = __importDefault(require("./logger"));
|
|
13
|
+
const config_1 = require("./config");
|
|
14
|
+
const httpAgent = new http_1.default.Agent({ keepAlive: false });
|
|
15
|
+
const httpsAgent = new https_1.default.Agent({ keepAlive: false });
|
|
16
|
+
httpsAgent.setMaxListeners(100);
|
|
17
|
+
httpAgent.setMaxListeners(100);
|
|
18
|
+
const googleAxios = axios_1.default.create({
|
|
19
|
+
httpAgent,
|
|
20
|
+
httpsAgent,
|
|
21
|
+
timeout: 30000,
|
|
22
|
+
});
|
|
23
|
+
class GoogleDriveService {
|
|
24
|
+
tokens = {};
|
|
25
|
+
constructor() {
|
|
26
|
+
if (!config_1.CONFIG.GOOGLE_CLIENT_ID) {
|
|
27
|
+
logger_1.default.error('GOOGLE_CLIENT_ID is missing from .env!');
|
|
28
|
+
}
|
|
29
|
+
this.loadTokens();
|
|
30
|
+
}
|
|
31
|
+
loadTokens() {
|
|
32
|
+
if (fs_1.default.existsSync(config_1.CONFIG.TOKEN_PATH)) {
|
|
33
|
+
try {
|
|
34
|
+
this.tokens = JSON.parse(fs_1.default.readFileSync(config_1.CONFIG.TOKEN_PATH, 'utf-8'));
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
logger_1.default.error({ err: error }, 'Failed to load Google tokens');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
saveTokens(tokens) {
|
|
42
|
+
const merged = { ...this.tokens, ...tokens };
|
|
43
|
+
if (merged.expires_in && !merged.expiry_date) {
|
|
44
|
+
merged.expiry_date = Date.now() + merged.expires_in * 1000;
|
|
45
|
+
}
|
|
46
|
+
this.tokens = merged;
|
|
47
|
+
try {
|
|
48
|
+
fs_1.default.writeFileSync(config_1.CONFIG.TOKEN_PATH, JSON.stringify(this.tokens));
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
logger_1.default.error({ err: error }, 'Failed to save refreshed tokens');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
getGoogleClientConfig() {
|
|
55
|
+
if (!config_1.CONFIG.GOOGLE_CLIENT_ID || !config_1.CONFIG.GOOGLE_CLIENT_SECRET) {
|
|
56
|
+
throw new Error('Google OAuth credentials are not configured');
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
clientId: config_1.CONFIG.GOOGLE_CLIENT_ID,
|
|
60
|
+
clientSecret: config_1.CONFIG.GOOGLE_CLIENT_SECRET,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
async refreshAccessToken() {
|
|
64
|
+
if (!this.tokens.refresh_token) {
|
|
65
|
+
throw new Error('Missing refresh token');
|
|
66
|
+
}
|
|
67
|
+
const { clientId, clientSecret } = this.getGoogleClientConfig();
|
|
68
|
+
const params = new URLSearchParams({
|
|
69
|
+
client_id: clientId,
|
|
70
|
+
client_secret: clientSecret,
|
|
71
|
+
refresh_token: this.tokens.refresh_token,
|
|
72
|
+
grant_type: 'refresh_token',
|
|
73
|
+
});
|
|
74
|
+
const { data } = await googleAxios.post('https://oauth2.googleapis.com/token', params.toString(), {
|
|
75
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
76
|
+
});
|
|
77
|
+
this.saveTokens(data);
|
|
78
|
+
}
|
|
79
|
+
async ensureAccessToken() {
|
|
80
|
+
const expiresSoon = !this.tokens.expiry_date || Date.now() >= this.tokens.expiry_date - 60_000;
|
|
81
|
+
if (!this.tokens.access_token || expiresSoon) {
|
|
82
|
+
await this.refreshAccessToken();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async googleRequest(config) {
|
|
86
|
+
await this.ensureAccessToken();
|
|
87
|
+
return googleAxios.request({
|
|
88
|
+
...config,
|
|
89
|
+
headers: {
|
|
90
|
+
Authorization: `Bearer ${this.tokens.access_token}`,
|
|
91
|
+
...(config.headers ?? {}),
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
isAuthenticated() {
|
|
96
|
+
return !!this.tokens.refresh_token;
|
|
97
|
+
}
|
|
98
|
+
getAuthUrl() {
|
|
99
|
+
const { clientId } = this.getGoogleClientConfig();
|
|
100
|
+
const url = new URL('https://accounts.google.com/o/oauth2/v2/auth');
|
|
101
|
+
url.searchParams.set('client_id', clientId);
|
|
102
|
+
url.searchParams.set('redirect_uri', config_1.CONFIG.GOOGLE_REDIRECT_URI);
|
|
103
|
+
url.searchParams.set('response_type', 'code');
|
|
104
|
+
url.searchParams.set('access_type', 'offline');
|
|
105
|
+
url.searchParams.set('prompt', 'consent');
|
|
106
|
+
url.searchParams.set('scope', config_1.CONFIG.GOOGLE_SCOPES.join(' '));
|
|
107
|
+
return url.toString();
|
|
108
|
+
}
|
|
109
|
+
async handleCallback(code) {
|
|
110
|
+
const { clientId, clientSecret } = this.getGoogleClientConfig();
|
|
111
|
+
const params = new URLSearchParams({
|
|
112
|
+
code,
|
|
113
|
+
client_id: clientId,
|
|
114
|
+
client_secret: clientSecret,
|
|
115
|
+
redirect_uri: config_1.CONFIG.GOOGLE_REDIRECT_URI,
|
|
116
|
+
grant_type: 'authorization_code',
|
|
117
|
+
});
|
|
118
|
+
const { data } = await googleAxios.post('https://oauth2.googleapis.com/token', params.toString(), {
|
|
119
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
120
|
+
});
|
|
121
|
+
this.saveTokens(data);
|
|
122
|
+
return this.tokens;
|
|
123
|
+
}
|
|
124
|
+
async getUserProfile() {
|
|
125
|
+
if (!this.isAuthenticated())
|
|
126
|
+
return null;
|
|
127
|
+
try {
|
|
128
|
+
const res = await this.googleRequest({
|
|
129
|
+
method: 'GET',
|
|
130
|
+
url: 'https://www.googleapis.com/oauth2/v2/userinfo',
|
|
131
|
+
});
|
|
132
|
+
return res.data;
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
logger_1.default.error({ err: error }, 'Failed to fetch user profile');
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async logout() {
|
|
140
|
+
if (fs_1.default.existsSync(config_1.CONFIG.TOKEN_PATH)) {
|
|
141
|
+
fs_1.default.unlinkSync(config_1.CONFIG.TOKEN_PATH);
|
|
142
|
+
}
|
|
143
|
+
this.tokens = {};
|
|
144
|
+
}
|
|
145
|
+
async ensureFolder(folderName) {
|
|
146
|
+
if (!this.isAuthenticated())
|
|
147
|
+
throw new Error('Not authenticated');
|
|
148
|
+
const existing = await this.findFile(folderName, undefined, 'application/vnd.google-apps.folder');
|
|
149
|
+
if (existing) {
|
|
150
|
+
return existing.id;
|
|
151
|
+
}
|
|
152
|
+
const fileMetadata = {
|
|
153
|
+
name: folderName,
|
|
154
|
+
mimeType: 'application/vnd.google-apps.folder',
|
|
155
|
+
};
|
|
156
|
+
try {
|
|
157
|
+
const res = await this.googleRequest({
|
|
158
|
+
method: 'POST',
|
|
159
|
+
url: 'https://www.googleapis.com/drive/v3/files',
|
|
160
|
+
params: { fields: 'id' },
|
|
161
|
+
data: fileMetadata,
|
|
162
|
+
headers: { 'Content-Type': 'application/json' },
|
|
163
|
+
});
|
|
164
|
+
return res.data.id;
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
logger_1.default.error({ err: error }, `Failed to create folder ${folderName}`);
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
async findFile(filename, parentId, mimeType) {
|
|
172
|
+
if (!this.isAuthenticated())
|
|
173
|
+
return null;
|
|
174
|
+
const safeName = filename.replace(/'/g, "\\'");
|
|
175
|
+
let query = `name = '${safeName}' and trashed = false`;
|
|
176
|
+
if (parentId) {
|
|
177
|
+
const safeParentId = parentId.replace(/'/g, "\\'");
|
|
178
|
+
query += ` and '${safeParentId}' in parents`;
|
|
179
|
+
}
|
|
180
|
+
if (mimeType) {
|
|
181
|
+
const safeMimeType = mimeType.replace(/'/g, "\\'");
|
|
182
|
+
query += ` and mimeType = '${safeMimeType}'`;
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
const res = await this.googleRequest({
|
|
186
|
+
method: 'GET',
|
|
187
|
+
url: 'https://www.googleapis.com/drive/v3/files',
|
|
188
|
+
params: {
|
|
189
|
+
q: query,
|
|
190
|
+
fields: 'files(id, name)',
|
|
191
|
+
spaces: 'drive',
|
|
192
|
+
orderBy: 'createdTime desc',
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
if (res.data.files && res.data.files.length > 0) {
|
|
196
|
+
return { id: res.data.files[0].id, name: res.data.files[0].name };
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
logger_1.default.error({ err: error }, `Failed to find file ${filename}`);
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async downloadFile(fileId, destPath) {
|
|
206
|
+
if (!this.isAuthenticated())
|
|
207
|
+
throw new Error('Not authenticated');
|
|
208
|
+
const dest = fs_1.default.createWriteStream(destPath);
|
|
209
|
+
try {
|
|
210
|
+
const res = await this.googleRequest({
|
|
211
|
+
method: 'GET',
|
|
212
|
+
url: `https://www.googleapis.com/drive/v3/files/${encodeURIComponent(fileId)}`,
|
|
213
|
+
params: { alt: 'media' },
|
|
214
|
+
responseType: 'stream',
|
|
215
|
+
});
|
|
216
|
+
await (0, promises_1.pipeline)(res.data, dest);
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
dest.destroy();
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
async uploadFile(filePath, filename, mimeType = 'application/octet-stream', parentId, existingFileId) {
|
|
224
|
+
if (!this.isAuthenticated())
|
|
225
|
+
throw new Error('Not authenticated');
|
|
226
|
+
let targetId = existingFileId;
|
|
227
|
+
if (!targetId) {
|
|
228
|
+
const existing = await this.findFile(filename, parentId, mimeType);
|
|
229
|
+
if (existing)
|
|
230
|
+
targetId = existing.id;
|
|
231
|
+
}
|
|
232
|
+
const media = {
|
|
233
|
+
mimeType,
|
|
234
|
+
body: fs_1.default.createReadStream(filePath),
|
|
235
|
+
};
|
|
236
|
+
try {
|
|
237
|
+
if (targetId) {
|
|
238
|
+
await this.googleRequest({
|
|
239
|
+
method: 'PATCH',
|
|
240
|
+
url: `https://www.googleapis.com/upload/drive/v3/files/${encodeURIComponent(targetId)}`,
|
|
241
|
+
params: { uploadType: 'media' },
|
|
242
|
+
data: media.body,
|
|
243
|
+
headers: { 'Content-Type': media.mimeType },
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
const resource = { name: filename };
|
|
248
|
+
if (parentId) {
|
|
249
|
+
resource.parents = [parentId];
|
|
250
|
+
}
|
|
251
|
+
const created = await this.googleRequest({
|
|
252
|
+
method: 'POST',
|
|
253
|
+
url: 'https://www.googleapis.com/drive/v3/files',
|
|
254
|
+
params: { fields: 'id' },
|
|
255
|
+
data: resource,
|
|
256
|
+
headers: { 'Content-Type': 'application/json' },
|
|
257
|
+
});
|
|
258
|
+
await this.googleRequest({
|
|
259
|
+
method: 'PATCH',
|
|
260
|
+
url: `https://www.googleapis.com/upload/drive/v3/files/${encodeURIComponent(created.data.id)}`,
|
|
261
|
+
params: { uploadType: 'media' },
|
|
262
|
+
data: media.body,
|
|
263
|
+
headers: { 'Content-Type': media.mimeType },
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
logger_1.default.error({ err: error }, `Failed to upload file ${filename}`);
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
exports.GoogleDriveService = GoogleDriveService;
|
|
274
|
+
exports.googleDriveService = new GoogleDriveService();
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
const pino_1 = __importDefault(require("pino"));
|
|
7
|
+
const isDevelopment = process.env.NODE_ENV !== 'production';
|
|
8
|
+
const logger = (0, pino_1.default)({
|
|
9
|
+
level: isDevelopment ? 'debug' : 'info',
|
|
10
|
+
transport: isDevelopment
|
|
11
|
+
? {
|
|
12
|
+
target: 'pino-pretty',
|
|
13
|
+
options: {
|
|
14
|
+
colorize: true,
|
|
15
|
+
translateTime: 'SYS:standard',
|
|
16
|
+
ignore: 'pid,hostname',
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
: undefined,
|
|
20
|
+
});
|
|
21
|
+
exports.default = logger;
|
|
@@ -0,0 +1,229 @@
|
|
|
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._123AnimeProvider = void 0;
|
|
7
|
+
const logger_1 = __importDefault(require("../logger"));
|
|
8
|
+
const BASE_URL = 'https://shirayuki-scrapper-api.onrender.com';
|
|
9
|
+
class _123AnimeProvider {
|
|
10
|
+
name = '123Anime';
|
|
11
|
+
cache;
|
|
12
|
+
constructor(cache) {
|
|
13
|
+
this.cache = cache;
|
|
14
|
+
}
|
|
15
|
+
createSlug(title) {
|
|
16
|
+
return title
|
|
17
|
+
.toLowerCase()
|
|
18
|
+
.replace(/[^\w\s-]/g, '')
|
|
19
|
+
.replace(/\s+/g, '-')
|
|
20
|
+
.replace(/-+/g, '-')
|
|
21
|
+
.replace(/^-+|-+$/g, '');
|
|
22
|
+
}
|
|
23
|
+
normalizeSlugForSearch(title) {
|
|
24
|
+
return title
|
|
25
|
+
.toLowerCase()
|
|
26
|
+
.replace(/[^\w\s-]/g, '')
|
|
27
|
+
.replace(/['"]/g, '')
|
|
28
|
+
.replace(/\s+/g, '-')
|
|
29
|
+
.replace(/-+/g, '-')
|
|
30
|
+
.replace(/^-+|-+$/g, '');
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Picks the best-matching show from a list of search results by comparing
|
|
34
|
+
* how closely each result's title / id matches the query.
|
|
35
|
+
* Scoring (highest wins):
|
|
36
|
+
* 3 – id/slug exact match
|
|
37
|
+
* 2 – title exact match (case-insensitive)
|
|
38
|
+
* 1 – title starts with query
|
|
39
|
+
* 0 – title contains query word (partial)
|
|
40
|
+
* -1 – no match (but still returned as last resort)
|
|
41
|
+
*/
|
|
42
|
+
bestMatch(results, query) {
|
|
43
|
+
const q = query.toLowerCase().trim();
|
|
44
|
+
const qSlug = this.normalizeSlugForSearch(q);
|
|
45
|
+
let best = results[0];
|
|
46
|
+
let bestScore = -1;
|
|
47
|
+
for (const s of results) {
|
|
48
|
+
const id = (s.id || s._id || '').toLowerCase();
|
|
49
|
+
const title = (s.name || '').toLowerCase();
|
|
50
|
+
let score = -1;
|
|
51
|
+
if (id === qSlug || id === q) {
|
|
52
|
+
score = 3;
|
|
53
|
+
}
|
|
54
|
+
else if (title === q) {
|
|
55
|
+
score = 2;
|
|
56
|
+
}
|
|
57
|
+
else if (title.startsWith(q)) {
|
|
58
|
+
score = 1;
|
|
59
|
+
}
|
|
60
|
+
else if (title.includes(q) || id.startsWith(qSlug)) {
|
|
61
|
+
score = 0;
|
|
62
|
+
}
|
|
63
|
+
if (score > bestScore) {
|
|
64
|
+
bestScore = score;
|
|
65
|
+
best = s;
|
|
66
|
+
if (score === 3)
|
|
67
|
+
break; // can't do better
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return best;
|
|
71
|
+
}
|
|
72
|
+
extractSlugFromUrl(url) {
|
|
73
|
+
if (!url)
|
|
74
|
+
return null;
|
|
75
|
+
try {
|
|
76
|
+
const parts = url.split('/');
|
|
77
|
+
const lastPart = parts[parts.length - 1];
|
|
78
|
+
if (lastPart) {
|
|
79
|
+
return lastPart.replace(/\.(jpg|jpeg|png|webp|gif)$/i, '');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
// ignore
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
async search(options) {
|
|
88
|
+
try {
|
|
89
|
+
const rawQuery = options.query || '';
|
|
90
|
+
const query = rawQuery.replace(/[""]/g, '').replace(/[']/g, '').replace(/\s+/g, ' ').trim();
|
|
91
|
+
const url = `${BASE_URL}/search?keyword=${encodeURIComponent(query)}`;
|
|
92
|
+
const response = await fetch(url);
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
logger_1.default.warn({ url, status: response.status }, '123Anime search failed with non-200 status');
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
const data = (await response.json());
|
|
98
|
+
if (!data.success || !data.data) {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
return (data.data || []).map((anime) => {
|
|
102
|
+
const imageUrl = anime.thumbnail || anime.image || anime.poster;
|
|
103
|
+
const slugFromUrl = this.extractSlugFromUrl(imageUrl);
|
|
104
|
+
const titleForSlug = anime.japanese_title || anime.title;
|
|
105
|
+
const id = anime.id || slugFromUrl || this.normalizeSlugForSearch(titleForSlug);
|
|
106
|
+
return {
|
|
107
|
+
_id: id,
|
|
108
|
+
id: id,
|
|
109
|
+
name: anime.title,
|
|
110
|
+
englishName: anime.title,
|
|
111
|
+
thumbnail: imageUrl,
|
|
112
|
+
type: anime.type,
|
|
113
|
+
availableEpisodesDetail: {
|
|
114
|
+
sub: Array.from({ length: Number(anime.episode) || 0 }, (_, i) => (i + 1).toString()),
|
|
115
|
+
dub: [],
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
logger_1.default.error({ err: error }, '123Anime search failed');
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async getEpisodes(showId) {
|
|
126
|
+
try {
|
|
127
|
+
const cacheKey = `123anime_eps_${showId}`;
|
|
128
|
+
const cached = this.cache.get(cacheKey);
|
|
129
|
+
if (cached) {
|
|
130
|
+
return cached;
|
|
131
|
+
}
|
|
132
|
+
const results = await this.search({ query: showId.replace(/ /g, '-') });
|
|
133
|
+
const show = results.find((s) => s.id === showId || s._id === showId) ||
|
|
134
|
+
(results.length > 0 ? this.bestMatch(results, showId) : undefined);
|
|
135
|
+
if (!show || !show.availableEpisodesDetail) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
const episodes = show.availableEpisodesDetail.sub || [];
|
|
139
|
+
const result = {
|
|
140
|
+
episodes,
|
|
141
|
+
description: '',
|
|
142
|
+
};
|
|
143
|
+
this.cache.set(cacheKey, result, 3600);
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
logger_1.default.error({ err: error, showId }, '123Anime getEpisodes failed');
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async getStreamUrls(showId, episodeNumber) {
|
|
152
|
+
try {
|
|
153
|
+
const query = showId.replace(/ /g, '-');
|
|
154
|
+
const searchResults = await this.search({ query });
|
|
155
|
+
if (!searchResults || searchResults.length === 0) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
const match = searchResults.find((s) => s.id === showId || s._id === showId) ||
|
|
159
|
+
this.bestMatch(searchResults, showId);
|
|
160
|
+
const animeId = match.id || match._id;
|
|
161
|
+
const url = `${BASE_URL}/episode-stream?id=${animeId}&ep=${episodeNumber}`;
|
|
162
|
+
const response = await fetch(url);
|
|
163
|
+
if (!response.ok) {
|
|
164
|
+
logger_1.default.warn({ url, status: response.status }, '123Anime stream request failed');
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
const data = (await response.json());
|
|
168
|
+
if (!data.success || !data.data) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
const streamingLink = data.data['streaming_link'] || data.data['stream'] || data.data['url'];
|
|
172
|
+
if (!streamingLink) {
|
|
173
|
+
logger_1.default.warn({ data }, '123Anime No streaming link found in response data');
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
const separator = streamingLink.includes('?') ? '&' : '?';
|
|
177
|
+
const finalUrl = `${streamingLink}${separator}autoplay=1`;
|
|
178
|
+
return [
|
|
179
|
+
{
|
|
180
|
+
sourceName: '123Anime',
|
|
181
|
+
links: [
|
|
182
|
+
{
|
|
183
|
+
resolutionStr: 'auto',
|
|
184
|
+
link: finalUrl,
|
|
185
|
+
hls: false,
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
type: 'iframe',
|
|
189
|
+
},
|
|
190
|
+
];
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
logger_1.default.error({ err: error, showId, episodeNumber }, '123Anime getStreamUrls failed');
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async getShowMeta(showId) {
|
|
198
|
+
const results = await this.search({ query: showId.replace(/ /g, '-') });
|
|
199
|
+
return results.find((s) => s.id === showId || s._id === showId) || null;
|
|
200
|
+
}
|
|
201
|
+
async getPopular() {
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
async getSchedule() {
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
async getSeasonal() {
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
210
|
+
async getLatestReleases() {
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
async getSkipTimes() {
|
|
214
|
+
return { found: false, results: [] };
|
|
215
|
+
}
|
|
216
|
+
async getShowDetails() {
|
|
217
|
+
return { status: 'Unknown' };
|
|
218
|
+
}
|
|
219
|
+
async getAllmangaDetails() {
|
|
220
|
+
return {
|
|
221
|
+
Rating: 'N/A',
|
|
222
|
+
Season: 'N/A',
|
|
223
|
+
Episodes: 'N/A',
|
|
224
|
+
Date: 'N/A',
|
|
225
|
+
'Original Broadcast': 'N/A',
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
exports._123AnimeProvider = _123AnimeProvider;
|