@anjishnusengupta/ny-cli 3.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/LICENSE +21 -0
- package/README.md +419 -0
- package/backend.mjs +189 -0
- package/install.sh +161 -0
- package/ny-cli +1219 -0
- package/package.json +42 -0
package/ny-cli
ADDED
|
@@ -0,0 +1,1219 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
3
|
+
# NY-CLI - Terminal-based Anime Streaming Client
|
|
4
|
+
# Version: 3.0.0
|
|
5
|
+
# Author: Anjishnu Sengupta
|
|
6
|
+
# Website: https://nyanime.tech
|
|
7
|
+
# Repository: https://github.com/AnjishnuSengupta/ny-cli
|
|
8
|
+
#
|
|
9
|
+
# Inspired by ani-cli, powered by aniwatch npm package (self-hosted scraping)
|
|
10
|
+
# Just install and run - no external API dependency!
|
|
11
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
12
|
+
|
|
13
|
+
VERSION="3.0.0"
|
|
14
|
+
|
|
15
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
16
|
+
# BACKEND CONFIGURATION (Self-hosted via aniwatch npm package)
|
|
17
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
18
|
+
|
|
19
|
+
# NyAnime Website (for cloud sync & stream proxy)
|
|
20
|
+
NYANIME_BASE="https://www.nyanime.tech"
|
|
21
|
+
|
|
22
|
+
# Local backend script (uses aniwatch npm package directly)
|
|
23
|
+
# Resolved relative to the ny-cli script location (resolves symlinks)
|
|
24
|
+
SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")")" && pwd)"
|
|
25
|
+
BACKEND_SCRIPT="${SCRIPT_DIR}/backend.mjs"
|
|
26
|
+
|
|
27
|
+
# Firebase Configuration (NyAnime project)
|
|
28
|
+
FIREBASE_PROJECT_ID="nyanime-tech"
|
|
29
|
+
FIRESTORE_BASE="https://firestore.googleapis.com/v1/projects/${FIREBASE_PROJECT_ID}/databases/(default)/documents"
|
|
30
|
+
|
|
31
|
+
# User agent
|
|
32
|
+
USER_AGENT="Mozilla/5.0 (X11; Linux x86_64; rv:121.0) Gecko/20100101 Firefox/121.0"
|
|
33
|
+
|
|
34
|
+
# Local storage directories (XDG compliant)
|
|
35
|
+
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/ny-cli"
|
|
36
|
+
CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/ny-cli"
|
|
37
|
+
DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/ny-cli"
|
|
38
|
+
|
|
39
|
+
# Files
|
|
40
|
+
AUTH_FILE="$CONFIG_DIR/auth"
|
|
41
|
+
HISTORY_FILE="$DATA_DIR/history"
|
|
42
|
+
PENDING_SYNC_FILE="$DATA_DIR/pending_sync"
|
|
43
|
+
|
|
44
|
+
# Player (can be overridden with NYCLI_PLAYER env var)
|
|
45
|
+
PLAYER="${NYCLI_PLAYER:-mpv}"
|
|
46
|
+
|
|
47
|
+
# Sync interval (seconds)
|
|
48
|
+
SYNC_INTERVAL=30
|
|
49
|
+
|
|
50
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
51
|
+
# COLORS & STYLING
|
|
52
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
53
|
+
|
|
54
|
+
if [ -t 1 ]; then
|
|
55
|
+
RED='\033[1;31m'
|
|
56
|
+
GREEN='\033[1;32m'
|
|
57
|
+
YELLOW='\033[1;33m'
|
|
58
|
+
BLUE='\033[1;34m'
|
|
59
|
+
MAGENTA='\033[1;35m'
|
|
60
|
+
CYAN='\033[1;36m'
|
|
61
|
+
WHITE='\033[1;37m'
|
|
62
|
+
DIM='\033[2m'
|
|
63
|
+
BOLD='\033[1m'
|
|
64
|
+
RESET='\033[0m'
|
|
65
|
+
else
|
|
66
|
+
RED='' GREEN='' YELLOW='' BLUE='' MAGENTA='' CYAN='' WHITE='' DIM='' BOLD='' RESET=''
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
70
|
+
# ASCII ART BANNER
|
|
71
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
72
|
+
|
|
73
|
+
show_banner() {
|
|
74
|
+
clear
|
|
75
|
+
printf '%b' "${MAGENTA}"
|
|
76
|
+
cat << 'EOF'
|
|
77
|
+
|
|
78
|
+
███╗ ██╗██╗ ██╗ ██████╗██╗ ██╗
|
|
79
|
+
████╗ ██║╚██╗ ██╔╝ ██╔════╝██║ ██║
|
|
80
|
+
██╔██╗ ██║ ╚████╔╝ █████╗██║ ██║ ██║
|
|
81
|
+
██║╚██╗██║ ╚██╔╝ ╚════╝██║ ██║ ██║
|
|
82
|
+
██║ ╚████║ ██║ ╚██████╗███████╗██║
|
|
83
|
+
╚═╝ ╚═══╝ ╚═╝ ╚═════╝╚══════╝╚═╝
|
|
84
|
+
|
|
85
|
+
EOF
|
|
86
|
+
printf '%b' "${RESET}"
|
|
87
|
+
printf '%b\n' "${DIM}${CYAN} ⟨ Your Gateway to Anime Streaming ⟩${RESET}"
|
|
88
|
+
printf '%b\n' "${DIM} ─────────────────────────────────────${RESET}"
|
|
89
|
+
printf '%b\n\n' "${DIM} v${VERSION} • ${CYAN}nyanime.tech${RESET}"
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
93
|
+
# UI HELPERS
|
|
94
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
95
|
+
|
|
96
|
+
print_line() {
|
|
97
|
+
printf '%b\n' "${CYAN}─────────────────────────────────────────────${RESET}"
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
msg() {
|
|
101
|
+
printf '%b\n' "${CYAN}▸ ${WHITE}$1${RESET}" >&2
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
msg_success() {
|
|
105
|
+
printf '%b\n' "${GREEN}✓ ${WHITE}$1${RESET}"
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
msg_error() {
|
|
109
|
+
printf '%b\n' "${RED}✗ ${WHITE}$1${RESET}" >&2
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
msg_warn() {
|
|
113
|
+
printf '%b\n' "${YELLOW}⚠ ${WHITE}$1${RESET}"
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
msg_info() {
|
|
117
|
+
printf '%b\n' "${BLUE}ℹ ${WHITE}$1${RESET}"
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
prompt() {
|
|
121
|
+
printf '%b' "${CYAN}▸ ${WHITE}$1${RESET}"
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
die() {
|
|
125
|
+
msg_error "$1"
|
|
126
|
+
exit 1
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
130
|
+
# UTILITY FUNCTIONS
|
|
131
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
132
|
+
|
|
133
|
+
check_deps() {
|
|
134
|
+
for cmd in curl sed grep node; do
|
|
135
|
+
command -v "$cmd" >/dev/null 2>&1 || die "Missing required dependency: $cmd"
|
|
136
|
+
done
|
|
137
|
+
|
|
138
|
+
# Check that backend.mjs and node_modules exist
|
|
139
|
+
if [ ! -f "$BACKEND_SCRIPT" ]; then
|
|
140
|
+
die "Backend script not found at $BACKEND_SCRIPT. Run install.sh or npm install in the ny-cli directory."
|
|
141
|
+
fi
|
|
142
|
+
if [ ! -d "${SCRIPT_DIR}/node_modules" ]; then
|
|
143
|
+
msg "Installing aniwatch package..."
|
|
144
|
+
(cd "$SCRIPT_DIR" && npm install --production 2>/dev/null) || die "Failed to install dependencies. Run 'npm install' in $SCRIPT_DIR"
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
if ! command -v "$PLAYER" >/dev/null 2>&1; then
|
|
148
|
+
if command -v mpv >/dev/null 2>&1; then
|
|
149
|
+
PLAYER="mpv"
|
|
150
|
+
elif command -v vlc >/dev/null 2>&1; then
|
|
151
|
+
PLAYER="vlc"
|
|
152
|
+
elif command -v iina >/dev/null 2>&1; then
|
|
153
|
+
PLAYER="iina"
|
|
154
|
+
else
|
|
155
|
+
msg_warn "No video player found. Install mpv:"
|
|
156
|
+
msg_info " Arch: sudo pacman -S mpv"
|
|
157
|
+
msg_info " Ubuntu: sudo apt install mpv"
|
|
158
|
+
msg_info " macOS: brew install mpv"
|
|
159
|
+
fi
|
|
160
|
+
fi
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
init_dirs() {
|
|
164
|
+
mkdir -p "$CONFIG_DIR" "$CACHE_DIR" "$DATA_DIR"
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
urlencode() {
|
|
168
|
+
# URL encode for query string parameters
|
|
169
|
+
printf '%s' "$1" | sed 's/ /%20/g; s/!/%21/g; s/"/%22/g; s/#/%23/g; s/\$/%24/g; s/&/%26/g; s/'\''/%27/g; s/(/%28/g; s/)/%29/g; s/+/%2B/g; s/,/%2C/g; s/:/%3A/g; s/;/%3B/g; s/=/%3D/g; s/?/%3F/g; s/@/%40/g'
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
# Base64 encode (use openssl or base64 command)
|
|
173
|
+
base64_encode() {
|
|
174
|
+
if command -v base64 >/dev/null 2>&1; then
|
|
175
|
+
printf '%s' "$1" | base64 -w0 2>/dev/null || printf '%s' "$1" | base64 2>/dev/null
|
|
176
|
+
elif command -v openssl >/dev/null 2>&1; then
|
|
177
|
+
printf '%s' "$1" | openssl base64 -A 2>/dev/null
|
|
178
|
+
else
|
|
179
|
+
printf '%s' "$1"
|
|
180
|
+
fi
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
# Get proxied stream URL (using nyanime.tech stream proxy)
|
|
184
|
+
get_proxied_url() {
|
|
185
|
+
local url="$1"
|
|
186
|
+
local referer="${2:-https://megacloud.blog/}"
|
|
187
|
+
|
|
188
|
+
# Encode headers as base64 JSON
|
|
189
|
+
local headers_json="{\"Referer\":\"${referer}\"}"
|
|
190
|
+
local headers_b64
|
|
191
|
+
headers_b64=$(base64_encode "$headers_json")
|
|
192
|
+
|
|
193
|
+
# Build proxy URL - URL encode the stream URL
|
|
194
|
+
local encoded_url
|
|
195
|
+
encoded_url=$(printf '%s' "$url" | sed 's/%/%25/g; s/ /%20/g; s/!/%21/g; s/"/%22/g; s/#/%23/g; s/\$/%24/g; s/&/%26/g; s/'\''/%27/g; s/(/%28/g; s/)/%29/g; s/+/%2B/g; s/,/%2C/g; s/:/%3A/g; s/;/%3B/g; s/=/%3D/g; s/?/%3F/g; s/@/%40/g')
|
|
196
|
+
|
|
197
|
+
printf '%s/stream?url=%s&h=%s' "$NYANIME_BASE" "$encoded_url" "$headers_b64"
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
open_url() {
|
|
201
|
+
if command -v xdg-open >/dev/null 2>&1; then
|
|
202
|
+
xdg-open "$1" 2>/dev/null &
|
|
203
|
+
elif command -v open >/dev/null 2>&1; then
|
|
204
|
+
open "$1" 2>/dev/null &
|
|
205
|
+
elif command -v wslview >/dev/null 2>&1; then
|
|
206
|
+
wslview "$1" 2>/dev/null &
|
|
207
|
+
else
|
|
208
|
+
msg_info "Open this URL: $1"
|
|
209
|
+
fi
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
213
|
+
# AUTHENTICATION
|
|
214
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
215
|
+
|
|
216
|
+
is_logged_in() {
|
|
217
|
+
[ -f "$AUTH_FILE" ] && [ -s "$AUTH_FILE" ]
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
get_username() {
|
|
221
|
+
is_logged_in && head -1 "$AUTH_FILE"
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
get_token() {
|
|
225
|
+
is_logged_in && sed -n '2p' "$AUTH_FILE"
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
do_login() {
|
|
229
|
+
printf "\n"
|
|
230
|
+
printf '%b\n' "${MAGENTA}╭─────────────────────────────────────────────────╮${RESET}"
|
|
231
|
+
printf '%b\n' "${MAGENTA}│${RESET} ${BOLD}🔐 Login to NyAnime${RESET} ${MAGENTA}│${RESET}"
|
|
232
|
+
printf '%b\n' "${MAGENTA}├─────────────────────────────────────────────────┤${RESET}"
|
|
233
|
+
printf '%b\n' "${MAGENTA}│${RESET} 1. Sign up/login at nyanime.tech ${MAGENTA}│${RESET}"
|
|
234
|
+
printf '%b\n' "${MAGENTA}│${RESET} 2. Go to your Profile page ${MAGENTA}│${RESET}"
|
|
235
|
+
printf '%b\n' "${MAGENTA}│${RESET} 3. Copy your User ID shown there ${MAGENTA}│${RESET}"
|
|
236
|
+
printf '%b\n' "${MAGENTA}│${RESET} 4. Paste it here ${MAGENTA}│${RESET}"
|
|
237
|
+
printf '%b\n' "${MAGENTA}╰─────────────────────────────────────────────────╯${RESET}"
|
|
238
|
+
printf "\n"
|
|
239
|
+
|
|
240
|
+
local login_url="https://www.nyanime.tech/signup"
|
|
241
|
+
msg "Opening browser..."
|
|
242
|
+
open_url "$login_url"
|
|
243
|
+
|
|
244
|
+
msg_info "If browser didn't open, visit: ${login_url}"
|
|
245
|
+
msg_info "After signing in, go to Profile to find your User ID"
|
|
246
|
+
printf "\n"
|
|
247
|
+
|
|
248
|
+
prompt "Your username: "
|
|
249
|
+
read -r username
|
|
250
|
+
[ -z "$username" ] && { msg_error "Login cancelled"; return; }
|
|
251
|
+
|
|
252
|
+
prompt "Paste User ID: "
|
|
253
|
+
read -r firebase_uid
|
|
254
|
+
[ -z "$firebase_uid" ] && { msg_error "Login cancelled"; return; }
|
|
255
|
+
|
|
256
|
+
# Verify the UID exists in Firebase by checking the user document
|
|
257
|
+
msg "Verifying account..."
|
|
258
|
+
local verify_resp
|
|
259
|
+
verify_resp=$(curl -s --connect-timeout 5 --max-time 8 \
|
|
260
|
+
"${FIRESTORE_BASE}/users/${firebase_uid}" 2>/dev/null)
|
|
261
|
+
|
|
262
|
+
# Check if we got a valid response (user exists)
|
|
263
|
+
if printf '%s' "$verify_resp" | grep -q '"fields"' 2>/dev/null; then
|
|
264
|
+
printf '%s\n%s\n' "$username" "$firebase_uid" > "$AUTH_FILE"
|
|
265
|
+
chmod 600 "$AUTH_FILE"
|
|
266
|
+
printf "\n"
|
|
267
|
+
msg_success "Welcome, ${username}! 🎉"
|
|
268
|
+
msg_info "Your watch history will sync to the cloud"
|
|
269
|
+
# Fetch cloud history and merge with local
|
|
270
|
+
fetch_and_merge_cloud_history
|
|
271
|
+
# Try to sync any pending local watch history to cloud
|
|
272
|
+
sync_pending_history
|
|
273
|
+
else
|
|
274
|
+
# User might not exist yet or network error - store locally anyway
|
|
275
|
+
printf '%s\n%s\n' "$username" "$firebase_uid" > "$AUTH_FILE"
|
|
276
|
+
chmod 600 "$AUTH_FILE"
|
|
277
|
+
printf "\n"
|
|
278
|
+
msg_success "Welcome, ${username}!"
|
|
279
|
+
msg_warn "Could not verify account - history will sync when online"
|
|
280
|
+
fi
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
do_logout() {
|
|
284
|
+
if [ -f "$AUTH_FILE" ]; then
|
|
285
|
+
local user
|
|
286
|
+
user=$(get_username)
|
|
287
|
+
rm -f "$AUTH_FILE"
|
|
288
|
+
msg_success "Goodbye, ${user}!"
|
|
289
|
+
else
|
|
290
|
+
msg_info "Not logged in"
|
|
291
|
+
fi
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
295
|
+
# WATCH HISTORY & CLOUD SYNC
|
|
296
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
297
|
+
|
|
298
|
+
# Check if online
|
|
299
|
+
is_online() {
|
|
300
|
+
curl -s --connect-timeout 2 --max-time 3 "https://www.google.com" >/dev/null 2>&1
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
# Fetch cloud history and merge with local history
|
|
304
|
+
# This pulls watch history from the website and merges it with local history
|
|
305
|
+
fetch_and_merge_cloud_history() {
|
|
306
|
+
is_logged_in || return 0
|
|
307
|
+
is_online || return 0
|
|
308
|
+
|
|
309
|
+
local firebase_uid
|
|
310
|
+
firebase_uid=$(get_token)
|
|
311
|
+
[ -z "$firebase_uid" ] && return 0
|
|
312
|
+
|
|
313
|
+
msg "Syncing with cloud..."
|
|
314
|
+
|
|
315
|
+
# Fetch history from the CLI history API (GET request)
|
|
316
|
+
local response
|
|
317
|
+
response=$(curl -s --connect-timeout 5 --max-time 10 \
|
|
318
|
+
"${NYANIME_BASE}/api/cli/history" \
|
|
319
|
+
-H "X-Firebase-UID: ${firebase_uid}" \
|
|
320
|
+
2>/dev/null)
|
|
321
|
+
|
|
322
|
+
# Check if we got valid history from the API
|
|
323
|
+
if printf '%s' "$response" | grep -q '"success"' 2>/dev/null; then
|
|
324
|
+
# Parse API format - contains animeSlug, animeTitle, episodeNum
|
|
325
|
+
local cloud_tmp="${CACHE_DIR}/cloud_merged.tmp"
|
|
326
|
+
> "$cloud_tmp"
|
|
327
|
+
|
|
328
|
+
# Extract each history item with slug and title
|
|
329
|
+
printf '%s' "$response" | tr '{' '\n' | grep '"animeSlug"' | while read -r item; do
|
|
330
|
+
local slug title ep_num
|
|
331
|
+
slug=$(printf '%s' "$item" | sed -n 's/.*"animeSlug"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
|
|
332
|
+
title=$(printf '%s' "$item" | sed -n 's/.*"animeTitle"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
|
|
333
|
+
ep_num=$(printf '%s' "$item" | sed -n 's/.*"episodeNum"[[:space:]]*:[[:space:]]*\([0-9]*\).*/\1/p')
|
|
334
|
+
|
|
335
|
+
if [ -n "$slug" ] && [ -n "$title" ]; then
|
|
336
|
+
[ -z "$ep_num" ] && ep_num=1
|
|
337
|
+
# Check if this anime is already in local history
|
|
338
|
+
if [ -f "$HISTORY_FILE" ]; then
|
|
339
|
+
if ! grep -q "^${slug}|" "$HISTORY_FILE" 2>/dev/null; then
|
|
340
|
+
# Add to local history (new entry from cloud)
|
|
341
|
+
local timestamp
|
|
342
|
+
timestamp=$(date +%s)
|
|
343
|
+
printf '%s|%s|%s|%s\n' "$slug" "$title" "$ep_num" "$timestamp" >> "$cloud_tmp"
|
|
344
|
+
fi
|
|
345
|
+
else
|
|
346
|
+
local timestamp
|
|
347
|
+
timestamp=$(date +%s)
|
|
348
|
+
printf '%s|%s|%s|%s\n' "$slug" "$title" "$ep_num" "$timestamp" >> "$cloud_tmp"
|
|
349
|
+
fi
|
|
350
|
+
fi
|
|
351
|
+
done
|
|
352
|
+
|
|
353
|
+
# Merge cloud items into local history
|
|
354
|
+
if [ -f "$cloud_tmp" ] && [ -s "$cloud_tmp" ]; then
|
|
355
|
+
if [ -f "$HISTORY_FILE" ]; then
|
|
356
|
+
# Append cloud items and deduplicate
|
|
357
|
+
cat "$cloud_tmp" >> "$HISTORY_FILE"
|
|
358
|
+
awk -F'|' '!seen[$1]++' "$HISTORY_FILE" > "${HISTORY_FILE}.tmp"
|
|
359
|
+
mv "${HISTORY_FILE}.tmp" "$HISTORY_FILE"
|
|
360
|
+
else
|
|
361
|
+
mv "$cloud_tmp" "$HISTORY_FILE"
|
|
362
|
+
fi
|
|
363
|
+
local added_count
|
|
364
|
+
added_count=$(wc -l < "$cloud_tmp" 2>/dev/null || echo 0)
|
|
365
|
+
[ "$added_count" -gt 0 ] && msg_success "Added $added_count items from cloud"
|
|
366
|
+
fi
|
|
367
|
+
rm -f "$cloud_tmp" 2>/dev/null
|
|
368
|
+
return 0
|
|
369
|
+
fi
|
|
370
|
+
|
|
371
|
+
msg_info "Cloud sync not available - using local history"
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
# Save watch progress to pending sync file (for offline support)
|
|
375
|
+
save_pending_sync() {
|
|
376
|
+
local anime_slug="$1" anime_title="$2" ep_num="$3"
|
|
377
|
+
local sync_time
|
|
378
|
+
sync_time=$(date +%s)
|
|
379
|
+
|
|
380
|
+
# Remove existing entry for this anime
|
|
381
|
+
[ -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
|
+
|
|
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"
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
# Sync pending history to cloud via nyanime API
|
|
388
|
+
sync_pending_history() {
|
|
389
|
+
[ ! -f "$PENDING_SYNC_FILE" ] && return 0
|
|
390
|
+
[ ! -s "$PENDING_SYNC_FILE" ] && return 0
|
|
391
|
+
|
|
392
|
+
is_logged_in || return 0
|
|
393
|
+
is_online || return 0
|
|
394
|
+
|
|
395
|
+
local firebase_uid
|
|
396
|
+
firebase_uid=$(get_token)
|
|
397
|
+
[ -z "$firebase_uid" ] && return 0
|
|
398
|
+
|
|
399
|
+
local synced=0
|
|
400
|
+
local temp_file="${PENDING_SYNC_FILE}.syncing"
|
|
401
|
+
|
|
402
|
+
# Process each pending entry
|
|
403
|
+
while IFS='|' read -r anime_slug anime_title ep_num sync_time; do
|
|
404
|
+
[ -z "$anime_slug" ] && continue
|
|
405
|
+
[ -z "$anime_title" ] && anime_title="$anime_slug"
|
|
406
|
+
[ -z "$ep_num" ] && ep_num=1
|
|
407
|
+
|
|
408
|
+
# Sync via nyanime API with slug-based format
|
|
409
|
+
local response
|
|
410
|
+
response=$(curl -s -X POST "${NYANIME_BASE}/api/cli/sync-watch" \
|
|
411
|
+
-H "Content-Type: application/json" \
|
|
412
|
+
-H "X-Firebase-UID: ${firebase_uid}" \
|
|
413
|
+
-d "{\"animeSlug\": \"${anime_slug}\", \"animeTitle\": \"${anime_title}\", \"episodeNum\": ${ep_num}}" \
|
|
414
|
+
--connect-timeout 5 --max-time 10 2>/dev/null)
|
|
415
|
+
|
|
416
|
+
if printf '%s' "$response" | grep -q '"success"' 2>/dev/null; then
|
|
417
|
+
synced=$((synced + 1))
|
|
418
|
+
else
|
|
419
|
+
# Keep failed entries for retry
|
|
420
|
+
printf '%s|%s|%s|%s\n' "$anime_slug" "$anime_title" "$ep_num" "$sync_time" >> "$temp_file"
|
|
421
|
+
fi
|
|
422
|
+
done < "$PENDING_SYNC_FILE"
|
|
423
|
+
|
|
424
|
+
# Replace pending file with failed entries only
|
|
425
|
+
if [ -f "$temp_file" ]; then
|
|
426
|
+
mv "$temp_file" "$PENDING_SYNC_FILE"
|
|
427
|
+
else
|
|
428
|
+
rm -f "$PENDING_SYNC_FILE"
|
|
429
|
+
fi
|
|
430
|
+
|
|
431
|
+
[ $synced -gt 0 ] && msg_success "Synced $synced items to cloud"
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
# Background sync daemon (called during playback)
|
|
435
|
+
start_sync_daemon() {
|
|
436
|
+
local anime_slug="$1" anime_title="$2" ep_num="$3"
|
|
437
|
+
|
|
438
|
+
# Create a background process that syncs every 10 seconds
|
|
439
|
+
(
|
|
440
|
+
while true; do
|
|
441
|
+
sleep "$SYNC_INTERVAL"
|
|
442
|
+
|
|
443
|
+
# Save to pending sync with slug and title
|
|
444
|
+
save_pending_sync "$anime_slug" "$anime_title" "$ep_num"
|
|
445
|
+
|
|
446
|
+
# Try to sync if online
|
|
447
|
+
sync_pending_history >/dev/null 2>&1
|
|
448
|
+
done
|
|
449
|
+
) &
|
|
450
|
+
SYNC_PID=$!
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
stop_sync_daemon() {
|
|
454
|
+
[ -n "${SYNC_PID:-}" ] && kill "$SYNC_PID" 2>/dev/null
|
|
455
|
+
SYNC_PID=""
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
save_to_history() {
|
|
459
|
+
local anime_slug="$1" title="$2" episode="$3"
|
|
460
|
+
local timestamp
|
|
461
|
+
timestamp=$(date +%s)
|
|
462
|
+
|
|
463
|
+
# Save to local history - remove ALL existing entries for this anime (deduplicate)
|
|
464
|
+
if [ -f "$HISTORY_FILE" ]; then
|
|
465
|
+
grep -v "^${anime_slug}|" "$HISTORY_FILE" > "${HISTORY_FILE}.tmp" 2>/dev/null
|
|
466
|
+
mv "${HISTORY_FILE}.tmp" "$HISTORY_FILE" 2>/dev/null || true
|
|
467
|
+
fi
|
|
468
|
+
|
|
469
|
+
# Create new entry and prepend to history
|
|
470
|
+
local new_entry
|
|
471
|
+
new_entry=$(printf '%s|%s|%s|%s\n' "$anime_slug" "$title" "$episode" "$timestamp")
|
|
472
|
+
|
|
473
|
+
if [ -f "$HISTORY_FILE" ] && [ -s "$HISTORY_FILE" ]; then
|
|
474
|
+
printf '%s\n' "$new_entry" | cat - "$HISTORY_FILE" > "${HISTORY_FILE}.tmp"
|
|
475
|
+
mv "${HISTORY_FILE}.tmp" "$HISTORY_FILE"
|
|
476
|
+
else
|
|
477
|
+
printf '%s\n' "$new_entry" > "$HISTORY_FILE"
|
|
478
|
+
fi
|
|
479
|
+
|
|
480
|
+
# Keep only the most recent 50 entries and ensure no duplicates
|
|
481
|
+
if [ -f "$HISTORY_FILE" ]; then
|
|
482
|
+
awk -F'|' '!seen[$1]++' "$HISTORY_FILE" | head -50 > "${HISTORY_FILE}.tmp"
|
|
483
|
+
mv "${HISTORY_FILE}.tmp" "$HISTORY_FILE"
|
|
484
|
+
fi
|
|
485
|
+
|
|
486
|
+
# Save to pending sync for cloud backup (slug, title, episode)
|
|
487
|
+
save_pending_sync "$anime_slug" "$title" "$episode"
|
|
488
|
+
|
|
489
|
+
# Try immediate sync if online
|
|
490
|
+
sync_pending_history >/dev/null 2>&1 &
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
494
|
+
# ANIME API - Self-hosted scraping via aniwatch npm package
|
|
495
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
496
|
+
|
|
497
|
+
# Call the Node.js backend helper
|
|
498
|
+
# Usage: call_backend <action> [args...]
|
|
499
|
+
# Returns JSON to stdout (pino logger noise is filtered out)
|
|
500
|
+
call_backend() {
|
|
501
|
+
NODE_ENV=production node "$BACKEND_SCRIPT" "$@" 2>/dev/null | grep -m1 '"success"\|"error"'
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
# JSON parsing helpers (pure POSIX shell)
|
|
505
|
+
# Extract a JSON string value by key
|
|
506
|
+
json_get_string() {
|
|
507
|
+
local json="$1" key="$2"
|
|
508
|
+
printf '%s' "$json" | sed -n 's/.*"'"$key"'"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
# Extract a JSON number value by key
|
|
512
|
+
json_get_number() {
|
|
513
|
+
local json="$1" key="$2"
|
|
514
|
+
printf '%s' "$json" | sed -n 's/.*"'"$key"'"[[:space:]]*:[[:space:]]*\([0-9]*\).*/\1/p' | head -1
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
# Search for anime by title
|
|
518
|
+
# Returns: id|name|type|episodes lines
|
|
519
|
+
search_anime() {
|
|
520
|
+
local query="$1"
|
|
521
|
+
|
|
522
|
+
msg "Searching for '$query'..."
|
|
523
|
+
|
|
524
|
+
local response
|
|
525
|
+
response=$(call_backend search "$query")
|
|
526
|
+
|
|
527
|
+
[ -z "$response" ] && return 1
|
|
528
|
+
printf '%s' "$response" | grep -q '"success"' || return 1
|
|
529
|
+
|
|
530
|
+
# Parse animes array from response
|
|
531
|
+
# Each anime has: id, name, type, episodes.sub, episodes.dub
|
|
532
|
+
printf '%s' "$response" | tr '{' '\n' | grep '"id"' | while IFS= read -r line; do
|
|
533
|
+
local id name type sub
|
|
534
|
+
id=$(printf '%s' "$line" | grep -oP '"id"\s*:\s*"\K[^"]+' 2>/dev/null)
|
|
535
|
+
name=$(printf '%s' "$line" | grep -oP '"name"\s*:\s*"\K[^"]+' 2>/dev/null)
|
|
536
|
+
type=$(printf '%s' "$line" | grep -oP '"type"\s*:\s*"\K[^"]+' 2>/dev/null)
|
|
537
|
+
sub=$(printf '%s' "$line" | grep -oP '"sub"\s*:\s*\K[0-9]+' 2>/dev/null)
|
|
538
|
+
[ -n "$id" ] && [ -n "$name" ] && printf '%s|%s|%s|%s\n' "$id" "$name" "${type:-TV}" "${sub:-0}"
|
|
539
|
+
done
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
# Get episodes for an anime
|
|
543
|
+
# Returns: ep_num|episode_id|title lines
|
|
544
|
+
get_episodes() {
|
|
545
|
+
local anime_id="$1"
|
|
546
|
+
|
|
547
|
+
msg "Loading episodes..."
|
|
548
|
+
|
|
549
|
+
local response
|
|
550
|
+
response=$(call_backend episodes "$anime_id")
|
|
551
|
+
|
|
552
|
+
[ -z "$response" ] && return 1
|
|
553
|
+
printf '%s' "$response" | grep -q '"success"' || return 1
|
|
554
|
+
|
|
555
|
+
# Parse episodes array
|
|
556
|
+
printf '%s' "$response" | tr '{' '\n' | grep '"episodeId"' | while IFS= read -r line; do
|
|
557
|
+
local ep_id ep_num title is_filler
|
|
558
|
+
ep_id=$(printf '%s' "$line" | grep -oP '"episodeId"\s*:\s*"\K[^"]+' 2>/dev/null)
|
|
559
|
+
ep_num=$(printf '%s' "$line" | grep -oP '"number"\s*:\s*\K[0-9]+' 2>/dev/null)
|
|
560
|
+
title=$(printf '%s' "$line" | grep -oP '"title"\s*:\s*"\K[^"]+' 2>/dev/null)
|
|
561
|
+
is_filler="0"
|
|
562
|
+
printf '%s' "$line" | grep -q '"isFiller"[[:space:]]*:[[:space:]]*true' && is_filler="1"
|
|
563
|
+
[ -n "$ep_id" ] && [ -n "$ep_num" ] && printf '%s|%s|%s|%s\n' "$ep_num" "$ep_id" "${title:-Episode $ep_num}" "$is_filler"
|
|
564
|
+
done
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
# Get available servers for an episode
|
|
568
|
+
# Returns: server_name lines
|
|
569
|
+
get_servers() {
|
|
570
|
+
local episode_id="$1"
|
|
571
|
+
|
|
572
|
+
local response
|
|
573
|
+
response=$(call_backend servers "$episode_id")
|
|
574
|
+
|
|
575
|
+
[ -z "$response" ] && return 1
|
|
576
|
+
|
|
577
|
+
# Extract server names from sub array
|
|
578
|
+
printf '%s' "$response" | grep -o '"serverName"[[:space:]]*:[[:space:]]*"[^"]*"' | \
|
|
579
|
+
sed 's/.*"\([^"]*\)"/\1/' | sort -u
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
# Get streaming sources for an episode (uses multi-server fallback in backend)
|
|
583
|
+
# Sets global variables: STREAM_URL, STREAM_REFERER, STREAM_SUBTITLES, STREAM_INTRO_START, STREAM_INTRO_END
|
|
584
|
+
get_stream() {
|
|
585
|
+
local episode_id="$1"
|
|
586
|
+
local category="${2:-sub}"
|
|
587
|
+
|
|
588
|
+
msg "Getting stream (self-hosted scraping, category: $category)..."
|
|
589
|
+
|
|
590
|
+
# Reset globals
|
|
591
|
+
STREAM_URL=""
|
|
592
|
+
STREAM_REFERER=""
|
|
593
|
+
STREAM_SUBTITLES=""
|
|
594
|
+
STREAM_INTRO_START=""
|
|
595
|
+
STREAM_INTRO_END=""
|
|
596
|
+
|
|
597
|
+
local response
|
|
598
|
+
response=$(call_backend sources "$episode_id" "$category")
|
|
599
|
+
|
|
600
|
+
[ -z "$response" ] && return 1
|
|
601
|
+
printf '%s' "$response" | grep -q '"success"' || return 1
|
|
602
|
+
|
|
603
|
+
# Handle data wrapper
|
|
604
|
+
if printf '%s' "$response" | grep -q '"data"' 2>/dev/null; then
|
|
605
|
+
response=$(printf '%s' "$response" | sed 's/.*"data"[[:space:]]*:[[:space:]]*//' | sed 's/}[[:space:]]*$//')
|
|
606
|
+
fi
|
|
607
|
+
|
|
608
|
+
# Extract stream URL from sources array
|
|
609
|
+
STREAM_URL=$(printf '%s' "$response" | sed -n 's/.*"sources"[[:space:]]*:[[:space:]]*\[.*"url"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
|
|
610
|
+
|
|
611
|
+
# Fallback: try extracting m3u8 URL directly
|
|
612
|
+
[ -z "$STREAM_URL" ] && STREAM_URL=$(printf '%s' "$response" | grep -o '"url"[[:space:]]*:[[:space:]]*"[^"]*\.m3u8"' | head -1 | sed 's/.*"\([^"]*\)"/\1/')
|
|
613
|
+
|
|
614
|
+
# Extract referer from headers
|
|
615
|
+
STREAM_REFERER=$(printf '%s' "$response" | grep -o '"Referer"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"/\1/')
|
|
616
|
+
[ -z "$STREAM_REFERER" ] && STREAM_REFERER="https://megacloud.blog/"
|
|
617
|
+
|
|
618
|
+
# Extract ALL subtitles from tracks/subtitles array
|
|
619
|
+
STREAM_SUBTITLES=""
|
|
620
|
+
STREAM_SUBTITLES_ALL=""
|
|
621
|
+
|
|
622
|
+
STREAM_SUBTITLES_ALL=$(printf '%s' "$response" | tr '{' '\n' | grep '"lang"' | grep -v -i 'thumbnails' | while read -r track; do
|
|
623
|
+
lang=$(printf '%s' "$track" | sed -n 's/.*"lang"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
|
|
624
|
+
url=$(printf '%s' "$track" | sed -n 's/.*"url"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
|
|
625
|
+
[ -n "$url" ] && [ -n "$lang" ] && printf '%s|%s\n' "$lang" "$url"
|
|
626
|
+
done)
|
|
627
|
+
|
|
628
|
+
# Set default English subtitle
|
|
629
|
+
STREAM_SUBTITLES=$(printf '%s\n' "$STREAM_SUBTITLES_ALL" | grep -i '^English|' | head -1 | cut -d'|' -f2)
|
|
630
|
+
[ -z "$STREAM_SUBTITLES" ] && STREAM_SUBTITLES=$(printf '%s\n' "$STREAM_SUBTITLES_ALL" | head -1 | cut -d'|' -f2)
|
|
631
|
+
|
|
632
|
+
# Extract intro timestamps
|
|
633
|
+
STREAM_INTRO_START=$(printf '%s' "$response" | sed -n 's/.*"intro"[[:space:]]*:[[:space:]]*{[^}]*"start"[[:space:]]*:[[:space:]]*\([0-9]*\).*/\1/p' | head -1)
|
|
634
|
+
STREAM_INTRO_END=$(printf '%s' "$response" | sed -n 's/.*"intro"[[:space:]]*:[[:space:]]*{[^}]*"end"[[:space:]]*:[[:space:]]*\([0-9]*\).*/\1/p' | head -1)
|
|
635
|
+
|
|
636
|
+
[ -n "$STREAM_URL" ] && return 0
|
|
637
|
+
return 1
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
# Get stream with category fallback (sub → dub)
|
|
641
|
+
get_stream_with_fallback() {
|
|
642
|
+
local episode_id="$1"
|
|
643
|
+
local category="${2:-sub}"
|
|
644
|
+
|
|
645
|
+
# The backend already handles multi-server fallback and sub→dub fallback
|
|
646
|
+
get_stream "$episode_id" "$category"
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
# Get trending/home anime
|
|
650
|
+
# Returns: id|name lines
|
|
651
|
+
get_trending() {
|
|
652
|
+
msg "Fetching trending..."
|
|
653
|
+
|
|
654
|
+
local response
|
|
655
|
+
response=$(call_backend home)
|
|
656
|
+
|
|
657
|
+
[ -z "$response" ] && return 1
|
|
658
|
+
|
|
659
|
+
# Handle data wrapper
|
|
660
|
+
if printf '%s' "$response" | grep -q '"data"' 2>/dev/null; then
|
|
661
|
+
response=$(printf '%s' "$response" | sed 's/.*"data"[[:space:]]*:[[:space:]]*//' | sed 's/}[[:space:]]*$//')
|
|
662
|
+
fi
|
|
663
|
+
|
|
664
|
+
# Parse trending animes
|
|
665
|
+
printf '%s' "$response" | tr '{' '\n' | grep '"id"' | while read -r line; do
|
|
666
|
+
local id name
|
|
667
|
+
id=$(json_get_string "$line" "id")
|
|
668
|
+
name=$(json_get_string "$line" "name")
|
|
669
|
+
[ -n "$id" ] && [ -n "$name" ] && printf '%s|%s\n' "$id" "$name"
|
|
670
|
+
done | head -15
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
674
|
+
# VIDEO PLAYER
|
|
675
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
676
|
+
|
|
677
|
+
# Build the stream URL (optionally through nyanime proxy for CORS bypass)
|
|
678
|
+
build_stream_url() {
|
|
679
|
+
local url="$1"
|
|
680
|
+
local use_proxy="${2:-0}"
|
|
681
|
+
|
|
682
|
+
if [ "$use_proxy" = "1" ] && [ -n "$NYANIME_BASE" ]; then
|
|
683
|
+
# Use nyanime.tech stream proxy for CORS bypass
|
|
684
|
+
local headers_json="{\"Referer\": \"${STREAM_REFERER:-https://megacloud.blog/}\"}"
|
|
685
|
+
local headers_b64
|
|
686
|
+
headers_b64=$(printf '%s' "$headers_json" | base64 -w 0 2>/dev/null || printf '%s' "$headers_json" | base64 2>/dev/null)
|
|
687
|
+
printf '%s/stream?url=%s&h=%s' "$NYANIME_BASE" "$(urlencode "$url")" "$headers_b64"
|
|
688
|
+
else
|
|
689
|
+
printf '%s' "$url"
|
|
690
|
+
fi
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
play_video() {
|
|
694
|
+
local url="$1" title="$2" subtitle="$3" referer="${4:-https://megacloud.blog/}"
|
|
695
|
+
|
|
696
|
+
[ -z "$url" ] && die "No stream available"
|
|
697
|
+
|
|
698
|
+
msg_success "Starting playback..."
|
|
699
|
+
printf '%b\n' " ${DIM}${title}${RESET}"
|
|
700
|
+
|
|
701
|
+
# Show intro skip info if available
|
|
702
|
+
if [ -n "$STREAM_INTRO_START" ] && [ -n "$STREAM_INTRO_END" ] && [ "$STREAM_INTRO_END" != "0" ]; then
|
|
703
|
+
printf '%b\n\n' " ${DIM}Intro: ${STREAM_INTRO_START}s - ${STREAM_INTRO_END}s (press 's' to skip)${RESET}"
|
|
704
|
+
else
|
|
705
|
+
printf '\n'
|
|
706
|
+
fi
|
|
707
|
+
|
|
708
|
+
case "$PLAYER" in
|
|
709
|
+
mpv*)
|
|
710
|
+
# Build mpv command with proper argument handling
|
|
711
|
+
local mpv_cmd="$PLAYER"
|
|
712
|
+
mpv_cmd="$mpv_cmd --force-media-title='$title'"
|
|
713
|
+
|
|
714
|
+
# Use --referrer for HLS streams (passes referer to ffmpeg for segment requests)
|
|
715
|
+
mpv_cmd="$mpv_cmd --referrer='$referer'"
|
|
716
|
+
|
|
717
|
+
# Disable ytdl to prevent interference with HLS
|
|
718
|
+
mpv_cmd="$mpv_cmd --ytdl=no"
|
|
719
|
+
|
|
720
|
+
# Add all available subtitles so user can switch between languages
|
|
721
|
+
if [ -n "$STREAM_SUBTITLES_ALL" ]; then
|
|
722
|
+
# Write subtitle URLs to temp file - English first, then others
|
|
723
|
+
local sub_file="${CACHE_DIR}/sub_urls.txt"
|
|
724
|
+
# Put English first so it's the default track
|
|
725
|
+
printf '%s\n' "$STREAM_SUBTITLES_ALL" | grep -i '^English|' | cut -d'|' -f2 > "$sub_file"
|
|
726
|
+
printf '%s\n' "$STREAM_SUBTITLES_ALL" | grep -vi '^English|' | cut -d'|' -f2 >> "$sub_file"
|
|
727
|
+
while IFS= read -r sub_url; do
|
|
728
|
+
[ -n "$sub_url" ] && mpv_cmd="$mpv_cmd --sub-files-append='$sub_url'"
|
|
729
|
+
done < "$sub_file"
|
|
730
|
+
rm -f "$sub_file"
|
|
731
|
+
mpv_cmd="$mpv_cmd --slang=eng,en,English --sid=1 --sub-visibility=yes"
|
|
732
|
+
elif [ -n "$subtitle" ]; then
|
|
733
|
+
# Fallback to single subtitle file
|
|
734
|
+
mpv_cmd="$mpv_cmd --sub-file='$subtitle' --sub-visibility=yes"
|
|
735
|
+
fi
|
|
736
|
+
|
|
737
|
+
# Add script for intro skip if available and intro times are valid
|
|
738
|
+
if [ -n "$STREAM_INTRO_START" ] && [ -n "$STREAM_INTRO_END" ] && [ "$STREAM_INTRO_END" != "0" ]; then
|
|
739
|
+
# Create a temporary script for intro skip
|
|
740
|
+
local skip_script="${CACHE_DIR}/intro_skip.lua"
|
|
741
|
+
cat > "$skip_script" << LUAEOF
|
|
742
|
+
-- Intro skip script
|
|
743
|
+
local intro_start = ${STREAM_INTRO_START}
|
|
744
|
+
local intro_end = ${STREAM_INTRO_END}
|
|
745
|
+
local skipped = false
|
|
746
|
+
|
|
747
|
+
mp.add_key_binding("s", "skip-intro", function()
|
|
748
|
+
local pos = mp.get_property_number("time-pos", 0)
|
|
749
|
+
if pos >= intro_start - 5 and pos <= intro_end then
|
|
750
|
+
mp.set_property_number("time-pos", intro_end)
|
|
751
|
+
mp.osd_message("Skipped intro")
|
|
752
|
+
else
|
|
753
|
+
mp.osd_message("No intro to skip")
|
|
754
|
+
end
|
|
755
|
+
end)
|
|
756
|
+
|
|
757
|
+
mp.observe_property("time-pos", "number", function(_, pos)
|
|
758
|
+
if pos and not skipped and pos >= intro_start and pos <= intro_start + 2 then
|
|
759
|
+
mp.osd_message("Press 's' to skip intro", 5)
|
|
760
|
+
end
|
|
761
|
+
end)
|
|
762
|
+
LUAEOF
|
|
763
|
+
mpv_cmd="$mpv_cmd --script='$skip_script'"
|
|
764
|
+
fi
|
|
765
|
+
|
|
766
|
+
# Run mpv with direct URL (--referrer passes to ffmpeg for HLS segments)
|
|
767
|
+
eval "$mpv_cmd '$url'"
|
|
768
|
+
;;
|
|
769
|
+
vlc*)
|
|
770
|
+
$PLAYER --meta-title="$title" --http-referrer="$referer" --play-and-exit "$url"
|
|
771
|
+
;;
|
|
772
|
+
iina*)
|
|
773
|
+
$PLAYER --mpv-force-media-title="$title" --mpv-referrer="$referer" "$url"
|
|
774
|
+
;;
|
|
775
|
+
*)
|
|
776
|
+
$PLAYER "$url"
|
|
777
|
+
;;
|
|
778
|
+
esac
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
782
|
+
# MAIN FEATURES
|
|
783
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
784
|
+
|
|
785
|
+
do_search() {
|
|
786
|
+
printf "\n"
|
|
787
|
+
prompt "Search anime: "
|
|
788
|
+
read -r query
|
|
789
|
+
[ -z "$query" ] && return
|
|
790
|
+
|
|
791
|
+
printf "\n"
|
|
792
|
+
results=$(search_anime "$query")
|
|
793
|
+
|
|
794
|
+
[ -z "$results" ] && msg_error "No results found for '$query'" && return
|
|
795
|
+
|
|
796
|
+
printf '%b\n' "\n${CYAN}Search Results:${RESET}"
|
|
797
|
+
print_line
|
|
798
|
+
i=1
|
|
799
|
+
printf '%s\n' "$results" | while IFS='|' read -r id title type eps_sub eps_dub; do
|
|
800
|
+
local info=""
|
|
801
|
+
[ -n "$type" ] && info="$type"
|
|
802
|
+
[ -n "$eps_sub" ] && [ "$eps_sub" != "0" ] && info="$info, ${eps_sub} eps"
|
|
803
|
+
[ -n "$info" ] && info=" ${DIM}($info)${RESET}"
|
|
804
|
+
printf '%b\n' " ${WHITE}$(printf '%2d' $i))${RESET} ${title}${info}"
|
|
805
|
+
i=$((i + 1))
|
|
806
|
+
done
|
|
807
|
+
print_line
|
|
808
|
+
|
|
809
|
+
printf "\n"
|
|
810
|
+
prompt "Select [1-20]: "
|
|
811
|
+
read -r choice
|
|
812
|
+
[ "$choice" = "q" ] && return
|
|
813
|
+
|
|
814
|
+
selected=$(printf '%s\n' "$results" | sed -n "${choice}p")
|
|
815
|
+
[ -z "$selected" ] && return
|
|
816
|
+
|
|
817
|
+
anime_id=$(printf '%s' "$selected" | cut -d'|' -f1)
|
|
818
|
+
anime_title=$(printf '%s' "$selected" | cut -d'|' -f2)
|
|
819
|
+
|
|
820
|
+
watch_anime "$anime_id" "$anime_title"
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
watch_anime() {
|
|
824
|
+
local anime_id="$1" anime_title="$2"
|
|
825
|
+
|
|
826
|
+
printf "\n"
|
|
827
|
+
episodes=$(get_episodes "$anime_id")
|
|
828
|
+
[ -z "$episodes" ] && msg_error "No episodes" && return
|
|
829
|
+
|
|
830
|
+
ep_count=$(printf '%s\n' "$episodes" | wc -l)
|
|
831
|
+
|
|
832
|
+
printf '%b\n' "\n${CYAN}${anime_title}${RESET} ${DIM}(${ep_count} eps)${RESET}"
|
|
833
|
+
print_line
|
|
834
|
+
|
|
835
|
+
if [ "$ep_count" -gt 20 ]; then
|
|
836
|
+
printf '%s\n' "$episodes" | head -10 | while IFS='|' read -r num id title; do
|
|
837
|
+
printf '%b\n' " ${WHITE}$(printf '%3s' "$num"))${RESET} Episode $num"
|
|
838
|
+
done
|
|
839
|
+
printf '%b\n' " ${DIM} ... ($((ep_count - 15)) more) ...${RESET}"
|
|
840
|
+
printf '%s\n' "$episodes" | tail -5 | while IFS='|' read -r num id title; do
|
|
841
|
+
printf '%b\n' " ${WHITE}$(printf '%3s' "$num"))${RESET} Episode $num"
|
|
842
|
+
done
|
|
843
|
+
else
|
|
844
|
+
printf '%s\n' "$episodes" | while IFS='|' read -r num id title; do
|
|
845
|
+
printf '%b\n' " ${WHITE}$(printf '%3s' "$num"))${RESET} Episode $num"
|
|
846
|
+
done
|
|
847
|
+
fi
|
|
848
|
+
print_line
|
|
849
|
+
|
|
850
|
+
printf "\n"
|
|
851
|
+
prompt "Episode [1-$ep_count]: "
|
|
852
|
+
read -r ep_choice
|
|
853
|
+
[ "$ep_choice" = "q" ] && return
|
|
854
|
+
|
|
855
|
+
ep_line=$(printf '%s\n' "$episodes" | grep "^${ep_choice}|")
|
|
856
|
+
[ -z "$ep_line" ] && msg_error "Invalid" && return
|
|
857
|
+
|
|
858
|
+
episode_id=$(printf '%s' "$ep_line" | cut -d'|' -f2)
|
|
859
|
+
play_episode "$anime_id" "$anime_title" "$ep_choice" "$episode_id" "$episodes"
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
play_episode() {
|
|
863
|
+
local anime_id="$1" anime_title="$2" ep_num="$3" episode_id="$4" episodes="$5"
|
|
864
|
+
|
|
865
|
+
printf "\n"
|
|
866
|
+
|
|
867
|
+
# Get streaming sources with fallback to multiple servers
|
|
868
|
+
if ! get_stream_with_fallback "$episode_id" "sub"; then
|
|
869
|
+
msg_error "No stream available for this episode"
|
|
870
|
+
return
|
|
871
|
+
fi
|
|
872
|
+
|
|
873
|
+
[ -z "$STREAM_URL" ] && msg_error "No stream URL found" && return
|
|
874
|
+
|
|
875
|
+
# Save to history (slug, title, episode)
|
|
876
|
+
save_to_history "$anime_id" "$anime_title" "$ep_num"
|
|
877
|
+
|
|
878
|
+
# Start background sync daemon during playback (slug, title, episode)
|
|
879
|
+
start_sync_daemon "$anime_id" "$anime_title" "$ep_num"
|
|
880
|
+
|
|
881
|
+
# Play the video with referer header
|
|
882
|
+
play_video "$STREAM_URL" "${anime_title} - Episode ${ep_num}" "$STREAM_SUBTITLES" "$STREAM_REFERER"
|
|
883
|
+
|
|
884
|
+
# Stop sync daemon after playback ends
|
|
885
|
+
stop_sync_daemon
|
|
886
|
+
|
|
887
|
+
# Final sync attempt
|
|
888
|
+
sync_pending_history >/dev/null 2>&1 &
|
|
889
|
+
|
|
890
|
+
printf "\n"
|
|
891
|
+
print_line
|
|
892
|
+
printf '%b\n' " ${WHITE}n)${RESET} Next ${WHITE}r)${RESET} Replay ${WHITE}s)${RESET} Select ${WHITE}q)${RESET} Quit"
|
|
893
|
+
print_line
|
|
894
|
+
prompt "Choice: "
|
|
895
|
+
read -r choice
|
|
896
|
+
|
|
897
|
+
case "$choice" in
|
|
898
|
+
n)
|
|
899
|
+
next_ep=$((ep_num + 1))
|
|
900
|
+
next_line=$(printf '%s\n' "$episodes" | grep "^${next_ep}|")
|
|
901
|
+
if [ -n "$next_line" ]; then
|
|
902
|
+
next_id=$(printf '%s' "$next_line" | cut -d'|' -f2)
|
|
903
|
+
play_episode "$anime_id" "$anime_title" "$next_ep" "$next_id" "$episodes"
|
|
904
|
+
else
|
|
905
|
+
msg_info "No more episodes"
|
|
906
|
+
fi
|
|
907
|
+
;;
|
|
908
|
+
r) play_episode "$anime_id" "$anime_title" "$ep_num" "$episode_id" "$episodes" ;;
|
|
909
|
+
s) watch_anime "$anime_id" "$anime_title" ;;
|
|
910
|
+
esac
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
do_continue() {
|
|
914
|
+
# First, try to fetch and merge cloud history if logged in
|
|
915
|
+
if is_logged_in; then
|
|
916
|
+
fetch_and_merge_cloud_history >/dev/null 2>&1
|
|
917
|
+
fi
|
|
918
|
+
|
|
919
|
+
# Also clean up any duplicates in local history
|
|
920
|
+
if [ -f "$HISTORY_FILE" ]; then
|
|
921
|
+
awk -F'|' '!seen[$1]++' "$HISTORY_FILE" > "${HISTORY_FILE}.tmp" 2>/dev/null
|
|
922
|
+
mv "${HISTORY_FILE}.tmp" "$HISTORY_FILE" 2>/dev/null || true
|
|
923
|
+
fi
|
|
924
|
+
|
|
925
|
+
[ ! -f "$HISTORY_FILE" ] && msg_info "No history yet" && return
|
|
926
|
+
|
|
927
|
+
# Try to sync any pending history first
|
|
928
|
+
sync_pending_history >/dev/null 2>&1
|
|
929
|
+
|
|
930
|
+
printf '%b\n' "\n${CYAN}Continue Watching:${RESET}"
|
|
931
|
+
print_line
|
|
932
|
+
|
|
933
|
+
i=1
|
|
934
|
+
head -10 "$HISTORY_FILE" | while IFS='|' read -r id title episode ts; do
|
|
935
|
+
printf '%b\n' " ${WHITE}$(printf '%2d' $i))${RESET} $title ${DIM}(Ep $episode)${RESET}"
|
|
936
|
+
i=$((i + 1))
|
|
937
|
+
done
|
|
938
|
+
print_line
|
|
939
|
+
|
|
940
|
+
printf "\n"
|
|
941
|
+
prompt "Select: "
|
|
942
|
+
read -r choice
|
|
943
|
+
[ "$choice" = "q" ] && return
|
|
944
|
+
|
|
945
|
+
selected=$(sed -n "${choice}p" "$HISTORY_FILE")
|
|
946
|
+
[ -z "$selected" ] && return
|
|
947
|
+
|
|
948
|
+
anime_id=$(printf '%s' "$selected" | cut -d'|' -f1)
|
|
949
|
+
anime_title=$(printf '%s' "$selected" | cut -d'|' -f2)
|
|
950
|
+
last_ep=$(printf '%s' "$selected" | cut -d'|' -f3)
|
|
951
|
+
|
|
952
|
+
episodes=$(get_episodes "$anime_id")
|
|
953
|
+
next_ep=$((last_ep + 1))
|
|
954
|
+
next_line=$(printf '%s\n' "$episodes" | grep "^${next_ep}|")
|
|
955
|
+
|
|
956
|
+
if [ -n "$next_line" ]; then
|
|
957
|
+
msg_info "Continuing Episode ${next_ep}"
|
|
958
|
+
episode_id=$(printf '%s' "$next_line" | cut -d'|' -f2)
|
|
959
|
+
play_episode "$anime_id" "$anime_title" "$next_ep" "$episode_id" "$episodes"
|
|
960
|
+
else
|
|
961
|
+
watch_anime "$anime_id" "$anime_title"
|
|
962
|
+
fi
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
do_recommendations() {
|
|
966
|
+
printf "\n"
|
|
967
|
+
trending=$(get_trending)
|
|
968
|
+
[ -z "$trending" ] && msg_error "Failed to fetch" && return
|
|
969
|
+
|
|
970
|
+
printf '%b\n' "\n${CYAN}📚 Trending:${RESET}"
|
|
971
|
+
print_line
|
|
972
|
+
|
|
973
|
+
i=1
|
|
974
|
+
printf '%s\n' "$trending" | while IFS='|' read -r id title; do
|
|
975
|
+
printf '%b\n' " ${WHITE}$(printf '%2d' $i))${RESET} $title"
|
|
976
|
+
i=$((i + 1))
|
|
977
|
+
done
|
|
978
|
+
print_line
|
|
979
|
+
|
|
980
|
+
printf "\n"
|
|
981
|
+
prompt "Select: "
|
|
982
|
+
read -r choice
|
|
983
|
+
[ "$choice" = "q" ] && return
|
|
984
|
+
|
|
985
|
+
selected=$(printf '%s\n' "$trending" | sed -n "${choice}p")
|
|
986
|
+
[ -z "$selected" ] && return
|
|
987
|
+
|
|
988
|
+
anime_id=$(printf '%s' "$selected" | cut -d'|' -f1)
|
|
989
|
+
anime_title=$(printf '%s' "$selected" | cut -d'|' -f2)
|
|
990
|
+
watch_anime "$anime_id" "$anime_title"
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
do_random() {
|
|
994
|
+
printf "\n"
|
|
995
|
+
msg "Finding random anime..."
|
|
996
|
+
|
|
997
|
+
# Generate random search query from common anime terms for true randomness
|
|
998
|
+
local random_terms="a b c d e f g h i j k l m n o p q r s t u v w x y z 1 2 3"
|
|
999
|
+
local term_count=29
|
|
1000
|
+
local rand_idx=$(awk 'BEGIN { srand(); print int(rand() * '"$term_count"') + 1 }')
|
|
1001
|
+
local query=$(printf '%s' "$random_terms" | tr ' ' '\n' | sed -n "${rand_idx}p")
|
|
1002
|
+
|
|
1003
|
+
# Search with random letter/number to get diverse results
|
|
1004
|
+
local results
|
|
1005
|
+
results=$(search_anime "$query" 2>/dev/null)
|
|
1006
|
+
[ -z "$results" ] && { msg_error "Failed to find anime"; return; }
|
|
1007
|
+
|
|
1008
|
+
# Pick random anime from results
|
|
1009
|
+
local count=$(printf '%s\n' "$results" | wc -l)
|
|
1010
|
+
local idx=$(awk 'BEGIN { srand(); print int(rand() * '"$count"') + 1 }')
|
|
1011
|
+
local selected=$(printf '%s\n' "$results" | sed -n "${idx}p")
|
|
1012
|
+
|
|
1013
|
+
local anime_id=$(printf '%s' "$selected" | cut -d'|' -f1)
|
|
1014
|
+
local anime_title=$(printf '%s' "$selected" | cut -d'|' -f2)
|
|
1015
|
+
|
|
1016
|
+
printf '%b\n\n' "\n${YELLOW}🎲 Random:${RESET} ${WHITE}${anime_title}${RESET}"
|
|
1017
|
+
prompt "Watch? [Y/n]: "
|
|
1018
|
+
read -r ans
|
|
1019
|
+
|
|
1020
|
+
case "$ans" in
|
|
1021
|
+
[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
|
+
;;
|
|
1030
|
+
esac
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
show_profile() {
|
|
1034
|
+
is_logged_in || { msg_info "Not logged in"; return; }
|
|
1035
|
+
|
|
1036
|
+
local username
|
|
1037
|
+
username=$(get_username)
|
|
1038
|
+
|
|
1039
|
+
printf '%b\n' "\n${MAGENTA}╭────────────────────────────────╮${RESET}"
|
|
1040
|
+
printf '%b\n' "${MAGENTA}│${RESET} ${BOLD}👤 ${username}${RESET}"
|
|
1041
|
+
printf '%b\n\n' "${MAGENTA}╰────────────────────────────────╯${RESET}"
|
|
1042
|
+
|
|
1043
|
+
# Show sync status
|
|
1044
|
+
if [ -f "$PENDING_SYNC_FILE" ] && [ -s "$PENDING_SYNC_FILE" ]; then
|
|
1045
|
+
local pending_count
|
|
1046
|
+
pending_count=$(wc -l < "$PENDING_SYNC_FILE")
|
|
1047
|
+
printf '%b\n' " ${YELLOW}⏳ ${pending_count} items pending sync${RESET}"
|
|
1048
|
+
printf '%b\n\n' " ${DIM}Will sync when online${RESET}"
|
|
1049
|
+
fi
|
|
1050
|
+
|
|
1051
|
+
printf '%b\n\n' " ${WHITE}1)${RESET} Logout ${WHITE}2)${RESET} Sync Now ${WHITE}3)${RESET} Back"
|
|
1052
|
+
prompt "Choice: "
|
|
1053
|
+
read -r c
|
|
1054
|
+
case "$c" in
|
|
1055
|
+
1) do_logout ;;
|
|
1056
|
+
2) msg "Syncing..."; sync_pending_history ;;
|
|
1057
|
+
esac
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
1061
|
+
# HELP & MENUS
|
|
1062
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
1063
|
+
|
|
1064
|
+
show_help() {
|
|
1065
|
+
printf '%b\n' "
|
|
1066
|
+
${BOLD}NY-CLI${RESET} v${VERSION} - Terminal Anime Streaming
|
|
1067
|
+
|
|
1068
|
+
${CYAN}USAGE:${RESET} ny-cli [OPTIONS] [QUERY]
|
|
1069
|
+
|
|
1070
|
+
${CYAN}OPTIONS:${RESET}
|
|
1071
|
+
-s, --search <q> Search anime
|
|
1072
|
+
-c, --continue Continue watching
|
|
1073
|
+
-r, --random Random anime
|
|
1074
|
+
-t, --trending Show trending
|
|
1075
|
+
-l, --login Login
|
|
1076
|
+
-L, --logout Logout
|
|
1077
|
+
-h, --help Help
|
|
1078
|
+
-v, --version Version
|
|
1079
|
+
|
|
1080
|
+
${CYAN}EXAMPLES:${RESET}
|
|
1081
|
+
ny-cli Interactive mode
|
|
1082
|
+
ny-cli \"one piece\" Quick search
|
|
1083
|
+
ny-cli -c Continue watching
|
|
1084
|
+
|
|
1085
|
+
${CYAN}PLAYER CONTROLS (mpv):${RESET}
|
|
1086
|
+
Space Play/Pause f Fullscreen
|
|
1087
|
+
←/→ Seek 5s q Quit
|
|
1088
|
+
↑/↓ Seek 60s v Subtitles
|
|
1089
|
+
"
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
main_menu() {
|
|
1093
|
+
while true; do
|
|
1094
|
+
show_banner
|
|
1095
|
+
|
|
1096
|
+
if is_logged_in; then
|
|
1097
|
+
local username
|
|
1098
|
+
username=$(get_username)
|
|
1099
|
+
printf '%b\n\n' "${CYAN}Welcome, ${WHITE}${username}${CYAN}! 🎉${RESET}"
|
|
1100
|
+
|
|
1101
|
+
printf '%b\n' "${CYAN}╭────────────────────────────────────────╮${RESET}"
|
|
1102
|
+
printf '%b\n' "${CYAN}│${RESET} ${WHITE}1)${RESET} 👤 Profile ${CYAN}│${RESET}"
|
|
1103
|
+
printf '%b\n' "${CYAN}│${RESET} ${WHITE}2)${RESET} ▶️ Continue Watching ${CYAN}│${RESET}"
|
|
1104
|
+
printf '%b\n' "${CYAN}│${RESET} ${WHITE}3)${RESET} 🔍 Search ${CYAN}│${RESET}"
|
|
1105
|
+
printf '%b\n' "${CYAN}│${RESET} ${WHITE}4)${RESET} 📚 Trending ${CYAN}│${RESET}"
|
|
1106
|
+
printf '%b\n' "${CYAN}│${RESET} ${WHITE}5)${RESET} 🎲 Random ${CYAN}│${RESET}"
|
|
1107
|
+
printf '%b\n' "${CYAN}│${RESET} ${WHITE}h)${RESET} ❓ Help ${CYAN}│${RESET}"
|
|
1108
|
+
printf '%b\n' "${CYAN}│${RESET} ${WHITE}q)${RESET} 🚪 Exit ${CYAN}│${RESET}"
|
|
1109
|
+
printf '%b\n\n' "${CYAN}╰────────────────────────────────────────╯${RESET}"
|
|
1110
|
+
prompt "Choice: "
|
|
1111
|
+
read -r c
|
|
1112
|
+
|
|
1113
|
+
case "$c" in
|
|
1114
|
+
1) show_profile ;;
|
|
1115
|
+
2) do_continue ;;
|
|
1116
|
+
3) do_search ;;
|
|
1117
|
+
4) do_recommendations ;;
|
|
1118
|
+
5) do_random ;;
|
|
1119
|
+
h|H) show_help; printf '%b' "\n${DIM}Press Enter...${RESET}"; read -r _ ;;
|
|
1120
|
+
q|Q) printf '%b\n\n' "\n${MAGENTA}Sayounara! 👋${RESET}"; exit 0 ;;
|
|
1121
|
+
esac
|
|
1122
|
+
else
|
|
1123
|
+
printf '%b\n\n' "${CYAN}Welcome! Sign in for all features.${RESET}"
|
|
1124
|
+
|
|
1125
|
+
printf '%b\n' "${CYAN}╭────────────────────────────────────────╮${RESET}"
|
|
1126
|
+
printf '%b\n' "${CYAN}│${RESET} ${WHITE}1)${RESET} 🔍 Search ${CYAN}│${RESET}"
|
|
1127
|
+
printf '%b\n' "${CYAN}│${RESET} ${WHITE}2)${RESET} 📚 Trending ${CYAN}│${RESET}"
|
|
1128
|
+
printf '%b\n' "${CYAN}│${RESET} ${WHITE}3)${RESET} 🔐 Login ${CYAN}│${RESET}"
|
|
1129
|
+
printf '%b\n' "${CYAN}│${RESET} ${WHITE}h)${RESET} ❓ Help ${CYAN}│${RESET}"
|
|
1130
|
+
printf '%b\n' "${CYAN}│${RESET} ${WHITE}q)${RESET} 🚪 Exit ${CYAN}│${RESET}"
|
|
1131
|
+
printf '%b\n\n' "${CYAN}╰────────────────────────────────────────╯${RESET}"
|
|
1132
|
+
prompt "Choice: "
|
|
1133
|
+
read -r c
|
|
1134
|
+
|
|
1135
|
+
case "$c" in
|
|
1136
|
+
1) do_search ;;
|
|
1137
|
+
2) do_recommendations ;;
|
|
1138
|
+
3) do_login ;;
|
|
1139
|
+
h|H) show_help; printf '%b' "\n${DIM}Press Enter...${RESET}"; read -r _ ;;
|
|
1140
|
+
q|Q) printf '%b\n\n' "\n${MAGENTA}Sayounara! 👋${RESET}"; exit 0 ;;
|
|
1141
|
+
esac
|
|
1142
|
+
fi
|
|
1143
|
+
done
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
1147
|
+
# ENTRY POINT
|
|
1148
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
1149
|
+
|
|
1150
|
+
main() {
|
|
1151
|
+
check_deps
|
|
1152
|
+
init_dirs
|
|
1153
|
+
|
|
1154
|
+
case "${1:-}" in
|
|
1155
|
+
-h|--help) show_help; exit 0 ;;
|
|
1156
|
+
-v|--version) printf "ny-cli %s\n" "$VERSION"; exit 0 ;;
|
|
1157
|
+
-l|--login) show_banner; do_login; exit 0 ;;
|
|
1158
|
+
-L|--logout) do_logout; exit 0 ;;
|
|
1159
|
+
-c|--continue) show_banner; do_continue; exit 0 ;;
|
|
1160
|
+
-r|--random) show_banner; do_random; exit 0 ;;
|
|
1161
|
+
-t|--trending) show_banner; do_recommendations; exit 0 ;;
|
|
1162
|
+
-s|--search)
|
|
1163
|
+
shift
|
|
1164
|
+
show_banner
|
|
1165
|
+
[ -n "$1" ] && {
|
|
1166
|
+
results=$(search_anime "$*")
|
|
1167
|
+
[ -n "$results" ] && {
|
|
1168
|
+
printf '%b\n' "\n${CYAN}Results:${RESET}"
|
|
1169
|
+
print_line
|
|
1170
|
+
i=1
|
|
1171
|
+
printf '%s\n' "$results" | while IFS='|' read -r id title type eps; do
|
|
1172
|
+
local info=""
|
|
1173
|
+
[ -n "$type" ] && info="$type"
|
|
1174
|
+
[ -n "$eps" ] && [ "$eps" != "0" ] && info="$info, ${eps} eps"
|
|
1175
|
+
[ -n "$info" ] && info=" ${DIM}($info)${RESET}"
|
|
1176
|
+
printf '%b\n' " ${WHITE}$(printf '%2d' $i))${RESET} ${title}${info}"
|
|
1177
|
+
i=$((i + 1))
|
|
1178
|
+
done
|
|
1179
|
+
print_line
|
|
1180
|
+
printf "\n"
|
|
1181
|
+
prompt "Select: "
|
|
1182
|
+
read -r c
|
|
1183
|
+
selected=$(printf '%s\n' "$results" | sed -n "${c}p")
|
|
1184
|
+
[ -n "$selected" ] && watch_anime "$(printf '%s' "$selected" | cut -d'|' -f1)" "$(printf '%s' "$selected" | cut -d'|' -f2)"
|
|
1185
|
+
} || msg_error "No results"
|
|
1186
|
+
}
|
|
1187
|
+
exit 0
|
|
1188
|
+
;;
|
|
1189
|
+
"")
|
|
1190
|
+
main_menu
|
|
1191
|
+
;;
|
|
1192
|
+
*)
|
|
1193
|
+
show_banner
|
|
1194
|
+
results=$(search_anime "$*")
|
|
1195
|
+
[ -n "$results" ] && {
|
|
1196
|
+
printf '%b\n' "\n${CYAN}Results:${RESET}"
|
|
1197
|
+
print_line
|
|
1198
|
+
i=1
|
|
1199
|
+
printf '%s\n' "$results" | while IFS='|' read -r id title type eps; do
|
|
1200
|
+
local info=""
|
|
1201
|
+
[ -n "$type" ] && info="$type"
|
|
1202
|
+
[ -n "$eps" ] && [ "$eps" != "0" ] && info="$info, ${eps} eps"
|
|
1203
|
+
[ -n "$info" ] && info=" ${DIM}($info)${RESET}"
|
|
1204
|
+
printf '%b\n' " ${WHITE}$(printf '%2d' $i))${RESET} ${title}${info}"
|
|
1205
|
+
i=$((i + 1))
|
|
1206
|
+
done
|
|
1207
|
+
print_line
|
|
1208
|
+
printf "\n"
|
|
1209
|
+
prompt "Select: "
|
|
1210
|
+
read -r c
|
|
1211
|
+
selected=$(printf '%s\n' "$results" | sed -n "${c}p")
|
|
1212
|
+
[ -n "$selected" ] && watch_anime "$(printf '%s' "$selected" | cut -d'|' -f1)" "$(printf '%s' "$selected" | cut -d'|' -f2)"
|
|
1213
|
+
} || msg_error "No results"
|
|
1214
|
+
exit 0
|
|
1215
|
+
;;
|
|
1216
|
+
esac
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
main "$@"
|