nekostream 2026.0.1
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/README.md +30 -0
- package/bin/nekostream.js +4 -0
- package/dist/discord.js +144 -0
- package/dist/fetch-avs.js +22 -0
- package/dist/index.js +808 -0
- package/dist/player.js +37 -0
- package/dist/prompts-wrapper.js +81 -0
- package/dist/providers.js +35 -0
- package/dist/scrapers/anilist-api.js +516 -0
- package/dist/scrapers/anilist.js +82 -0
- package/dist/scrapers/auth-service.js +1245 -0
- package/dist/scrapers/base.js +41 -0
- package/dist/scrapers/crawler.js +301 -0
- package/dist/scrapers/external-api.js +68 -0
- package/dist/scrapers/fingerprint.js +240 -0
- package/dist/scrapers/interceptor.js +288 -0
- package/dist/scrapers/jikan-api.js +60 -0
- package/dist/scrapers/providers/anime47.js +941 -0
- package/dist/scrapers/providers/animehay.js +640 -0
- package/dist/scrapers/providers/animevietsub.js +1455 -0
- package/dist/storage.js +215 -0
- package/dist/test-a47-eps.js +69 -0
- package/dist/test-a47-real.js +56 -0
- package/dist/test-a47.js +8 -0
- package/dist/ui.js +187 -0
- package/isolate.css +115 -0
- package/isolate.js +178 -0
- package/package.json +60 -0
- package/player-main.js +225 -0
- package/player.html +376 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,808 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
const fs_1 = __importDefault(require("fs"));
|
|
40
|
+
const path_1 = __importDefault(require("path"));
|
|
41
|
+
const child_process_1 = require("child_process");
|
|
42
|
+
const prompts_wrapper_1 = __importDefault(require("./prompts-wrapper"));
|
|
43
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
44
|
+
const ora_1 = __importDefault(require("ora"));
|
|
45
|
+
const providers_1 = require("./providers");
|
|
46
|
+
const player_1 = require("./player");
|
|
47
|
+
const ui_1 = require("./ui");
|
|
48
|
+
const storage_1 = require("./storage");
|
|
49
|
+
const discord_1 = require("./discord");
|
|
50
|
+
const auth_service_1 = require("./scrapers/auth-service");
|
|
51
|
+
async function sleep(ms) {
|
|
52
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
53
|
+
}
|
|
54
|
+
async function showSettingsMenu() {
|
|
55
|
+
(0, discord_1.setBrowsingPresence)('Đang cài đặt Client', undefined, 'Cài đặt');
|
|
56
|
+
while (true) {
|
|
57
|
+
(0, ui_1.clearScreen)();
|
|
58
|
+
(0, ui_1.printBanner)('Settings', 'Configure your default preferences');
|
|
59
|
+
const settings = (0, storage_1.loadSettings)();
|
|
60
|
+
const { action } = await (0, prompts_wrapper_1.default)({
|
|
61
|
+
type: 'select',
|
|
62
|
+
name: 'action',
|
|
63
|
+
message: 'Cấu hình hệ thống (Esc: Thoát)',
|
|
64
|
+
choices: [
|
|
65
|
+
{ separator: 'CẤU HÌNH' },
|
|
66
|
+
{ title: `Default Provider: ${chalk_1.default.green(settings.defaultProvider)}`, value: 'provider' },
|
|
67
|
+
{ title: `Default Quality: ${chalk_1.default.green(settings.defaultQuality)}`, value: 'quality' },
|
|
68
|
+
{ title: `Auto-Play Next: ${settings.autoPlayNext ? chalk_1.default.green('ON') : chalk_1.default.red('OFF')}`, value: 'autoplay' },
|
|
69
|
+
{ title: `Discord RPC: ${settings.discordRpcEnabled ? chalk_1.default.green('ON') : chalk_1.default.red('OFF')}`, value: 'discord' },
|
|
70
|
+
{ title: `Developer Mode (Preserve Logs): ${settings.developerMode ? chalk_1.default.green('ON') : chalk_1.default.red('OFF')}`, value: 'devmode' },
|
|
71
|
+
{ title: `Configure Domains`, value: 'domains' },
|
|
72
|
+
{ separator: 'TRỞ VỀ' },
|
|
73
|
+
{ title: chalk_1.default.gray('🔙 Back to Home'), value: 'back' }
|
|
74
|
+
]
|
|
75
|
+
});
|
|
76
|
+
if (!action || action === 'back')
|
|
77
|
+
break;
|
|
78
|
+
if (action === 'provider') {
|
|
79
|
+
const { newProvider } = await (0, prompts_wrapper_1.default)({
|
|
80
|
+
type: 'select',
|
|
81
|
+
name: 'newProvider',
|
|
82
|
+
message: 'Select Default Provider',
|
|
83
|
+
choices: Object.keys(providers_1.providers).map((name, idx) => ({ title: `[${idx + 1}] ${name}`, value: name }))
|
|
84
|
+
});
|
|
85
|
+
if (newProvider)
|
|
86
|
+
(0, storage_1.saveSettings)({ defaultProvider: newProvider });
|
|
87
|
+
}
|
|
88
|
+
if (action === 'quality') {
|
|
89
|
+
const { newQuality } = await (0, prompts_wrapper_1.default)({
|
|
90
|
+
type: 'select',
|
|
91
|
+
name: 'newQuality',
|
|
92
|
+
message: 'Select Default Quality',
|
|
93
|
+
choices: ['1080p', '720p', '480p', 'auto'].map((q, idx) => ({ title: `[${idx + 1}] ${q}`, value: q }))
|
|
94
|
+
});
|
|
95
|
+
if (newQuality)
|
|
96
|
+
(0, storage_1.saveSettings)({ defaultQuality: newQuality });
|
|
97
|
+
}
|
|
98
|
+
if (action === 'autoplay') {
|
|
99
|
+
(0, storage_1.saveSettings)({ autoPlayNext: !settings.autoPlayNext });
|
|
100
|
+
}
|
|
101
|
+
if (action === 'discord') {
|
|
102
|
+
const nextVal = !settings.discordRpcEnabled;
|
|
103
|
+
(0, storage_1.saveSettings)({ discordRpcEnabled: nextVal });
|
|
104
|
+
(0, discord_1.toggleDiscordPresence)(nextVal);
|
|
105
|
+
}
|
|
106
|
+
if (action === 'devmode') {
|
|
107
|
+
(0, storage_1.saveSettings)({ developerMode: !settings.developerMode });
|
|
108
|
+
}
|
|
109
|
+
if (action === 'domains') {
|
|
110
|
+
while (true) {
|
|
111
|
+
(0, ui_1.clearScreen)();
|
|
112
|
+
(0, ui_1.printBanner)('Provider Domains', 'Set custom domains to bypass blocks');
|
|
113
|
+
const currentDomains = (0, storage_1.loadSettings)().providerDomains || {};
|
|
114
|
+
const domainChoices = Object.keys(providers_1.providers).map((name, idx) => {
|
|
115
|
+
const defaultDomain = providers_1.providers[name].baseUrl;
|
|
116
|
+
const currentDomain = currentDomains[name] || defaultDomain;
|
|
117
|
+
const isCustom = !!currentDomains[name];
|
|
118
|
+
return {
|
|
119
|
+
title: `[${idx + 1}] ${chalk_1.default.bold(name)}: ${isCustom ? chalk_1.default.green(currentDomain) : chalk_1.default.gray(currentDomain)}`,
|
|
120
|
+
value: name
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
domainChoices.push({ title: chalk_1.default.red('Reset All to Default'), value: 'reset' });
|
|
124
|
+
domainChoices.push({ title: chalk_1.default.gray('Back to Settings'), value: 'back' });
|
|
125
|
+
const { selectedProvider } = await (0, prompts_wrapper_1.default)({
|
|
126
|
+
type: 'select',
|
|
127
|
+
name: 'selectedProvider',
|
|
128
|
+
message: 'Select a provider to configure (Press Esc to go back)',
|
|
129
|
+
choices: domainChoices
|
|
130
|
+
});
|
|
131
|
+
if (!selectedProvider || selectedProvider === 'back')
|
|
132
|
+
break;
|
|
133
|
+
if (selectedProvider === 'reset') {
|
|
134
|
+
(0, storage_1.saveSettings)({ providerDomains: {} });
|
|
135
|
+
console.log(chalk_1.default.green('All domains reset to default.'));
|
|
136
|
+
await sleep(1000);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const { newDomain } = await (0, prompts_wrapper_1.default)({
|
|
140
|
+
type: 'text',
|
|
141
|
+
name: 'newDomain',
|
|
142
|
+
message: `Enter new domain for ${selectedProvider} (e.g. animevietsub.tv) - Leave empty to reset:`,
|
|
143
|
+
initial: currentDomains[selectedProvider] || ''
|
|
144
|
+
});
|
|
145
|
+
if (newDomain !== undefined) {
|
|
146
|
+
const newDomains = { ...(0, storage_1.loadSettings)().providerDomains };
|
|
147
|
+
if (newDomain.trim() === '') {
|
|
148
|
+
delete newDomains[selectedProvider];
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
newDomains[selectedProvider] = newDomain.trim();
|
|
152
|
+
}
|
|
153
|
+
(0, storage_1.saveSettings)({ providerDomains: newDomains });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async function showHistoryMenu() {
|
|
160
|
+
(0, discord_1.setBrowsingPresence)('Đang xem Lịch Sử', undefined, 'Lịch sử Toàn cục');
|
|
161
|
+
while (true) {
|
|
162
|
+
(0, ui_1.clearScreen)();
|
|
163
|
+
(0, ui_1.printBanner)('Continue Watching', 'Resume from where you left off');
|
|
164
|
+
const history = (0, storage_1.loadHistory)();
|
|
165
|
+
if (history.length === 0) {
|
|
166
|
+
console.log(chalk_1.default.yellow('Your history is empty.'));
|
|
167
|
+
await sleep(2000);
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
const choices = [{ separator: 'LỊCH SỬ LOCAL' }];
|
|
171
|
+
history.forEach((item, index) => {
|
|
172
|
+
choices.push({
|
|
173
|
+
title: `[${index + 1}] ${chalk_1.default.magenta(item.provider)} | ${chalk_1.default.bold.white(item.animeTitle)} - ${chalk_1.default.cyan(item.episodeTitle)}`,
|
|
174
|
+
description: `Watched on: ${new Date(item.timestamp).toLocaleString()}`,
|
|
175
|
+
value: index
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
choices.push({ title: chalk_1.default.red('Clear History'), description: '', value: -2 });
|
|
179
|
+
choices.push({ title: chalk_1.default.gray('Back to Home'), description: '', value: -1 });
|
|
180
|
+
const { selectedIndex } = await (0, prompts_wrapper_1.default)({
|
|
181
|
+
type: 'select',
|
|
182
|
+
name: 'selectedIndex',
|
|
183
|
+
message: 'Select an episode to resume (Press Esc to go back)',
|
|
184
|
+
choices
|
|
185
|
+
});
|
|
186
|
+
if (selectedIndex === undefined || selectedIndex === -1)
|
|
187
|
+
break;
|
|
188
|
+
if (selectedIndex === -2) {
|
|
189
|
+
const { confirm } = await (0, prompts_wrapper_1.default)({
|
|
190
|
+
type: 'confirm',
|
|
191
|
+
name: 'confirm',
|
|
192
|
+
message: 'Are you sure you want to clear your history?'
|
|
193
|
+
});
|
|
194
|
+
if (confirm) {
|
|
195
|
+
(0, storage_1.clearHistory)();
|
|
196
|
+
console.log(chalk_1.default.green('History cleared.'));
|
|
197
|
+
await sleep(1000);
|
|
198
|
+
}
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const item = history[selectedIndex];
|
|
202
|
+
await openAnimeMenu(item.provider, item.animeId);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async function showAnimeList(providerName, title, list) {
|
|
206
|
+
(0, discord_1.setBrowsingPresence)(`Danh sách: ${title}`, providerName, title);
|
|
207
|
+
while (true) {
|
|
208
|
+
(0, ui_1.clearScreen)();
|
|
209
|
+
(0, ui_1.printBanner)(`Provider: ${providerName.toUpperCase()}`, title.toUpperCase());
|
|
210
|
+
// Fix for prompts Windows bug: Flush any leftover escape sequences or buffered keys
|
|
211
|
+
// that might corrupt the next prompt's raw mode and cause the ^[[B bug.
|
|
212
|
+
if (process.stdin.isTTY) {
|
|
213
|
+
process.stdin.resume();
|
|
214
|
+
while (process.stdin.read() !== null) { }
|
|
215
|
+
}
|
|
216
|
+
const { animeId } = await (0, prompts_wrapper_1.default)({
|
|
217
|
+
type: 'select',
|
|
218
|
+
name: 'animeId',
|
|
219
|
+
message: 'Select an Anime (Press Esc to go back)',
|
|
220
|
+
choices: list.map((anime, idx) => {
|
|
221
|
+
const desc = anime.status || anime.year?.toString() || '';
|
|
222
|
+
const cleanTitle = anime.title.replace(/\r?\n|\r/g, ' ').trim();
|
|
223
|
+
const cleanDesc = desc.replace(/\r?\n|\r/g, ' ').trim();
|
|
224
|
+
return {
|
|
225
|
+
title: `[${idx + 1}] ${cleanTitle}${cleanDesc ? ` - ${cleanDesc}` : ''}`,
|
|
226
|
+
value: anime.id
|
|
227
|
+
};
|
|
228
|
+
})
|
|
229
|
+
});
|
|
230
|
+
if (!animeId)
|
|
231
|
+
break;
|
|
232
|
+
await openAnimeMenu(providerName, animeId);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async function openAnimeMenu(providerName, animeId) {
|
|
236
|
+
(0, discord_1.setBrowsingPresence)(`Đang tải dữ liệu: ${animeId}...`, providerName, 'Tải dữ liệu');
|
|
237
|
+
const provider = (0, providers_1.getProvider)(providerName);
|
|
238
|
+
// Fetch details & episodes
|
|
239
|
+
const epsSpinner = (0, ora_1.default)('Fetching details and episodes...').start();
|
|
240
|
+
let episodes = [];
|
|
241
|
+
let selectedAnime = null;
|
|
242
|
+
try {
|
|
243
|
+
episodes = await provider.getEpisodes(animeId);
|
|
244
|
+
epsSpinner.stop();
|
|
245
|
+
}
|
|
246
|
+
catch (e) {
|
|
247
|
+
epsSpinner.stop();
|
|
248
|
+
console.error(chalk_1.default.red('\nFailed to fetch episodes:'), e);
|
|
249
|
+
await sleep(2000);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
// Detail is non-fatal — 401 on detail page shouldn't block episode viewing
|
|
253
|
+
if (provider.getAnimeDetail) {
|
|
254
|
+
try {
|
|
255
|
+
selectedAnime = await provider.getAnimeDetail(animeId);
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
// silently skip — detail unavailable but episodes still work
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (!episodes || episodes.length === 0) {
|
|
262
|
+
console.log(chalk_1.default.yellow('\nNo episodes found.'));
|
|
263
|
+
await sleep(2000);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
// Fallback if no detail is available
|
|
267
|
+
if (!selectedAnime) {
|
|
268
|
+
selectedAnime = {
|
|
269
|
+
id: animeId,
|
|
270
|
+
source: providerName,
|
|
271
|
+
title: episodes.length > 0 && episodes[0].title ? episodes[0].title.split(' - ')[0] : animeId,
|
|
272
|
+
genres: []
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
while (true) {
|
|
276
|
+
(0, discord_1.setBrowsingPresence)(`Đang xem thông tin: ${selectedAnime ? selectedAnime.title : animeId}`, providerName, 'Thông tin Phim');
|
|
277
|
+
(0, ui_1.clearScreen)();
|
|
278
|
+
(0, ui_1.printBanner)(`Provider: ${providerName.toUpperCase()}`, selectedAnime ? selectedAnime.title : animeId);
|
|
279
|
+
if (selectedAnime)
|
|
280
|
+
(0, ui_1.drawAnimeCard)(selectedAnime);
|
|
281
|
+
if (process.stdin.isTTY) {
|
|
282
|
+
process.stdin.resume();
|
|
283
|
+
while (process.stdin.read() !== null) { }
|
|
284
|
+
}
|
|
285
|
+
const episodeChoices = [{ separator: 'DANH SÁCH TẬP' }];
|
|
286
|
+
episodes.forEach((ep, idx) => {
|
|
287
|
+
const cleanTitle = (ep.title || `Episode ${ep.number}`).replace(/\r?\n|\r/g, ' ').trim();
|
|
288
|
+
episodeChoices.push({
|
|
289
|
+
title: `[${idx + 1}] ▶️ ${cleanTitle}`,
|
|
290
|
+
value: ep
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
const { episode } = await (0, prompts_wrapper_1.default)({
|
|
294
|
+
type: 'select',
|
|
295
|
+
name: 'episode',
|
|
296
|
+
message: 'Select an Episode (Esc: Thoát)',
|
|
297
|
+
choices: episodeChoices
|
|
298
|
+
});
|
|
299
|
+
if (!episode)
|
|
300
|
+
break;
|
|
301
|
+
while (true) {
|
|
302
|
+
(0, ui_1.clearScreen)();
|
|
303
|
+
(0, ui_1.printBanner)(`Provider: ${providerName.toUpperCase()}`, selectedAnime ? selectedAnime.title : animeId);
|
|
304
|
+
if (selectedAnime)
|
|
305
|
+
(0, ui_1.drawAnimeCard)(selectedAnime);
|
|
306
|
+
console.log(chalk_1.default.blue(`▶️ Selected: `) + chalk_1.default.bold.white(episode.title || `Episode ${episode.number}`) + '\n');
|
|
307
|
+
const serversSpinner = (0, ora_1.default)('Fetching video servers...').start();
|
|
308
|
+
let servers = [];
|
|
309
|
+
try {
|
|
310
|
+
const episodeIdentifier = episode.href || episode.id;
|
|
311
|
+
servers = await provider.getVideoServers(episodeIdentifier);
|
|
312
|
+
serversSpinner.stop();
|
|
313
|
+
}
|
|
314
|
+
catch (e) {
|
|
315
|
+
serversSpinner.stop();
|
|
316
|
+
console.error(chalk_1.default.red('\nFailed to fetch servers:'), e);
|
|
317
|
+
await sleep(2000);
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
if (!servers || servers.length === 0) {
|
|
321
|
+
console.log(chalk_1.default.red('\nNo video servers found for this episode.'));
|
|
322
|
+
await sleep(2000);
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
const { server } = await (0, prompts_wrapper_1.default)({
|
|
326
|
+
type: 'select',
|
|
327
|
+
name: 'server',
|
|
328
|
+
message: 'Select a Server (Press Esc to go back)',
|
|
329
|
+
choices: servers.map((s, idx) => ({
|
|
330
|
+
title: `[${idx + 1}] ${s.name} [${s.quality || 'Auto'}] (${s.type})`,
|
|
331
|
+
value: s
|
|
332
|
+
}))
|
|
333
|
+
});
|
|
334
|
+
if (!server)
|
|
335
|
+
break;
|
|
336
|
+
const streamSpinner = (0, ora_1.default)('Extracting stream URL...').start();
|
|
337
|
+
let streamInfo = null;
|
|
338
|
+
try {
|
|
339
|
+
streamInfo = await provider.extractStreamUrl(server);
|
|
340
|
+
streamSpinner.stop();
|
|
341
|
+
}
|
|
342
|
+
catch (e) {
|
|
343
|
+
streamSpinner.stop();
|
|
344
|
+
console.error(chalk_1.default.red('\nFailed to extract stream:'), e);
|
|
345
|
+
await sleep(2000);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
if (!streamInfo || !streamInfo.url) {
|
|
349
|
+
console.log(chalk_1.default.red('\nFailed to extract stream URL.'));
|
|
350
|
+
await sleep(2000);
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
console.log(chalk_1.default.green(`\n✅ Ready to play! Opening Player...`));
|
|
354
|
+
// Save history
|
|
355
|
+
(0, storage_1.saveHistoryEntry)({
|
|
356
|
+
provider: providerName,
|
|
357
|
+
animeId: selectedAnime.id,
|
|
358
|
+
animeTitle: selectedAnime.title,
|
|
359
|
+
episodeId: episode.id,
|
|
360
|
+
episodeTitle: episode.title || `Episode ${episode.number}`
|
|
361
|
+
});
|
|
362
|
+
try {
|
|
363
|
+
(0, discord_1.setWatchingPresence)(selectedAnime.title, episode.title || `Episode ${episode.number}`, providerName);
|
|
364
|
+
await (0, player_1.launchPlayer)(streamInfo);
|
|
365
|
+
(0, discord_1.setBrowsingPresence)(`Đang xem thông tin: ${selectedAnime.title}`, providerName, 'Thông tin Phim');
|
|
366
|
+
console.log(chalk_1.default.green('\nPlayer closed.'));
|
|
367
|
+
await sleep(500);
|
|
368
|
+
}
|
|
369
|
+
catch (e) {
|
|
370
|
+
console.error(chalk_1.default.red('\nPlayer error:'), e);
|
|
371
|
+
await sleep(2000);
|
|
372
|
+
}
|
|
373
|
+
break; // Go back to Select Episode!
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
/** Display a paginated interactive list of UserDataItems — user can select to watch */
|
|
378
|
+
async function showUserDataList(title, items, providerName) {
|
|
379
|
+
(0, discord_1.setBrowsingPresence)(`Đang xem ${title}`, providerName, title);
|
|
380
|
+
if (items.length === 0) {
|
|
381
|
+
console.log(chalk_1.default.yellow('\n (Danh sách trống)'));
|
|
382
|
+
await sleep(1500);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const PAGE_SIZE = 20;
|
|
386
|
+
let offset = 0;
|
|
387
|
+
while (true) {
|
|
388
|
+
(0, ui_1.clearScreen)();
|
|
389
|
+
(0, ui_1.printBanner)(title, `${items.length} anime`);
|
|
390
|
+
const page = items.slice(offset, offset + PAGE_SIZE);
|
|
391
|
+
const choices = page.map((item, i) => {
|
|
392
|
+
const ep = item.episodeNumber ? chalk_1.default.gray(` [Tập ${item.episodeNumber}]`) : '';
|
|
393
|
+
const status = item.status ? chalk_1.default.cyan(` (${item.status})`) : '';
|
|
394
|
+
return {
|
|
395
|
+
title: `${chalk_1.default.bold(String(offset + i + 1).padStart(3))}. ${item.title}${ep}${status}`,
|
|
396
|
+
value: item.animeId
|
|
397
|
+
};
|
|
398
|
+
});
|
|
399
|
+
if (offset + PAGE_SIZE < items.length)
|
|
400
|
+
choices.push({ title: chalk_1.default.yellow(`▼ Xem thêm (${items.length - offset - PAGE_SIZE} còn lại)`), value: '__next__' });
|
|
401
|
+
if (offset > 0)
|
|
402
|
+
choices.push({ title: chalk_1.default.yellow('▲ Trang trước'), value: '__prev__' });
|
|
403
|
+
choices.push({ title: chalk_1.default.gray('← Quay lại'), value: '__back__' });
|
|
404
|
+
const { animeId } = await (0, prompts_wrapper_1.default)({
|
|
405
|
+
type: 'select',
|
|
406
|
+
name: 'animeId',
|
|
407
|
+
message: 'Chọn anime để xem (Esc để quay lại)',
|
|
408
|
+
choices
|
|
409
|
+
});
|
|
410
|
+
if (!animeId || animeId === '__back__')
|
|
411
|
+
break;
|
|
412
|
+
if (animeId === '__next__') {
|
|
413
|
+
offset = Math.min(offset + PAGE_SIZE, items.length - 1);
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
if (animeId === '__prev__') {
|
|
417
|
+
offset = Math.max(0, offset - PAGE_SIZE);
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
// Open anime detail & episode selection
|
|
421
|
+
await openAnimeMenu(providerName, animeId);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
async function showProviderAccountMenu(provider) {
|
|
425
|
+
const label = provider === 'animevietsub' ? 'AnimeVietsub' : 'Anime47';
|
|
426
|
+
while (true) {
|
|
427
|
+
(0, discord_1.setBrowsingPresence)('Quản lý Tài khoản', provider, 'Tài khoản');
|
|
428
|
+
(0, ui_1.clearScreen)();
|
|
429
|
+
const status = (0, auth_service_1.getAuthStatus)(provider);
|
|
430
|
+
const loginLabel = status.loggedIn
|
|
431
|
+
? chalk_1.default.green(`✅ ${status.userDisplayName || 'Đã đăng nhập'} (${status.cookieCount} cookies)`)
|
|
432
|
+
: chalk_1.default.red('❌ Chưa đăng nhập');
|
|
433
|
+
(0, ui_1.printBanner)(`${label} — Account`, loginLabel);
|
|
434
|
+
const choices = status.loggedIn
|
|
435
|
+
? [
|
|
436
|
+
{ title: 'Hộp phim / Yêu thích', value: 'favorites' },
|
|
437
|
+
{ title: 'Lịch sử xem', value: 'history' },
|
|
438
|
+
...(provider === 'animevietsub' ? [] : [
|
|
439
|
+
{ title: 'Đang xem', value: 'watching' },
|
|
440
|
+
{ title: 'Hoàn thành', value: 'completed' },
|
|
441
|
+
{ title: 'Dự định xem', value: 'plan_to_watch' },
|
|
442
|
+
]),
|
|
443
|
+
{ title: 'Thông báo', value: 'notifications' },
|
|
444
|
+
{ title: chalk_1.default.red('Đăng xuất'), value: 'logout' },
|
|
445
|
+
{ title: chalk_1.default.gray('Quay lại'), value: 'back' }
|
|
446
|
+
]
|
|
447
|
+
: [
|
|
448
|
+
{ title: chalk_1.default.cyan('🔑 Đăng nhập'), value: 'login' },
|
|
449
|
+
{ title: chalk_1.default.gray('Quay lại'), value: 'back' }
|
|
450
|
+
];
|
|
451
|
+
const { action } = await (0, prompts_wrapper_1.default)({ type: 'select', name: 'action', message: `${label} — Chọn hành động`, choices });
|
|
452
|
+
if (!action || action === 'back')
|
|
453
|
+
break;
|
|
454
|
+
if (action === 'login') {
|
|
455
|
+
try {
|
|
456
|
+
const result = provider === 'animevietsub'
|
|
457
|
+
? await (0, auth_service_1.loginAnimeVietsubInteractive)()
|
|
458
|
+
: await (0, auth_service_1.loginAnime47Interactive)();
|
|
459
|
+
console.log(chalk_1.default.green(`\n✅ Đăng nhập thành công!`));
|
|
460
|
+
if (result.userDisplayName)
|
|
461
|
+
console.log(chalk_1.default.cyan(` Xin chào, ${result.userDisplayName}!`));
|
|
462
|
+
}
|
|
463
|
+
catch (e) {
|
|
464
|
+
console.error(chalk_1.default.red(`\n❌ Đăng nhập thất bại:`), e);
|
|
465
|
+
}
|
|
466
|
+
await sleep(2000);
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
if (action === 'logout') {
|
|
470
|
+
(0, auth_service_1.logoutProvider)(provider);
|
|
471
|
+
console.log(chalk_1.default.yellow(`\n👋 Đã đăng xuất khỏi ${label}.`));
|
|
472
|
+
await sleep(1500);
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
// Data fetching actions
|
|
476
|
+
const spinner = (0, ora_1.default)(`Đang tải dữ liệu từ ${label}...`).start();
|
|
477
|
+
try {
|
|
478
|
+
if (provider === 'animevietsub') {
|
|
479
|
+
if (action === 'notifications') {
|
|
480
|
+
const res = await (0, auth_service_1.fetchAnimeVietsubNotifications)();
|
|
481
|
+
spinner.stop();
|
|
482
|
+
await showUserDataList('Thông báo — AnimeVietsub', res.items, provider);
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
const listType = action;
|
|
486
|
+
const res = await (0, auth_service_1.fetchAllAnimeVietsubList)(listType);
|
|
487
|
+
spinner.stop();
|
|
488
|
+
const titleMap = { favorites: 'Hộp phim / Yêu thích', history: 'Lịch sử xem' };
|
|
489
|
+
if (!res.success) {
|
|
490
|
+
console.log(chalk_1.default.red(`\n❌ ${res.error}`));
|
|
491
|
+
await sleep(2000);
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
await showUserDataList(`${titleMap[listType]} — AnimeVietsub`, res.items, provider);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
if (action === 'notifications') {
|
|
500
|
+
const res = await (0, auth_service_1.fetchAnime47Notifications)();
|
|
501
|
+
spinner.stop();
|
|
502
|
+
await showUserDataList('Thông báo — Anime47', res.notifications?.map(n => ({
|
|
503
|
+
animeId: n.animeId || '', title: n.title, url: n.url, thumbnail: n.thumbnail
|
|
504
|
+
})) || [], provider);
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
const { fetchAllAnime47List: fetchList } = await Promise.resolve().then(() => __importStar(require('./scrapers/auth-service')));
|
|
508
|
+
const res = await fetchList(action);
|
|
509
|
+
spinner.stop();
|
|
510
|
+
const titleMap = {
|
|
511
|
+
favorites: 'Yêu thích', history: 'Lịch sử xem',
|
|
512
|
+
watching: 'Đang xem', completed: 'Hoàn thành', plan_to_watch: 'Dự định xem'
|
|
513
|
+
};
|
|
514
|
+
if (!res.success) {
|
|
515
|
+
console.log(chalk_1.default.red(`\n❌ ${res.error}`));
|
|
516
|
+
await sleep(2000);
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
await showUserDataList(`${titleMap[action] || action} — Anime47`, res.items, provider);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
catch (e) {
|
|
525
|
+
spinner.stop();
|
|
526
|
+
console.error(chalk_1.default.red(`\n❌ Lỗi:`), e);
|
|
527
|
+
await sleep(2000);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
async function showAccountMenu() {
|
|
532
|
+
while (true) {
|
|
533
|
+
(0, ui_1.clearScreen)();
|
|
534
|
+
const avsStatus = (0, auth_service_1.getAuthStatus)('animevietsub');
|
|
535
|
+
const a47Status = (0, auth_service_1.getAuthStatus)('anime47');
|
|
536
|
+
const avsLabel = avsStatus.loggedIn
|
|
537
|
+
? chalk_1.default.green(`✅ ${avsStatus.userDisplayName || 'Đã đăng nhập'}`)
|
|
538
|
+
: chalk_1.default.red('❌ Chưa đăng nhập');
|
|
539
|
+
const a47Label = a47Status.loggedIn
|
|
540
|
+
? chalk_1.default.green(`✅ ${a47Status.userDisplayName || 'Đã đăng nhập'}`)
|
|
541
|
+
: chalk_1.default.red('❌ Chưa đăng nhập');
|
|
542
|
+
(0, ui_1.printBanner)('👤 Account', 'Quản lý tài khoản theo nhà cung cấp');
|
|
543
|
+
const { provider } = await (0, prompts_wrapper_1.default)({
|
|
544
|
+
type: 'select',
|
|
545
|
+
name: 'provider',
|
|
546
|
+
message: 'Chọn nhà cung cấp',
|
|
547
|
+
choices: [
|
|
548
|
+
{ title: `AnimeVietsub — ${avsLabel}`, value: 'animevietsub' },
|
|
549
|
+
{ title: `Anime47 — ${a47Label}`, value: 'anime47' },
|
|
550
|
+
{ title: chalk_1.default.gray('Quay lại Home'), value: 'back' }
|
|
551
|
+
]
|
|
552
|
+
});
|
|
553
|
+
if (!provider || provider === 'back')
|
|
554
|
+
break;
|
|
555
|
+
await showProviderAccountMenu(provider);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
async function checkUpdate() {
|
|
559
|
+
try {
|
|
560
|
+
const isCompiled = __dirname.endsWith('dist') || __dirname.endsWith('dist\\') || __dirname.endsWith('dist/');
|
|
561
|
+
const basePath = isCompiled ? path_1.default.join(__dirname, '..') : __dirname;
|
|
562
|
+
const pkg = JSON.parse(fs_1.default.readFileSync(path_1.default.join(basePath, 'package.json'), 'utf-8'));
|
|
563
|
+
const currentVersion = pkg.version;
|
|
564
|
+
const res = await fetch('https://registry.npmjs.org/nekostream-cli/latest', { signal: AbortSignal.timeout(2000) });
|
|
565
|
+
const data = await res.json();
|
|
566
|
+
const latestVersion = data.version;
|
|
567
|
+
if (latestVersion && latestVersion !== currentVersion) {
|
|
568
|
+
(0, ui_1.clearScreen)();
|
|
569
|
+
console.log(chalk_1.default.yellow('╭─────────────────────────────────────────────────────────────╮'));
|
|
570
|
+
console.log(chalk_1.default.yellow('│ │'));
|
|
571
|
+
console.log(chalk_1.default.yellow(`│ NEKOSTREAM ĐÃ CÓ CẬP NHẬT MỚI ! ${chalk_1.default.red(currentVersion)} → ${chalk_1.default.green(latestVersion)} │`));
|
|
572
|
+
console.log(chalk_1.default.yellow('│ Vui lòng cập nhật ứng dụng CLI để tiếp tục sử dụng ứng dụng. │'));
|
|
573
|
+
console.log(chalk_1.default.yellow('│ │'));
|
|
574
|
+
console.log(chalk_1.default.yellow('╰─────────────────────────────────────────────────────────────╯\n'));
|
|
575
|
+
const { update } = await (0, prompts_wrapper_1.default)({
|
|
576
|
+
type: 'confirm',
|
|
577
|
+
name: 'update',
|
|
578
|
+
message: 'Bạn có muốn tự động cập nhật ngay bây giờ?',
|
|
579
|
+
initial: true
|
|
580
|
+
});
|
|
581
|
+
if (update) {
|
|
582
|
+
const spinner = (0, ora_1.default)('Đang cập nhật (npm i -g nekostream-cli@latest)...').start();
|
|
583
|
+
try {
|
|
584
|
+
(0, child_process_1.execSync)('npm i -g nekostream-cli@latest', { stdio: 'ignore' });
|
|
585
|
+
spinner.succeed(chalk_1.default.green('Đã cập nhật thành công! Vui lòng chạy lại lệnh để sử dụng bản mới.'));
|
|
586
|
+
}
|
|
587
|
+
catch (e) {
|
|
588
|
+
spinner.fail(chalk_1.default.red('Cập nhật thất bại. Vui lòng chạy thủ công: npm i -g nekostream-cli@latest'));
|
|
589
|
+
}
|
|
590
|
+
process.exit(0);
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
console.log(chalk_1.default.red('\nVui lòng chạy `npm i -g nekostream-cli@latest` để cập nhật thủ công.'));
|
|
594
|
+
process.exit(0);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
catch (e) {
|
|
599
|
+
// Ignore network/fetch errors to not block startup
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
async function main() {
|
|
603
|
+
await checkUpdate();
|
|
604
|
+
await (0, discord_1.initDiscord)();
|
|
605
|
+
let settings = (0, storage_1.loadSettings)();
|
|
606
|
+
let currentProviderName = settings.defaultProvider;
|
|
607
|
+
while (true) {
|
|
608
|
+
(0, discord_1.setBrowsingPresence)('Đang lướt Menu Chính');
|
|
609
|
+
(0, ui_1.clearScreen)();
|
|
610
|
+
const authStatus = await (0, auth_service_1.getAuthStatus)(currentProviderName);
|
|
611
|
+
const usernameDisplay = authStatus.loggedIn
|
|
612
|
+
? chalk_1.default.green(`${authStatus.userDisplayName || 'Đã đăng nhập'} (${authStatus.cookieCount || 0} cookies)`)
|
|
613
|
+
: chalk_1.default.red('Chưa đăng nhập');
|
|
614
|
+
(0, ui_1.printBanner)(`Provider: ${currentProviderName.toUpperCase()}`, 'Home Dashboard');
|
|
615
|
+
console.log(chalk_1.default.cyan('NekoStream Dashboard | Hôm nay xem gì?'));
|
|
616
|
+
console.log(`Account: ${usernameDisplay}\n`);
|
|
617
|
+
const dynamicChoices = [
|
|
618
|
+
{ separator: 'KHÁM PHÁ' },
|
|
619
|
+
{ title: 'Search Anime', value: 'search' },
|
|
620
|
+
{ title: 'Trending Now', value: 'trending' },
|
|
621
|
+
{ title: 'Recently Added', value: 'latest' }
|
|
622
|
+
];
|
|
623
|
+
const usernameStr = authStatus.loggedIn ? (authStatus.userDisplayName || 'MEMBER') : 'KHÁCH';
|
|
624
|
+
dynamicChoices.push({ separator: `TÀI KHOẢN : ${usernameStr.toUpperCase()}` });
|
|
625
|
+
if (currentProviderName === 'anime47') {
|
|
626
|
+
if (authStatus.loggedIn) {
|
|
627
|
+
dynamicChoices.push({ title: 'Hộp phim / Yêu thích', value: 'favorites' }, { title: 'Lịch sử xem', value: 'history_provider' }, { title: 'Đang xem', value: 'watching' }, { title: 'Hoàn thành', value: 'completed' }, { title: 'Dự định xem', value: 'plan_to_watch' }, { title: 'Thông báo', value: 'notifications' }, { title: chalk_1.default.yellow('Đăng xuất'), value: 'logout' });
|
|
628
|
+
}
|
|
629
|
+
else {
|
|
630
|
+
dynamicChoices.push({ title: chalk_1.default.green('Đăng nhập Anime47'), value: 'login' });
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
else if (currentProviderName === 'animevietsub') {
|
|
634
|
+
if (authStatus.loggedIn) {
|
|
635
|
+
dynamicChoices.push({ title: 'Hộp phim', value: 'favorites' }, { title: 'Lịch sử', value: 'history_provider' }, { title: 'Thông báo', value: 'notifications' }, { title: chalk_1.default.yellow('Đăng xuất'), value: 'logout' });
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
dynamicChoices.push({ title: chalk_1.default.green('Đăng nhập AnimeVietsub'), value: 'login' });
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
dynamicChoices.push({ separator: 'HỆ THỐNG' }, { title: 'Tiếp tục xem (Lịch sử Local)', value: 'history' }, { title: 'Cài đặt hệ thống', value: 'settings' }, { title: 'Đổi Provider', value: 'change_provider' }, { title: chalk_1.default.red('Thoát ứng dụng'), value: 'exit' });
|
|
642
|
+
const { action } = await (0, prompts_wrapper_1.default)({
|
|
643
|
+
type: 'select',
|
|
644
|
+
name: 'action',
|
|
645
|
+
message: 'Home Dashboard (Press Esc to Exit)',
|
|
646
|
+
choices: dynamicChoices
|
|
647
|
+
});
|
|
648
|
+
if (!action || action === 'exit') {
|
|
649
|
+
(0, ui_1.clearScreen)();
|
|
650
|
+
(0, discord_1.clearDiscordPresence)();
|
|
651
|
+
console.log(chalk_1.default.magenta('\nThanks for using NekoStream CLI! 🎬\n'));
|
|
652
|
+
process.exit(0);
|
|
653
|
+
}
|
|
654
|
+
const provider = (0, providers_1.getProvider)(currentProviderName);
|
|
655
|
+
if (action === 'login') {
|
|
656
|
+
try {
|
|
657
|
+
if (currentProviderName === 'anime47')
|
|
658
|
+
await (0, auth_service_1.loginAnime47Interactive)();
|
|
659
|
+
else if (currentProviderName === 'animevietsub')
|
|
660
|
+
await (0, auth_service_1.loginAnimeVietsubInteractive)();
|
|
661
|
+
}
|
|
662
|
+
catch (e) {
|
|
663
|
+
if (e.name === 'TimeoutError') {
|
|
664
|
+
console.log(chalk_1.default.red('\nĐăng nhập thất bại/Đã hủy: Hết thời gian chờ (Timeout).'));
|
|
665
|
+
}
|
|
666
|
+
else {
|
|
667
|
+
console.log(chalk_1.default.red(`\nLỗi: ${e.message}`));
|
|
668
|
+
}
|
|
669
|
+
await sleep(2000);
|
|
670
|
+
}
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
if (action === 'logout') {
|
|
674
|
+
(0, auth_service_1.logoutProvider)(currentProviderName);
|
|
675
|
+
console.log(chalk_1.default.yellow(`\n👋 Đã đăng xuất.`));
|
|
676
|
+
await sleep(1500);
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
if (['favorites', 'history_provider', 'watching', 'completed', 'plan_to_watch', 'notifications'].includes(action)) {
|
|
680
|
+
const spinner = (0, ora_1.default)(`Đang tải dữ liệu từ ${currentProviderName}...`).start();
|
|
681
|
+
try {
|
|
682
|
+
if (currentProviderName === 'animevietsub') {
|
|
683
|
+
if (action === 'notifications') {
|
|
684
|
+
const res = await (0, auth_service_1.fetchAnimeVietsubNotifications)();
|
|
685
|
+
spinner.stop();
|
|
686
|
+
await showUserDataList('Thông báo', res.items, currentProviderName);
|
|
687
|
+
}
|
|
688
|
+
else {
|
|
689
|
+
const listType = action === 'history_provider' ? 'history' : action;
|
|
690
|
+
const res = await (0, auth_service_1.fetchAllAnimeVietsubList)(listType);
|
|
691
|
+
spinner.stop();
|
|
692
|
+
if (!res.success) {
|
|
693
|
+
console.log(chalk_1.default.red(`\n❌ ${res.error}`));
|
|
694
|
+
await sleep(2000);
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
await showUserDataList(listType === 'history' ? 'Lịch sử' : 'Hộp phim', res.items, currentProviderName);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
else if (currentProviderName === 'anime47') {
|
|
702
|
+
if (action === 'notifications') {
|
|
703
|
+
const res = await (0, auth_service_1.fetchAnime47Notifications)();
|
|
704
|
+
spinner.stop();
|
|
705
|
+
await showUserDataList('Thông báo', res.notifications?.map(n => ({
|
|
706
|
+
animeId: n.animeId || '', title: n.title, url: n.url, thumbnail: n.thumbnail
|
|
707
|
+
})) || [], currentProviderName);
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
const listType = action === 'history_provider' ? 'history' : action;
|
|
711
|
+
const res = await (0, auth_service_1.fetchAllAnime47List)(listType);
|
|
712
|
+
spinner.stop();
|
|
713
|
+
if (!res.success) {
|
|
714
|
+
console.log(chalk_1.default.red(`\n❌ ${res.error}`));
|
|
715
|
+
await sleep(2000);
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
const titleMap = { favorites: 'Yêu thích', history: 'Lịch sử xem', watching: 'Đang xem', completed: 'Hoàn thành', plan_to_watch: 'Dự định xem' };
|
|
719
|
+
await showUserDataList(titleMap[listType] || listType, res.items, currentProviderName);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
catch (e) {
|
|
725
|
+
spinner.stop();
|
|
726
|
+
console.error(chalk_1.default.red(`\nLỗi:`), e);
|
|
727
|
+
await sleep(2000);
|
|
728
|
+
}
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
if (action === 'search') {
|
|
732
|
+
const { keyword } = await (0, prompts_wrapper_1.default)({
|
|
733
|
+
type: 'text',
|
|
734
|
+
name: 'keyword',
|
|
735
|
+
message: 'Enter anime name to search (Press Esc to go back)'
|
|
736
|
+
});
|
|
737
|
+
if (!keyword)
|
|
738
|
+
continue;
|
|
739
|
+
const searchSpinner = (0, ora_1.default)(`Searching for "${keyword}" on ${currentProviderName}...`).start();
|
|
740
|
+
try {
|
|
741
|
+
const results = await provider.search(keyword);
|
|
742
|
+
searchSpinner.stop();
|
|
743
|
+
if (!results || results.length === 0) {
|
|
744
|
+
console.log(chalk_1.default.yellow(`\nNo results found.`));
|
|
745
|
+
await sleep(2000);
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
(0, discord_1.setBrowsingPresence)(`Đang Tìm kiếm: ${keyword}`, currentProviderName, 'Tìm kiếm');
|
|
749
|
+
await showAnimeList(currentProviderName, `Search Results: ${keyword}`, results);
|
|
750
|
+
}
|
|
751
|
+
catch (e) {
|
|
752
|
+
searchSpinner.stop();
|
|
753
|
+
console.error(chalk_1.default.red('\nSearch failed:'), e);
|
|
754
|
+
await sleep(2000);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
if (action === 'trending' || action === 'latest') {
|
|
758
|
+
const spinner = (0, ora_1.default)(`Fetching ${action} anime...`).start();
|
|
759
|
+
try {
|
|
760
|
+
const results = await provider.getHomeCards(action);
|
|
761
|
+
spinner.stop();
|
|
762
|
+
if (!results || results.length === 0) {
|
|
763
|
+
console.log(chalk_1.default.yellow(`\nNo ${action} anime found on this provider.`));
|
|
764
|
+
await sleep(2000);
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
(0, discord_1.setBrowsingPresence)(`Đang Xem ${action === 'trending' ? 'Xu hướng' : 'Cập nhật gần đây'}`, currentProviderName, action === 'trending' ? 'Xu hướng' : 'Mới cập nhật');
|
|
768
|
+
await showAnimeList(currentProviderName, action === 'trending' ? '🔥 Trending Now' : '🆕 Recently Added', results);
|
|
769
|
+
}
|
|
770
|
+
catch (e) {
|
|
771
|
+
spinner.stop();
|
|
772
|
+
console.error(chalk_1.default.red(`\nFailed to fetch ${action} anime:`), e);
|
|
773
|
+
await sleep(2000);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
if (action === 'history') {
|
|
777
|
+
await showHistoryMenu();
|
|
778
|
+
}
|
|
779
|
+
// Account logic has been moved to main menu
|
|
780
|
+
if (action === 'settings') {
|
|
781
|
+
await showSettingsMenu();
|
|
782
|
+
// reload settings in case default provider changed
|
|
783
|
+
settings = (0, storage_1.loadSettings)();
|
|
784
|
+
currentProviderName = settings.defaultProvider;
|
|
785
|
+
}
|
|
786
|
+
if (action === 'change_provider') {
|
|
787
|
+
const { newProvider } = await (0, prompts_wrapper_1.default)({
|
|
788
|
+
type: 'select',
|
|
789
|
+
name: 'newProvider',
|
|
790
|
+
message: 'Select an Anime Provider',
|
|
791
|
+
choices: Object.keys(providers_1.providers).map(name => ({ title: name, value: name }))
|
|
792
|
+
});
|
|
793
|
+
if (newProvider) {
|
|
794
|
+
currentProviderName = newProvider;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
// Global hook to catch Ctrl+C gracefully
|
|
800
|
+
process.on('SIGINT', () => {
|
|
801
|
+
(0, ui_1.clearScreen)();
|
|
802
|
+
(0, discord_1.clearDiscordPresence)();
|
|
803
|
+
console.log(chalk_1.default.magenta('\nThanks for using NekoStream CLI! 🎬\n'));
|
|
804
|
+
process.exit(0);
|
|
805
|
+
});
|
|
806
|
+
main().catch(e => {
|
|
807
|
+
console.error(chalk_1.default.red('Fatal Error:'), e);
|
|
808
|
+
});
|