nekostream 2026.4.2 → 2026.4.4
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/dist/cli/application.js +12 -0
- package/dist/cli/feedback.js +10 -0
- package/dist/cli/menus/home-menu.js +15 -0
- package/dist/cli/menus/settings-menu.js +3 -1
- package/dist/cli/update-check.js +24 -1
- package/dist/cli/web-server.js +554 -0
- package/dist/player-policy.js +6 -0
- package/dist/scrapers/adblock-policy.js +10 -1
- package/dist/scrapers/anilist-api.js +24 -6
- package/dist/scrapers/base.js +4 -1
- package/dist/scrapers/providers/anime47.js +1 -1
- package/dist/scrapers/providers/animehay.js +5 -5
- package/dist/scrapers/providers/animevietsub/parsers/cards.js +4 -4
- package/dist/scrapers/providers/animevietsub/parsers/detail.js +19 -18
- package/dist/scrapers/providers/animevietsub/streams/extractors.js +30 -93
- package/dist/scrapers/providers/animevietsub/streams/server-parser.js +24 -4
- package/dist/scrapers/providers/animevietsub.js +124 -13
- package/dist/scrapers/vietnamese-titles.js +97 -0
- package/package.json +6 -3
- package/player-main.js +84 -23
- package/player.html +1 -0
- package/web/dist/assets/index-BByTtXWy.css +1 -0
- package/web/dist/assets/index-Y4PiRbR7.js +2171 -0
- package/web/dist/index.html +22 -0
- package/webview-preload.js +86 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.anilistApi = exports.AniListApiService = void 0;
|
|
4
|
-
// AniList API service for fetching anime metadata and images
|
|
5
4
|
const logger_1 = require("../logger");
|
|
5
|
+
const vietnamese_titles_1 = require("./vietnamese-titles");
|
|
6
6
|
// ============ Cache Implementation ============
|
|
7
7
|
const cache = new Map();
|
|
8
8
|
const CACHE_TTL = 1000 * 60 * 60 * 24; // 24 hours
|
|
@@ -276,18 +276,19 @@ class AniListApiService {
|
|
|
276
276
|
* Max 2 API queries in most cases.
|
|
277
277
|
*/
|
|
278
278
|
async findByTitleVariants(title, titleAlt) {
|
|
279
|
-
|
|
279
|
+
const mappedVn = (0, vietnamese_titles_1.lookupVietnameseTitle)(title) || (titleAlt ? (0, vietnamese_titles_1.lookupVietnameseTitle)(titleAlt) : undefined);
|
|
280
|
+
const vnVariants = mappedVn ? mappedVn.split(',').map(s => s.trim()).filter(s => s.length >= 2) : [];
|
|
280
281
|
const altVariants = titleAlt
|
|
281
282
|
? titleAlt.split(',').map(s => s.trim()).filter(s => s.length >= 2)
|
|
282
283
|
: [];
|
|
283
284
|
const titleVariants = title
|
|
284
285
|
? title.split(',').map(s => s.trim()).filter(s => s.length >= 2)
|
|
285
286
|
: [];
|
|
286
|
-
// Priority:
|
|
287
|
+
// Priority: Vietnamese mapped canonical names first, then alt names, then raw title names
|
|
287
288
|
// Use Set to deduplicate
|
|
288
289
|
const seen = new Set();
|
|
289
290
|
const queriesToTry = [];
|
|
290
|
-
for (const v of [...altVariants, ...titleVariants]) {
|
|
291
|
+
for (const v of [...vnVariants, ...altVariants, ...titleVariants]) {
|
|
291
292
|
const lower = v.toLowerCase();
|
|
292
293
|
if (!seen.has(lower)) {
|
|
293
294
|
seen.add(lower);
|
|
@@ -381,7 +382,7 @@ class AniListApiService {
|
|
|
381
382
|
malId: match.idMal,
|
|
382
383
|
cover: match.coverImage.extraLarge || match.coverImage.large,
|
|
383
384
|
banner: match.bannerImage,
|
|
384
|
-
synopsis: match.description,
|
|
385
|
+
synopsis: match.description ? this.cleanSynopsis(match.description) : undefined,
|
|
385
386
|
score: match.averageScore,
|
|
386
387
|
episodes: match.episodes,
|
|
387
388
|
status: match.status,
|
|
@@ -394,6 +395,21 @@ class AniListApiService {
|
|
|
394
395
|
return null;
|
|
395
396
|
}
|
|
396
397
|
}
|
|
398
|
+
cleanSynopsis(text) {
|
|
399
|
+
return text
|
|
400
|
+
.replace(/<br\s*\/?>/gi, ' ')
|
|
401
|
+
.replace(/<\/p>/gi, ' ')
|
|
402
|
+
.replace(/<[^>]+>/g, '')
|
|
403
|
+
.replace(/"/g, '"')
|
|
404
|
+
.replace(/'/g, "'")
|
|
405
|
+
.replace(/'/g, "'")
|
|
406
|
+
.replace(/&/g, '&')
|
|
407
|
+
.replace(/</g, '<')
|
|
408
|
+
.replace(/>/g, '>')
|
|
409
|
+
.replace(/ /g, ' ')
|
|
410
|
+
.replace(/\s+/g, ' ')
|
|
411
|
+
.trim();
|
|
412
|
+
}
|
|
397
413
|
/**
|
|
398
414
|
* Clean title for better search results
|
|
399
415
|
*/
|
|
@@ -425,6 +441,8 @@ class AniListApiService {
|
|
|
425
441
|
.replace(/[^\w\s]/g, '')
|
|
426
442
|
.replace(/\s+/g, ' ')
|
|
427
443
|
.trim();
|
|
444
|
+
const mappedVn = (0, vietnamese_titles_1.lookupVietnameseTitle)(title) || (titleAlt ? (0, vietnamese_titles_1.lookupVietnameseTitle)(titleAlt) : undefined);
|
|
445
|
+
const vnTerms = mappedVn ? mappedVn.split(',').map(s => normalizeStr(this.cleanTitle(s.trim()))).filter(Boolean) : [];
|
|
428
446
|
// We compare against all comma-split variants for better matching
|
|
429
447
|
const searchTitles = title
|
|
430
448
|
.split(',')
|
|
@@ -433,7 +451,7 @@ class AniListApiService {
|
|
|
433
451
|
const searchAltTitles = titleAlt
|
|
434
452
|
? titleAlt.split(',').map(s => normalizeStr(this.cleanTitle(s.trim()))).filter(Boolean)
|
|
435
453
|
: [];
|
|
436
|
-
const allSearchTerms = [...new Set([...searchAltTitles, ...searchTitles])];
|
|
454
|
+
const allSearchTerms = [...new Set([...vnTerms, ...searchAltTitles, ...searchTitles])];
|
|
437
455
|
// Threshold score - prevents unrelated matches (like Evangelion from "NGO")
|
|
438
456
|
let bestScore = 15;
|
|
439
457
|
let bestMatch = null;
|
package/dist/scrapers/base.js
CHANGED
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
exports.BaseScraper = void 0;
|
|
5
5
|
class BaseScraper {
|
|
6
|
-
async getHomeCards(_section = 'trending') {
|
|
6
|
+
async getHomeCards(_section = 'trending', _page = 1) {
|
|
7
7
|
return [];
|
|
8
8
|
}
|
|
9
9
|
async getSchedule() {
|
|
10
10
|
return [];
|
|
11
11
|
}
|
|
12
|
+
async getAnimeByGenre(_genre, _page = 1) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
12
15
|
// Helper to build full URL
|
|
13
16
|
buildUrl(path) {
|
|
14
17
|
if (path.startsWith('http'))
|
|
@@ -219,7 +219,7 @@ class Anime47Provider extends base_1.BaseScraper {
|
|
|
219
219
|
parseEpisodeLinksFromHtml(html, animeId) {
|
|
220
220
|
return (0, cards_1.parseEpisodeLinksFromHtml)(html, animeId, this.baseUrl);
|
|
221
221
|
}
|
|
222
|
-
async getHomeCards(section = 'trending') {
|
|
222
|
+
async getHomeCards(section = 'trending', _page = 1) {
|
|
223
223
|
try {
|
|
224
224
|
const html = await this.fetchHtml(this.baseUrl, { retries: 2 });
|
|
225
225
|
const state = this.extractInitialState(html);
|
|
@@ -190,16 +190,16 @@ class AnimehayProvider extends base_1.BaseScraper {
|
|
|
190
190
|
return results;
|
|
191
191
|
}
|
|
192
192
|
// ── BaseScraper interface ───────────────────────────────────────────────
|
|
193
|
-
async getHomeCards(section = 'trending') {
|
|
193
|
+
async getHomeCards(section = 'trending', page = 1) {
|
|
194
194
|
try {
|
|
195
195
|
// Main listing page shows all recently updated anime
|
|
196
196
|
const url = section === 'trending'
|
|
197
|
-
? `${this.baseUrl}/xep-hang`
|
|
198
|
-
: `${this.baseUrl}/phim-moi-cap-nhap/tat-ca
|
|
197
|
+
? (page > 1 ? `${this.baseUrl}/xep-hang?page=${page}` : `${this.baseUrl}/xep-hang`)
|
|
198
|
+
: `${this.baseUrl}/phim-moi-cap-nhap/tat-ca-${page}.html`;
|
|
199
199
|
const html = await this.fetchHtml(url);
|
|
200
200
|
const cards = this.parseCardsFromHtml(html);
|
|
201
|
-
// Also fetch page 2 for 'latest'
|
|
202
|
-
if (section === 'latest' && cards.length < 30) {
|
|
201
|
+
// Also fetch page 2 for 'latest' if page 1 has few items
|
|
202
|
+
if (section === 'latest' && page === 1 && cards.length < 30) {
|
|
203
203
|
try {
|
|
204
204
|
const html2 = await this.fetchHtml(`${this.baseUrl}/phim-moi-cap-nhap/tat-ca-2.html`);
|
|
205
205
|
const cards2 = this.parseCardsFromHtml(html2);
|
|
@@ -41,6 +41,7 @@ exports.parseHomeCards = parseHomeCards;
|
|
|
41
41
|
const cheerio = __importStar(require("cheerio"));
|
|
42
42
|
const html_utils_1 = require("../../../html-utils");
|
|
43
43
|
const logger_1 = require("../../../../logger");
|
|
44
|
+
const vietnamese_titles_1 = require("../../../vietnamese-titles");
|
|
44
45
|
function parseIdFromUrl(url) {
|
|
45
46
|
const cleanUrl = url.replace(/\.html$/, '').replace(/\/$/, '');
|
|
46
47
|
const pathMatch = cleanUrl.match(/\/phim\/([^/]+)/);
|
|
@@ -141,13 +142,12 @@ function extractCardData($root, $, baseUrl) {
|
|
|
141
142
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
142
143
|
.join(' ');
|
|
143
144
|
const isVietnamese = (s) => /[àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ]/i.test(s);
|
|
145
|
+
const mappedTitle = (0, vietnamese_titles_1.lookupVietnameseTitle)(title) || (0, vietnamese_titles_1.lookupVietnameseTitle)(titleFromSlug);
|
|
144
146
|
const titleAlt = subtitleFromCard && subtitleFromCard.toLowerCase() !== title.toLowerCase()
|
|
145
147
|
? isVietnamese(subtitleFromCard)
|
|
146
|
-
? undefined
|
|
148
|
+
? (mappedTitle || undefined)
|
|
147
149
|
: subtitleFromCard
|
|
148
|
-
: titleFromSlug.toLowerCase() !== title.toLowerCase()
|
|
149
|
-
? titleFromSlug
|
|
150
|
-
: undefined;
|
|
150
|
+
: mappedTitle || (titleFromSlug.toLowerCase() !== title.toLowerCase() ? titleFromSlug : undefined);
|
|
151
151
|
const thumbnail = (0, html_utils_1.absolutizeUrl)(baseUrl, $img.attr('data-src') || $img.attr('src') || '');
|
|
152
152
|
let cover = (0, html_utils_1.absolutizeUrl)(baseUrl, $root.find('[style*="background-image"]').first().attr('data-src') ||
|
|
153
153
|
$root.find('[style*="background-image"]').first().attr('src') ||
|
|
@@ -99,9 +99,14 @@ function parseDetailFromHtml(html, id, baseUrl) {
|
|
|
99
99
|
return (/[àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ]/i.test(value) ||
|
|
100
100
|
/\b(người|một|của|và|không|nhưng|phải|đã|để|trong|tình|gia tộc|sát thủ|kết hôn)\b/i.test(value));
|
|
101
101
|
};
|
|
102
|
+
const isSeoSpam = (value) => {
|
|
103
|
+
if (!value)
|
|
104
|
+
return true;
|
|
105
|
+
return /kanefusa|fansub|webdescription|vua hải tặc đảo hải tặc|hoạt hình trung quốc/i.test(value);
|
|
106
|
+
};
|
|
102
107
|
const cleanDescriptionCandidate = (value) => {
|
|
103
108
|
const cleaned = normalizeInfoText((0, html_utils_1.decodeHtml)(String(value || '')));
|
|
104
|
-
if (!cleaned || cleaned.length <
|
|
109
|
+
if (!cleaned || cleaned.length < 15 || looksLikeInfoBlock(cleaned) || isSeoSpam(cleaned))
|
|
105
110
|
return '';
|
|
106
111
|
return cleaned;
|
|
107
112
|
};
|
|
@@ -117,32 +122,28 @@ function parseDetailFromHtml(html, id, baseUrl) {
|
|
|
117
122
|
};
|
|
118
123
|
// Extract descriptions (VI/EN)
|
|
119
124
|
const descriptionSelectors = [
|
|
120
|
-
'.Description',
|
|
121
|
-
'.desc',
|
|
122
|
-
'.summary',
|
|
123
|
-
'.Synopsis',
|
|
124
|
-
'.synopsis',
|
|
125
|
-
'.overview',
|
|
126
|
-
'.MovieDescription',
|
|
125
|
+
'.TPost .Description',
|
|
127
126
|
'.entry-content .Description',
|
|
128
127
|
'.post-content .Description',
|
|
129
|
-
'.
|
|
130
|
-
'.
|
|
131
|
-
'.
|
|
128
|
+
'.MovieDescription',
|
|
129
|
+
'.Description:not(.WebDescription)',
|
|
130
|
+
'.film-content',
|
|
131
|
+
'#film-content',
|
|
132
|
+
'.detail-content',
|
|
132
133
|
'[itemprop="description"]',
|
|
133
|
-
'
|
|
134
|
-
'
|
|
135
|
-
'
|
|
136
|
-
'
|
|
137
|
-
'[class*="summary"]',
|
|
138
|
-
'[class*="Summary"]'
|
|
134
|
+
'.desc:not(.WebDescription)',
|
|
135
|
+
'.summary:not(.WebDescription)',
|
|
136
|
+
'.Synopsis',
|
|
137
|
+
'.synopsis',
|
|
139
138
|
];
|
|
140
139
|
const descriptionCandidates = [];
|
|
141
140
|
for (const selector of descriptionSelectors) {
|
|
142
141
|
$(selector).each((_, el) => {
|
|
142
|
+
if ($(el).hasClass('WebDescription') || $(el).closest('.WebDescription').length > 0)
|
|
143
|
+
return;
|
|
143
144
|
const $el = $(el).clone();
|
|
144
145
|
$el
|
|
145
|
-
.find('script, style, nav, a[href*="/the-loai/"], a[href*="/tag/"], .InfoList, .MovieInfo, .breadcrumb')
|
|
146
|
+
.find('script, style, nav, a[href*="/the-loai/"], a[href*="/tag/"], .InfoList, .MovieInfo, .breadcrumb, .WebDescription')
|
|
146
147
|
.remove();
|
|
147
148
|
const candidate = cleanDescriptionCandidate($el.text());
|
|
148
149
|
if (candidate && !descriptionCandidates.includes(candidate)) {
|
|
@@ -45,106 +45,37 @@ function extractFromAbyssServer(embedUrl, server, defaultBaseUrl) {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
/**
|
|
48
|
-
* Extract video from DU server (storage.googleapiscdn.com)
|
|
48
|
+
* Extract video from DU server (stream.googleapiscdn.com / storage.googleapiscdn.com)
|
|
49
49
|
*/
|
|
50
|
-
|
|
51
|
-
const crawler = await (0, crawler_1.getCrawler)();
|
|
52
|
-
const page = await crawler.newPage();
|
|
50
|
+
function extractFromDUServer(embedUrl, server, defaultBaseUrl) {
|
|
53
51
|
try {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
const url = response.url();
|
|
57
|
-
const contentType = response.headers()['content-type'] || '';
|
|
58
|
-
if (url.includes('.m3u8') ||
|
|
59
|
-
url.includes('.mp4') ||
|
|
60
|
-
url.includes('/playlist/') ||
|
|
61
|
-
url.includes('/hls/') ||
|
|
62
|
-
url.includes('/video/') ||
|
|
63
|
-
contentType.includes('mpegurl') ||
|
|
64
|
-
contentType.includes('video/')) {
|
|
65
|
-
interceptedUrls.push(url);
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
await page.goto(embedUrl, {
|
|
69
|
-
waitUntil: 'domcontentloaded',
|
|
70
|
-
timeout: 30000
|
|
71
|
-
});
|
|
72
|
-
await page.waitForTimeout(3000);
|
|
73
|
-
const jsUrls = await (0, interceptor_1.extractVideoFromJS)(page);
|
|
74
|
-
interceptedUrls.push(...jsUrls);
|
|
52
|
+
const refererVal = server.referer || defaultBaseUrl;
|
|
53
|
+
let origin = defaultBaseUrl;
|
|
75
54
|
try {
|
|
76
|
-
|
|
77
|
-
await page.waitForTimeout(2000);
|
|
55
|
+
origin = new URL(refererVal).origin;
|
|
78
56
|
}
|
|
79
57
|
catch { }
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return {
|
|
88
|
-
url: embedUrl,
|
|
89
|
-
type: 'iframe',
|
|
90
|
-
quality: server.quality || 'FHD',
|
|
91
|
-
headers: {
|
|
92
|
-
'Referer': baseUrl,
|
|
93
|
-
'Origin': baseUrl
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
if (streamUrl.includes('/chunks/')) {
|
|
98
|
-
return {
|
|
99
|
-
url: embedUrl,
|
|
100
|
-
type: 'iframe',
|
|
101
|
-
quality: server.quality || 'FHD',
|
|
102
|
-
headers: {
|
|
103
|
-
'Referer': baseUrl,
|
|
104
|
-
'Origin': baseUrl
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
if (streamUrl.includes('.m3u8')) {
|
|
109
|
-
const finalM3u8 = toMediaProxyUrl(streamUrl, embedUrl);
|
|
110
|
-
const playlist = await (0, interceptor_1.fetchM3U8)(streamUrl, { Referer: embedUrl });
|
|
111
|
-
if (playlist && playlist.qualities.length > 0) {
|
|
112
|
-
return {
|
|
113
|
-
url: finalM3u8,
|
|
114
|
-
type: 'hls',
|
|
115
|
-
quality: playlist.qualities[0].quality,
|
|
116
|
-
headers: {
|
|
117
|
-
'Referer': embedUrl,
|
|
118
|
-
'Origin': new URL(embedUrl).origin
|
|
119
|
-
}
|
|
120
|
-
};
|
|
121
|
-
}
|
|
58
|
+
return {
|
|
59
|
+
url: embedUrl,
|
|
60
|
+
type: 'iframe',
|
|
61
|
+
quality: server.quality || 'FHD',
|
|
62
|
+
headers: {
|
|
63
|
+
'Referer': refererVal,
|
|
64
|
+
'Origin': origin
|
|
122
65
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
: streamUrl,
|
|
127
|
-
type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
|
|
128
|
-
quality: server.quality || 'FHD',
|
|
129
|
-
headers: {
|
|
130
|
-
'Referer': embedUrl,
|
|
131
|
-
'Origin': new URL(embedUrl).origin
|
|
132
|
-
}
|
|
133
|
-
};
|
|
134
|
-
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
135
69
|
return {
|
|
136
70
|
url: embedUrl,
|
|
137
71
|
type: 'iframe',
|
|
138
72
|
quality: server.quality || 'HD',
|
|
139
73
|
headers: {
|
|
140
|
-
'Referer':
|
|
141
|
-
'Origin':
|
|
74
|
+
'Referer': defaultBaseUrl,
|
|
75
|
+
'Origin': defaultBaseUrl
|
|
142
76
|
}
|
|
143
77
|
};
|
|
144
78
|
}
|
|
145
|
-
finally {
|
|
146
|
-
await page.close();
|
|
147
|
-
}
|
|
148
79
|
}
|
|
149
80
|
/**
|
|
150
81
|
* Generic Playwright-based extraction
|
|
@@ -246,9 +177,11 @@ async function extractFromAPIServer(hash, server, context) {
|
|
|
246
177
|
const dummyServer = { ...server, embedUrl: link, referer: refererUrl };
|
|
247
178
|
return extractFromAbyssServer(link, dummyServer, context.baseUrl);
|
|
248
179
|
}
|
|
249
|
-
if (link.includes('
|
|
250
|
-
|
|
251
|
-
|
|
180
|
+
if (link.includes('googleapiscdn.com') ||
|
|
181
|
+
link.includes('googleapis.com') ||
|
|
182
|
+
link.includes('storage.')) {
|
|
183
|
+
const dummyServer = { ...server, embedUrl: link, referer: refererUrl };
|
|
184
|
+
return extractFromDUServer(link, dummyServer, context.baseUrl);
|
|
252
185
|
}
|
|
253
186
|
return {
|
|
254
187
|
url: link,
|
|
@@ -305,15 +238,19 @@ async function extractAnimeVietsubStream(server, context) {
|
|
|
305
238
|
if (!embedUrl.startsWith('http')) {
|
|
306
239
|
embedUrl = embedUrl.startsWith('//') ? `https:${embedUrl}` : `https://${embedUrl}`;
|
|
307
240
|
}
|
|
308
|
-
if (embedUrl.includes('
|
|
309
|
-
res = await extractFromDUServer(embedUrl, server, context.baseUrl);
|
|
310
|
-
}
|
|
311
|
-
else if (embedUrl.includes('abyss') ||
|
|
241
|
+
if (embedUrl.includes('abyss') ||
|
|
312
242
|
embedUrl.includes('playhydrax') ||
|
|
313
243
|
server.name.includes('HDX') ||
|
|
314
244
|
server.name.includes('Hydrax')) {
|
|
315
245
|
res = extractFromAbyssServer(embedUrl, server, context.baseUrl);
|
|
316
246
|
}
|
|
247
|
+
else if (embedUrl.includes('googleapiscdn.com') ||
|
|
248
|
+
embedUrl.includes('googleapis.com') ||
|
|
249
|
+
embedUrl.includes('storage.') ||
|
|
250
|
+
server.name.includes('DU') ||
|
|
251
|
+
server.type === 'iframe') {
|
|
252
|
+
res = extractFromDUServer(embedUrl, server, context.baseUrl);
|
|
253
|
+
}
|
|
317
254
|
else if (server.type === 'api') {
|
|
318
255
|
res = await extractFromAPIServer(embedUrl, server, context);
|
|
319
256
|
}
|
|
@@ -34,19 +34,39 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.formatServerName = formatServerName;
|
|
37
|
+
exports.sortVideoServers = sortVideoServers;
|
|
37
38
|
exports.parseServersFromBackupHtml = parseServersFromBackupHtml;
|
|
38
39
|
exports.parseServersFromWatchHtml = parseServersFromWatchHtml;
|
|
39
40
|
const cheerio = __importStar(require("cheerio"));
|
|
40
41
|
function formatServerName(nameOrSource, url) {
|
|
41
42
|
const value = `${nameOrSource || ''} ${url || ''}`.toLowerCase();
|
|
42
43
|
if (value.includes('du') || value.includes('googleapiscdn') || value.includes('storage.')) {
|
|
43
|
-
return 'DU (
|
|
44
|
+
return 'DU (Dự phòng • Tốc độ trung bình)';
|
|
44
45
|
}
|
|
45
46
|
if (value.includes('hdx') || value.includes('hydrax') || value.includes('abyss') || value.includes('playhydrax')) {
|
|
46
|
-
return 'HDX (
|
|
47
|
+
return 'HDX (Khuyên dùng • Tốc độ cao • Không quảng cáo)';
|
|
47
48
|
}
|
|
48
49
|
return nameOrSource?.trim() || 'Server';
|
|
49
50
|
}
|
|
51
|
+
function sortVideoServers(servers) {
|
|
52
|
+
return [...servers].sort((a, b) => {
|
|
53
|
+
const score = (s) => {
|
|
54
|
+
const n = (s.name || '').toUpperCase();
|
|
55
|
+
if (n.includes('HDX') || n.includes('HYDRAX') || n.includes('ABYSS'))
|
|
56
|
+
return 100;
|
|
57
|
+
if (n.includes('VIP'))
|
|
58
|
+
return 80;
|
|
59
|
+
if (n.includes('FB'))
|
|
60
|
+
return 70;
|
|
61
|
+
if (n.includes('GOOGLE'))
|
|
62
|
+
return 60;
|
|
63
|
+
if (n.includes('DU'))
|
|
64
|
+
return 10;
|
|
65
|
+
return 50;
|
|
66
|
+
};
|
|
67
|
+
return score(b) - score(a);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
50
70
|
/**
|
|
51
71
|
* Parse servers returned by the AJAX POST backup endpoint
|
|
52
72
|
*/
|
|
@@ -87,7 +107,7 @@ function parseServersFromBackupHtml(html, episodeId, baseUrl) {
|
|
|
87
107
|
});
|
|
88
108
|
}
|
|
89
109
|
});
|
|
90
|
-
return servers;
|
|
110
|
+
return sortVideoServers(servers);
|
|
91
111
|
}
|
|
92
112
|
/**
|
|
93
113
|
* Parse video servers from the watch page HTML
|
|
@@ -218,5 +238,5 @@ function parseServersFromWatchHtml(html) {
|
|
|
218
238
|
});
|
|
219
239
|
}
|
|
220
240
|
}
|
|
221
|
-
return servers;
|
|
241
|
+
return sortVideoServers(servers);
|
|
222
242
|
}
|
|
@@ -490,10 +490,23 @@ class AnimeVietsubProvider extends base_1.BaseScraper {
|
|
|
490
490
|
return [];
|
|
491
491
|
}
|
|
492
492
|
}
|
|
493
|
-
async getHomeCards(section = 'trending') {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
493
|
+
async getHomeCards(section = 'trending', page = 1) {
|
|
494
|
+
let listUrl;
|
|
495
|
+
if (section === 'upcoming') {
|
|
496
|
+
listUrl = page > 1
|
|
497
|
+
? `${this.baseUrl}/danh-sach/phim-sap-chieu/trang-${page}.html`
|
|
498
|
+
: `${this.baseUrl}/danh-sach/phim-sap-chieu/`;
|
|
499
|
+
}
|
|
500
|
+
else if (section === 'latest') {
|
|
501
|
+
listUrl = page > 1
|
|
502
|
+
? `${this.baseUrl}/danh-sach/phim-moi-cap-nhat/trang-${page}.html`
|
|
503
|
+
: `${this.baseUrl}/danh-sach/phim-moi-cap-nhat/`;
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
listUrl = page > 1
|
|
507
|
+
? `${this.baseUrl}/danh-sach/phim-hot/trang-${page}.html`
|
|
508
|
+
: `${this.baseUrl}/`;
|
|
509
|
+
}
|
|
497
510
|
try {
|
|
498
511
|
// useBrowserFallback: true → 403 after retries auto-escalates to Playwright
|
|
499
512
|
const html = await this.fetchHtml(listUrl, { timeoutMs: 25000, retries: 2, useBrowserFallback: true });
|
|
@@ -501,26 +514,124 @@ class AnimeVietsubProvider extends base_1.BaseScraper {
|
|
|
501
514
|
if (results.length > 0)
|
|
502
515
|
return results.slice(0, 48);
|
|
503
516
|
// Only fallback to search when HTML parsed OK but yielded 0 cards (not a 403/down scenario)
|
|
504
|
-
|
|
505
|
-
|
|
517
|
+
if (page === 1) {
|
|
518
|
+
(0, logger_1.debugWarn)('[Scraper] getHomeCards: HTML loaded but 0 cards parsed, trying search fallback');
|
|
519
|
+
return this.searchInternal('anime', true);
|
|
520
|
+
}
|
|
521
|
+
return [];
|
|
506
522
|
}
|
|
507
523
|
catch (error) {
|
|
508
524
|
// Site is temporarily down — propagate as a typed error so UI can show proper message
|
|
509
525
|
if (error instanceof fingerprint_1.SiteDownError) {
|
|
510
|
-
(0, logger_1.debugWarn)(`[Scraper] getHomeCards(${section}): ${error.message}`);
|
|
526
|
+
(0, logger_1.debugWarn)(`[Scraper] getHomeCards(${section}, page=${page}): ${error.message}`);
|
|
511
527
|
throw error;
|
|
512
528
|
}
|
|
513
|
-
console.error(`[Scraper] getHomeCards(${section}) error:`, error);
|
|
514
|
-
if (section === 'latest') {
|
|
529
|
+
console.error(`[Scraper] getHomeCards(${section}, page=${page}) error:`, error);
|
|
530
|
+
if (section === 'latest' && page === 1) {
|
|
515
531
|
// Fallback to trending homepage when latest page is unstable (non-down errors only)
|
|
516
|
-
return this.getHomeCards('trending');
|
|
532
|
+
return this.getHomeCards('trending', 1);
|
|
517
533
|
}
|
|
518
|
-
// Do NOT fall back to search() — it would also fail and make the error worse.
|
|
519
|
-
// Return empty array so HomeView can show a graceful empty/retry state.
|
|
520
|
-
console.error('[Scraper] getHomeCards completely failed — returning empty array for graceful UI');
|
|
521
534
|
return [];
|
|
522
535
|
}
|
|
523
536
|
}
|
|
537
|
+
static GENRE_SLUGS = {
|
|
538
|
+
'hanh-dong': 'hanh-dong',
|
|
539
|
+
'action': 'hanh-dong',
|
|
540
|
+
'hành động': 'hanh-dong',
|
|
541
|
+
'phieu-luu': 'phieu-luu',
|
|
542
|
+
'adventure': 'phieu-luu',
|
|
543
|
+
'phiêu lưu': 'phieu-luu',
|
|
544
|
+
'hai-huoc': 'hai-huoc',
|
|
545
|
+
'comedy': 'hai-huoc',
|
|
546
|
+
'hài hước': 'hai-huoc',
|
|
547
|
+
'phep-thuat': 'phep-thuat',
|
|
548
|
+
'fantasy': 'phep-thuat',
|
|
549
|
+
'huyền ảo': 'phep-thuat',
|
|
550
|
+
'phép thuật': 'phep-thuat',
|
|
551
|
+
'tinh-cam': 'tinh-cam',
|
|
552
|
+
'romance': 'tinh-cam',
|
|
553
|
+
'tình cảm': 'tinh-cam',
|
|
554
|
+
'truong-hoc': 'truong-hoc',
|
|
555
|
+
'school': 'truong-hoc',
|
|
556
|
+
'học đường': 'truong-hoc',
|
|
557
|
+
'doi-thuong': 'doi-thuong',
|
|
558
|
+
'slice of life': 'doi-thuong',
|
|
559
|
+
'đời thường': 'doi-thuong',
|
|
560
|
+
'isekai': 'isekai',
|
|
561
|
+
'sci-fi': 'sci-fi',
|
|
562
|
+
'kinh-di': 'kinh-di',
|
|
563
|
+
'horror': 'kinh-di',
|
|
564
|
+
'kinh dị': 'kinh-di',
|
|
565
|
+
'mystery': 'mystery',
|
|
566
|
+
'trinh thám': 'mystery',
|
|
567
|
+
'trinh-tham': 'mystery',
|
|
568
|
+
'sieu-nhien': 'sieu-nhien',
|
|
569
|
+
'supernatural': 'sieu-nhien',
|
|
570
|
+
'siêu nhiên': 'sieu-nhien',
|
|
571
|
+
'the-thao': 'the-thao',
|
|
572
|
+
'sports': 'the-thao',
|
|
573
|
+
'thể thao': 'the-thao',
|
|
574
|
+
'mecha': 'mecha',
|
|
575
|
+
'am-nhac': 'am-nhac',
|
|
576
|
+
'music': 'am-nhac',
|
|
577
|
+
'âm nhạc': 'am-nhac',
|
|
578
|
+
'co-trang': 'co-trang',
|
|
579
|
+
'cổ trang': 'co-trang',
|
|
580
|
+
'tro-choi': 'tro-choi',
|
|
581
|
+
'game': 'tro-choi',
|
|
582
|
+
'trò chơi': 'tro-choi',
|
|
583
|
+
'harem': 'harem',
|
|
584
|
+
'psychological': 'psychological',
|
|
585
|
+
'tâm lý': 'psychological',
|
|
586
|
+
'tam-ly': 'psychological',
|
|
587
|
+
'hoi-hop': 'hoi-hop',
|
|
588
|
+
'suspense': 'hoi-hop',
|
|
589
|
+
'hồi hộp': 'hoi-hop',
|
|
590
|
+
'thriller': 'thriller',
|
|
591
|
+
'martial-arts': 'martial-arts',
|
|
592
|
+
'võ thuật': 'martial-arts',
|
|
593
|
+
'vo-thuat': 'martial-arts',
|
|
594
|
+
'lich-su': 'lich-su',
|
|
595
|
+
'historical': 'lich-su',
|
|
596
|
+
'lịch sử': 'lich-su',
|
|
597
|
+
'quan-doi': 'quan-doi',
|
|
598
|
+
'military': 'quan-doi',
|
|
599
|
+
'quân đội': 'quan-doi',
|
|
600
|
+
'shounen': 'shounen',
|
|
601
|
+
'seinen': 'seinen',
|
|
602
|
+
'shoujo': 'shoujo',
|
|
603
|
+
'vampire': 'vampire',
|
|
604
|
+
'ma cà rồng': 'vampire',
|
|
605
|
+
'parody': 'parody',
|
|
606
|
+
'tokusatsu': 'tokusatsu',
|
|
607
|
+
'drama': 'drama',
|
|
608
|
+
'ecchi': 'ecchi',
|
|
609
|
+
'demons': 'demons',
|
|
610
|
+
'magic': 'ma-thuat',
|
|
611
|
+
'police': 'police',
|
|
612
|
+
'space': 'space',
|
|
613
|
+
'super-power': 'super-power',
|
|
614
|
+
};
|
|
615
|
+
async getAnimeByGenre(genre, page = 1) {
|
|
616
|
+
const rawKey = genre.trim().toLowerCase();
|
|
617
|
+
const slug = AnimeVietsubProvider.GENRE_SLUGS[rawKey] || rawKey.replace(/\s+/g, '-');
|
|
618
|
+
const listUrl = page > 1
|
|
619
|
+
? `${this.baseUrl}/the-loai/${slug}/trang-${page}.html`
|
|
620
|
+
: `${this.baseUrl}/the-loai/${slug}/`;
|
|
621
|
+
try {
|
|
622
|
+
const html = await this.fetchHtml(listUrl, { timeoutMs: 25000, retries: 2, useBrowserFallback: true });
|
|
623
|
+
const results = (0, cards_1.parseHomeCards)(html, this.baseUrl);
|
|
624
|
+
if (results.length > 0)
|
|
625
|
+
return results.slice(0, 48);
|
|
626
|
+
return this.searchInternal(genre, true);
|
|
627
|
+
}
|
|
628
|
+
catch (error) {
|
|
629
|
+
if (error instanceof fingerprint_1.SiteDownError)
|
|
630
|
+
throw error;
|
|
631
|
+
(0, logger_1.debugWarn)(`[Scraper] getAnimeByGenre(${genre}, page=${page}) error: ${error}, falling back to search`);
|
|
632
|
+
return this.searchInternal(genre, true);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
524
635
|
parseSearchCards(html) {
|
|
525
636
|
if (html.length < 100)
|
|
526
637
|
return [];
|