nekostream 2026.2.3 → 2026.2.6
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/index.js +15 -6
- package/dist/player.js +63 -35
- package/dist/prompts-wrapper.js +124 -12
- package/dist/scrapers/providers/animevietsub.js +40 -39
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -215,9 +215,10 @@ async function showAnimeList(providerName, title, list) {
|
|
|
215
215
|
(0, ui_1.printBanner)(`Provider: ${providerName.toUpperCase()}`, title.toUpperCase());
|
|
216
216
|
flushStdin();
|
|
217
217
|
const { animeId } = await (0, prompts_wrapper_1.default)({
|
|
218
|
-
type: '
|
|
218
|
+
type: 'search',
|
|
219
219
|
name: 'animeId',
|
|
220
220
|
message: 'Chọn anime (Esc: quay lại)',
|
|
221
|
+
pageSize: 15,
|
|
221
222
|
choices: list.map((anime, idx) => {
|
|
222
223
|
const desc = anime.status || anime.year?.toString() || '';
|
|
223
224
|
const cleanTitle = anime.title.replace(/\r?\n|\r/g, ' ').trim();
|
|
@@ -327,10 +328,17 @@ async function openAnimeMenu(providerName, animeId) {
|
|
|
327
328
|
type: 'select',
|
|
328
329
|
name: 'server',
|
|
329
330
|
message: 'Chọn server phát (Esc: quay lại)',
|
|
330
|
-
choices: servers.map((s, idx) =>
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
331
|
+
choices: servers.map((s, idx) => {
|
|
332
|
+
const number = chalk_1.default.bold(`${idx + 1}.`);
|
|
333
|
+
const quality = chalk_1.default.gray(`[${s.quality || 'Auto'}]`);
|
|
334
|
+
const detail = providerName === 'animevietsub'
|
|
335
|
+
? quality
|
|
336
|
+
: chalk_1.default.gray(`[${s.quality || 'Auto'}] (${s.type || 'stream'})`);
|
|
337
|
+
return {
|
|
338
|
+
title: `${number} ${s.name} ${detail}`,
|
|
339
|
+
value: s
|
|
340
|
+
};
|
|
341
|
+
})
|
|
334
342
|
});
|
|
335
343
|
if (!server)
|
|
336
344
|
break;
|
|
@@ -403,9 +411,10 @@ async function showUserDataList(title, items, providerName) {
|
|
|
403
411
|
choices.push({ title: chalk_1.default.yellow('▲ Trang trước'), value: '__prev__' });
|
|
404
412
|
choices.push({ title: chalk_1.default.gray('← Quay lại'), value: '__back__' });
|
|
405
413
|
const { animeId } = await (0, prompts_wrapper_1.default)({
|
|
406
|
-
type: '
|
|
414
|
+
type: 'search',
|
|
407
415
|
name: 'animeId',
|
|
408
416
|
message: 'Chọn anime để xem (Esc: quay lại)',
|
|
417
|
+
pageSize: PAGE_SIZE,
|
|
409
418
|
choices
|
|
410
419
|
});
|
|
411
420
|
if (!animeId || animeId === '__back__')
|
package/dist/player.js
CHANGED
|
@@ -7,6 +7,7 @@ exports.launchPlayer = launchPlayer;
|
|
|
7
7
|
const child_process_1 = require("child_process");
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const os_1 = __importDefault(require("os"));
|
|
10
11
|
function runWithTimeout(fn, ms) {
|
|
11
12
|
return new Promise((resolve, reject) => {
|
|
12
13
|
const timer = setTimeout(() => {
|
|
@@ -21,15 +22,50 @@ function runWithTimeout(fn, ms) {
|
|
|
21
22
|
});
|
|
22
23
|
});
|
|
23
24
|
}
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
function electronExecutablePath(rootDir) {
|
|
26
|
+
if (process.platform === 'win32')
|
|
27
|
+
return path_1.default.join(rootDir, 'electron.exe');
|
|
28
|
+
if (process.platform === 'darwin')
|
|
29
|
+
return path_1.default.join(rootDir, 'Electron.app', 'Contents', 'MacOS', 'Electron');
|
|
30
|
+
return path_1.default.join(rootDir, 'electron');
|
|
31
|
+
}
|
|
32
|
+
function resolveElectronPackageDir(basePath) {
|
|
33
|
+
try {
|
|
34
|
+
return path_1.default.dirname(require.resolve('electron/package.json', { paths: [basePath, __dirname] }));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return path_1.default.join(basePath, 'node_modules', 'electron');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function resolveInstalledElectron(basePath) {
|
|
41
|
+
const electronBinary = require('electron');
|
|
42
|
+
if (typeof electronBinary === 'string' && fs_1.default.existsSync(electronBinary)) {
|
|
43
|
+
return electronBinary;
|
|
44
|
+
}
|
|
45
|
+
const electronPkgDir = resolveElectronPackageDir(basePath);
|
|
46
|
+
const directBinary = electronExecutablePath(path_1.default.join(electronPkgDir, 'dist'));
|
|
47
|
+
if (fs_1.default.existsSync(directBinary)) {
|
|
48
|
+
return directBinary;
|
|
49
|
+
}
|
|
50
|
+
throw new Error('Electron binary executable file not found');
|
|
51
|
+
}
|
|
52
|
+
function getElectronVersion(basePath) {
|
|
53
|
+
const electronPkgPath = path_1.default.join(resolveElectronPackageDir(basePath), 'package.json');
|
|
26
54
|
if (!fs_1.default.existsSync(electronPkgPath)) {
|
|
27
55
|
throw new Error('electron package.json not found');
|
|
28
56
|
}
|
|
29
57
|
const pkg = JSON.parse(fs_1.default.readFileSync(electronPkgPath, 'utf8'));
|
|
30
|
-
|
|
58
|
+
return pkg.version;
|
|
59
|
+
}
|
|
60
|
+
async function resolveCachedElectron(basePath) {
|
|
61
|
+
const version = getElectronVersion(basePath);
|
|
31
62
|
const platform = process.platform;
|
|
32
63
|
const arch = process.arch;
|
|
64
|
+
const cacheDir = path_1.default.join(os_1.default.homedir(), '.nekostream-cli', 'electron', version, `${platform}-${arch}`);
|
|
65
|
+
const cachedBinary = electronExecutablePath(cacheDir);
|
|
66
|
+
if (fs_1.default.existsSync(cachedBinary)) {
|
|
67
|
+
return cachedBinary;
|
|
68
|
+
}
|
|
33
69
|
console.log(`[download] Downloading Electron v${version} for ${platform}-${arch}...`);
|
|
34
70
|
const { downloadArtifact } = require('@electron/get');
|
|
35
71
|
const zipPath = await downloadArtifact({
|
|
@@ -38,49 +74,44 @@ async function repairElectron(basePath) {
|
|
|
38
74
|
platform,
|
|
39
75
|
arch
|
|
40
76
|
});
|
|
41
|
-
console.log('[extract] Extracting Electron zip to
|
|
42
|
-
|
|
43
|
-
|
|
77
|
+
console.log('[extract] Extracting Electron zip to local cache...');
|
|
78
|
+
fs_1.default.mkdirSync(path_1.default.dirname(cacheDir), { recursive: true });
|
|
79
|
+
const tempDir = `${cacheDir}.tmp-${process.pid}`;
|
|
80
|
+
fs_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
81
|
+
fs_1.default.mkdirSync(tempDir, { recursive: true });
|
|
44
82
|
try {
|
|
45
83
|
// Try extract-zip with a 10s timeout to avoid deadlocking on Node v24+
|
|
46
84
|
await runWithTimeout(async () => {
|
|
47
85
|
const extract = require('extract-zip');
|
|
48
|
-
await extract(zipPath, { dir:
|
|
86
|
+
await extract(zipPath, { dir: tempDir });
|
|
49
87
|
}, 10000);
|
|
50
88
|
console.log('[ok] Extraction via extract-zip complete.');
|
|
51
89
|
}
|
|
52
90
|
catch (extractErr) {
|
|
53
91
|
console.log('[warn] extract-zip timed out or failed. Falling back to native system extraction...');
|
|
54
92
|
if (process.platform === 'win32') {
|
|
55
|
-
(0, child_process_1.execSync)(`powershell -Command "Expand-Archive -Path '${zipPath}' -DestinationPath '${
|
|
93
|
+
(0, child_process_1.execSync)(`powershell -Command "Expand-Archive -Path '${zipPath}' -DestinationPath '${tempDir}' -Force"`, { stdio: 'inherit' });
|
|
56
94
|
}
|
|
57
95
|
else {
|
|
58
|
-
(0, child_process_1.execSync)(`unzip -o "${zipPath}" -d "${
|
|
96
|
+
(0, child_process_1.execSync)(`unzip -o "${zipPath}" -d "${tempDir}"`, { stdio: 'inherit' });
|
|
59
97
|
}
|
|
60
98
|
console.log('[ok] Extraction via native fallback complete.');
|
|
61
99
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
100
|
+
if (!fs_1.default.existsSync(electronExecutablePath(tempDir))) {
|
|
101
|
+
fs_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
102
|
+
throw new Error('Electron binary missing after cache extraction');
|
|
103
|
+
}
|
|
104
|
+
fs_1.default.rmSync(cacheDir, { recursive: true, force: true });
|
|
105
|
+
fs_1.default.renameSync(tempDir, cacheDir);
|
|
106
|
+
if (process.platform !== 'win32') {
|
|
66
107
|
try {
|
|
67
|
-
fs_1.default.
|
|
108
|
+
fs_1.default.chmodSync(cachedBinary, 0o755);
|
|
68
109
|
}
|
|
69
|
-
catch
|
|
70
|
-
// Ignore
|
|
110
|
+
catch {
|
|
111
|
+
// Ignore chmod failures on filesystems that do not support it.
|
|
71
112
|
}
|
|
72
113
|
}
|
|
73
|
-
|
|
74
|
-
const platformPath = process.platform === 'win32' ? 'electron.exe' : (process.platform === 'darwin' ? 'Electron.app/Contents/MacOS/Electron' : 'electron');
|
|
75
|
-
const pathTxtPath = path_1.default.join(basePath, 'node_modules', 'electron', 'path.txt');
|
|
76
|
-
fs_1.default.writeFileSync(pathTxtPath, platformPath);
|
|
77
|
-
// Clear require cache for electron module to force reload
|
|
78
|
-
delete require.cache[require.resolve('electron')];
|
|
79
|
-
const electronBinary = require('electron');
|
|
80
|
-
if (!fs_1.default.existsSync(electronBinary)) {
|
|
81
|
-
throw new Error('Electron binary still missing after fallback repair');
|
|
82
|
-
}
|
|
83
|
-
return electronBinary;
|
|
114
|
+
return cachedBinary;
|
|
84
115
|
}
|
|
85
116
|
async function launchPlayer(streamInfo) {
|
|
86
117
|
// If compiled, __dirname is .../dist. If dev, __dirname is root.
|
|
@@ -93,19 +124,16 @@ async function launchPlayer(streamInfo) {
|
|
|
93
124
|
};
|
|
94
125
|
let electronBinary;
|
|
95
126
|
try {
|
|
96
|
-
electronBinary =
|
|
97
|
-
if (!fs_1.default.existsSync(electronBinary)) {
|
|
98
|
-
throw new Error('Electron binary executable file not found');
|
|
99
|
-
}
|
|
127
|
+
electronBinary = resolveInstalledElectron(basePath);
|
|
100
128
|
}
|
|
101
129
|
catch (e) {
|
|
102
|
-
console.log('[warn] Electron is
|
|
130
|
+
console.log('[warn] Electron binary is missing. Resolving cached player runtime...');
|
|
103
131
|
try {
|
|
104
|
-
electronBinary = await
|
|
105
|
-
console.log('[ok] Electron
|
|
132
|
+
electronBinary = await resolveCachedElectron(basePath);
|
|
133
|
+
console.log('[ok] Electron runtime ready.');
|
|
106
134
|
}
|
|
107
135
|
catch (repairErr) {
|
|
108
|
-
throw new Error(`Failed to resolve
|
|
136
|
+
throw new Error(`Failed to resolve Electron runtime automatically. Please run "npm install electron" or reinstall NekoStream.\nDetails: ${repairErr.message}`);
|
|
109
137
|
}
|
|
110
138
|
}
|
|
111
139
|
return new Promise((resolve, reject) => {
|
package/dist/prompts-wrapper.js
CHANGED
|
@@ -65,19 +65,49 @@ function getGridChoices(choices) {
|
|
|
65
65
|
value: choice.value
|
|
66
66
|
}));
|
|
67
67
|
}
|
|
68
|
+
function normalized(value) {
|
|
69
|
+
return value.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
|
70
|
+
}
|
|
68
71
|
function gridLayout(choices) {
|
|
69
72
|
const width = (0, text_1.terminalWidth)(96, 58);
|
|
70
73
|
const longest = choices.reduce((max, choice) => Math.max(max, (0, text_1.visibleLength)(choice.name)), 0);
|
|
71
74
|
const columnWidth = Math.max(14, Math.min(28, longest + 2));
|
|
72
75
|
const columns = Math.max(1, Math.min(6, Math.floor(width / (columnWidth + 3)), choices.length));
|
|
73
|
-
const
|
|
74
|
-
|
|
76
|
+
const maxRows = Math.max(6, Math.min(12, (process.stdout.rows || 30) - 12));
|
|
77
|
+
const rows = Math.max(1, Math.min(maxRows, Math.ceil(choices.length / columns)));
|
|
78
|
+
const pageSize = columns * rows;
|
|
79
|
+
return { columnWidth, columns, rows, pageSize };
|
|
80
|
+
}
|
|
81
|
+
function filterGridChoices(choices, query) {
|
|
82
|
+
const term = normalized(query.trim());
|
|
83
|
+
if (!term)
|
|
84
|
+
return choices;
|
|
85
|
+
return choices.filter((choice, index) => {
|
|
86
|
+
const haystack = normalized(`${index + 1} ${choice.name} ${String(choice.value?.number ?? '')}`);
|
|
87
|
+
return haystack.includes(term);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function findNumericChoice(choices, query) {
|
|
91
|
+
const raw = query.trim();
|
|
92
|
+
if (!/^\d+$/.test(raw))
|
|
93
|
+
return undefined;
|
|
94
|
+
const target = Number(raw);
|
|
95
|
+
return choices.find((choice, index) => {
|
|
96
|
+
const valueNumber = Number(choice.value?.number);
|
|
97
|
+
if (Number.isFinite(valueNumber) && valueNumber === target)
|
|
98
|
+
return true;
|
|
99
|
+
if (index + 1 === target)
|
|
100
|
+
return true;
|
|
101
|
+
return new RegExp(`\\b0*${target}\\b`).test(choice.name);
|
|
102
|
+
});
|
|
75
103
|
}
|
|
76
104
|
async function selectGrid(options, signal) {
|
|
77
105
|
const choices = getGridChoices(options.choices || []);
|
|
78
106
|
if (choices.length === 0)
|
|
79
107
|
return undefined;
|
|
80
108
|
let selected = 0;
|
|
109
|
+
let page = 0;
|
|
110
|
+
let query = '';
|
|
81
111
|
let renderedLines = 0;
|
|
82
112
|
const message = normalizeMessage(options.message || 'Chọn');
|
|
83
113
|
const render = () => {
|
|
@@ -86,25 +116,34 @@ async function selectGrid(options, signal) {
|
|
|
86
116
|
readline_1.default.cursorTo(process.stdout, 0);
|
|
87
117
|
readline_1.default.clearScreenDown(process.stdout);
|
|
88
118
|
}
|
|
89
|
-
const
|
|
119
|
+
const filtered = filterGridChoices(choices, query);
|
|
120
|
+
const { columnWidth, columns, rows, pageSize } = gridLayout(filtered);
|
|
121
|
+
const pageCount = Math.max(1, Math.ceil(filtered.length / pageSize));
|
|
122
|
+
page = Math.max(0, Math.min(page, pageCount - 1));
|
|
123
|
+
selected = Math.max(0, Math.min(selected, Math.max(0, filtered.length - 1)));
|
|
124
|
+
const pageStart = page * pageSize;
|
|
125
|
+
const pageChoices = filtered.slice(pageStart, pageStart + pageSize);
|
|
126
|
+
const selectedOnPage = selected - pageStart;
|
|
90
127
|
const lines = [
|
|
91
128
|
`${theme_1.uiText.focus('?')} ${promptMessage(message)}`,
|
|
129
|
+
`${theme_1.uiText.label('Search')} ${theme_1.uiText.value(query || '')}${theme_1.uiText.subtle(query ? '' : 'Gõ số tập/tên để lọc')}`,
|
|
92
130
|
formatSeparator('DANH SÁCH TẬP', false)
|
|
93
131
|
];
|
|
94
132
|
for (let row = 0; row < rows; row++) {
|
|
95
133
|
const cells = [];
|
|
96
134
|
for (let col = 0; col < columns; col++) {
|
|
97
135
|
const index = col * rows + row;
|
|
98
|
-
if (index >=
|
|
136
|
+
if (index >= pageChoices.length)
|
|
99
137
|
continue;
|
|
100
|
-
const prefix = index ===
|
|
101
|
-
const label = (0, text_1.truncate)(
|
|
102
|
-
const cell = `${prefix} ${index ===
|
|
138
|
+
const prefix = index === selectedOnPage ? theme_1.uiText.focus(theme_1.glyphs.pointer) : ' ';
|
|
139
|
+
const label = (0, text_1.truncate)(pageChoices[index].name, columnWidth - 2);
|
|
140
|
+
const cell = `${prefix} ${index === selectedOnPage ? theme_1.uiText.focus(label) : theme_1.uiText.value(label)}`;
|
|
103
141
|
cells.push((0, text_1.padRight)(cell, columnWidth));
|
|
104
142
|
}
|
|
105
143
|
lines.push(cells.join(theme_1.uiText.border(` ${theme_1.glyphs.vertical} `)));
|
|
106
144
|
}
|
|
107
|
-
|
|
145
|
+
const pageLabel = filtered.length > pageSize ? ` · Trang ${page + 1}/${pageCount}` : '';
|
|
146
|
+
lines.push(chalk_1.default.gray(`↑↓←→ di chuyển · PgUp/PgDn trang · Enter chọn · Esc quay lại${pageLabel}`));
|
|
108
147
|
process.stdout.write(`${lines.join('\n')}\n`);
|
|
109
148
|
renderedLines = lines.length;
|
|
110
149
|
};
|
|
@@ -128,23 +167,62 @@ async function selectGrid(options, signal) {
|
|
|
128
167
|
reject(new Error('ESC_PRESSED'));
|
|
129
168
|
};
|
|
130
169
|
const onKeypress = (_str, key) => {
|
|
131
|
-
const
|
|
170
|
+
const filtered = filterGridChoices(choices, query);
|
|
171
|
+
const { rows, pageSize } = gridLayout(filtered);
|
|
172
|
+
const pageCount = Math.max(1, Math.ceil(filtered.length / pageSize));
|
|
132
173
|
if (key?.name === 'escape') {
|
|
133
174
|
done(undefined);
|
|
134
175
|
return;
|
|
135
176
|
}
|
|
177
|
+
if (key?.name === 'backspace') {
|
|
178
|
+
query = query.slice(0, -1);
|
|
179
|
+
selected = 0;
|
|
180
|
+
page = 0;
|
|
181
|
+
render();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
136
184
|
if (key?.name === 'return' || key?.name === 'enter') {
|
|
137
|
-
|
|
185
|
+
const numericChoice = findNumericChoice(choices, query);
|
|
186
|
+
if (numericChoice) {
|
|
187
|
+
done(numericChoice.value);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (filtered[selected]) {
|
|
191
|
+
done(filtered[selected].value);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
render();
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (_str && _str >= ' ' && !key?.ctrl && !key?.meta && key?.name !== 'return') {
|
|
199
|
+
query += _str;
|
|
200
|
+
selected = 0;
|
|
201
|
+
page = 0;
|
|
202
|
+
render();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (key?.name === 'pageup') {
|
|
206
|
+
page = Math.max(0, page - 1);
|
|
207
|
+
selected = page * pageSize;
|
|
208
|
+
render();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (key?.name === 'pagedown') {
|
|
212
|
+
page = Math.min(pageCount - 1, page + 1);
|
|
213
|
+
selected = page * pageSize;
|
|
214
|
+
render();
|
|
138
215
|
return;
|
|
139
216
|
}
|
|
140
217
|
if (key?.name === 'up')
|
|
141
218
|
selected = Math.max(0, selected - 1);
|
|
142
219
|
if (key?.name === 'down')
|
|
143
|
-
selected = Math.min(
|
|
220
|
+
selected = Math.min(filtered.length - 1, selected + 1);
|
|
144
221
|
if (key?.name === 'left')
|
|
145
222
|
selected = Math.max(0, selected - rows);
|
|
146
223
|
if (key?.name === 'right')
|
|
147
|
-
selected = Math.min(
|
|
224
|
+
selected = Math.min(filtered.length - 1, selected + rows);
|
|
225
|
+
page = Math.floor(selected / pageSize);
|
|
148
226
|
render();
|
|
149
227
|
};
|
|
150
228
|
signal.addEventListener('abort', onAbort);
|
|
@@ -198,6 +276,40 @@ async function prompts(options) {
|
|
|
198
276
|
return {};
|
|
199
277
|
return { [options.name]: result };
|
|
200
278
|
}
|
|
279
|
+
if (options.type === 'search') {
|
|
280
|
+
const message = normalizeMessage(options.message || 'Tìm');
|
|
281
|
+
const includeBack = shouldOfferBack(options.message || '');
|
|
282
|
+
const baseChoices = mapChoices(options.choices || [], includeBack).filter((choice) => !(choice instanceof prompts_1.Separator));
|
|
283
|
+
const result = await (0, prompts_1.search)({
|
|
284
|
+
message: promptMessage(message),
|
|
285
|
+
pageSize: options.pageSize || 15,
|
|
286
|
+
source: async (term) => {
|
|
287
|
+
const query = normalized(term || '');
|
|
288
|
+
if (!query)
|
|
289
|
+
return baseChoices.slice(0, options.pageSize || 15);
|
|
290
|
+
return baseChoices.filter((choice, index) => {
|
|
291
|
+
const haystack = normalized(`${index + 1} ${choice.name || ''} ${choice.description || ''}`);
|
|
292
|
+
return haystack.includes(query);
|
|
293
|
+
}).slice(0, options.searchLimit || 50);
|
|
294
|
+
},
|
|
295
|
+
theme: {
|
|
296
|
+
helpMode: 'always',
|
|
297
|
+
prefix: {
|
|
298
|
+
idle: theme_1.uiText.focus('?'),
|
|
299
|
+
done: theme_1.uiText.success('>')
|
|
300
|
+
},
|
|
301
|
+
icon: {
|
|
302
|
+
cursor: theme_1.uiText.focus(theme_1.glyphs.pointer)
|
|
303
|
+
},
|
|
304
|
+
style: {
|
|
305
|
+
keysHelpTip: () => chalk_1.default.gray('Gõ tên/số · Enter chọn · Esc quay lại')
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}, { signal: ac.signal, clearPromptOnDone: true });
|
|
309
|
+
if (result === '__GOBACK__')
|
|
310
|
+
return {};
|
|
311
|
+
return { [options.name]: result };
|
|
312
|
+
}
|
|
201
313
|
if (options.type === 'text') {
|
|
202
314
|
const result = await (0, prompts_1.input)({
|
|
203
315
|
message: promptMessage(normalizeMessage(options.message || 'Nhập')),
|
|
@@ -64,6 +64,16 @@ class AnimeVietsubProvider extends base_1.BaseScraper {
|
|
|
64
64
|
secChUaPlatform: '"Windows"',
|
|
65
65
|
acceptLanguage: 'vi-VN,vi;q=0.9,en-US;q=0.8,en;q=0.7',
|
|
66
66
|
};
|
|
67
|
+
formatServerName(nameOrSource, url) {
|
|
68
|
+
const value = `${nameOrSource || ''} ${url || ''}`.toLowerCase();
|
|
69
|
+
if (value.includes('du') || value.includes('googleapiscdn') || value.includes('storage.')) {
|
|
70
|
+
return 'DU (Normal Speed Fetch/Sometime Unstable)';
|
|
71
|
+
}
|
|
72
|
+
if (value.includes('hdx') || value.includes('hydrax') || value.includes('abyss') || value.includes('playhydrax')) {
|
|
73
|
+
return 'HDX (Fastest Speed Fetch/Very Stable)';
|
|
74
|
+
}
|
|
75
|
+
return nameOrSource?.trim() || 'Server';
|
|
76
|
+
}
|
|
67
77
|
normalizeBaseUrl(value) {
|
|
68
78
|
const url = value.startsWith('http') ? value : `https://${value}`;
|
|
69
79
|
return url.replace(/\/$/, '');
|
|
@@ -1233,13 +1243,7 @@ class AnimeVietsubProvider extends base_1.BaseScraper {
|
|
|
1233
1243
|
const dataPlay = $el.attr('data-play') || 'iframe';
|
|
1234
1244
|
const dataHref = $el.attr('data-href') || '';
|
|
1235
1245
|
const dataId = $el.attr('data-id') || '';
|
|
1236
|
-
|
|
1237
|
-
if (name.toLowerCase().includes('du'))
|
|
1238
|
-
serverName = 'DU (Recommended)';
|
|
1239
|
-
else if (name.toLowerCase().includes('hdx'))
|
|
1240
|
-
serverName = 'HDX (Abyss)';
|
|
1241
|
-
else if (name.toLowerCase().includes('hydrax'))
|
|
1242
|
-
serverName = 'Hydrax (Abyss)';
|
|
1246
|
+
const serverName = this.formatServerName(name, dataHref);
|
|
1243
1247
|
if (dataHref) {
|
|
1244
1248
|
// Construct composite query parameters
|
|
1245
1249
|
const params = new URLSearchParams({
|
|
@@ -1280,7 +1284,7 @@ class AnimeVietsubProvider extends base_1.BaseScraper {
|
|
|
1280
1284
|
if (data?.link) {
|
|
1281
1285
|
const link = String(data.link);
|
|
1282
1286
|
servers.push({
|
|
1283
|
-
name:
|
|
1287
|
+
name: this.formatServerName(undefined, link),
|
|
1284
1288
|
embedUrl: link,
|
|
1285
1289
|
quality: 'FHD',
|
|
1286
1290
|
type: data.playTech || 'iframe',
|
|
@@ -1319,7 +1323,7 @@ class AnimeVietsubProvider extends base_1.BaseScraper {
|
|
|
1319
1323
|
playerData.link.includes('storage.');
|
|
1320
1324
|
const quality = 'FHD';
|
|
1321
1325
|
servers.push({
|
|
1322
|
-
name: isDUServer ? '
|
|
1326
|
+
name: this.formatServerName(isDUServer ? 'du' : undefined, playerData.link),
|
|
1323
1327
|
embedUrl: playerData.link,
|
|
1324
1328
|
quality,
|
|
1325
1329
|
type: playerData.playTech, // 'iframe', 'api', 'embed'
|
|
@@ -1345,23 +1349,21 @@ class AnimeVietsubProvider extends base_1.BaseScraper {
|
|
|
1345
1349
|
if (!isActive || !dataHash)
|
|
1346
1350
|
return;
|
|
1347
1351
|
// Create server entry based on data-source
|
|
1348
|
-
let serverName =
|
|
1349
|
-
if (
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
else if (dataSource)
|
|
1358
|
-
serverName = dataSource.toUpperCase();
|
|
1352
|
+
let serverName = this.formatServerName(dataSource);
|
|
1353
|
+
if (serverName === 'Server') {
|
|
1354
|
+
if (dataSource === 'fb')
|
|
1355
|
+
serverName = 'FB Server';
|
|
1356
|
+
else if (dataSource === 'gg')
|
|
1357
|
+
serverName = 'Google';
|
|
1358
|
+
else if (dataSource)
|
|
1359
|
+
serverName = dataSource.toUpperCase();
|
|
1360
|
+
}
|
|
1359
1361
|
// Don't add duplicates
|
|
1360
1362
|
if (!servers.some(s => s.name.includes(serverName))) {
|
|
1361
1363
|
servers.push({
|
|
1362
1364
|
name: serverName,
|
|
1363
1365
|
embedUrl: dataHash, // This is the encrypted hash to decode
|
|
1364
|
-
quality: '
|
|
1366
|
+
quality: 'FHD',
|
|
1365
1367
|
type: dataPlay || 'api',
|
|
1366
1368
|
source: 'animevietsub'
|
|
1367
1369
|
});
|
|
@@ -1375,11 +1377,12 @@ class AnimeVietsubProvider extends base_1.BaseScraper {
|
|
|
1375
1377
|
const dataHash = $activeEp.attr('data-hash');
|
|
1376
1378
|
const dataPlay = $activeEp.attr('data-play');
|
|
1377
1379
|
const dataSource = $activeEp.attr('data-source');
|
|
1378
|
-
|
|
1380
|
+
const normalizedServerName = this.formatServerName(serverName || dataSource);
|
|
1381
|
+
if (dataHash && !servers.some(s => s.name === normalizedServerName)) {
|
|
1379
1382
|
servers.push({
|
|
1380
|
-
name:
|
|
1383
|
+
name: normalizedServerName,
|
|
1381
1384
|
embedUrl: dataHash,
|
|
1382
|
-
quality: '
|
|
1385
|
+
quality: 'FHD',
|
|
1383
1386
|
type: dataPlay || 'api',
|
|
1384
1387
|
source: 'animevietsub'
|
|
1385
1388
|
});
|
|
@@ -1394,9 +1397,9 @@ class AnimeVietsubProvider extends base_1.BaseScraper {
|
|
|
1394
1397
|
src = `https:${src}`;
|
|
1395
1398
|
if (src && src.startsWith('http') && !servers.some(s => s.embedUrl === src)) {
|
|
1396
1399
|
servers.push({
|
|
1397
|
-
name: name
|
|
1400
|
+
name: this.formatServerName(name, src),
|
|
1398
1401
|
embedUrl: src,
|
|
1399
|
-
quality: '
|
|
1402
|
+
quality: 'FHD',
|
|
1400
1403
|
source: 'animevietsub'
|
|
1401
1404
|
});
|
|
1402
1405
|
}
|
|
@@ -1408,21 +1411,19 @@ class AnimeVietsubProvider extends base_1.BaseScraper {
|
|
|
1408
1411
|
if (src.startsWith('//'))
|
|
1409
1412
|
src = `https:${src}`;
|
|
1410
1413
|
if (src && src.startsWith('http')) {
|
|
1411
|
-
let name =
|
|
1412
|
-
if (
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
else if (src.includes('drive.google'))
|
|
1421
|
-
name = 'Google Drive';
|
|
1414
|
+
let name = this.formatServerName(undefined, src);
|
|
1415
|
+
if (name === 'Server') {
|
|
1416
|
+
if (src.includes('fb') || src.includes('facebook'))
|
|
1417
|
+
name = 'FB';
|
|
1418
|
+
else if (src.includes('stream'))
|
|
1419
|
+
name = 'Stream';
|
|
1420
|
+
else if (src.includes('drive.google'))
|
|
1421
|
+
name = 'Google Drive';
|
|
1422
|
+
}
|
|
1422
1423
|
servers.push({
|
|
1423
1424
|
name,
|
|
1424
1425
|
embedUrl: src,
|
|
1425
|
-
quality: '
|
|
1426
|
+
quality: 'FHD',
|
|
1426
1427
|
type: 'iframe',
|
|
1427
1428
|
source: 'animevietsub'
|
|
1428
1429
|
});
|
|
@@ -1436,7 +1437,7 @@ class AnimeVietsubProvider extends base_1.BaseScraper {
|
|
|
1436
1437
|
servers.push({
|
|
1437
1438
|
name: 'Direct HLS',
|
|
1438
1439
|
embedUrl: scriptMatch[1],
|
|
1439
|
-
quality: '
|
|
1440
|
+
quality: 'FHD',
|
|
1440
1441
|
type: 'hls',
|
|
1441
1442
|
source: 'animevietsub'
|
|
1442
1443
|
});
|