@anjishnusengupta/ny-cli 3.0.2 → 3.1.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/backend.mjs CHANGED
@@ -14,10 +14,24 @@ dns.setServers(["1.1.1.1", "8.8.8.8", "1.0.0.1", "8.8.4.4"]);
14
14
 
15
15
  // Custom lookup: prefer IPv6 (bypasses SNI-based DPI blocking on many ISPs),
16
16
  // fall back to IPv4 via dns.resolve4, then OS resolver as last resort.
17
+ // Note: IPv4-only consistently fails due to ISP blocking; IPv6 is required.
17
18
  function customLookup(hostname, options, callback) {
18
19
  if (typeof options === "function") { callback = options; options = {}; }
20
+
21
+ // Cache resolved addresses per hostname for the lifetime of this process
22
+ const cacheKey = hostname;
23
+ if (customLookup._cache && customLookup._cache[cacheKey]) {
24
+ const cached = customLookup._cache[cacheKey];
25
+ if (options.all) {
26
+ return callback(null, [{ address: cached.address, family: cached.family }]);
27
+ }
28
+ return callback(null, cached.address, cached.family);
29
+ }
30
+
19
31
  dns.resolve6(hostname, (err6, addr6) => {
20
32
  if (!err6 && addr6 && addr6.length > 0) {
33
+ if (!customLookup._cache) customLookup._cache = {};
34
+ customLookup._cache[cacheKey] = { address: addr6[0], family: 6 };
21
35
  if (options.all) {
22
36
  return callback(null, addr6.map(a => ({ address: a, family: 6 })));
23
37
  }
@@ -25,6 +39,8 @@ function customLookup(hostname, options, callback) {
25
39
  }
26
40
  dns.resolve4(hostname, (err4, addr4) => {
27
41
  if (!err4 && addr4 && addr4.length > 0) {
42
+ if (!customLookup._cache) customLookup._cache = {};
43
+ customLookup._cache[cacheKey] = { address: addr4[0], family: 4 };
28
44
  if (options.all) {
29
45
  return callback(null, addr4.map(a => ({ address: a, family: 4 })));
30
46
  }
@@ -35,9 +51,10 @@ function customLookup(hostname, options, callback) {
35
51
  });
36
52
  }
37
53
 
38
- // Replace global agents so ALL http/https requests use our DNS
39
- http.globalAgent = new http.Agent({ lookup: customLookup });
40
- https.globalAgent = new https.Agent({ lookup: customLookup });
54
+ // Replace global agents so ALL http/https requests use our DNS.
55
+ // keepAlive reduces connection overhead for multiple requests to the same host.
56
+ http.globalAgent = new http.Agent({ lookup: customLookup, keepAlive: true });
57
+ https.globalAgent = new https.Agent({ lookup: customLookup, keepAlive: true });
41
58
 
42
59
  // Dynamic import so aniwatch picks up patched global agents
43
60
  const { HiAnime } = await import("aniwatch");
@@ -46,6 +63,22 @@ const hianime = new HiAnime.Scraper();
46
63
 
47
64
  const action = process.argv[2];
48
65
 
66
+ // Retry helper: retries an async fn up to `retries` times with a delay between attempts.
67
+ async function withRetry(fn, { retries = 2, delay = 1000, label = "" } = {}) {
68
+ let lastErr;
69
+ for (let i = 0; i <= retries; i++) {
70
+ try {
71
+ return await fn();
72
+ } catch (err) {
73
+ lastErr = err;
74
+ if (i < retries) {
75
+ await new Promise(r => setTimeout(r, delay));
76
+ }
77
+ }
78
+ }
79
+ throw lastErr;
80
+ }
81
+
49
82
  async function main() {
50
83
  try {
51
84
  switch (action) {
@@ -57,14 +90,14 @@ async function main() {
57
90
  console.log(JSON.stringify({ error: "Missing search query" }));
58
91
  process.exit(1);
59
92
  }
60
- const data = await hianime.search(query, page);
93
+ const data = await withRetry(() => hianime.search(query, page), { label: "search" });
61
94
  console.log(JSON.stringify({ success: true, data }));
62
95
  break;
63
96
  }
64
97
 
65
98
  // ── Home / Trending ──
66
99
  case "home": {
67
- const data = await hianime.getHomePage();
100
+ const data = await withRetry(() => hianime.getHomePage(), { label: "home" });
68
101
  console.log(JSON.stringify({ success: true, data }));
69
102
  break;
70
103
  }
@@ -76,7 +109,7 @@ async function main() {
76
109
  console.log(JSON.stringify({ error: "Missing anime id" }));
77
110
  process.exit(1);
78
111
  }
79
- const data = await hianime.getInfo(animeId);
112
+ const data = await withRetry(() => hianime.getInfo(animeId), { label: "info" });
80
113
  console.log(JSON.stringify({ success: true, data }));
81
114
  break;
82
115
  }
@@ -88,7 +121,7 @@ async function main() {
88
121
  console.log(JSON.stringify({ error: "Missing anime id" }));
89
122
  process.exit(1);
90
123
  }
91
- const data = await hianime.getEpisodes(animeId);
124
+ const data = await withRetry(() => hianime.getEpisodes(animeId), { label: "episodes" });
92
125
  console.log(JSON.stringify({ success: true, data }));
93
126
  break;
94
127
  }
@@ -100,12 +133,12 @@ async function main() {
100
133
  console.log(JSON.stringify({ error: "Missing episode id" }));
101
134
  process.exit(1);
102
135
  }
103
- const data = await hianime.getEpisodeServers(episodeId);
136
+ const data = await withRetry(() => hianime.getEpisodeServers(episodeId), { label: "servers" });
104
137
  console.log(JSON.stringify({ success: true, data }));
105
138
  break;
106
139
  }
107
140
 
108
- // ── Episode Sources (with multi-server fallback) ──
141
+ // ── Episode Sources (with sequential server fallback + retry) ──
109
142
  case "sources": {
110
143
  const episodeId = process.argv[3];
111
144
  const category = process.argv[4] || "sub";
@@ -114,35 +147,41 @@ async function main() {
114
147
  process.exit(1);
115
148
  }
116
149
 
117
- // Faster timeout — self-hosted scraping is local, no cold-start
118
- const PER_SERVER_TIMEOUT = 8000;
150
+ // Increased timeout — source extraction can take 10-15s on slow connections
151
+ const PER_SERVER_TIMEOUT = 20000;
119
152
 
120
- // Helper: try a single server with timeout
121
- const tryServer = (server, cat) =>
122
- Promise.race([
123
- hianime.getEpisodeSources(episodeId, server, cat),
153
+ // Helper: try a single server with timeout + retry
154
+ const tryServer = async (server, cat) => {
155
+ const srcData = await Promise.race([
156
+ withRetry(
157
+ () => hianime.getEpisodeSources(episodeId, server, cat),
158
+ { retries: 1, delay: 800, label: `sources-${server}` }
159
+ ),
124
160
  new Promise((_, reject) =>
125
161
  setTimeout(() => reject(new Error(`${server} timed out`)), PER_SERVER_TIMEOUT)
126
162
  ),
127
- ]).then((srcData) => {
128
- if (srcData?.sources?.length > 0) {
129
- srcData._usedServer = server;
130
- srcData._usedCategory = cat;
131
- return srcData;
132
- }
133
- throw new Error(`${server}: no sources`);
134
- });
163
+ ]);
164
+ if (srcData?.sources?.length > 0) {
165
+ srcData._usedServer = server;
166
+ srcData._usedCategory = cat;
167
+ return srcData;
168
+ }
169
+ throw new Error(`${server}: no sources`);
170
+ };
135
171
 
136
172
  // Preferred order: hd-1/hd-2 are fastest, then others
137
173
  const preferredOrder = ["hd-1", "hd-2", "streamtape", "streamsb"];
138
174
 
139
- // Race all servers in parallel first success wins
140
- const raceServers = async (cat) => {
175
+ // Try servers SEQUENTIALLY to avoid rate-limiting (403 errors)
176
+ const tryServersSequentially = async (cat) => {
141
177
  let availableServers;
142
178
  try {
143
179
  const serverData = await Promise.race([
144
- hianime.getEpisodeServers(episodeId),
145
- new Promise((_, reject) => setTimeout(() => reject(new Error("server list timeout")), 5000)),
180
+ withRetry(
181
+ () => hianime.getEpisodeServers(episodeId),
182
+ { retries: 1, delay: 500, label: "server-list" }
183
+ ),
184
+ new Promise((_, reject) => setTimeout(() => reject(new Error("server list timeout")), 8000)),
146
185
  ]);
147
186
  const serverList = cat === "dub" ? serverData.dub : serverData.sub;
148
187
  availableServers = (serverList || []).map((s) => s.serverName);
@@ -153,16 +192,25 @@ async function main() {
153
192
  const serversToTry = preferredOrder.filter((s) => availableServers.includes(s));
154
193
  if (serversToTry.length === 0) serversToTry.push("hd-1");
155
194
 
156
- // Promise.any resolves as soon as ANY server succeeds
157
- return Promise.any(serversToTry.map((s) => tryServer(s, cat))).then((srcData) => {
158
- srcData._availableServers = availableServers;
159
- srcData._triedServers = serversToTry;
160
- return srcData;
161
- });
195
+ // Try each server one at a time — avoids rate-limiting
196
+ let lastError;
197
+ for (const server of serversToTry) {
198
+ try {
199
+ const srcData = await tryServer(server, cat);
200
+ srcData._availableServers = availableServers;
201
+ srcData._triedServers = serversToTry;
202
+ return srcData;
203
+ } catch (err) {
204
+ lastError = err;
205
+ // Small delay between servers to avoid triggering rate limits
206
+ await new Promise(r => setTimeout(r, 300));
207
+ }
208
+ }
209
+ throw lastError || new Error("No servers available");
162
210
  };
163
211
 
164
212
  try {
165
- const srcData = await raceServers(category);
213
+ const srcData = await tryServersSequentially(category);
166
214
  console.log(JSON.stringify({ success: true, data: srcData }));
167
215
  return;
168
216
  } catch {
@@ -172,7 +220,7 @@ async function main() {
172
220
  // If sub failed, try dub as fallback
173
221
  if (category === "sub") {
174
222
  try {
175
- const srcData = await raceServers("dub");
223
+ const srcData = await tryServersSequentially("dub");
176
224
  console.log(JSON.stringify({ success: true, data: srcData }));
177
225
  return;
178
226
  } catch {
@@ -192,7 +240,7 @@ async function main() {
192
240
  console.log(JSON.stringify({ error: "Missing query" }));
193
241
  process.exit(1);
194
242
  }
195
- const data = await hianime.searchSuggestions(query);
243
+ const data = await withRetry(() => hianime.searchSuggestions(query), { label: "suggestions" });
196
244
  console.log(JSON.stringify({ success: true, data }));
197
245
  break;
198
246
  }
@@ -205,7 +253,7 @@ async function main() {
205
253
  console.log(JSON.stringify({ error: "Missing category name" }));
206
254
  process.exit(1);
207
255
  }
208
- const data = await hianime.getCategoryAnime(name, page);
256
+ const data = await withRetry(() => hianime.getCategoryAnime(name, page), { label: "category" });
209
257
  console.log(JSON.stringify({ success: true, data }));
210
258
  break;
211
259
  }
package/install.sh CHANGED
@@ -25,7 +25,7 @@ printf " ╚═╝ ╚═══╝ ╚═╝ ╚═════╝
25
25
  printf "${RESET}\n"
26
26
  printf "${DIM}${CYAN} ⟨ Your Gateway to Anime Streaming ⟩${RESET}\n"
27
27
  printf "${DIM} ─────────────────────────────────────${RESET}\n"
28
- printf "${DIM} v3.0.2 • nyanime.tech${RESET}\n"
28
+ printf "${DIM} v3.1.0 • nyanime.tech${RESET}\n"
29
29
  printf "\n"
30
30
 
31
31
  REPO_URL="https://raw.githubusercontent.com/AnjishnuSengupta/ny-cli/main"
@@ -155,7 +155,7 @@ printf "\n"
155
155
  printf "${GREEN}╭────────────────────────────────────────╮${RESET}\n"
156
156
  printf "${GREEN}│${RESET} ${WHITE}Installation complete!${RESET} 🎉 ${GREEN}│${RESET}\n"
157
157
  printf "${GREEN}├────────────────────────────────────────┤${RESET}\n"
158
- printf "${GREEN}│${RESET} Run ${CYAN}ny-cli${RESET} to start watching anime ${GREEN}│${RESET}\n"
159
- printf "${GREEN}│${RESET} Run ${CYAN}ny-cli --help${RESET} for options ${GREEN}│${RESET}\n"
158
+ printf "${GREEN}│${RESET} Run ${CYAN}ny-cli${RESET} to start watching anime ${GREEN}│${RESET}\n"
159
+ printf "${GREEN}│${RESET} Run ${CYAN}ny-cli --help${RESET} for options ${GREEN}│${RESET}\n"
160
160
  printf "${GREEN}╰────────────────────────────────────────╯${RESET}\n"
161
161
  printf "\n"
package/ny-cli CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/bin/sh
2
2
  # ══════════════════════════════════════════════════════════════════════════════
3
3
  # NY-CLI - Terminal-based Anime Streaming Client
4
- # Version: 3.0.2
4
+ # Version: 3.1.0
5
5
  # Author: Anjishnu Sengupta
6
6
  # Website: https://nyanime.tech
7
7
  # Repository: https://github.com/AnjishnuSengupta/ny-cli
@@ -10,7 +10,7 @@
10
10
  # Just install and run - no external API dependency!
11
11
  # ══════════════════════════════════════════════════════════════════════════════
12
12
 
13
- VERSION="3.0.2"
13
+ VERSION="3.1.0"
14
14
 
15
15
  # ══════════════════════════════════════════════════════════════════════════════
16
16
  # BACKEND CONFIGURATION (Self-hosted via aniwatch npm package)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anjishnusengupta/ny-cli",
3
- "version": "3.0.2",
3
+ "version": "3.1.0",
4
4
  "description": "Terminal-based anime streaming client — self-hosted scraping via aniwatch",
5
5
  "main": "backend.mjs",
6
6
  "bin": {
package/3.0.2 DELETED
File without changes