@anjishnusengupta/ny-cli 3.1.0 → 4.0.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 +43 -35
- package/backend.mjs +137 -39
- package/install.sh +1 -1
- package/ny-cli +109 -64
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
<br/>
|
|
8
8
|
|
|
9
|
-
[](https://github.com/AnjishnuSengupta/ny-cli/releases)
|
|
10
10
|
[](https://www.npmjs.com/package/@anjishnusengupta/ny-cli)
|
|
11
11
|
[](LICENSE)
|
|
12
12
|
[](https://github.com/AnjishnuSengupta/ny-cli/stargazers)
|
|
@@ -26,33 +26,33 @@
|
|
|
26
26
|
|
|
27
27
|
<br/>
|
|
28
28
|
|
|
29
|
-
## 🎯 What's New in
|
|
29
|
+
## 🎯 What's New in v4.0.0
|
|
30
30
|
|
|
31
31
|
<table>
|
|
32
32
|
<tr>
|
|
33
|
-
<td
|
|
34
|
-
<td><b>
|
|
35
|
-
<td>
|
|
33
|
+
<td>🎙️</td>
|
|
34
|
+
<td><b>Sub / Dub Selection</b></td>
|
|
35
|
+
<td>Choose between sub (Japanese + subtitles) or dub (English audio) when you select an anime — preference is saved per anime</td>
|
|
36
36
|
</tr>
|
|
37
37
|
<tr>
|
|
38
38
|
<td>⚡</td>
|
|
39
|
-
<td><b>
|
|
40
|
-
<td
|
|
39
|
+
<td><b>Background Episode Fetching</b></td>
|
|
40
|
+
<td>Episodes load in the background while you pick sub or dub — zero extra wait time</td>
|
|
41
41
|
</tr>
|
|
42
42
|
<tr>
|
|
43
|
-
<td
|
|
44
|
-
<td><b>
|
|
45
|
-
<td>
|
|
43
|
+
<td>🌐</td>
|
|
44
|
+
<td><b>Global Network Compatibility</b></td>
|
|
45
|
+
<td>Happy Eyeballs (RFC 6555) dual-stack support — works on IPv4-only, IPv6-only, or dual-stack networks worldwide</td>
|
|
46
46
|
</tr>
|
|
47
47
|
<tr>
|
|
48
|
-
<td
|
|
49
|
-
<td><b>
|
|
50
|
-
<td>
|
|
48
|
+
<td>🛡️</td>
|
|
49
|
+
<td><b>Resilient Connectivity</b></td>
|
|
50
|
+
<td>Mixed IPv4/IPv6 DNS servers, 3-endpoint online check, exponential-backoff retries, crash protection for transient socket errors</td>
|
|
51
51
|
</tr>
|
|
52
52
|
<tr>
|
|
53
53
|
<td>☁️</td>
|
|
54
|
-
<td><b>Cloud Sync</b></td>
|
|
55
|
-
<td>
|
|
54
|
+
<td><b>Category-Aware Cloud Sync</b></td>
|
|
55
|
+
<td>Sub/dub preference syncs to <a href="https://nyanime.tech">nyanime.tech</a> — Continue Watching remembers your choice</td>
|
|
56
56
|
</tr>
|
|
57
57
|
</table>
|
|
58
58
|
|
|
@@ -74,9 +74,9 @@
|
|
|
74
74
|
│ │
|
|
75
75
|
│ ▸ HLS Streaming ▸ User Accounts ▸ POSIX Shell │
|
|
76
76
|
│ ▸ Multi-Server ▸ Watch History ▸ Node.js 18+ │
|
|
77
|
-
│ ▸ Sub/Dub
|
|
77
|
+
│ ▸ Sub/Dub Select ▸ Cloud Sync ▸ aniwatch pkg │
|
|
78
78
|
│ ▸ Skip Intro ▸ Continue Watch ▸ Self-Hosted │
|
|
79
|
-
│ ▸ Auto Subtitles ▸ Random Anime ▸
|
|
79
|
+
│ ▸ Auto Subtitles ▸ Random Anime ▸ Dual-Stack │
|
|
80
80
|
│ ▸ MPV/VLC/IINA ▸ Profile System ▸ Zero Config │
|
|
81
81
|
│ │
|
|
82
82
|
╰─────────────────────────────────────────────────────────────────╯
|
|
@@ -93,10 +93,12 @@
|
|
|
93
93
|
|
|
94
94
|
| Feature | Description |
|
|
95
95
|
|:--------|:------------|
|
|
96
|
+
| **🎙️ Sub/Dub Selection** | Choose sub or dub per anime — preference saved and synced to cloud |
|
|
96
97
|
| **🔄 Multi-Server Racing** | Races HD-1, HD-2, StreamTape, StreamSB in parallel via `Promise.any()` |
|
|
97
98
|
| **⏭️ Skip Intro** | Press `s` during intro to skip — uses API-provided timestamps |
|
|
98
99
|
| **📝 Multi-Language Subs** | Auto-selects English, with all available languages loaded for switching |
|
|
99
100
|
| **🔁 Sub/Dub Fallback** | If all sub servers fail, automatically retries with dub |
|
|
101
|
+
| **🌐 Dual-Stack Networking** | Happy Eyeballs (RFC 6555) — works on any IPv4, IPv6, or dual-stack network |
|
|
100
102
|
| **🎚️ Player Support** | MPV (recommended), VLC, IINA — auto-detected or configurable |
|
|
101
103
|
|
|
102
104
|
</details>
|
|
@@ -111,8 +113,8 @@
|
|
|
111
113
|
| **🔐 Browser Auth** | Login via nyanime.tech — just paste your User ID |
|
|
112
114
|
| **📜 Watch History** | Track all watched episodes with timestamps |
|
|
113
115
|
| **☁️ Cloud Sync** | Seamless sync between CLI and nyanime.tech website |
|
|
114
|
-
| **📍 Continue Watching** | Resume from
|
|
115
|
-
| **🎲 Random Mode** | Discover new anime with random selection |
|
|
116
|
+
| **📍 Continue Watching** | Resume from where you left off — remembers your sub/dub choice |
|
|
117
|
+
| **🎲 Random Mode** | Discover new anime with random selection + sub/dub prompt |
|
|
116
118
|
| **🔍 Quick Search** | Search directly from command line or interactive menu |
|
|
117
119
|
|
|
118
120
|
</details>
|
|
@@ -128,22 +130,28 @@
|
|
|
128
130
|
<div align="center">
|
|
129
131
|
|
|
130
132
|
```
|
|
131
|
-
|
|
132
|
-
║
|
|
133
|
-
║ $ ny-cli "one piece"
|
|
134
|
-
║
|
|
135
|
-
║ Searching for 'one piece'...
|
|
136
|
-
║ 1) One Piece (TV,
|
|
137
|
-
║ 2) One Piece Film: Red (Movie)
|
|
138
|
-
║
|
|
139
|
-
║ Select [1-20]: 1
|
|
140
|
-
║ Loading episodes...
|
|
141
|
-
║
|
|
142
|
-
║
|
|
143
|
-
║
|
|
144
|
-
║
|
|
145
|
-
║
|
|
146
|
-
|
|
133
|
+
╔══════════════════════════════════════════════╗
|
|
134
|
+
║ ║
|
|
135
|
+
║ $ ny-cli "one piece" ║
|
|
136
|
+
║ ║
|
|
137
|
+
║ Searching for 'one piece'... ║
|
|
138
|
+
║ 1) One Piece (TV, 1155 sub / 1143 dub) ║
|
|
139
|
+
║ 2) One Piece Film: Red (Movie, 1 sub) ║
|
|
140
|
+
║ ║
|
|
141
|
+
║ Select [1-20]: 1 ║
|
|
142
|
+
║ Loading episodes... ║
|
|
143
|
+
║ ║
|
|
144
|
+
║ 1) Sub (Japanese audio + subtitles) ║
|
|
145
|
+
║ 2) Dub (English audio) ║
|
|
146
|
+
║ Sub or Dub? [1/2]: 1 ║
|
|
147
|
+
║ ║
|
|
148
|
+
║ One Piece (1155 eps) [SUB] ║
|
|
149
|
+
║ Episode [1-1155]: 1155 ║
|
|
150
|
+
║ ║
|
|
151
|
+
║ ▸ Starting playback... ║
|
|
152
|
+
║ One Piece - Episode 1155 [SUB] ║
|
|
153
|
+
║ ║
|
|
154
|
+
╚══════════════════════════════════════════════╝
|
|
147
155
|
```
|
|
148
156
|
|
|
149
157
|
</div>
|
package/backend.mjs
CHANGED
|
@@ -7,54 +7,141 @@
|
|
|
7
7
|
import dns from "node:dns";
|
|
8
8
|
import https from "node:https";
|
|
9
9
|
import http from "node:http";
|
|
10
|
+
import os from "node:os";
|
|
11
|
+
|
|
12
|
+
// ── Safety net: prevent unhandled errors from crashing the process ──────
|
|
13
|
+
// The aniwatch library can emit socket errors that aren't caught internally.
|
|
14
|
+
process.on("uncaughtException", (err) => {
|
|
15
|
+
// Silently ignore connection errors during retry/fallback — they're expected
|
|
16
|
+
if (["ENETUNREACH", "ECONNREFUSED", "ECONNRESET", "ETIMEDOUT", "EHOSTUNREACH", "EAI_AGAIN"].includes(err.code)) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
// For other errors, output JSON so the shell script can parse it
|
|
20
|
+
console.log(JSON.stringify({ error: err.message || "Unexpected error" }));
|
|
21
|
+
process.exit(1);
|
|
22
|
+
});
|
|
10
23
|
|
|
11
24
|
// Use public DNS (Cloudflare + Google) to bypass ISP-level domain blocking.
|
|
12
|
-
//
|
|
13
|
-
|
|
25
|
+
// Include BOTH IPv4 and IPv6 DNS server addresses so resolution works on:
|
|
26
|
+
// - IPv4-only WiFi / hotspots
|
|
27
|
+
// - IPv6-only carrier networks (T-Mobile, etc.)
|
|
28
|
+
// - Dual-stack environments
|
|
29
|
+
// Order: put the family matching the system first to minimise latency.
|
|
30
|
+
const DNS_V4 = ["1.1.1.1", "8.8.8.8", "1.0.0.1", "8.8.4.4"];
|
|
31
|
+
const DNS_V6 = ["2606:4700:4700::1111", "2001:4860:4860::8888", "2606:4700:4700::1001", "2001:4860:4860::8844"];
|
|
14
32
|
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
33
|
+
// ── Detect system IPv6 connectivity ─────────────────────────────────────
|
|
34
|
+
// Check if the system has a routable (non-link-local, non-loopback) IPv6 address.
|
|
35
|
+
// If there's no IPv6 address, connecting to IPv6 endpoints will fail with ENETUNREACH.
|
|
36
|
+
function systemHasIPv6() {
|
|
37
|
+
try {
|
|
38
|
+
const interfaces = os.networkInterfaces();
|
|
39
|
+
for (const name of Object.keys(interfaces)) {
|
|
40
|
+
for (const addr of interfaces[name]) {
|
|
41
|
+
if (
|
|
42
|
+
addr.family === "IPv6" &&
|
|
43
|
+
!addr.internal &&
|
|
44
|
+
!addr.address.startsWith("fe80") && // skip link-local
|
|
45
|
+
!addr.address.startsWith("::1") // skip loopback
|
|
46
|
+
) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch {}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const HAS_IPV6 = systemHasIPv6();
|
|
56
|
+
|
|
57
|
+
// ── Configure DNS servers based on system network capabilities ────────
|
|
58
|
+
// On IPv6-only networks, IPv4 DNS servers (1.1.1.1) are unreachable.
|
|
59
|
+
// On IPv4-only WiFi, IPv6 DNS servers are unreachable.
|
|
60
|
+
// Order the server list so reachable servers come first.
|
|
61
|
+
try {
|
|
62
|
+
const servers = HAS_IPV6
|
|
63
|
+
? [...DNS_V6, ...DNS_V4] // IPv6 DNS first, then IPv4 fallback
|
|
64
|
+
: [...DNS_V4, ...DNS_V6]; // IPv4 DNS first (most common)
|
|
65
|
+
dns.setServers(servers);
|
|
66
|
+
} catch {
|
|
67
|
+
// If setServers fails (unusual), leave the OS defaults in place.
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── DNS resolution with timeout ─────────────────────────────────────────
|
|
71
|
+
// Wraps dns.resolve{4,6} with a timeout to prevent hanging on unresponsive DNS.
|
|
72
|
+
function resolveWithTimeout(resolver, hostname, timeoutMs = 3000) {
|
|
73
|
+
return new Promise((resolve) => {
|
|
74
|
+
const timer = setTimeout(() => resolve([]), timeoutMs);
|
|
75
|
+
resolver(hostname, (err, addrs) => {
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
resolve(err || !addrs ? [] : addrs);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Custom DNS lookup (Happy Eyeballs compatible) ───────────────────────
|
|
83
|
+
// When called with `all: true` (by Node.js autoSelectFamily / Happy Eyeballs),
|
|
84
|
+
// returns addresses from BOTH families so Node can race connections and pick
|
|
85
|
+
// whichever connects first. When called for a single address, returns the
|
|
86
|
+
// best-available family based on system capability.
|
|
18
87
|
function customLookup(hostname, options, callback) {
|
|
19
88
|
if (typeof options === "function") { callback = options; options = {}; }
|
|
20
89
|
|
|
21
|
-
|
|
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
|
-
}
|
|
90
|
+
const wantAll = !!options.all;
|
|
30
91
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (options.all) {
|
|
45
|
-
return callback(null, addr4.map(a => ({ address: a, family: 4 })));
|
|
46
|
-
}
|
|
47
|
-
return callback(null, addr4[0], 4);
|
|
92
|
+
if (wantAll) {
|
|
93
|
+
// ── Happy Eyeballs path: resolve both families in parallel ──
|
|
94
|
+
Promise.all([
|
|
95
|
+
HAS_IPV6 ? resolveWithTimeout(dns.resolve6.bind(dns), hostname) : Promise.resolve([]),
|
|
96
|
+
resolveWithTimeout(dns.resolve4.bind(dns), hostname),
|
|
97
|
+
]).then(([v6, v4]) => {
|
|
98
|
+
const results = [];
|
|
99
|
+
// IPv6 first (preferred when available), then IPv4
|
|
100
|
+
for (const a of v6) results.push({ address: a, family: 6 });
|
|
101
|
+
for (const a of v4) results.push({ address: a, family: 4 });
|
|
102
|
+
|
|
103
|
+
if (results.length > 0) {
|
|
104
|
+
return callback(null, results);
|
|
48
105
|
}
|
|
49
|
-
|
|
106
|
+
// All custom DNS failed — fall back to OS resolver
|
|
107
|
+
dns.lookup(hostname, { all: true }, callback);
|
|
108
|
+
}).catch(() => {
|
|
109
|
+
dns.lookup(hostname, { all: true }, callback);
|
|
50
110
|
});
|
|
51
|
-
}
|
|
111
|
+
} else {
|
|
112
|
+
// ── Single-address path ──
|
|
113
|
+
const tryIPv4 = () => {
|
|
114
|
+
resolveWithTimeout(dns.resolve4.bind(dns), hostname).then((v4) => {
|
|
115
|
+
if (v4.length > 0) return callback(null, v4[0], 4);
|
|
116
|
+
// Last resort: OS resolver
|
|
117
|
+
dns.lookup(hostname, options, callback);
|
|
118
|
+
}).catch(() => dns.lookup(hostname, options, callback));
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
if (HAS_IPV6) {
|
|
122
|
+
resolveWithTimeout(dns.resolve6.bind(dns), hostname).then((v6) => {
|
|
123
|
+
if (v6.length > 0) return callback(null, v6[0], 6);
|
|
124
|
+
tryIPv4();
|
|
125
|
+
}).catch(() => tryIPv4());
|
|
126
|
+
} else {
|
|
127
|
+
tryIPv4();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
52
130
|
}
|
|
53
131
|
|
|
54
|
-
// Replace global agents
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
132
|
+
// ── Replace global HTTP agents ──────────────────────────────────────────
|
|
133
|
+
// autoSelectFamily enables Node.js "Happy Eyeballs" (RFC 6555): when the
|
|
134
|
+
// lookup returns both IPv4 and IPv6 addresses, Node races connections and
|
|
135
|
+
// uses whichever family connects first. This ensures the app works on
|
|
136
|
+
// IPv4-only WiFi, IPv6-only networks, and dual-stack setups alike.
|
|
137
|
+
const agentOptions = {
|
|
138
|
+
lookup: customLookup,
|
|
139
|
+
keepAlive: true,
|
|
140
|
+
autoSelectFamily: true,
|
|
141
|
+
autoSelectFamilyAttemptTimeout: 2500, // ms before trying the next family
|
|
142
|
+
};
|
|
143
|
+
http.globalAgent = new http.Agent(agentOptions);
|
|
144
|
+
https.globalAgent = new https.Agent(agentOptions);
|
|
58
145
|
|
|
59
146
|
// Dynamic import so aniwatch picks up patched global agents
|
|
60
147
|
const { HiAnime } = await import("aniwatch");
|
|
@@ -64,15 +151,26 @@ const hianime = new HiAnime.Scraper();
|
|
|
64
151
|
const action = process.argv[2];
|
|
65
152
|
|
|
66
153
|
// Retry helper: retries an async fn up to `retries` times with a delay between attempts.
|
|
67
|
-
|
|
154
|
+
// On transient network errors (ENETUNREACH, ECONNRESET, etc.) it retries automatically.
|
|
155
|
+
async function withRetry(fn, { retries = 3, delay = 1200, label = "" } = {}) {
|
|
156
|
+
const TRANSIENT_CODES = new Set([
|
|
157
|
+
"ENETUNREACH", "ECONNREFUSED", "ECONNRESET", "ETIMEDOUT",
|
|
158
|
+
"EHOSTUNREACH", "EAI_AGAIN", "EPIPE", "ERR_SOCKET_CONNECTION_TIMEOUT",
|
|
159
|
+
"UND_ERR_CONNECT_TIMEOUT", "UND_ERR_SOCKET", "ENOTFOUND",
|
|
160
|
+
]);
|
|
68
161
|
let lastErr;
|
|
69
162
|
for (let i = 0; i <= retries; i++) {
|
|
70
163
|
try {
|
|
71
164
|
return await fn();
|
|
72
165
|
} catch (err) {
|
|
73
166
|
lastErr = err;
|
|
167
|
+
const isTransient = TRANSIENT_CODES.has(err.code) ||
|
|
168
|
+
(err.cause && TRANSIENT_CODES.has(err.cause.code)) ||
|
|
169
|
+
/timeout|ENETUNREACH|ECONNR|socket/i.test(err.message);
|
|
74
170
|
if (i < retries) {
|
|
75
|
-
|
|
171
|
+
// Longer back-off for transient network errors
|
|
172
|
+
const backoff = isTransient ? delay * (i + 1) : delay;
|
|
173
|
+
await new Promise(r => setTimeout(r, backoff));
|
|
76
174
|
}
|
|
77
175
|
}
|
|
78
176
|
}
|
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}
|
|
28
|
+
printf "${DIM} v4.0.0 • nyanime.tech${RESET}\n"
|
|
29
29
|
printf "\n"
|
|
30
30
|
|
|
31
31
|
REPO_URL="https://raw.githubusercontent.com/AnjishnuSengupta/ny-cli/main"
|
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:
|
|
4
|
+
# Version: 4.0.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="
|
|
13
|
+
VERSION="4.0.0"
|
|
14
14
|
|
|
15
15
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
16
16
|
# BACKEND CONFIGURATION (Self-hosted via aniwatch npm package)
|
|
@@ -295,9 +295,11 @@ do_logout() {
|
|
|
295
295
|
# WATCH HISTORY & CLOUD SYNC
|
|
296
296
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
297
297
|
|
|
298
|
-
# Check if online
|
|
298
|
+
# Check if online (try multiple endpoints — google is blocked in some countries)
|
|
299
299
|
is_online() {
|
|
300
|
-
curl -s --connect-timeout 2 --max-time 3 "https://www.google.com" >/dev/null 2>&1
|
|
300
|
+
curl -s --connect-timeout 2 --max-time 3 "https://www.google.com" >/dev/null 2>&1 \
|
|
301
|
+
|| curl -s --connect-timeout 2 --max-time 3 "https://cloudflare.com/cdn-cgi/trace" >/dev/null 2>&1 \
|
|
302
|
+
|| curl -s --connect-timeout 2 --max-time 3 "https://www.nyanime.tech" >/dev/null 2>&1
|
|
301
303
|
}
|
|
302
304
|
|
|
303
305
|
# Fetch cloud history and merge with local history
|
|
@@ -325,27 +327,29 @@ fetch_and_merge_cloud_history() {
|
|
|
325
327
|
local cloud_tmp="${CACHE_DIR}/cloud_merged.tmp"
|
|
326
328
|
> "$cloud_tmp"
|
|
327
329
|
|
|
328
|
-
# Extract each history item with slug and
|
|
330
|
+
# Extract each history item with slug, title, and category
|
|
329
331
|
printf '%s' "$response" | tr '{' '\n' | grep '"animeSlug"' | while read -r item; do
|
|
330
|
-
local slug title ep_num
|
|
332
|
+
local slug title ep_num category
|
|
331
333
|
slug=$(printf '%s' "$item" | sed -n 's/.*"animeSlug"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
|
|
332
334
|
title=$(printf '%s' "$item" | sed -n 's/.*"animeTitle"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
|
|
333
335
|
ep_num=$(printf '%s' "$item" | sed -n 's/.*"episodeNum"[[:space:]]*:[[:space:]]*\([0-9]*\).*/\1/p')
|
|
336
|
+
category=$(printf '%s' "$item" | sed -n 's/.*"category"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
|
|
334
337
|
|
|
335
338
|
if [ -n "$slug" ] && [ -n "$title" ]; then
|
|
336
339
|
[ -z "$ep_num" ] && ep_num=1
|
|
340
|
+
[ -z "$category" ] && category="sub"
|
|
337
341
|
# Check if this anime is already in local history
|
|
338
342
|
if [ -f "$HISTORY_FILE" ]; then
|
|
339
343
|
if ! grep -q "^${slug}|" "$HISTORY_FILE" 2>/dev/null; then
|
|
340
|
-
# Add to local history (new entry from cloud)
|
|
344
|
+
# Add to local history (new entry from cloud) with category
|
|
341
345
|
local timestamp
|
|
342
346
|
timestamp=$(date +%s)
|
|
343
|
-
printf '%s|%s|%s|%s\n' "$slug" "$title" "$ep_num" "$timestamp" >> "$cloud_tmp"
|
|
347
|
+
printf '%s|%s|%s|%s|%s\n' "$slug" "$title" "$ep_num" "$timestamp" "$category" >> "$cloud_tmp"
|
|
344
348
|
fi
|
|
345
349
|
else
|
|
346
350
|
local timestamp
|
|
347
351
|
timestamp=$(date +%s)
|
|
348
|
-
printf '%s|%s|%s|%s\n' "$slug" "$title" "$ep_num" "$timestamp" >> "$cloud_tmp"
|
|
352
|
+
printf '%s|%s|%s|%s|%s\n' "$slug" "$title" "$ep_num" "$timestamp" "$category" >> "$cloud_tmp"
|
|
349
353
|
fi
|
|
350
354
|
fi
|
|
351
355
|
done
|
|
@@ -373,15 +377,15 @@ fetch_and_merge_cloud_history() {
|
|
|
373
377
|
|
|
374
378
|
# Save watch progress to pending sync file (for offline support)
|
|
375
379
|
save_pending_sync() {
|
|
376
|
-
local anime_slug="$1" anime_title="$2" ep_num="$3"
|
|
380
|
+
local anime_slug="$1" anime_title="$2" ep_num="$3" category="${4:-sub}"
|
|
377
381
|
local sync_time
|
|
378
382
|
sync_time=$(date +%s)
|
|
379
383
|
|
|
380
384
|
# Remove existing entry for this anime
|
|
381
385
|
[ -f "$PENDING_SYNC_FILE" ] && grep -v "^${anime_slug}|" "$PENDING_SYNC_FILE" > "${PENDING_SYNC_FILE}.tmp" 2>/dev/null && mv "${PENDING_SYNC_FILE}.tmp" "$PENDING_SYNC_FILE"
|
|
382
386
|
|
|
383
|
-
# Add new entry: anime_slug|anime_title|ep_num|sync_time
|
|
384
|
-
printf '%s|%s|%s|%s\n' "$anime_slug" "$anime_title" "$ep_num" "$sync_time" >> "$PENDING_SYNC_FILE"
|
|
387
|
+
# Add new entry: anime_slug|anime_title|ep_num|sync_time|category
|
|
388
|
+
printf '%s|%s|%s|%s|%s\n' "$anime_slug" "$anime_title" "$ep_num" "$sync_time" "$category" >> "$PENDING_SYNC_FILE"
|
|
385
389
|
}
|
|
386
390
|
|
|
387
391
|
# Sync pending history to cloud via nyanime API
|
|
@@ -399,25 +403,26 @@ sync_pending_history() {
|
|
|
399
403
|
local synced=0
|
|
400
404
|
local temp_file="${PENDING_SYNC_FILE}.syncing"
|
|
401
405
|
|
|
402
|
-
# Process each pending entry
|
|
403
|
-
while IFS='|' read -r anime_slug anime_title ep_num sync_time; do
|
|
406
|
+
# Process each pending entry (slug|title|ep_num|sync_time|category)
|
|
407
|
+
while IFS='|' read -r anime_slug anime_title ep_num sync_time category; do
|
|
404
408
|
[ -z "$anime_slug" ] && continue
|
|
405
409
|
[ -z "$anime_title" ] && anime_title="$anime_slug"
|
|
406
410
|
[ -z "$ep_num" ] && ep_num=1
|
|
411
|
+
[ -z "$category" ] && category="sub"
|
|
407
412
|
|
|
408
|
-
# Sync via nyanime API with slug-based format
|
|
413
|
+
# Sync via nyanime API with slug-based format + category
|
|
409
414
|
local response
|
|
410
415
|
response=$(curl -s -X POST "${NYANIME_BASE}/api/cli/sync-watch" \
|
|
411
416
|
-H "Content-Type: application/json" \
|
|
412
417
|
-H "X-Firebase-UID: ${firebase_uid}" \
|
|
413
|
-
-d "{\"animeSlug\": \"${anime_slug}\", \"animeTitle\": \"${anime_title}\", \"episodeNum\": ${ep_num}}" \
|
|
418
|
+
-d "{\"animeSlug\": \"${anime_slug}\", \"animeTitle\": \"${anime_title}\", \"episodeNum\": ${ep_num}, \"category\": \"${category}\"}" \
|
|
414
419
|
--connect-timeout 5 --max-time 10 2>/dev/null)
|
|
415
420
|
|
|
416
421
|
if printf '%s' "$response" | grep -q '"success"' 2>/dev/null; then
|
|
417
422
|
synced=$((synced + 1))
|
|
418
423
|
else
|
|
419
424
|
# Keep failed entries for retry
|
|
420
|
-
printf '%s|%s|%s|%s\n' "$anime_slug" "$anime_title" "$ep_num" "$sync_time" >> "$temp_file"
|
|
425
|
+
printf '%s|%s|%s|%s|%s\n' "$anime_slug" "$anime_title" "$ep_num" "$sync_time" "$category" >> "$temp_file"
|
|
421
426
|
fi
|
|
422
427
|
done < "$PENDING_SYNC_FILE"
|
|
423
428
|
|
|
@@ -433,15 +438,15 @@ sync_pending_history() {
|
|
|
433
438
|
|
|
434
439
|
# Background sync daemon (called during playback)
|
|
435
440
|
start_sync_daemon() {
|
|
436
|
-
local anime_slug="$1" anime_title="$2" ep_num="$3"
|
|
441
|
+
local anime_slug="$1" anime_title="$2" ep_num="$3" category="${4:-sub}"
|
|
437
442
|
|
|
438
443
|
# Create a background process that syncs every 10 seconds
|
|
439
444
|
(
|
|
440
445
|
while true; do
|
|
441
446
|
sleep "$SYNC_INTERVAL"
|
|
442
447
|
|
|
443
|
-
# Save to pending sync with slug and
|
|
444
|
-
save_pending_sync "$anime_slug" "$anime_title" "$ep_num"
|
|
448
|
+
# Save to pending sync with slug, title, and category
|
|
449
|
+
save_pending_sync "$anime_slug" "$anime_title" "$ep_num" "$category"
|
|
445
450
|
|
|
446
451
|
# Try to sync if online
|
|
447
452
|
sync_pending_history >/dev/null 2>&1
|
|
@@ -456,7 +461,7 @@ stop_sync_daemon() {
|
|
|
456
461
|
}
|
|
457
462
|
|
|
458
463
|
save_to_history() {
|
|
459
|
-
local anime_slug="$1" title="$2" episode="$3"
|
|
464
|
+
local anime_slug="$1" title="$2" episode="$3" category="${4:-sub}"
|
|
460
465
|
local timestamp
|
|
461
466
|
timestamp=$(date +%s)
|
|
462
467
|
|
|
@@ -466,9 +471,9 @@ save_to_history() {
|
|
|
466
471
|
mv "${HISTORY_FILE}.tmp" "$HISTORY_FILE" 2>/dev/null || true
|
|
467
472
|
fi
|
|
468
473
|
|
|
469
|
-
# Create new entry and prepend to history
|
|
474
|
+
# Create new entry and prepend to history (slug|title|episode|timestamp|category)
|
|
470
475
|
local new_entry
|
|
471
|
-
new_entry=$(printf '%s|%s|%s|%s\n' "$anime_slug" "$title" "$episode" "$timestamp")
|
|
476
|
+
new_entry=$(printf '%s|%s|%s|%s|%s\n' "$anime_slug" "$title" "$episode" "$timestamp" "$category")
|
|
472
477
|
|
|
473
478
|
if [ -f "$HISTORY_FILE" ] && [ -s "$HISTORY_FILE" ]; then
|
|
474
479
|
printf '%s\n' "$new_entry" | cat - "$HISTORY_FILE" > "${HISTORY_FILE}.tmp"
|
|
@@ -483,8 +488,8 @@ save_to_history() {
|
|
|
483
488
|
mv "${HISTORY_FILE}.tmp" "$HISTORY_FILE"
|
|
484
489
|
fi
|
|
485
490
|
|
|
486
|
-
# Save to pending sync for cloud backup (slug, title, episode)
|
|
487
|
-
save_pending_sync "$anime_slug" "$title" "$episode"
|
|
491
|
+
# Save to pending sync for cloud backup (slug, title, episode, category)
|
|
492
|
+
save_pending_sync "$anime_slug" "$title" "$episode" "$category"
|
|
488
493
|
|
|
489
494
|
# Try immediate sync if online
|
|
490
495
|
sync_pending_history >/dev/null 2>&1 &
|
|
@@ -498,7 +503,7 @@ save_to_history() {
|
|
|
498
503
|
# Usage: call_backend <action> [args...]
|
|
499
504
|
# Returns JSON to stdout (pino logger noise is filtered out)
|
|
500
505
|
call_backend() {
|
|
501
|
-
NODE_ENV=production node "$BACKEND_SCRIPT" "$@" 2>/dev/null | grep -m1 '"success"\|"error"'
|
|
506
|
+
LOG_LEVEL=silent NODE_ENV=production node "$BACKEND_SCRIPT" "$@" 2>/dev/null | grep -m1 '"success"\|"error"'
|
|
502
507
|
}
|
|
503
508
|
|
|
504
509
|
# JSON parsing helpers (pure POSIX shell)
|
|
@@ -529,13 +534,15 @@ search_anime() {
|
|
|
529
534
|
|
|
530
535
|
# Parse animes array from response
|
|
531
536
|
# Each anime has: id, name, type, episodes.sub, episodes.dub
|
|
532
|
-
|
|
537
|
+
# After tr '{' split, "sub" and "dub" are on the line AFTER "id", so merge them
|
|
538
|
+
printf '%s' "$response" | tr '{' '\n' | awk '/"id"/{buf=$0; getline; print buf $0}' | while IFS= read -r line; do
|
|
533
539
|
local id name type sub
|
|
534
540
|
id=$(printf '%s' "$line" | grep -oP '"id"\s*:\s*"\K[^"]+' 2>/dev/null)
|
|
535
541
|
name=$(printf '%s' "$line" | grep -oP '"name"\s*:\s*"\K[^"]+' 2>/dev/null)
|
|
536
542
|
type=$(printf '%s' "$line" | grep -oP '"type"\s*:\s*"\K[^"]+' 2>/dev/null)
|
|
537
543
|
sub=$(printf '%s' "$line" | grep -oP '"sub"\s*:\s*\K[0-9]+' 2>/dev/null)
|
|
538
|
-
|
|
544
|
+
dub=$(printf '%s' "$line" | grep -oP '"dub"\s*:\s*\K[0-9]+' 2>/dev/null)
|
|
545
|
+
[ -n "$id" ] && [ -n "$name" ] && printf '%s|%s|%s|%s|%s\n' "$id" "$name" "${type:-TV}" "${sub:-0}" "${dub:-0}"
|
|
539
546
|
done
|
|
540
547
|
}
|
|
541
548
|
|
|
@@ -799,7 +806,10 @@ do_search() {
|
|
|
799
806
|
printf '%s\n' "$results" | while IFS='|' read -r id title type eps_sub eps_dub; do
|
|
800
807
|
local info=""
|
|
801
808
|
[ -n "$type" ] && info="$type"
|
|
802
|
-
[ -n "$eps_sub" ] && [ "$eps_sub" != "0" ]
|
|
809
|
+
if [ -n "$eps_sub" ] && [ "$eps_sub" != "0" ]; then
|
|
810
|
+
info="$info, ${eps_sub} sub"
|
|
811
|
+
[ -n "$eps_dub" ] && [ "$eps_dub" != "0" ] && [ "$eps_dub" != "null" ] && info="$info / ${eps_dub} dub"
|
|
812
|
+
fi
|
|
803
813
|
[ -n "$info" ] && info=" ${DIM}($info)${RESET}"
|
|
804
814
|
printf '%b\n' " ${WHITE}$(printf '%2d' $i))${RESET} ${title}${info}"
|
|
805
815
|
i=$((i + 1))
|
|
@@ -821,15 +831,41 @@ do_search() {
|
|
|
821
831
|
}
|
|
822
832
|
|
|
823
833
|
watch_anime() {
|
|
824
|
-
local anime_id="$1" anime_title="$2"
|
|
834
|
+
local anime_id="$1" anime_title="$2" category="${3:-}"
|
|
825
835
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
836
|
+
# Start fetching episodes in background while we ask sub/dub
|
|
837
|
+
local ep_cache="${CACHE_DIR}/ep_fetch.tmp"
|
|
838
|
+
rm -f "$ep_cache" 2>/dev/null
|
|
839
|
+
msg "Loading episodes..."
|
|
840
|
+
(get_episodes "$anime_id" > "$ep_cache" 2>/dev/null) &
|
|
841
|
+
local ep_fetch_pid=$!
|
|
842
|
+
|
|
843
|
+
# Ask sub/dub preference if not already chosen
|
|
844
|
+
if [ -z "$category" ]; then
|
|
845
|
+
printf "\n"
|
|
846
|
+
printf '%b\n' " ${GREEN}1)${RESET} Sub ${DIM}(Japanese audio + subtitles)${RESET}"
|
|
847
|
+
printf '%b\n' " ${YELLOW}2)${RESET} Dub ${DIM}(English audio)${RESET}"
|
|
848
|
+
prompt "Sub or Dub? [1/2]: "
|
|
849
|
+
read -r cat_choice
|
|
850
|
+
case "$cat_choice" in
|
|
851
|
+
2|d|D|dub|Dub|DUB) category="dub" ;;
|
|
852
|
+
*) category="sub" ;;
|
|
853
|
+
esac
|
|
854
|
+
fi
|
|
855
|
+
|
|
856
|
+
# Wait for episode fetch to complete
|
|
857
|
+
wait $ep_fetch_pid 2>/dev/null
|
|
858
|
+
episodes=$(cat "$ep_cache" 2>/dev/null)
|
|
859
|
+
rm -f "$ep_cache" 2>/dev/null
|
|
860
|
+
|
|
861
|
+
[ -z "$episodes" ] && msg_error "No episodes found" && return
|
|
829
862
|
|
|
830
863
|
ep_count=$(printf '%s\n' "$episodes" | wc -l)
|
|
831
864
|
|
|
832
|
-
|
|
865
|
+
local cat_label
|
|
866
|
+
[ "$category" = "dub" ] && cat_label="${YELLOW}DUB${RESET}" || cat_label="${GREEN}SUB${RESET}"
|
|
867
|
+
|
|
868
|
+
printf '%b\n' "\n${CYAN}${anime_title}${RESET} ${DIM}(${ep_count} eps)${RESET} [${cat_label}]"
|
|
833
869
|
print_line
|
|
834
870
|
|
|
835
871
|
if [ "$ep_count" -gt 20 ]; then
|
|
@@ -856,30 +892,33 @@ watch_anime() {
|
|
|
856
892
|
[ -z "$ep_line" ] && msg_error "Invalid" && return
|
|
857
893
|
|
|
858
894
|
episode_id=$(printf '%s' "$ep_line" | cut -d'|' -f2)
|
|
859
|
-
play_episode "$anime_id" "$anime_title" "$ep_choice" "$episode_id" "$episodes"
|
|
895
|
+
play_episode "$anime_id" "$anime_title" "$ep_choice" "$episode_id" "$episodes" "$category"
|
|
860
896
|
}
|
|
861
897
|
|
|
862
898
|
play_episode() {
|
|
863
|
-
local anime_id="$1" anime_title="$2" ep_num="$3" episode_id="$4" episodes="$5"
|
|
899
|
+
local anime_id="$1" anime_title="$2" ep_num="$3" episode_id="$4" episodes="$5" category="${6:-sub}"
|
|
864
900
|
|
|
865
901
|
printf "\n"
|
|
866
902
|
|
|
867
|
-
# Get streaming sources with
|
|
868
|
-
if ! get_stream_with_fallback "$episode_id" "
|
|
903
|
+
# Get streaming sources with the user's chosen category
|
|
904
|
+
if ! get_stream_with_fallback "$episode_id" "$category"; then
|
|
869
905
|
msg_error "No stream available for this episode"
|
|
870
906
|
return
|
|
871
907
|
fi
|
|
872
908
|
|
|
873
909
|
[ -z "$STREAM_URL" ] && msg_error "No stream URL found" && return
|
|
874
910
|
|
|
875
|
-
# Save to history (slug, title, episode)
|
|
876
|
-
save_to_history "$anime_id" "$anime_title" "$ep_num"
|
|
911
|
+
# Save to history with category preference (slug, title, episode, category)
|
|
912
|
+
save_to_history "$anime_id" "$anime_title" "$ep_num" "$category"
|
|
877
913
|
|
|
878
|
-
# Start background sync daemon during playback (slug, title, episode)
|
|
879
|
-
start_sync_daemon "$anime_id" "$anime_title" "$ep_num"
|
|
914
|
+
# Start background sync daemon during playback (slug, title, episode, category)
|
|
915
|
+
start_sync_daemon "$anime_id" "$anime_title" "$ep_num" "$category"
|
|
916
|
+
|
|
917
|
+
local cat_label
|
|
918
|
+
[ "$category" = "dub" ] && cat_label="DUB" || cat_label="SUB"
|
|
880
919
|
|
|
881
920
|
# Play the video with referer header
|
|
882
|
-
play_video "$STREAM_URL" "${anime_title} - Episode ${ep_num}" "$STREAM_SUBTITLES" "$STREAM_REFERER"
|
|
921
|
+
play_video "$STREAM_URL" "${anime_title} - Episode ${ep_num} [${cat_label}]" "$STREAM_SUBTITLES" "$STREAM_REFERER"
|
|
883
922
|
|
|
884
923
|
# Stop sync daemon after playback ends
|
|
885
924
|
stop_sync_daemon
|
|
@@ -900,13 +939,13 @@ play_episode() {
|
|
|
900
939
|
next_line=$(printf '%s\n' "$episodes" | grep "^${next_ep}|")
|
|
901
940
|
if [ -n "$next_line" ]; then
|
|
902
941
|
next_id=$(printf '%s' "$next_line" | cut -d'|' -f2)
|
|
903
|
-
play_episode "$anime_id" "$anime_title" "$next_ep" "$next_id" "$episodes"
|
|
942
|
+
play_episode "$anime_id" "$anime_title" "$next_ep" "$next_id" "$episodes" "$category"
|
|
904
943
|
else
|
|
905
944
|
msg_info "No more episodes"
|
|
906
945
|
fi
|
|
907
946
|
;;
|
|
908
|
-
r) play_episode "$anime_id" "$anime_title" "$ep_num" "$episode_id" "$episodes" ;;
|
|
909
|
-
s) watch_anime "$anime_id" "$anime_title" ;;
|
|
947
|
+
r) play_episode "$anime_id" "$anime_title" "$ep_num" "$episode_id" "$episodes" "$category" ;;
|
|
948
|
+
s) watch_anime "$anime_id" "$anime_title" "$category" ;;
|
|
910
949
|
esac
|
|
911
950
|
}
|
|
912
951
|
|
|
@@ -931,8 +970,11 @@ do_continue() {
|
|
|
931
970
|
print_line
|
|
932
971
|
|
|
933
972
|
i=1
|
|
934
|
-
head -10 "$HISTORY_FILE" | while IFS='|' read -r id title episode ts; do
|
|
935
|
-
|
|
973
|
+
head -10 "$HISTORY_FILE" | while IFS='|' read -r id title episode ts category; do
|
|
974
|
+
[ -z "$category" ] && category="sub"
|
|
975
|
+
local cat_badge
|
|
976
|
+
[ "$category" = "dub" ] && cat_badge="${YELLOW}DUB${RESET}" || cat_badge="${GREEN}SUB${RESET}"
|
|
977
|
+
printf '%b\n' " ${WHITE}$(printf '%2d' $i))${RESET} $title ${DIM}(Ep $episode)${RESET} [${cat_badge}]"
|
|
936
978
|
i=$((i + 1))
|
|
937
979
|
done
|
|
938
980
|
print_line
|
|
@@ -948,17 +990,21 @@ do_continue() {
|
|
|
948
990
|
anime_id=$(printf '%s' "$selected" | cut -d'|' -f1)
|
|
949
991
|
anime_title=$(printf '%s' "$selected" | cut -d'|' -f2)
|
|
950
992
|
last_ep=$(printf '%s' "$selected" | cut -d'|' -f3)
|
|
993
|
+
category=$(printf '%s' "$selected" | cut -d'|' -f5)
|
|
994
|
+
[ -z "$category" ] && category="sub"
|
|
951
995
|
|
|
952
996
|
episodes=$(get_episodes "$anime_id")
|
|
953
997
|
next_ep=$((last_ep + 1))
|
|
954
998
|
next_line=$(printf '%s\n' "$episodes" | grep "^${next_ep}|")
|
|
955
999
|
|
|
956
1000
|
if [ -n "$next_line" ]; then
|
|
957
|
-
|
|
1001
|
+
local cat_label
|
|
1002
|
+
[ "$category" = "dub" ] && cat_label="DUB" || cat_label="SUB"
|
|
1003
|
+
msg_info "Continuing Episode ${next_ep} [${cat_label}]"
|
|
958
1004
|
episode_id=$(printf '%s' "$next_line" | cut -d'|' -f2)
|
|
959
|
-
play_episode "$anime_id" "$anime_title" "$next_ep" "$episode_id" "$episodes"
|
|
1005
|
+
play_episode "$anime_id" "$anime_title" "$next_ep" "$episode_id" "$episodes" "$category"
|
|
960
1006
|
else
|
|
961
|
-
watch_anime "$anime_id" "$anime_title"
|
|
1007
|
+
watch_anime "$anime_id" "$anime_title" "$category"
|
|
962
1008
|
fi
|
|
963
1009
|
}
|
|
964
1010
|
|
|
@@ -1019,14 +1065,7 @@ do_random() {
|
|
|
1019
1065
|
|
|
1020
1066
|
case "$ans" in
|
|
1021
1067
|
[nN]*) return ;;
|
|
1022
|
-
*)
|
|
1023
|
-
episodes=$(get_episodes "$anime_id")
|
|
1024
|
-
first=$(printf '%s\n' "$episodes" | head -1)
|
|
1025
|
-
[ -n "$first" ] && {
|
|
1026
|
-
ep_id=$(printf '%s' "$first" | cut -d'|' -f2)
|
|
1027
|
-
play_episode "$anime_id" "$anime_title" "1" "$ep_id" "$episodes"
|
|
1028
|
-
}
|
|
1029
|
-
;;
|
|
1068
|
+
*) watch_anime "$anime_id" "$anime_title" ;;
|
|
1030
1069
|
esac
|
|
1031
1070
|
}
|
|
1032
1071
|
|
|
@@ -1100,7 +1139,7 @@ main_menu() {
|
|
|
1100
1139
|
|
|
1101
1140
|
printf '%b\n' "${CYAN}╭────────────────────────────────────────╮${RESET}"
|
|
1102
1141
|
printf '%b\n' "${CYAN}│${RESET} ${WHITE}1)${RESET} 👤 Profile ${CYAN}│${RESET}"
|
|
1103
|
-
printf '%b\n' "${CYAN}│${RESET} ${WHITE}2)${RESET} ▶️ Continue Watching
|
|
1142
|
+
printf '%b\n' "${CYAN}│${RESET} ${WHITE}2)${RESET} ▶️ Continue Watching ${CYAN}│${RESET}"
|
|
1104
1143
|
printf '%b\n' "${CYAN}│${RESET} ${WHITE}3)${RESET} 🔍 Search ${CYAN}│${RESET}"
|
|
1105
1144
|
printf '%b\n' "${CYAN}│${RESET} ${WHITE}4)${RESET} 📚 Trending ${CYAN}│${RESET}"
|
|
1106
1145
|
printf '%b\n' "${CYAN}│${RESET} ${WHITE}5)${RESET} 🎲 Random ${CYAN}│${RESET}"
|
|
@@ -1168,10 +1207,13 @@ main() {
|
|
|
1168
1207
|
printf '%b\n' "\n${CYAN}Results:${RESET}"
|
|
1169
1208
|
print_line
|
|
1170
1209
|
i=1
|
|
1171
|
-
printf '%s\n' "$results" | while IFS='|' read -r id title type
|
|
1210
|
+
printf '%s\n' "$results" | while IFS='|' read -r id title type eps_sub eps_dub; do
|
|
1172
1211
|
local info=""
|
|
1173
1212
|
[ -n "$type" ] && info="$type"
|
|
1174
|
-
[ -n "$
|
|
1213
|
+
if [ -n "$eps_sub" ] && [ "$eps_sub" != "0" ]; then
|
|
1214
|
+
info="$info, ${eps_sub} sub"
|
|
1215
|
+
[ -n "$eps_dub" ] && [ "$eps_dub" != "0" ] && [ "$eps_dub" != "null" ] && info="$info / ${eps_dub} dub"
|
|
1216
|
+
fi
|
|
1175
1217
|
[ -n "$info" ] && info=" ${DIM}($info)${RESET}"
|
|
1176
1218
|
printf '%b\n' " ${WHITE}$(printf '%2d' $i))${RESET} ${title}${info}"
|
|
1177
1219
|
i=$((i + 1))
|
|
@@ -1196,10 +1238,13 @@ main() {
|
|
|
1196
1238
|
printf '%b\n' "\n${CYAN}Results:${RESET}"
|
|
1197
1239
|
print_line
|
|
1198
1240
|
i=1
|
|
1199
|
-
printf '%s\n' "$results" | while IFS='|' read -r id title type
|
|
1241
|
+
printf '%s\n' "$results" | while IFS='|' read -r id title type eps_sub eps_dub; do
|
|
1200
1242
|
local info=""
|
|
1201
1243
|
[ -n "$type" ] && info="$type"
|
|
1202
|
-
[ -n "$
|
|
1244
|
+
if [ -n "$eps_sub" ] && [ "$eps_sub" != "0" ]; then
|
|
1245
|
+
info="$info, ${eps_sub} sub"
|
|
1246
|
+
[ -n "$eps_dub" ] && [ "$eps_dub" != "0" ] && [ "$eps_dub" != "null" ] && info="$info / ${eps_dub} dub"
|
|
1247
|
+
fi
|
|
1203
1248
|
[ -n "$info" ] && info=" ${DIM}($info)${RESET}"
|
|
1204
1249
|
printf '%b\n' " ${WHITE}$(printf '%2d' $i))${RESET} ${title}${info}"
|
|
1205
1250
|
i=$((i + 1))
|