nekostream 2026.2.3 → 2026.2.5

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 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: 'select',
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();
@@ -403,9 +404,10 @@ async function showUserDataList(title, items, providerName) {
403
404
  choices.push({ title: chalk_1.default.yellow('▲ Trang trước'), value: '__prev__' });
404
405
  choices.push({ title: chalk_1.default.gray('← Quay lại'), value: '__back__' });
405
406
  const { animeId } = await (0, prompts_wrapper_1.default)({
406
- type: 'select',
407
+ type: 'search',
407
408
  name: 'animeId',
408
409
  message: 'Chọn anime để xem (Esc: quay lại)',
410
+ pageSize: PAGE_SIZE,
409
411
  choices
410
412
  });
411
413
  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
- async function repairElectron(basePath) {
25
- const electronPkgPath = path_1.default.join(basePath, 'node_modules', 'electron', 'package.json');
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
- const version = pkg.version;
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 dist...');
42
- const distDir = path_1.default.join(basePath, 'node_modules', 'electron', 'dist');
43
- fs_1.default.mkdirSync(distDir, { recursive: true });
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: distDir });
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 '${distDir}' -Force"`, { stdio: 'inherit' });
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 "${distDir}"`, { stdio: 'inherit' });
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
- // Move types if present
63
- const srcTypeDefPath = path_1.default.join(distDir, 'electron.d.ts');
64
- const targetTypeDefPath = path_1.default.join(basePath, 'node_modules', 'electron', 'electron.d.ts');
65
- if (fs_1.default.existsSync(srcTypeDefPath)) {
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.renameSync(srcTypeDefPath, targetTypeDefPath);
108
+ fs_1.default.chmodSync(cachedBinary, 0o755);
68
109
  }
69
- catch (e) {
70
- // Ignore
110
+ catch {
111
+ // Ignore chmod failures on filesystems that do not support it.
71
112
  }
72
113
  }
73
- // Write path.txt reference
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 = require('electron');
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 not installed correctly or missing. Attempting automatic download/repair...');
130
+ console.log('[warn] Electron binary is missing. Resolving cached player runtime...');
103
131
  try {
104
- electronBinary = await repairElectron(basePath);
105
- console.log('[ok] Electron repaired successfully.');
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 or install Electron automatically. Please run "npm install electron" or "node node_modules/electron/install.js" manually.\nDetails: ${repairErr.message}`);
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) => {
@@ -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 rows = Math.ceil(choices.length / columns);
74
- return { columnWidth, columns, rows };
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 { columnWidth, columns, rows } = gridLayout(choices);
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 >= choices.length)
136
+ if (index >= pageChoices.length)
99
137
  continue;
100
- const prefix = index === selected ? theme_1.uiText.focus(theme_1.glyphs.pointer) : ' ';
101
- const label = (0, text_1.truncate)(choices[index].name, columnWidth - 2);
102
- const cell = `${prefix} ${index === selected ? theme_1.uiText.focus(label) : theme_1.uiText.value(label)}`;
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
- lines.push(chalk_1.default.gray('↑↓←→ di chuyển · Enter chọn · Esc quay lại'));
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 { rows } = gridLayout(choices);
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
- done(choices[selected].value);
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(choices.length - 1, selected + 1);
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(choices.length - 1, selected + rows);
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')),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nekostream",
3
- "version": "2026.2.3",
3
+ "version": "2026.2.5",
4
4
  "description": "Bettter version of NekoStream ",
5
5
  "keywords": [
6
6
  "anime",