anichi 2.2.5 → 2.3.0

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 CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  <div align="center">
4
4
 
5
- ![Version](https://img.shields.io/badge/version-2.2.3-blue.svg)
5
+ ![Version](https://img.shields.io/badge/version-2.3.0-blue.svg)
6
6
  ![Node](https://img.shields.io/badge/node-%3E%3D16.0.0-green.svg)
7
7
  ![License](https://img.shields.io/badge/license-MIT-orange.svg)
8
8
 
9
- **Ultimate Anime CLI Experience**
9
+ **Streaming Anime CLI By ShDitz**
10
10
 
11
11
  Your modern, feature-rich, and interactive command-line tool for Anime streaming, downloading, and scheduling.
12
12
 
@@ -18,11 +18,16 @@ Your modern, feature-rich, and interactive command-line tool for Anime streaming
18
18
 
19
19
  ## 🌟 Features
20
20
 
21
+ ## 🌟 Features
22
+
21
23
  Anichi is built to provide a premium experience right from your terminal.
22
24
 
23
25
  - **🎨 Modern UI**: Gradient banners, clean tables, and colorful typography using `chalk`, `boxen`, and `figlet`.
24
- - **📅 Interactive Schedule**: View anime airing schedule by day (Senin - Minggu) or search by keyword.
25
- - **🔍 Smart Search**: Quickly find anime by title.
26
+ - **📂 Comprehensive Browsing**:
27
+ - **Ongoing & Completed Anime**: Browse lists with pagination (15 items per page).
28
+ - **Search by Genre**: Filter anime by specific genres.
29
+ - **Schedule**: View anime airing schedule by day (Senin - Minggu).
30
+ - **🔍 Smart Search**: Quickly find anime by title with detailed results.
26
31
  - **📺 Streaming**: Play anime directly with your favorite external media player (MPV, VLC) or fallback to browser.
27
32
  - **⬇️ Downloads**: Support for single episode downloads and **Batch Downloads** with multiple quality and provider options.
28
33
  - **⚡ Performance**: Integrated caching system (`node-cache`) for instant loading of previously accessed data.
@@ -113,11 +118,11 @@ You will be greeted with the main menu banner. From there, you can navigate usin
113
118
 
114
119
  #### 1. Ongoing Anime
115
120
 
116
- Shows currently airing anime with their release day and latest episode.
121
+ Shows currently airing anime with their release day and latest episode. Supports **pagination** for browsing more results.
117
122
 
118
123
  #### 2. Completed Anime
119
124
 
120
- Shows completed anime with their score and last release date.
125
+ Shows completed anime with their score and last release date. Supports **pagination**.
121
126
 
122
127
  #### 3. Search Anime
123
128
 
@@ -125,13 +130,21 @@ Search for any anime by its title.
125
130
 
126
131
  > **Tip**: Type "Naruto" or "One Piece".
127
132
 
128
- #### 4. Schedule Anime
133
+ #### 4. Search by Genre
134
+
135
+ Filter anime by specific genres (Action, Adventure, Comedy, etc.) and browse through **paginated** results.
136
+
137
+ #### 5. Schedule Anime
129
138
 
130
139
  View the broadcast schedule.
131
140
 
132
141
  - **By Number**: Type `1` for Senin, `2` for Selasa, etc.
133
142
  - **By Name**: Type "Senin", "Selasa", or "Minggu" to filter.
134
143
 
144
+ #### 6. FAQ
145
+
146
+ View frequently asked questions and help guides.
147
+
135
148
  ---
136
149
 
137
150
  ### 🎞️ How to Install MPV (Recommended Player)
@@ -156,7 +169,25 @@ choco install mpv
156
169
 
157
170
  #### 3. Verify Installation
158
171
 
159
- Type `mpv` in your terminal. If the player opens, installation is successful. Anichi will automatically detect `C:\Program Files\mpv\mpv.exe` on Windows.
172
+ Type `mpv` in your terminal. If the player opens, installation is successful.
173
+
174
+ ---
175
+
176
+ ### 📼 How to Install yt-dlp (Required for some streams)
177
+
178
+ Some streaming links or sources require `yt-dlp` to fetch the direct video URL. Without it, the player might fail to load certain streams.
179
+
180
+ #### 1. Install yt-dlp using Chocolatey
181
+
182
+ Open **PowerShell** or **CMD** and run:
183
+
184
+ ```bash
185
+ choco install yt-dlp
186
+ ```
187
+
188
+ #### 2. Verify Installation
189
+
190
+ Type `yt-dlp --version` in your terminal. If it outputs a version number, it's installed successfully.
160
191
 
161
192
  ---
162
193
 
package/dist/api.d.ts CHANGED
@@ -1,9 +1,12 @@
1
1
  declare class ApiClient {
2
2
  private client;
3
3
  private cache;
4
+ private lastRequestTime;
4
5
  constructor();
5
6
  private request;
6
7
  getHome(): Promise<any>;
8
+ getOngoing(page?: number): Promise<any>;
9
+ getCompleted(page?: number): Promise<any>;
7
10
  getAnime(slug: string): Promise<any>;
8
11
  getGenre(): Promise<any>;
9
12
  getEpisode(slug: string): Promise<any>;
package/dist/api.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAoBA,cAAM,SAAS;IACb,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,KAAK,CAAY;;YAUX,OAAO;IAwCf,OAAO;IAGP,QAAQ,CAAC,IAAI,EAAE,MAAM;IAGrB,QAAQ;IAGR,UAAU,CAAC,IAAI,EAAE,MAAM;IAGvB,SAAS,CAAC,EAAE,EAAE,MAAM;IAGpB,QAAQ,CAAC,IAAI,EAAE,MAAM;IAGrB,SAAS,CAAC,OAAO,EAAE,MAAM;IAGzB,WAAW;IAIX,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,MAAU;IAIlD,UAAU;CAIX;;AAED,wBAA+B"}
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAwBA,cAAM,SAAS;IACb,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,KAAK,CAAY;IACzB,OAAO,CAAC,eAAe,CAAa;;YAUtB,OAAO;IAgDf,OAAO;IAIP,UAAU,CAAC,IAAI,GAAE,MAAU;IAI3B,YAAY,CAAC,IAAI,GAAE,MAAU;IAI7B,QAAQ,CAAC,IAAI,EAAE,MAAM;IAGrB,QAAQ;IAGR,UAAU,CAAC,IAAI,EAAE,MAAM;IAGvB,SAAS,CAAC,EAAE,EAAE,MAAM;IAGpB,QAAQ,CAAC,IAAI,EAAE,MAAM;IAGrB,SAAS,CAAC,OAAO,EAAE,MAAM;IAGzB,WAAW;IAGX,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,MAAU;IAIlD,UAAU;CAIX;;AAED,wBAA+B"}
package/dist/api.js CHANGED
@@ -10,8 +10,10 @@ const ui_1 = require("./ui");
10
10
  const BASE_URL = "https://www.sankavollerei.com/anime";
11
11
  const CACHE_TTL = 3600;
12
12
  const MAX_RETRIES = 3;
13
+ const MIN_REQUEST_DELAY = 600;
13
14
  class ApiClient {
14
15
  constructor() {
16
+ this.lastRequestTime = 0;
15
17
  this.client = axios_1.default.create({
16
18
  baseURL: BASE_URL,
17
19
  timeout: 60000,
@@ -19,6 +21,13 @@ class ApiClient {
19
21
  this.cache = new node_cache_1.default({ stdTTL: CACHE_TTL, checkperiod: 600 });
20
22
  }
21
23
  async request(url) {
24
+ const now = Date.now();
25
+ const elapsed = now - this.lastRequestTime;
26
+ if (elapsed < MIN_REQUEST_DELAY) {
27
+ const waitTime = MIN_REQUEST_DELAY - elapsed;
28
+ await new Promise((r) => setTimeout(r, waitTime));
29
+ }
30
+ this.lastRequestTime = now;
22
31
  const cached = this.cache.get(url);
23
32
  if (cached)
24
33
  return cached;
@@ -35,7 +44,6 @@ class ApiClient {
35
44
  }
36
45
  if (status !== 200)
37
46
  throw new Error(`HTTP ${status}`);
38
- // Hanya simpan jika success (perbaikan sebelumnya)
39
47
  if (data.ok === true || data.status === "success") {
40
48
  this.cache.set(url, data);
41
49
  }
@@ -57,6 +65,12 @@ class ApiClient {
57
65
  async getHome() {
58
66
  return this.request("/home");
59
67
  }
68
+ async getOngoing(page = 1) {
69
+ return this.request(`/ongoing-anime?page=${page}`);
70
+ }
71
+ async getCompleted(page = 1) {
72
+ return this.request(`/complete-anime?page=${page}`);
73
+ }
60
74
  async getAnime(slug) {
61
75
  return this.request(`/anime/${slug}`);
62
76
  }
@@ -78,7 +92,6 @@ class ApiClient {
78
92
  async getSchedule() {
79
93
  return this.request(`/schedule`);
80
94
  }
81
- // Method Baru: Get Anime by Genre with Pagination
82
95
  async getGenreAnime(slug, page = 1) {
83
96
  return this.request(`/genre/${slug}?page=${page}`);
84
97
  }
package/dist/api.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":";;;;;AAAA,kDAA2C;AAC3C,4DAAmC;AACnC,8CAAsB;AAYtB,6BAA4B;AAE5B,MAAM,QAAQ,GAAG,qCAAqC,CAAC;AACvD,MAAM,SAAS,GAAG,IAAI,CAAC;AACvB,MAAM,WAAW,GAAG,CAAC,CAAC;AAEtB,MAAM,SAAS;IAIb;QACE,IAAI,CAAC,MAAM,GAAG,eAAK,CAAC,MAAM,CAAC;YACzB,OAAO,EAAE,QAAQ;YACjB,OAAO,EAAE,KAAK;SACf,CAAC,CAAC;QACH,IAAI,CAAC,KAAK,GAAG,IAAI,oBAAS,CAAC,EAAC,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,GAAG,EAAC,CAAC,CAAC;IACpE,CAAC;IAEO,KAAK,CAAC,OAAO,CAAC,GAAW;QAC/B,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACnC,IAAI,MAAM;YAAE,OAAO,MAAM,CAAC;QAE1B,MAAM,OAAO,GAAG,IAAA,aAAG,EAAC;YAClB,IAAI,EAAE,YAAY,GAAG,KAAK;YAC1B,KAAK,EAAE,MAAM;SACd,CAAC,CAAC,KAAK,EAAE,CAAC;QAEX,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,WAAW,EAAE,OAAO,EAAE,EAAE,CAAC;YACvD,IAAI,CAAC;gBACH,MAAM,EAAC,IAAI,EAAE,MAAM,EAAC,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;gBAElD,IAAI,IAAI,CAAC,UAAU,KAAK,GAAG,EAAE,CAAC;oBAC5B,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;oBAC1B,OAAO,IAAI,CAAC;gBACd,CAAC;gBAED,IAAI,MAAM,KAAK,GAAG;oBAAE,MAAM,IAAI,KAAK,CAAC,QAAQ,MAAM,EAAE,CAAC,CAAC;gBAEtD,mDAAmD;gBACnD,IAAI,IAAI,CAAC,EAAE,KAAK,IAAI,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;oBAClD,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;gBAC5B,CAAC;gBAED,OAAO,CAAC,OAAO,EAAE,CAAC;gBAClB,OAAO,IAAI,CAAC;YACd,CAAC;YAAC,OAAO,GAAQ,EAAE,CAAC;gBAClB,IAAI,OAAO,KAAK,WAAW,GAAG,CAAC,EAAE,CAAC;oBAChC,OAAO,CAAC,IAAI,EAAE,CAAC;oBACf,MAAM,GAAG,CAAC;gBACZ,CAAC;gBACD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC;gBAC9C,OAAO,CAAC,IAAI,GAAG,SAAS,OAAO,GAAG,CAAC,IAAI,WAAW,KAAK,CAAC;gBACxD,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;YACjD,CAAC;QACH,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC;IACpC,CAAC;IAED,KAAK,CAAC,OAAO;QACX,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/B,CAAC;IACD,KAAK,CAAC,QAAQ,CAAC,IAAY;QACzB,OAAO,IAAI,CAAC,OAAO,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC;IACxC,CAAC;IACD,KAAK,CAAC,QAAQ;QACZ,OAAO,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAChC,CAAC;IACD,KAAK,CAAC,UAAU,CAAC,IAAY;QAC3B,OAAO,IAAI,CAAC,OAAO,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC;IAC1C,CAAC;IACD,KAAK,CAAC,SAAS,CAAC,EAAU;QACxB,OAAO,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;IACvC,CAAC;IACD,KAAK,CAAC,QAAQ,CAAC,IAAY;QACzB,OAAO,IAAI,CAAC,OAAO,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC;IACxC,CAAC;IACD,KAAK,CAAC,SAAS,CAAC,OAAe;QAC7B,OAAO,IAAI,CAAC,OAAO,CAAC,WAAW,OAAO,EAAE,CAAC,CAAC;IAC5C,CAAC;IACD,KAAK,CAAC,WAAW;QACf,OAAO,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IACnC,CAAC;IACD,kDAAkD;IAClD,KAAK,CAAC,aAAa,CAAC,IAAY,EAAE,OAAe,CAAC;QAChD,OAAO,IAAI,CAAC,OAAO,CAAC,UAAU,IAAI,SAAS,IAAI,EAAE,CAAC,CAAC;IACrD,CAAC;IAED,UAAU;QACR,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;QACtB,WAAM,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;IAClC,CAAC;CACF;AAED,kBAAe,IAAI,SAAS,EAAE,CAAC"}
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -46,7 +46,6 @@ const tryGetServer = async (serverId) => {
46
46
  return null;
47
47
  }
48
48
  };
49
- // --- Handle Batch ---
50
49
  const handleBatch = async (animeId, batchData) => {
51
50
  (0, ui_1.clearScreen)();
52
51
  if (!batchData?.downloadUrl?.formats || batchData.downloadUrl.formats.length === 0) {
@@ -58,7 +57,7 @@ const handleBatch = async (animeId, batchData) => {
58
57
  (0, ui_1.createHeader)("Download Batch", "#00ff9f");
59
58
  (0, ui_1.printBatchFormats)(formats);
60
59
  ui_1.logger.br();
61
- ui_1.logger.muted(" Select format (1-" + formats.length + ") or 'back'\n");
60
+ ui_1.logger.muted(" Pilih format (1-" + formats.length + ") atau 'back'\n");
62
61
  const ansFormat = await ask("Format:");
63
62
  const fmtChoice = ansFormat.toLowerCase();
64
63
  if (fmtChoice === "back" || fmtChoice === "0")
@@ -71,11 +70,11 @@ const handleBatch = async (animeId, batchData) => {
71
70
  }
72
71
  const selectedFormat = formats[fIndex];
73
72
  (0, ui_1.clearScreen)();
74
- (0, ui_1.createHeader)(`Select Quality (${selectedFormat.title})`, "#00ff9f");
73
+ (0, ui_1.createHeader)(`Pilih Quality (${selectedFormat.title})`, "#00ff9f");
75
74
  (0, ui_1.printBatchQualities)(selectedFormat.qualities);
76
75
  ui_1.logger.br();
77
- ui_1.logger.muted(" Select quality (1-" + selectedFormat.qualities.length + ") or 'back'\n");
78
- const ansQuality = await ask("Quality:");
76
+ ui_1.logger.muted(" Pilih kualitas (1-" + selectedFormat.qualities.length + ") atau 'back'\n");
77
+ const ansQuality = await ask("Kualitas:");
79
78
  const qChoice = ansQuality.toLowerCase();
80
79
  if (qChoice === "back" || qChoice === "0")
81
80
  return await handleBatch(animeId, batchData);
@@ -87,10 +86,10 @@ const handleBatch = async (animeId, batchData) => {
87
86
  }
88
87
  const selectedQuality = selectedFormat.qualities[qIndex];
89
88
  (0, ui_1.clearScreen)();
90
- (0, ui_1.createHeader)(`Select Provider (${selectedQuality.title})`, "#00ff9f");
89
+ (0, ui_1.createHeader)(`Pilih Provider (${selectedQuality.title})`, "#00ff9f");
91
90
  (0, ui_1.printBatchProviders)(selectedQuality.urls);
92
91
  ui_1.logger.br();
93
- ui_1.logger.muted(" Select provider (1-" + selectedQuality.urls.length + ") or 'back'\n");
92
+ ui_1.logger.muted(" Pilih provider (1-" + selectedQuality.urls.length + ") atau 'back'\n");
94
93
  const ansProv = await ask("Provider:");
95
94
  const pChoice = ansProv.toLowerCase();
96
95
  if (pChoice === "back" || pChoice === "0")
@@ -106,14 +105,13 @@ const handleBatch = async (animeId, batchData) => {
106
105
  await (0, open_1.default)(provider.url);
107
106
  await new Promise((r) => setTimeout(r, 2000));
108
107
  };
109
- // --- Handle Episode Play/Download ---
110
108
  const selectQuality = async (qualities) => {
111
109
  (0, ui_1.clearScreen)();
112
- (0, ui_1.createHeader)("Select Quality", "#ff6b9d");
110
+ (0, ui_1.createHeader)("Pilih Quality", "#ff6b9d");
113
111
  (0, ui_1.printQualityOptions)(qualities);
114
112
  ui_1.logger.br();
115
- ui_1.logger.muted(" Select quality (1-" + qualities.length + ") or 'back'\n");
116
- const answer = await ask("Quality:");
113
+ ui_1.logger.muted(" Pilih kualitas (1-" + qualities.length + ") atau 'back'\n");
114
+ const answer = await ask("Kualitas:");
117
115
  const choice = answer.toLowerCase();
118
116
  if (choice === "back" || choice === "0")
119
117
  return null;
@@ -135,8 +133,8 @@ const handleDownload = async (episodeData) => {
135
133
  (0, ui_1.createHeader)("Download Episode", "#00ff9f");
136
134
  (0, ui_1.printDownloadOptions)(downloads);
137
135
  ui_1.logger.br();
138
- ui_1.logger.muted(" Select quality (1-" + downloads.length + ") or 'back'\n");
139
- const answer = await ask("Quality:");
136
+ ui_1.logger.muted(" Pilih kualitas (1-" + downloads.length + ") atau 'back'\n");
137
+ const answer = await ask("Kualitas:");
140
138
  const choice = answer.toLowerCase();
141
139
  if (choice === "back" || choice === "0")
142
140
  return;
@@ -154,7 +152,7 @@ const handleDownload = async (episodeData) => {
154
152
  console.log(`${num} ${chalk_1.default.white(provider.title)}`);
155
153
  });
156
154
  ui_1.logger.br();
157
- ui_1.logger.muted(" Select provider (1-" + selected.urls.length + ") or 'back'\n");
155
+ ui_1.logger.muted(" Pilih provider (1-" + selected.urls.length + ") atau 'back'\n");
158
156
  const provAnswer = await ask("Provider:");
159
157
  const provChoice = provAnswer.toLowerCase();
160
158
  if (provChoice === "back" || provChoice === "0")
@@ -254,13 +252,13 @@ const showEpisodeMenu = async (slug, data) => {
254
252
  (0, ui_1.createHeader)("Episodes", "#ff6b9d");
255
253
  (0, ui_1.printEpisodeList)(data.episodeList);
256
254
  ui_1.logger.br();
257
- ui_1.logger.muted(" Commands:");
258
- ui_1.logger.muted(" • [number] or 'latest' - Watch episode");
255
+ ui_1.logger.muted(" Perintah:");
256
+ ui_1.logger.muted(" • [nomor] atau 'latest' - Tonton episode");
259
257
  if (data.batch)
260
- ui_1.logger.muted(" • 'b' or 'batch' - Download batch");
261
- ui_1.logger.muted(" • 'd' or 'download' - Download episode");
262
- ui_1.logger.muted(" • 'back' - Return to anime list\n");
263
- const answer = await ask("Command:");
258
+ ui_1.logger.muted(" • 'b' atau 'batch' - Download batch");
259
+ ui_1.logger.muted(" • 'd' atau 'download' - Download episode");
260
+ ui_1.logger.muted(" • 'back' - Kembali ke daftar anime\n");
261
+ const answer = await ask("Perintah:");
264
262
  const choice = answer.toLowerCase();
265
263
  if (choice === "back" || choice === "0")
266
264
  return;
@@ -295,14 +293,14 @@ const showEpisodeMenu = async (slug, data) => {
295
293
  }
296
294
  if (choice === "d" || choice === "download") {
297
295
  (0, ui_1.clearScreen)();
298
- (0, ui_1.createHeader)("Select Episode to Download", "#00ff9f");
296
+ (0, ui_1.createHeader)("Pilih Episode untuk Download", "#00ff9f");
299
297
  (0, ui_1.printEpisodeList)(data.episodeList);
300
298
  ui_1.logger.br();
301
- ui_1.logger.muted(" Type episode number or 'back'\n");
299
+ ui_1.logger.muted(" Ketik nomor episode atau 'back'\n");
302
300
  const epAnswer = await ask("Episode:");
303
301
  const epChoice = epAnswer.toLowerCase();
304
302
  if (epChoice !== "back" && epChoice !== "0") {
305
- const epNum = parseInt(epChoice);
303
+ const epNum = parseInt(epAnswer);
306
304
  if (!isNaN(epNum) || epChoice === "latest") {
307
305
  const episode = resolveEpisode(epChoice === "latest" ? "latest" : epNum, data.episodeList);
308
306
  if (episode) {
@@ -334,43 +332,148 @@ const showAnimeDetail = async (slug) => {
334
332
  ui_1.logger.error("Failed to load anime");
335
333
  }
336
334
  };
337
- const showAnimeList = async (list, title, isOngoing) => {
338
- (0, ui_1.clearScreen)();
339
- (0, ui_1.createHeader)(title, "#00d9ff");
340
- (0, ui_1.printAnimeList)(list, isOngoing);
341
- ui_1.logger.br();
342
- ui_1.logger.muted(` Select anime (1-${list.length}) or 'home' to return\n`);
343
- const answer = await ask("Select:");
344
- const choice = answer.toLowerCase();
345
- if (choice === "home" || choice === "0")
346
- return "home";
347
- const index = parseInt(choice) - 1;
348
- if (!isNaN(index) && index >= 0 && index < list.length) {
349
- await showAnimeDetail(list[index].animeId);
350
- return await showAnimeList(list, title, isOngoing);
335
+ const handleOngoingMenu = async (currentPage) => {
336
+ const spinner = (0, ora_1.default)(`Fetching Ongoing page ${currentPage}...`).start();
337
+ let res;
338
+ try {
339
+ res = await api_1.default.getOngoing(currentPage);
340
+ spinner.stop();
341
+ }
342
+ catch (err) {
343
+ spinner.fail();
344
+ ui_1.logger.error("Failed to fetch ongoing anime");
345
+ await new Promise((r) => setTimeout(r, 2000));
346
+ return await runHome();
347
+ }
348
+ if (res.ok && res.data && res.data.animeList) {
349
+ (0, ui_1.clearScreen)();
350
+ (0, ui_1.createHeader)("Ongoing Anime", "#00d9ff");
351
+ (0, ui_1.printAnimeList)(res.data.animeList, true);
352
+ (0, ui_1.printPaginationControls)(res.pagination, "Ongoing Anime", true);
353
+ const answer = await ask("Perintah:");
354
+ const choice = answer.toLowerCase();
355
+ if (choice === "b" || choice === "back") {
356
+ return await runHome();
357
+ }
358
+ if (choice === "n" || choice === "next") {
359
+ if (res.pagination.hasNextPage) {
360
+ return await handleOngoingMenu(res.pagination.nextPage);
361
+ }
362
+ else {
363
+ ui_1.logger.warn("No next page available");
364
+ return await handleOngoingMenu(currentPage);
365
+ }
366
+ }
367
+ if (choice === "p" || choice === "prev") {
368
+ if (res.pagination.hasPrevPage) {
369
+ return await handleOngoingMenu(res.pagination.prevPage);
370
+ }
371
+ else {
372
+ ui_1.logger.warn("No previous page available");
373
+ return await handleOngoingMenu(currentPage);
374
+ }
375
+ }
376
+ const index = parseInt(choice) - 1;
377
+ if (!isNaN(index) && index >= 0 && index < res.data.animeList.length) {
378
+ await showAnimeDetail(res.data.animeList[index].animeId);
379
+ return await handleOngoingMenu(currentPage);
380
+ }
381
+ else {
382
+ ui_1.logger.warn("Invalid selection");
383
+ return await handleOngoingMenu(currentPage);
384
+ }
385
+ }
386
+ else {
387
+ ui_1.logger.error("No ongoing anime found");
388
+ await new Promise((r) => setTimeout(r, 2000));
389
+ return await runHome();
390
+ }
391
+ };
392
+ const handleCompletedMenu = async (currentPage) => {
393
+ const spinner = (0, ora_1.default)(`Fetching Completed page ${currentPage}...`).start();
394
+ let res;
395
+ try {
396
+ res = await api_1.default.getCompleted(currentPage);
397
+ spinner.stop();
398
+ }
399
+ catch (err) {
400
+ spinner.fail();
401
+ ui_1.logger.error("Failed to fetch completed anime");
402
+ await new Promise((r) => setTimeout(r, 2000));
403
+ return await runHome();
404
+ }
405
+ if (res.ok && res.data && res.data.animeList) {
406
+ (0, ui_1.clearScreen)();
407
+ (0, ui_1.createHeader)("Completed Anime", "#00d9ff");
408
+ (0, ui_1.printAnimeList)(res.data.animeList, false);
409
+ (0, ui_1.printPaginationControls)(res.pagination, "Completed Anime", true);
410
+ const answer = await ask("Perintah:");
411
+ const choice = answer.toLowerCase();
412
+ if (choice === "b" || choice === "back") {
413
+ return await runHome();
414
+ }
415
+ if (choice === "n" || choice === "next") {
416
+ if (res.pagination.hasNextPage) {
417
+ return await handleCompletedMenu(res.pagination.nextPage);
418
+ }
419
+ else {
420
+ ui_1.logger.warn("No next page available");
421
+ return await handleCompletedMenu(currentPage);
422
+ }
423
+ }
424
+ if (choice === "p" || choice === "prev") {
425
+ if (res.pagination.hasPrevPage) {
426
+ return await handleCompletedMenu(res.pagination.prevPage);
427
+ }
428
+ else {
429
+ ui_1.logger.warn("No previous page available");
430
+ return await handleCompletedMenu(currentPage);
431
+ }
432
+ }
433
+ const index = parseInt(choice) - 1;
434
+ if (!isNaN(index) && index >= 0 && index < res.data.animeList.length) {
435
+ await showAnimeDetail(res.data.animeList[index].animeId);
436
+ return await handleCompletedMenu(currentPage);
437
+ }
438
+ else {
439
+ ui_1.logger.warn("Invalid selection");
440
+ return await handleCompletedMenu(currentPage);
441
+ }
442
+ }
443
+ else {
444
+ ui_1.logger.error("No completed anime found");
445
+ await new Promise((r) => setTimeout(r, 2000));
446
+ return await runHome();
351
447
  }
352
- ui_1.logger.warn("Invalid selection");
353
- await new Promise((r) => setTimeout(r, 1000));
354
- return await showAnimeList(list, title, isOngoing);
355
448
  };
356
449
  const handleSearch = async () => {
357
450
  (0, ui_1.clearScreen)();
358
451
  (0, ui_1.createHeader)("Search Anime", "#ff6b9d");
452
+ ui_1.logger.br();
359
453
  const keyword = await ask("Enter keyword (e.g. Naruto):");
360
454
  if (!keyword) {
361
455
  ui_1.logger.warn("Keyword cannot be empty");
362
456
  return await runHome();
363
457
  }
364
458
  const spinner = (0, ora_1.default)("Searching...").start();
365
- const res = await api_1.default.getSearch(keyword);
366
- spinner.stop();
459
+ let res;
460
+ try {
461
+ res = await api_1.default.getSearch(keyword);
462
+ spinner.stop();
463
+ }
464
+ catch (err) {
465
+ spinner.fail();
466
+ ui_1.logger.error("Network error or server issue (500).");
467
+ await new Promise((r) => setTimeout(r, 2000));
468
+ return await runHome();
469
+ }
367
470
  if (res.ok && res.data && res.data.animeList && res.data.animeList.length > 0) {
368
471
  (0, ui_1.clearScreen)();
369
- (0, ui_1.createHeader)("Search Results", "#ff6b9d");
472
+ (0, ui_1.createHeader)("Hasil Search", "#ff6b9d");
370
473
  (0, ui_1.printSearchResults)(res.data.animeList);
371
474
  ui_1.logger.br();
372
- ui_1.logger.muted(` Select anime (1-${res.data.animeList.length}) or 'back'\n`);
373
- const answer = await ask("Select:");
475
+ ui_1.logger.muted(` Pilih anime (1-${res.data.animeList.length}) atau 'back'\n`);
476
+ const answer = await ask("Pilih:");
374
477
  const choice = answer.toLowerCase();
375
478
  if (choice === "back" || choice === "0")
376
479
  return await runHome();
@@ -391,16 +494,29 @@ const handleSearch = async () => {
391
494
  }
392
495
  };
393
496
  const handleGenreMenu = async () => {
497
+ (0, ui_1.clearScreen)();
498
+ (0, ui_1.createHeader)("Pilih Genre", "#ffaa00");
499
+ ui_1.logger.br();
394
500
  const spinner = (0, ora_1.default)("Fetching genres...").start();
395
- const res = await api_1.default.getGenre();
396
- spinner.stop();
501
+ let res;
502
+ try {
503
+ res = await api_1.default.getGenre();
504
+ spinner.stop();
505
+ }
506
+ catch (err) {
507
+ spinner.fail();
508
+ ui_1.logger.error("Failed to fetch genres");
509
+ await new Promise((r) => setTimeout(r, 2000));
510
+ return await runHome();
511
+ }
397
512
  if (res.ok && res.data && res.data.genreList) {
398
513
  (0, ui_1.clearScreen)();
399
- (0, ui_1.createHeader)("Select Genre", "#ffaa00");
514
+ (0, ui_1.createHeader)("Pilih Genre", "#ffaa00");
515
+ ui_1.logger.br();
400
516
  (0, ui_1.printGenreList)(res.data.genreList);
401
517
  ui_1.logger.br();
402
- ui_1.logger.muted(` Select genre (1-${res.data.genreList.length}) or 'back'\n`);
403
- const answer = await ask("Select:");
518
+ ui_1.logger.muted(` Pilih genre (1-${res.data.genreList.length}) atau 'back'\n`);
519
+ const answer = await ask("Pilih:");
404
520
  const choice = answer.toLowerCase();
405
521
  if (choice === "back" || choice === "0")
406
522
  return await runHome();
@@ -422,15 +538,24 @@ const handleGenreMenu = async () => {
422
538
  };
423
539
  const handleGenreAnimeList = async (genreTitle, genreSlug, currentPage) => {
424
540
  const spinner = (0, ora_1.default)(`Fetching page ${currentPage}...`).start();
425
- const res = await api_1.default.getGenreAnime(genreSlug, currentPage);
426
- spinner.stop();
541
+ let res;
542
+ try {
543
+ res = await api_1.default.getGenreAnime(genreSlug, currentPage);
544
+ spinner.stop();
545
+ }
546
+ catch (err) {
547
+ spinner.fail();
548
+ ui_1.logger.error("Network error or server issue (500).");
549
+ await new Promise((r) => setTimeout(r, 2000));
550
+ return await handleGenreMenu();
551
+ }
427
552
  if (res.ok && res.data && res.data.animeList) {
428
553
  (0, ui_1.clearScreen)();
429
554
  (0, ui_1.createHeader)(`Genre: ${genreTitle}`, "#ffaa00");
430
- (0, ui_1.printSearchResults)(res.data.animeList);
431
- // PERBAIKAN: Gunakan res.pagination (root level), bukan res.data.pagination
432
- (0, ui_1.printPaginationControls)(res.pagination, genreTitle);
433
- const answer = await ask("Command:");
555
+ ui_1.logger.br();
556
+ (0, ui_1.printGenreAnimeList)(res.data.animeList);
557
+ (0, ui_1.printPaginationControls)(res.pagination, `: ${genreTitle}`, true);
558
+ const answer = await ask("Perintah:");
434
559
  const choice = answer.toLowerCase();
435
560
  if (choice === "b" || choice === "back") {
436
561
  return await handleGenreMenu();
@@ -470,18 +595,27 @@ const handleGenreAnimeList = async (genreTitle, genreSlug, currentPage) => {
470
595
  };
471
596
  const handleSchedule = async () => {
472
597
  const spinner = (0, ora_1.default)("Fetching schedule...").start();
473
- const res = await api_1.default.getSchedule();
474
- spinner.stop();
598
+ let res;
599
+ try {
600
+ res = await api_1.default.getSchedule();
601
+ spinner.stop();
602
+ }
603
+ catch (err) {
604
+ spinner.fail();
605
+ ui_1.logger.error("Failed to fetch schedule");
606
+ await new Promise((r) => setTimeout(r, 2000));
607
+ return await runHome();
608
+ }
475
609
  if (res && res.status === "success" && res.data) {
476
610
  (0, ui_1.clearScreen)();
477
611
  (0, ui_1.createHeader)("Anime Schedule", "#ffaa00");
478
612
  (0, ui_1.printSchedule)(res.data);
479
613
  ui_1.logger.br();
480
- ui_1.logger.muted(" Commands:");
481
- ui_1.logger.muted(" • [number] - Select day (1-Minggu)");
482
- ui_1.logger.muted(" • [day] - Filter by day (e.g. Senin, Selasa)");
483
- ui_1.logger.muted(" • 'back' - Return to home\n");
484
- const answer = await ask("Command:");
614
+ ui_1.logger.muted(" Perintah:");
615
+ ui_1.logger.muted(" • [nomor] - Pilih hari (1-Minggu)");
616
+ ui_1.logger.muted(" • [hari] - Filter berdasarkan hari (e.g. Senin, Selasa)");
617
+ ui_1.logger.muted(" • 'back' - Kembali ke home\n");
618
+ const answer = await ask("Perintah:");
485
619
  const choice = answer.toLowerCase();
486
620
  if (choice === "back" || choice === "0")
487
621
  return await runHome();
@@ -493,8 +627,8 @@ const handleSchedule = async () => {
493
627
  (0, ui_1.printSchedule)([selectedDay]);
494
628
  ui_1.logger.br();
495
629
  if (selectedDay.anime_list && selectedDay.anime_list.length > 0) {
496
- ui_1.logger.muted(` Select anime (1-${selectedDay.anime_list.length}) or 'back'\n`);
497
- const ansAnime = await ask("Select:");
630
+ ui_1.logger.muted(` Pilih anime (1-${selectedDay.anime_list.length}) atau 'back'\n`);
631
+ const ansAnime = await ask("Pilih:");
498
632
  const aChoice = ansAnime.toLowerCase();
499
633
  if (aChoice === "back" || aChoice === "0")
500
634
  return await handleSchedule();
@@ -515,8 +649,8 @@ const handleSchedule = async () => {
515
649
  (0, ui_1.createHeader)(`Schedule: ${dayData.day}`, "#ffaa00");
516
650
  (0, ui_1.printSchedule)([dayData]);
517
651
  ui_1.logger.br();
518
- ui_1.logger.muted(` Select anime (1-${dayData.anime_list.length}) or 'back'\n`);
519
- const ansAnime = await ask("Select:");
652
+ ui_1.logger.muted(` Pilih anime (1-${dayData.anime_list.length}) atau 'back'\n`);
653
+ const ansAnime = await ask("Pilih:");
520
654
  const aChoice = ansAnime.toLowerCase();
521
655
  if (aChoice === "back" || aChoice === "0")
522
656
  return await handleSchedule();
@@ -541,6 +675,19 @@ const handleSchedule = async () => {
541
675
  return await runHome();
542
676
  }
543
677
  };
678
+ const handleFAQ = async () => {
679
+ (0, ui_1.clearScreen)();
680
+ (0, ui_1.createHeader)("Frequently Asked Questions", "#ffaa00");
681
+ (0, ui_1.printFAQ)();
682
+ const answer = await ask("Perintah:");
683
+ const choice = answer.toLowerCase();
684
+ if (choice === "back" || choice === "0") {
685
+ return await runHome();
686
+ }
687
+ ui_1.logger.warn("Invalid command, returning to home...");
688
+ await new Promise((r) => setTimeout(r, 500));
689
+ return await runHome();
690
+ };
544
691
  const runHome = async () => {
545
692
  (0, ui_1.clearScreen)();
546
693
  (0, ui_1.showBanner)();
@@ -552,28 +699,14 @@ const runHome = async () => {
552
699
  "Search Anime",
553
700
  "Search by Genre",
554
701
  "Schedule Anime",
702
+ "FAQ",
555
703
  ]);
556
- const answer = await ask("Choose:");
557
- const res = await api_1.default.getHome();
704
+ const answer = await ask("Pilih:");
558
705
  if (answer === "1") {
559
- const list = res.data?.ongoing?.animeList || [];
560
- if (list.length === 0) {
561
- ui_1.logger.warn("No ongoing anime");
562
- return;
563
- }
564
- const action = await showAnimeList(list, "Ongoing Anime", true);
565
- if (action === "home")
566
- await runHome();
706
+ await handleOngoingMenu(1);
567
707
  }
568
708
  else if (answer === "2") {
569
- const list = res.data?.completed?.animeList || [];
570
- if (list.length === 0) {
571
- ui_1.logger.warn("No completed anime");
572
- return;
573
- }
574
- const action = await showAnimeList(list, "Completed Anime", false);
575
- if (action === "home")
576
- await runHome();
709
+ await handleCompletedMenu(1);
577
710
  }
578
711
  else if (answer === "3") {
579
712
  await handleSearch();
@@ -584,6 +717,9 @@ const runHome = async () => {
584
717
  else if (answer === "5") {
585
718
  await handleSchedule();
586
719
  }
720
+ else if (answer === "6") {
721
+ await handleFAQ();
722
+ }
587
723
  else {
588
724
  ui_1.logger.warn("Invalid choice");
589
725
  await new Promise((r) => setTimeout(r, 1000));