nekostream 2026.4.1 → 2026.4.2

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.
Files changed (70) hide show
  1. package/README.md +169 -90
  2. package/dist/cli/application.js +24 -0
  3. package/dist/cli/domain-startup.js +85 -0
  4. package/dist/cli/feedback.js +87 -0
  5. package/dist/cli/flows/anime-flow.js +225 -0
  6. package/dist/cli/flows/auth-flow.js +39 -0
  7. package/dist/cli/flows/provider-lists.js +72 -0
  8. package/dist/cli/menus/history-menu.js +65 -0
  9. package/dist/cli/menus/home-menu.js +198 -0
  10. package/dist/cli/menus/settings-menu.js +215 -0
  11. package/dist/cli/update-check.js +82 -0
  12. package/dist/index.js +9 -817
  13. package/dist/player-policy.js +96 -0
  14. package/dist/player.js +7 -1
  15. package/dist/prompts-wrapper.js +9 -6
  16. package/dist/provider-types.js +28 -0
  17. package/dist/providers.js +17 -17
  18. package/dist/scrapers/adblock-policy.js +148 -0
  19. package/dist/scrapers/anilist-api.js +1 -25
  20. package/dist/scrapers/anilist.js +11 -66
  21. package/dist/scrapers/auth/anime47/client.js +224 -0
  22. package/dist/scrapers/auth/anime47/lists.js +185 -0
  23. package/dist/scrapers/auth/anime47/login.js +230 -0
  24. package/dist/scrapers/auth/anime47/stream-interceptor.js +169 -0
  25. package/dist/scrapers/auth/animevietsub/lists.js +209 -0
  26. package/dist/scrapers/auth/animevietsub/login.js +89 -0
  27. package/dist/scrapers/auth/animevietsub/parsers.js +147 -0
  28. package/dist/scrapers/auth/auth-session-service.js +27 -0
  29. package/dist/scrapers/auth/auth-types.js +2 -0
  30. package/dist/scrapers/auth/browser-login.js +173 -0
  31. package/dist/scrapers/auth-service.js +28 -1208
  32. package/dist/scrapers/base.js +1 -1
  33. package/dist/scrapers/crawler.js +2 -17
  34. package/dist/scrapers/domain-registry.js +225 -0
  35. package/dist/scrapers/domain-resolver.js +308 -0
  36. package/dist/scrapers/errors.js +119 -0
  37. package/dist/scrapers/fingerprint.js +4 -14
  38. package/dist/scrapers/html-utils.js +59 -0
  39. package/dist/scrapers/http-client.js +58 -0
  40. package/dist/scrapers/interceptor.js +3 -3
  41. package/dist/scrapers/providers/anime47/parsers/cards.js +243 -0
  42. package/dist/scrapers/providers/anime47/parsers/detail.js +174 -0
  43. package/dist/scrapers/providers/anime47/parsers/episodes.js +160 -0
  44. package/dist/scrapers/providers/anime47/streams/extractors.js +109 -0
  45. package/dist/scrapers/providers/anime47.js +58 -603
  46. package/dist/scrapers/providers/animevietsub/parsers/cards.js +222 -0
  47. package/dist/scrapers/providers/animevietsub/parsers/detail.js +373 -0
  48. package/dist/scrapers/providers/animevietsub/parsers/episodes.js +76 -0
  49. package/dist/scrapers/providers/animevietsub/parsers/schedule.js +95 -0
  50. package/dist/scrapers/providers/animevietsub/streams/extractors.js +350 -0
  51. package/dist/scrapers/providers/animevietsub/streams/server-parser.js +222 -0
  52. package/dist/scrapers/providers/animevietsub.js +107 -1208
  53. package/dist/storage.js +136 -49
  54. package/package.json +26 -8
  55. package/player-main.js +434 -211
  56. package/player.html +123 -123
  57. package/webview-preload.js +465 -0
  58. package/dist/server/app.js +0 -53
  59. package/dist/server/config.js +0 -52
  60. package/dist/server/http.js +0 -78
  61. package/dist/server/index.js +0 -22
  62. package/dist/server/routes/admin.js +0 -39
  63. package/dist/server/routes/health.js +0 -14
  64. package/dist/server/routes/me.js +0 -50
  65. package/dist/server/routes/providers.js +0 -53
  66. package/dist/server/services/cache.js +0 -36
  67. package/dist/server/services/provider-service.js +0 -90
  68. package/dist/server/services/server-token.js +0 -51
  69. package/dist/server/services/session-store.js +0 -52
  70. package/dist/server/services/token-store.js +0 -58
@@ -0,0 +1,225 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.openAnimeMenu = openAnimeMenu;
7
+ exports.showAnimeList = showAnimeList;
8
+ exports.showUserDataList = showUserDataList;
9
+ const chalk_1 = __importDefault(require("chalk"));
10
+ const prompts_wrapper_1 = __importDefault(require("../../prompts-wrapper"));
11
+ const providers_1 = require("../../providers");
12
+ const player_1 = require("../../player");
13
+ const ui_1 = require("../../ui");
14
+ const storage_1 = require("../../storage");
15
+ const discord_1 = require("../../discord");
16
+ const feedback_1 = require("../feedback");
17
+ const USER_LIST_PAGE_SIZE = 20;
18
+ function episodeLabel(episode) {
19
+ return episode.title || `Episode ${episode.number}`;
20
+ }
21
+ function singleLine(value) {
22
+ return value.replace(/\r?\n|\r/g, ' ').trim();
23
+ }
24
+ /** Placeholder detail used when a provider cannot serve its detail page. */
25
+ function fallbackDetail(providerName, animeId, episodes) {
26
+ const firstTitle = episodes.length > 0 ? episodes[0].title : undefined;
27
+ return {
28
+ id: animeId,
29
+ source: providerName,
30
+ title: firstTitle ? firstTitle.split(' - ')[0] : animeId,
31
+ genres: [],
32
+ };
33
+ }
34
+ async function selectEpisode(anime, episodes) {
35
+ (0, ui_1.clearScreen)();
36
+ (0, ui_1.printBanner)(`Provider: ${anime.source.toUpperCase()}`, anime.title);
37
+ (0, ui_1.drawAnimeCard)(anime);
38
+ (0, feedback_1.flushStdin)();
39
+ const choices = episodes.map((episode, index) => ({
40
+ title: `${chalk_1.default.bold(String(index + 1).padStart(2))}. ${singleLine(episodeLabel(episode))}`,
41
+ value: episode,
42
+ }));
43
+ const { episode } = await (0, prompts_wrapper_1.default)({
44
+ type: 'grid',
45
+ name: 'episode',
46
+ message: 'Chọn tập phim (Esc: quay lại)',
47
+ choices,
48
+ });
49
+ return episode;
50
+ }
51
+ async function selectServer(providerName, servers) {
52
+ const { server } = await (0, prompts_wrapper_1.default)({
53
+ type: 'select',
54
+ name: 'server',
55
+ message: 'Chọn server phát (Esc: quay lại)',
56
+ choices: servers.map((entry, index) => {
57
+ const quality = chalk_1.default.gray(`[${entry.quality || 'Auto'}]`);
58
+ const detail = providerName === 'animevietsub'
59
+ ? quality
60
+ : chalk_1.default.gray(`[${entry.quality || 'Auto'}] (${entry.type || 'stream'})`);
61
+ return {
62
+ title: `${chalk_1.default.bold(`${index + 1}.`)} ${entry.name} ${detail}`,
63
+ value: entry,
64
+ };
65
+ }),
66
+ });
67
+ return server;
68
+ }
69
+ async function playEpisode(providerName, anime, episode, streamInfo) {
70
+ (0, ui_1.printSuccess)('\nSẵn sàng phát. Đang mở player...');
71
+ (0, storage_1.saveHistoryEntry)({
72
+ provider: providerName,
73
+ animeId: anime.id,
74
+ animeTitle: anime.title,
75
+ episodeId: episode.id,
76
+ episodeTitle: episodeLabel(episode),
77
+ });
78
+ try {
79
+ (0, discord_1.setWatchingPresence)(anime.title, episodeLabel(episode), providerName);
80
+ await (0, player_1.launchPlayer)(streamInfo);
81
+ (0, discord_1.setBrowsingPresence)('Đang xem thông tin phim', providerName, 'Thông tin Phim', anime.title);
82
+ (0, ui_1.printSuccess)('\nTrình phát đã đóng.');
83
+ await (0, feedback_1.sleep)(500);
84
+ }
85
+ catch (error) {
86
+ await (0, feedback_1.reportFailure)('Lỗi player', error);
87
+ }
88
+ }
89
+ /**
90
+ * Server selection and playback for one episode. Returns when the user backs
91
+ * out or a playback attempt finishes.
92
+ */
93
+ async function runEpisodeSession(providerName, anime, episode) {
94
+ const provider = (0, providers_1.getProvider)(providerName);
95
+ while (true) {
96
+ (0, ui_1.clearScreen)();
97
+ (0, ui_1.printBanner)('Chọn server', anime.title);
98
+ (0, ui_1.drawAnimeInfoCard)(anime, episodeLabel(episode));
99
+ const servers = await (0, feedback_1.withSpinner)('Đang tải server phát...', 'Không tải được server phát', () => provider.getVideoServers(episode.href || episode.id));
100
+ if (servers === undefined)
101
+ return;
102
+ if (servers.length === 0) {
103
+ await (0, feedback_1.reportEmpty)('Không tìm thấy server phát cho tập này.');
104
+ return;
105
+ }
106
+ const server = await selectServer(providerName, servers);
107
+ if (!server)
108
+ return;
109
+ const streamInfo = await (0, feedback_1.withSpinner)('Đang lấy stream URL...', 'Không lấy được stream', () => provider.extractStreamUrl(server));
110
+ if (streamInfo === undefined)
111
+ continue;
112
+ if (!streamInfo || !streamInfo.url) {
113
+ await (0, feedback_1.reportEmpty)('Không lấy được stream URL.');
114
+ continue;
115
+ }
116
+ await playEpisode(providerName, anime, episode, streamInfo);
117
+ return;
118
+ }
119
+ }
120
+ /** Detail view for one anime: browse episodes, pick a server, and play. */
121
+ async function openAnimeMenu(providerName, animeId) {
122
+ (0, discord_1.setBrowsingPresence)('Đang tải thông tin phim', providerName, 'Tải dữ liệu');
123
+ const provider = (0, providers_1.getProvider)(providerName);
124
+ const episodes = await (0, feedback_1.withSpinner)('Đang tải thông tin phim và danh sách tập...', 'Không tải được danh sách tập', () => provider.getEpisodes(animeId));
125
+ if (episodes === undefined)
126
+ return;
127
+ if (episodes.length === 0) {
128
+ (0, ui_1.printEmpty)('\nKhông tìm thấy tập phim nào.');
129
+ await (0, feedback_1.sleep)(feedback_1.NOTICE_DELAY_MS);
130
+ return;
131
+ }
132
+ // A 401 on the detail page must not block episode playback, so failures here
133
+ // fall back to a synthetic detail record.
134
+ let anime = null;
135
+ if (provider.getAnimeDetail) {
136
+ try {
137
+ anime = await provider.getAnimeDetail(animeId);
138
+ }
139
+ catch {
140
+ // Detail unavailable — episodes still work.
141
+ }
142
+ }
143
+ const detail = anime ?? fallbackDetail(providerName, animeId, episodes);
144
+ while (true) {
145
+ (0, discord_1.setBrowsingPresence)('Đang xem thông tin phim', providerName, 'Thông tin Phim', detail.title);
146
+ const episode = await selectEpisode(detail, episodes);
147
+ if (!episode)
148
+ return;
149
+ await runEpisodeSession(providerName, detail, episode);
150
+ }
151
+ }
152
+ /** Searchable list of anime cards from search results or home rails. */
153
+ async function showAnimeList(providerName, title, list) {
154
+ (0, discord_1.setBrowsingPresence)(`Danh sách: ${title}`, providerName, title);
155
+ while (true) {
156
+ (0, ui_1.clearScreen)();
157
+ (0, ui_1.printBanner)(`Provider: ${providerName.toUpperCase()}`, title.toUpperCase());
158
+ (0, feedback_1.flushStdin)();
159
+ const { animeId } = await (0, prompts_wrapper_1.default)({
160
+ type: 'search',
161
+ name: 'animeId',
162
+ message: 'Chọn anime (Esc: quay lại)',
163
+ pageSize: 15,
164
+ choices: list.map((anime, index) => {
165
+ const meta = singleLine(anime.status || anime.year?.toString() || '');
166
+ return {
167
+ title: `${chalk_1.default.bold(String(index + 1).padStart(2))}. ${singleLine(anime.title)}${meta ? chalk_1.default.gray(` - ${meta}`) : ''}`,
168
+ value: anime.id,
169
+ };
170
+ }),
171
+ });
172
+ if (!animeId)
173
+ return;
174
+ await openAnimeMenu(providerName, animeId);
175
+ }
176
+ }
177
+ /** Paginated list of a signed-in user's saved anime, selectable for playback. */
178
+ async function showUserDataList(title, items, providerName) {
179
+ (0, discord_1.setBrowsingPresence)(`Đang xem ${title}`, providerName, title);
180
+ if (items.length === 0) {
181
+ (0, ui_1.printEmpty)('\nDanh sách đang trống.');
182
+ await (0, feedback_1.sleep)(1500);
183
+ return;
184
+ }
185
+ let offset = 0;
186
+ while (true) {
187
+ (0, ui_1.clearScreen)();
188
+ (0, ui_1.printBanner)(title, `${items.length} anime`);
189
+ const page = items.slice(offset, offset + USER_LIST_PAGE_SIZE);
190
+ const choices = page.map((item, index) => {
191
+ const episode = item.episodeNumber ? chalk_1.default.gray(` [Tập ${item.episodeNumber}]`) : '';
192
+ const status = item.status ? chalk_1.default.cyan(` (${item.status})`) : '';
193
+ return {
194
+ title: `${chalk_1.default.bold(String(offset + index + 1).padStart(3))}. ${item.title}${episode}${status}`,
195
+ value: item.animeId,
196
+ };
197
+ });
198
+ const remaining = items.length - offset - USER_LIST_PAGE_SIZE;
199
+ if (remaining > 0) {
200
+ choices.push({ title: chalk_1.default.yellow(`▼ Xem thêm (${remaining} còn lại)`), value: '__next__' });
201
+ }
202
+ if (offset > 0) {
203
+ choices.push({ title: chalk_1.default.yellow('▲ Trang trước'), value: '__prev__' });
204
+ }
205
+ choices.push({ title: chalk_1.default.gray('← Quay lại'), value: '__back__' });
206
+ const { animeId } = await (0, prompts_wrapper_1.default)({
207
+ type: 'search',
208
+ name: 'animeId',
209
+ message: 'Chọn anime để xem (Esc: quay lại)',
210
+ pageSize: USER_LIST_PAGE_SIZE,
211
+ choices,
212
+ });
213
+ if (!animeId || animeId === '__back__')
214
+ return;
215
+ if (animeId === '__next__') {
216
+ offset = Math.min(offset + USER_LIST_PAGE_SIZE, items.length - 1);
217
+ continue;
218
+ }
219
+ if (animeId === '__prev__') {
220
+ offset = Math.max(0, offset - USER_LIST_PAGE_SIZE);
221
+ continue;
222
+ }
223
+ await openAnimeMenu(providerName, animeId);
224
+ }
225
+ }
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.loginToProvider = loginToProvider;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const auth_service_1 = require("../../scrapers/auth-service");
9
+ const ui_1 = require("../../ui");
10
+ const feedback_1 = require("../feedback");
11
+ /**
12
+ * Run the interactive browser login for a provider and report the outcome.
13
+ * Returns whether the session was established.
14
+ */
15
+ async function loginToProvider(provider) {
16
+ try {
17
+ const status = provider === 'animevietsub'
18
+ ? await (0, auth_service_1.loginAnimeVietsubInteractive)()
19
+ : await (0, auth_service_1.loginAnime47Interactive)();
20
+ (0, ui_1.printSuccess)('\nĐăng nhập thành công!');
21
+ if (status.userDisplayName) {
22
+ console.log(chalk_1.default.cyan(` Xin chào, ${status.userDisplayName}!`));
23
+ }
24
+ await (0, feedback_1.sleep)(feedback_1.NOTICE_DELAY_MS);
25
+ return true;
26
+ }
27
+ catch (error) {
28
+ // A cancelled login surfaces as a Playwright timeout, which deserves a
29
+ // clearer message than the raw error text.
30
+ if (error instanceof Error && error.name === 'TimeoutError') {
31
+ console.log(chalk_1.default.red('\nĐăng nhập thất bại/Đã hủy: Hết thời gian chờ (Timeout).'));
32
+ }
33
+ else {
34
+ console.log(chalk_1.default.red(`\nĐăng nhập thất bại: ${(0, feedback_1.formatError)(error)}`));
35
+ }
36
+ await (0, feedback_1.sleep)(feedback_1.NOTICE_DELAY_MS);
37
+ return false;
38
+ }
39
+ }
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PROVIDER_LABELS = void 0;
4
+ exports.listTitle = listTitle;
5
+ exports.openProviderList = openProviderList;
6
+ const auth_service_1 = require("../../scrapers/auth-service");
7
+ const feedback_1 = require("../feedback");
8
+ const anime_flow_1 = require("./anime-flow");
9
+ exports.PROVIDER_LABELS = {
10
+ animevietsub: 'AnimeVietsub',
11
+ anime47: 'Anime47',
12
+ };
13
+ const ANIME47_LIST_TITLES = {
14
+ favorites: 'Yêu thích',
15
+ history: 'Lịch sử xem',
16
+ watching: 'Đang xem',
17
+ completed: 'Hoàn thành',
18
+ plan_to_watch: 'Dự định xem',
19
+ };
20
+ const ANIMEVIETSUB_LIST_TITLES = {
21
+ favorites: 'Hộp phim / Yêu thích',
22
+ history: 'Lịch sử xem',
23
+ };
24
+ function listTitle(provider, request) {
25
+ if (request.kind === 'notifications')
26
+ return 'Thông báo';
27
+ if (provider === 'animevietsub') {
28
+ const action = request.listType === 'history' ? 'history' : 'favorites';
29
+ return ANIMEVIETSUB_LIST_TITLES[action];
30
+ }
31
+ return ANIME47_LIST_TITLES[request.listType];
32
+ }
33
+ function notificationsToItems(result) {
34
+ if (result.notifications) {
35
+ return result.notifications.map((entry) => ({
36
+ animeId: entry.animeId || '',
37
+ title: entry.title,
38
+ url: entry.url,
39
+ thumbnail: entry.thumbnail,
40
+ }));
41
+ }
42
+ return result.items;
43
+ }
44
+ function fetchProviderData(provider, request) {
45
+ if (provider === 'animevietsub') {
46
+ if (request.kind === 'notifications')
47
+ return (0, auth_service_1.fetchAnimeVietsubNotifications)();
48
+ const listType = request.listType === 'history' ? 'history' : 'favorites';
49
+ return (0, auth_service_1.fetchAllAnimeVietsubList)(listType);
50
+ }
51
+ if (request.kind === 'notifications')
52
+ return (0, auth_service_1.fetchAnime47Notifications)();
53
+ return (0, auth_service_1.fetchAllAnime47List)(request.listType === 'favorites' ? 'favorite' : request.listType);
54
+ }
55
+ /**
56
+ * Fetch a provider-side list behind a spinner and hand it to the interactive
57
+ * list view. Titles can be overridden for menus that use shorter labels.
58
+ */
59
+ async function openProviderList(provider, request, title = listTitle(provider, request)) {
60
+ const outcome = await (0, feedback_1.withSilentSpinner)(`Đang tải dữ liệu từ ${exports.PROVIDER_LABELS[provider]}...`, () => fetchProviderData(provider, request));
61
+ if (!outcome.ok) {
62
+ await (0, feedback_1.reportFailure)('Lỗi', outcome.error);
63
+ return;
64
+ }
65
+ const result = outcome.value;
66
+ if (!result.success) {
67
+ await (0, feedback_1.reportFailure)(result.error || 'Không tải được dữ liệu.');
68
+ return;
69
+ }
70
+ const items = request.kind === 'notifications' ? notificationsToItems(result) : result.items;
71
+ await (0, anime_flow_1.showUserDataList)(title, items, provider);
72
+ }
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.showHistoryMenu = showHistoryMenu;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const prompts_wrapper_1 = __importDefault(require("../../prompts-wrapper"));
9
+ const ui_1 = require("../../ui");
10
+ const storage_1 = require("../../storage");
11
+ const discord_1 = require("../../discord");
12
+ const feedback_1 = require("../feedback");
13
+ const anime_flow_1 = require("../flows/anime-flow");
14
+ const ACTION_CLEAR = -2;
15
+ const ACTION_BACK = -1;
16
+ async function confirmClearHistory() {
17
+ const { confirm } = await (0, prompts_wrapper_1.default)({
18
+ type: 'confirm',
19
+ name: 'confirm',
20
+ message: 'Bạn có chắc muốn xóa toàn bộ lịch sử?',
21
+ });
22
+ if (!confirm)
23
+ return;
24
+ (0, storage_1.clearHistory)();
25
+ (0, ui_1.printSuccess)('Đã xóa lịch sử.');
26
+ await (0, feedback_1.sleep)(feedback_1.CONFIRM_DELAY_MS);
27
+ }
28
+ /** Local watch history, newest first, selectable to resume playback. */
29
+ async function showHistoryMenu() {
30
+ (0, discord_1.setBrowsingPresence)('Đang xem Lịch Sử', undefined, 'Lịch sử Toàn cục');
31
+ while (true) {
32
+ (0, ui_1.clearScreen)();
33
+ (0, ui_1.printBanner)('Tiếp tục xem', 'Mở lại bộ phim bạn xem gần đây');
34
+ const history = (0, storage_1.loadHistory)();
35
+ if (history.length === 0) {
36
+ (0, ui_1.printEmpty)('Lịch sử xem đang trống.');
37
+ await (0, feedback_1.sleep)(feedback_1.NOTICE_DELAY_MS);
38
+ return;
39
+ }
40
+ const choices = [{ separator: 'LỊCH SỬ LOCAL' }];
41
+ history.forEach((item, index) => {
42
+ choices.push({
43
+ title: `${chalk_1.default.magenta(item.provider)} | ${chalk_1.default.bold.white(item.animeTitle)} - ${chalk_1.default.cyan(item.episodeTitle)}`,
44
+ description: `Đã xem: ${new Date(item.timestamp).toLocaleString()}`,
45
+ value: index,
46
+ });
47
+ });
48
+ choices.push({ title: chalk_1.default.red('Xóa lịch sử'), description: '', value: ACTION_CLEAR });
49
+ choices.push({ title: chalk_1.default.gray('Quay lại Home'), description: '', value: ACTION_BACK });
50
+ const { selectedIndex } = await (0, prompts_wrapper_1.default)({
51
+ type: 'select',
52
+ name: 'selectedIndex',
53
+ message: 'Chọn tập muốn xem tiếp (Esc: quay lại)',
54
+ choices,
55
+ });
56
+ if (selectedIndex === undefined || selectedIndex === ACTION_BACK)
57
+ return;
58
+ if (selectedIndex === ACTION_CLEAR) {
59
+ await confirmClearHistory();
60
+ continue;
61
+ }
62
+ const item = history[selectedIndex];
63
+ await (0, anime_flow_1.openAnimeMenu)(item.provider, item.animeId);
64
+ }
65
+ }
@@ -0,0 +1,198 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.showHomeMenu = showHomeMenu;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const prompts_wrapper_1 = __importDefault(require("../../prompts-wrapper"));
9
+ const providers_1 = require("../../providers");
10
+ const provider_types_1 = require("../../provider-types");
11
+ const ui_1 = require("../../ui");
12
+ const storage_1 = require("../../storage");
13
+ const discord_1 = require("../../discord");
14
+ const logger_1 = require("../../logger");
15
+ const auth_service_1 = require("../../scrapers/auth-service");
16
+ const feedback_1 = require("../feedback");
17
+ const anime_flow_1 = require("../flows/anime-flow");
18
+ const provider_lists_1 = require("../flows/provider-lists");
19
+ const auth_flow_1 = require("../flows/auth-flow");
20
+ const history_menu_1 = require("./history-menu");
21
+ const settings_menu_1 = require("./settings-menu");
22
+ const HOME_LIST_TITLES = {
23
+ favorites: 'Hộp phim',
24
+ history_provider: 'Lịch sử',
25
+ watching: 'Đang xem',
26
+ completed: 'Hoàn thành',
27
+ plan_to_watch: 'Dự định xem',
28
+ notifications: 'Thông báo',
29
+ };
30
+ /** AnimeHay has no account features, so it reports a permanently anonymous status. */
31
+ function anonymousStatus(provider) {
32
+ return {
33
+ provider: provider,
34
+ loggedIn: false,
35
+ cookieCount: 0,
36
+ hasAuthLikeCookie: false,
37
+ userDisplayName: undefined,
38
+ };
39
+ }
40
+ function resolveAuthStatus(provider) {
41
+ return (0, provider_types_1.isAccountProviderId)(provider) ? (0, auth_service_1.getAuthStatus)(provider) : anonymousStatus(provider);
42
+ }
43
+ function homeListRequest(action) {
44
+ if (action === 'notifications')
45
+ return { kind: 'notifications' };
46
+ const listType = action === 'history_provider' ? 'history' : action;
47
+ return { kind: 'list', listType };
48
+ }
49
+ function accountChoices(provider, status) {
50
+ if (!(0, provider_types_1.isAccountProviderId)(provider))
51
+ return [];
52
+ if (!status.loggedIn) {
53
+ const label = provider === 'anime47' ? 'Đăng nhập Anime47' : 'Đăng nhập AnimeVietsub';
54
+ return [{ title: chalk_1.default.green(label), value: 'login' }];
55
+ }
56
+ const listActions = provider === 'anime47'
57
+ ? ['favorites', 'history_provider', 'watching', 'completed', 'plan_to_watch', 'notifications']
58
+ : ['favorites', 'history_provider', 'notifications'];
59
+ return [
60
+ ...listActions.map((action) => ({
61
+ title: action === 'favorites' && provider === 'anime47'
62
+ ? 'Hộp phim / Yêu thích'
63
+ : HOME_LIST_TITLES[action],
64
+ value: action,
65
+ })),
66
+ { title: chalk_1.default.yellow('Đăng xuất'), value: 'logout' },
67
+ ];
68
+ }
69
+ function homeChoices(provider, status) {
70
+ const username = status.loggedIn ? status.userDisplayName || 'MEMBER' : 'KHÁCH';
71
+ return [
72
+ { separator: 'KHÁM PHÁ' },
73
+ { title: 'Tìm anime', value: 'search', description: 'Tìm theo tên phim' },
74
+ { title: 'Đang thịnh hành', value: 'trending', description: 'Danh sách nổi bật từ provider' },
75
+ { title: 'Mới cập nhật', value: 'latest', description: 'Các tập/phim vừa được cập nhật' },
76
+ { separator: `TÀI KHOẢN : ${username.toUpperCase()}` },
77
+ ...accountChoices(provider, status),
78
+ { separator: 'HỆ THỐNG' },
79
+ { title: 'Tiếp tục xem (Lịch sử Local)', value: 'history' },
80
+ { title: 'Cài đặt hệ thống', value: 'settings' },
81
+ { title: 'Đổi Provider', value: 'change_provider' },
82
+ { title: chalk_1.default.red('Thoát ứng dụng'), value: 'exit' },
83
+ ];
84
+ }
85
+ function renderHome(provider, status) {
86
+ (0, ui_1.clearScreen)();
87
+ const usernameDisplay = status.loggedIn
88
+ ? chalk_1.default.green(`${status.userDisplayName || 'Đã đăng nhập'} (${status.cookieCount || 0} cookies)`)
89
+ : chalk_1.default.red('Chưa đăng nhập');
90
+ (0, ui_1.printBanner)(`Provider: ${provider.toUpperCase()}`, 'Trang chủ');
91
+ (0, ui_1.printStatusStrip)([
92
+ { label: 'Tài khoản', value: usernameDisplay },
93
+ { label: 'Provider', value: chalk_1.default.green(provider) },
94
+ ]);
95
+ (0, logger_1.debugTrace)('home rendered', { provider, loggedIn: status.loggedIn });
96
+ }
97
+ async function runSearch(provider) {
98
+ const { keyword } = await (0, prompts_wrapper_1.default)({
99
+ type: 'text',
100
+ name: 'keyword',
101
+ message: 'Nhập tên anime cần tìm (Esc: quay lại)',
102
+ });
103
+ if (!keyword)
104
+ return;
105
+ const results = await (0, feedback_1.withSpinner)(`Đang tìm "${keyword}" trên ${provider}...`, 'Tìm kiếm thất bại', () => (0, providers_1.getProvider)(provider).search(keyword));
106
+ if (results === undefined)
107
+ return;
108
+ if (results.length === 0) {
109
+ (0, ui_1.printEmpty)('\nKhông tìm thấy kết quả phù hợp.');
110
+ await (0, feedback_1.sleep)(feedback_1.NOTICE_DELAY_MS);
111
+ return;
112
+ }
113
+ (0, discord_1.setBrowsingPresence)(`Đang Tìm kiếm: ${keyword}`, provider, 'Tìm kiếm');
114
+ await (0, anime_flow_1.showAnimeList)(provider, `Kết quả tìm kiếm: ${keyword}`, results);
115
+ }
116
+ async function runHomeRail(provider, rail) {
117
+ const label = rail === 'trending' ? 'thịnh hành' : 'mới cập nhật';
118
+ const results = await (0, feedback_1.withSpinner)(`Đang tải danh sách ${label}...`, `Không tải được danh sách ${label}`, () => (0, providers_1.getProvider)(provider).getHomeCards(rail));
119
+ if (results === undefined)
120
+ return;
121
+ if (results.length === 0) {
122
+ (0, ui_1.printEmpty)(`\nProvider này chưa có danh sách ${label}.`);
123
+ await (0, feedback_1.sleep)(feedback_1.NOTICE_DELAY_MS);
124
+ return;
125
+ }
126
+ const title = rail === 'trending' ? 'Đang thịnh hành' : 'Mới cập nhật';
127
+ (0, discord_1.setBrowsingPresence)(`Đang Xem ${rail === 'trending' ? 'Xu hướng' : 'Cập nhật gần đây'}`, provider, title);
128
+ await (0, anime_flow_1.showAnimeList)(provider, title, results);
129
+ }
130
+ async function changeProvider(current) {
131
+ const { newProvider } = await (0, prompts_wrapper_1.default)({
132
+ type: 'select',
133
+ name: 'newProvider',
134
+ message: 'Chọn provider',
135
+ choices: provider_types_1.PROVIDER_IDS.map((name) => ({ title: name, value: name })),
136
+ });
137
+ return newProvider ?? current;
138
+ }
139
+ function exitApplication() {
140
+ (0, ui_1.clearScreen)();
141
+ (0, discord_1.clearDiscordPresence)();
142
+ console.log(chalk_1.default.magenta('\nCảm ơn bạn đã dùng NekoStream CLI.\n'));
143
+ process.exit(0);
144
+ }
145
+ /** The main home loop. Runs until the user chooses to exit. */
146
+ async function showHomeMenu() {
147
+ let currentProvider = (0, storage_1.loadSettings)().defaultProvider;
148
+ while (true) {
149
+ (0, discord_1.setBrowsingPresence)('Đang lướt Menu Chính');
150
+ const status = resolveAuthStatus(currentProvider);
151
+ renderHome(currentProvider, status);
152
+ const { action } = await (0, prompts_wrapper_1.default)({
153
+ type: 'select',
154
+ name: 'action',
155
+ message: 'Trang chủ',
156
+ choices: homeChoices(currentProvider, status),
157
+ });
158
+ if (!action || action === 'exit')
159
+ exitApplication();
160
+ if ((0, provider_types_1.isHomeListAction)(action)) {
161
+ if ((0, provider_types_1.isAccountProviderId)(currentProvider)) {
162
+ const request = homeListRequest(action);
163
+ await (0, provider_lists_1.openProviderList)(currentProvider, request, HOME_LIST_TITLES[action]);
164
+ }
165
+ continue;
166
+ }
167
+ switch (action) {
168
+ case 'login':
169
+ if ((0, provider_types_1.isAccountProviderId)(currentProvider))
170
+ await (0, auth_flow_1.loginToProvider)(currentProvider);
171
+ break;
172
+ case 'logout':
173
+ if ((0, provider_types_1.isAccountProviderId)(currentProvider))
174
+ (0, auth_service_1.logoutProvider)(currentProvider);
175
+ console.log(chalk_1.default.yellow('\nĐã đăng xuất.'));
176
+ await (0, feedback_1.sleep)(feedback_1.CONFIRM_DELAY_MS);
177
+ break;
178
+ case 'search':
179
+ await runSearch(currentProvider);
180
+ break;
181
+ case 'trending':
182
+ case 'latest':
183
+ await runHomeRail(currentProvider, action);
184
+ break;
185
+ case 'history':
186
+ await (0, history_menu_1.showHistoryMenu)();
187
+ break;
188
+ case 'settings':
189
+ await (0, settings_menu_1.showSettingsMenu)();
190
+ // The default provider may have changed inside the settings menu.
191
+ currentProvider = (0, storage_1.loadSettings)().defaultProvider;
192
+ break;
193
+ case 'change_provider':
194
+ currentProvider = await changeProvider(currentProvider);
195
+ break;
196
+ }
197
+ }
198
+ }