forked from pystardust/ani-cli
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathani-cli
More file actions
executable file
·1020 lines (925 loc) · 42.5 KB
/
Copy pathani-cli
File metadata and controls
executable file
·1020 lines (925 loc) · 42.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/bin/sh
version_number="5.0.3"
# UI
# shellcheck disable=SC2086
# $1 = prompt, $2 = multi select flag (for fzf/rofi), $3 = extra flags by user
menu() {
case "$menu_program" in
fzf) fzf --reverse --cycle --prompt "$1" $2 $3 ;;
rofi) rofi -sort -dmenu -i -p "$1" $2 $3 ;;
dmenu) dmenu -l 20 -p "$1" $3 ;;
*) "$menu_program" $3 "$1" ;;
esac
}
# provide menu. input format is either (ep_list) or (number \t anime_id \t anime_name)
nth() {
_stdin=$(cat -)
[ -z "$_stdin" ] && return 1
_line_count="$(printf "%s\n" "$_stdin" | wc -l | tr -d "[:space:]")"
[ "$_line_count" -eq 1 ] && printf "%s" "$_stdin" | cut -f 2,3 && return 0
_prompt="$1"
[ $# -ne 1 ] && _multi_flag="$2"
_line=$(printf "%s" "$_stdin" | cut -f 1,3 | tr '\t' ' ' | menu "$_prompt" "$_multi_flag" "$menu_extra_flags" | cut -d " " -f 1)
_line_start=$(printf "%s" "$_line" | head -n 1)
_line_end=$(printf "%s" "$_line" | tail -n 1)
[ -n "$_line" ] || return 1
if [ "$_line_start" = "$_line_end" ]; then
printf "%s" "$_stdin" | grep -E '^'"${_line}"'($|[[:space:]])' | cut -f 2,3 || return 1
else
printf "%s" "$_stdin" | sed -n '/^'"${_line_start}"'$/,/^'"${_line_end}$"'/p' || return 1
fi
}
die() {
printf "\33[2K\r\033[1;31m%s\033[0m\n" "$*" >&2
exit 1
}
info() {
printf "\33[2K\r\033[1;34m%s\033[0m\n" "$*"
}
help_info() {
printf "
Usage:
%s [options] [query]
%s [query] [options]
%s [options] [query] [options]
Options:
-c, --continue
Continue watching from history
-d, --download
Download the video instead of playing it
-D, --delete
Delete history
-l, --logview
Show logs
-s, --scans
Read manga scans via Anime-Sama
--syncplay
Use Syncplay to watch with friends
-S, --select-nth
Select nth entry
-q, --quality
Specify the video quality
-v, --vlc
Use VLC to play the video
-V, --version
Show the version of the script
-h, --help
Show this help message and exit
-e, --episode, -r, --range
Specify the number of episodes to watch
--dub
Play dubbed version
--rofi
Use rofi instead of fzf for the interactive menu
--dmenu
Use dmenu instead of fzf for the interactive menu
--skip
Use ani-skip to skip the intro of the episode (mpv only)
--no-detach
Don't detach the player (useful for in-terminal playback, mpv only)
--exit-after-play
Exit the player, and return the player exit code (useful for non interactive scenarios)
--skip-title <title>
Use given title as ani-skip query
-N, --nextep-countdown
Display a countdown to the next episode
-a, --anime-sama
Use Anime-Sama as source (animes VF/VOSTFR)
--source <server>
Specify preferred player/server for Anime-Sama (e.g. sibnet, vidmoly, sendvid, streamtape, vk)
--vf
Play French dubbed version (VF) - for Anime-Sama source
-m, --movix
Use Movix.date as source (animes + films + series)
--movix-anime
Use Movix source - animes only
--movix-tv
Use Movix source - TV series only
--movix-movie
Use Movix source - movies only
--season <N>
Select season number (Movix TV, default: 1)
-U, --update
Update the script
Some example usages:
%s -q 720p banana fish
%s --skip -S 2 one piece
%s -d -e 2 cyberpunk edgerunners
%s --vlc cyberpunk edgerunners -q 1080p -e 4
%s blue lock -e 5-6
%s -e \"5 6\" blue lock
\n" "${0##*/}" "${0##*/}" "${0##*/}" "${0##*/}" "${0##*/}" "${0##*/}" "${0##*/}" "${0##*/}" "${0##*/}"
exit 0
}
version_info() {
printf "%s\n" "$version_number"
exit 0
}
# Bookkeeping
update_script() {
info "[branch:$branch] Checking for Update.."
update="$($curl_exe --fail-with-body -sL -A "$agent" "https://raw.githubusercontent.com/pystardust/ani-cli/$branch/ani-cli")" || die "[branch:$branch] Connection error: $update"
new_version="$(printf "%s" "$update" | sed -nE 's|^version_number="([^"]+)"$|\1|p')"
[ -z "$new_version" ] && die "[branch:$branch] Invalid response."
printf "%s" "$update" | grep -q "^version_number" || die "[branch:$branch] Invalid response."
update="$(printf "%s\n" "$update" | diff -u "$0" -)"
if [ -z "$update" ]; then
info "[branch:$branch] Script is up to date :) ($version_number)"
else
if printf "%s\n" "$update" | patch "$0" -; then
info "[branch:$branch] Updated: $version_number -> $new_version"
else
die "[branch:$branch] Failed to update. Read the above line!"
fi
fi
exit 0
}
# checks if dependencies are present
dep_ch() {
command -v "$1" >/dev/null || die "Program $1 not found. Please install it."
}
# checks for deps in order and returns the first available. format is "prog_a,prog_b,prog_c"
dep_ch_failover() {
[ -z "$1" ] && return 1
prog=$(printf "%s" "$1" | cut -d "," -f 1)
rest=$(printf "%s" "$1" | cut -s -d "," -f 2-)
command -v "$prog" >/dev/null 2>&1 && printf "%s" "$prog" && return 0
[ -e "$prog" ] && printf "%s" "$prog" && return 0
dep_ch_failover "$rest"
}
call_python_scraper() {
# Try multiple common python executable names
if command -v "python3" >/dev/null; then
python_cmd="python3"
elif command -v "python" >/dev/null; then
python_cmd="python"
else
die "Program \"python3\" or \"python\" not found. It is required for Anime-Sama source."
fi
script_dir=$(dirname "$0")
scraper_path="${script_dir}/scrap/animesama"
if [ ! -d "$scraper_path" ]; then
die "scrap/animesama/ not found in the same directory as ani-cli."
fi
# execute python package
_extra_args=""
[ "$mode" = "dub" ] || [ "$mode" = "vf" ] && _extra_args="$_extra_args --vf"
[ "$scans_mode" = "1" ] && _extra_args="$_extra_args --scans"
[ -n "$source_server" ] && _extra_args="$_extra_args --source $source_server"
# shellcheck disable=SC2086
"$python_cmd" "$scraper_path" "$1" "$2" $_extra_args
}
call_movix_scraper() {
# Try multiple common python executable names
if command -v "python3" >/dev/null; then
python_cmd="python3"
elif command -v "python" >/dev/null; then
python_cmd="python"
else
die "Program \"python3\" or \"python\" not found. Il est requis pour la source Movix."
fi
script_dir=$(dirname "$0")
scraper_path="${script_dir}/scrap/movix"
if [ ! -d "$scraper_path" ]; then
die "scrap/movix/ not found in the same directory as ani-cli."
fi
# $1 = action (search/episodes/extract)
# $2 = argument
# $3 = content type (anime/tv/movie/multi) - optional
# $4 = season number - optional
_movix_action="$1"
_movix_arg="$2"
_movix_type="${3:-multi}"
_movix_season="${4:-1}"
"$python_cmd" "$scraper_path" "$_movix_action" "$_movix_arg" --type "$_movix_type" --season "$_movix_season" --mode "$mode"
}
cleanup() {
tput sgr0 # clears colors
rm -f "${histfile}.new" # remove temp logfile
}
open_in_browser() {
target_path="$1"
# 1. Path conversion for Windows / Git Bash / MINGW / MSYS / WSL
win_path="$target_path"
if command -v cygpath >/dev/null 2>&1; then
win_path=$(cygpath -w "$target_path" 2>/dev/null)
elif command -v wslpath >/dev/null 2>&1; then
win_path=$(wslpath -w "$target_path" 2>/dev/null)
else
win_path=$(printf "%s" "$target_path" | sed -E 's|^/([a-zA-Z])/|\1:/|; s|/|\\|g')
fi
# 2. Try PowerShell Start-Process (most reliable on Windows)
if command -v powershell.exe >/dev/null 2>&1; then
powershell.exe -Command "Start-Process '$win_path'" >/dev/null 2>&1 && return 0
fi
# 3. Try cmd.exe start
if command -v cmd.exe >/dev/null 2>&1; then
cmd.exe /c start "" "$win_path" >/dev/null 2>&1 && return 0
fi
# 4. Try explorer.exe
if command -v explorer.exe >/dev/null 2>&1; then
explorer.exe "$win_path" >/dev/null 2>&1 && return 0
fi
# 5. Try macOS open
if command -v open >/dev/null 2>&1 && [ "$(uname -s)" = "Darwin" ]; then
open "$target_path" >/dev/null 2>&1 && return 0
fi
# 6. Try Linux xdg-open / sensible-browser
if command -v xdg-open >/dev/null 2>&1; then
xdg-open "$target_path" >/dev/null 2>&1 && return 0
fi
if command -v sensible-browser >/dev/null 2>&1; then
sensible-browser "$target_path" >/dev/null 2>&1 && return 0
fi
}
# SCRAPING
#shellcheck disable=SC2086
anidb_curl() {
$curl_exe -sL -A "$agent" --max-time 10 $cipher_flag "$@"
}
# search the query and give results. format is (id \t name)
anidb_search() {
#shellcheck disable=SC2059
_page="$(anidb_curl "$(printf "$search_api" "$1")" | tr '\n' ' ' | sed 's|<a href|\n<a href|g')"
if printf "%s" "$_page" | grep -qi "Just a moment"; then
[ "$curl_exe" = "curl" ] && die "Blocked by cloudflare. Try installing curl-impersonate"
die "Blocked by cloudflare."
fi
printf "%s" "$_page" | sed -nE 's|.*anime/([a-z0-9-]+-[0-9]+)".*alt="([^"]+)".*|\1\t\2|p' | sed -e "s|'|'|g" -e 's|"|"|g' -e "s|&|\&|g"
}
# extract mal_id and seasons metadata
anidb_desc() {
#shellcheck disable=SC2059
_page="$(anidb_curl "$(printf "$desc_api" "$1")" | tr '\n' ' ' | sed 's|<a href|\n<a href|g')"
mal_id="$(printf "%s" "$_page" | sed -nE 's|.*https://myanimelist.net/anime/([0-9]+)/.*|\1|p')"
seasons="$(printf "%s" "$_page" | sed -n '/>Seasons</,/>Details</p' | sed -nE 's|.*anime/([^"]+-[0-9]+)"[^>]*title="([^"]+)".*|\1\t\2|p' | sed -e "s|'|'|g")"
[ -n "$seasons" ] && seasons_option="\nchange_season"
}
# get the episodes list of the selected anime. format is (id \t ep_no)
anidb_episodes() {
#shellcheck disable=SC2059
_page="$(anidb_curl "$(printf "$episodes_api" "${1##*-}")")"
printf "%s" "$_page" | sed 's|},{|}\n{|g' | sed -nE 's|.*"id":([0-9]+).*"number":([0-9]+).*|\1\t\2|p'
}
# extract the individual m3u8 links from response of embed urls
anidb_m3u8() {
_ep_id=$(printf "%s" "$episode_maps" | sed -nE "s|^([0-9]+)\t${1}$|\1|p")
_lang="jpn"
[ "$2" = "dub" ] && _lang="eng"
#shellcheck disable=SC2059
_page="$(anidb_curl "$(printf "$m3u8_api" "${_ep_id}")")"
_embed="$(printf "%s" "$_page" | sed 's|},{|}\n{|g' | sed -nE 's|.*'"${_lang}"'.*embed_url":"([^"]+)".*|\1|p' | sed 's|\\/|/|g' | head -n 1)"
[ -z "$_embed" ] && return 1
_embed_page="$(anidb_curl "$_embed")"
_m3u8_master="$(printf "%s" "$_embed_page" | sed -nE "s|.*file: '([^']*)'.*|\1|p" | head -n 1)"
anidb_curl "${_m3u8_master}" | sed 's|^#EXT-X-STREAM.*x||g; s|,.*|p|g; /^#/d; $!N; s|\n| >|;/EXT-X-I-FRAME/d'
}
# search the query and give results
search_anime() {
if [ "$source_type" = "anime-sama" ]; then
# the python scraper prints "href\tTitle"
call_python_scraper "search" "$1"
return
fi
if [ "$source_type" = "movix" ]; then
# the movix scraper prints "type:id\tTitle"
call_movix_scraper "search" "$1" "$movix_content_type"
return
fi
anidb_search "$1"
}
# get the episodes list of the selected anime
episodes_list() {
if [ "$source_type" = "anime-sama" ]; then
if [ "$scans_mode" = "1" ]; then
call_python_scraper "chapters" "$1"
else
call_python_scraper "episodes" "$1"
fi
return
fi
if [ "$source_type" = "movix" ]; then
# the movix scraper prints index\tindex\tName\tserver_data
call_movix_scraper "episodes" "$1" "$movix_content_type" "${movix_season:-1}"
return
fi
anidb_episodes "$1" | cut -f 2
}
# select the quality according to the setting
select_quality() {
case "$1" in
best) _result=$(printf "%s" "$links" | head -n 1) ;;
worst) _result=$(printf "%s" "$links" | grep -E '^[0-9]{3,4}' | tail -n 1) ;;
*) _result=$(printf "%s" "$links" | grep -m 1 "$1") ;;
esac
[ -z "$_result" ] && printf "\33[2K\r\033[1;33m%s\033[0m\n" "$1 not found, defaulting to best" >&2 && _result=$(printf "%s" "$links" | head -n 1)
video_link=$(printf "%s" "$_result" | cut -d ">" -f 2)
}
# gets embed url, collects direct links and selects one with desired quality into $episode
get_video_link() {
if [ "$source_type" = "anime-sama" ]; then
# Use python scraper for anime-sama
server_vid=$(printf "%s\n" "$ep_list" | grep "^${ep_no}[[:space:]]" | head -n 1 | awk -F'\t' '{print $4}')
episode_links=$(call_python_scraper "extract" "$server_vid")
if [ -z "$episode_links" ]; then
die "Could not extract video link from Anime-Sama"
fi
server_type=$(printf "%s" "$server_vid" | cut -d, -f1)
if [ "$server_type" = "sibnet" ]; then
m3u8_refr="https://video.sibnet.ru/"
elif [ "$server_type" = "vidmoly" ]; then
m3u8_refr="https://vidmoly.net/"
else
m3u8_refr=""
fi
links="best >$episode_links
m3u8_refr >$m3u8_refr"
select_quality "$quality"
[ -n "$m3u8_refr" ] && player_extra_flags="$player_extra_flags --referrer=$m3u8_refr"
return
fi
if [ "$source_type" = "movix" ]; then
# Use movix python scraper
server_vid=$(printf "%s\n" "$ep_list" | grep "^${ep_no}[[:space:]]" | head -n 1 | awk -F'\t' '{print $4}')
episode_links=$(call_movix_scraper "extract" "$server_vid")
if [ -z "$episode_links" ]; then
die "Could not extract video link from Movix"
fi
movix_server_type=$(printf "%s" "$server_vid" | cut -d, -f1)
if [ "$movix_server_type" = "sibnet" ]; then
m3u8_refr="https://video.sibnet.ru/"
elif [ "$movix_server_type" = "vidmoly" ]; then
m3u8_refr="https://vidmoly.net/"
else
# dynamic referer from movix_scraper.py's active domain resolution
movix_domain=$("$python_cmd" -c "import movix_scraper; print(movix_scraper.ACTIVE_DOMAIN)")
m3u8_refr="https://${movix_domain:-movix.date}/"
fi
links="best >$episode_links
m3u8_refr >$m3u8_refr"
select_quality "$quality"
[ -n "$m3u8_refr" ] && player_extra_flags="$player_extra_flags --referrer=$m3u8_refr"
return
fi
links=$(anidb_m3u8 "$ep_no" "$mode" | sort -g -r -s)
[ -z "$links" ] && die "No sources found for $mode!"
info "anidb.app links fetched"
select_quality "$quality"
# should never trigger but just in case
if printf "%s" "$ep_list" | grep -q "^$ep_no$"; then
[ -z "$video_link" ] && die "Episode is released, but no valid sources!"
else
[ -z "$video_link" ] && die "Episode not released!"
fi
}
# provide countdown for the next episode release both sub and dub
time_until_next_ep() {
animeschedule="https://animeschedule.net"
_query="$(printf "%s" "$*" | tr ' ' '+')"
$curl_exe -s -G "$animeschedule/api/v3/anime" --data "q=${_query}" | sed 's|},{"id"|\n|g' | while read -r _entry; do
_route=$(printf "%s" "$_entry" | sed -nE 's|.*"route":"([^"]*)",.*|\1|p')
_eng_title=$(printf '%s' "$_entry" | sed -nE 's|.*"english":"([^"]*)".*|\1|p')
[ -z "$_eng_title" ] && _eng_title=$(printf '%s' "$_entry" | sed -nE 's|.*,"title":"([^"]*)".*|\1|p')
[ -z "$_eng_title" ] && _eng_title="N/A"
_jpn_title=$(printf '%s' "$_entry" | sed -nE 's|.*"romaji":"([^"]*)".*|\1|p')
[ -z "$_jpn_title" ] && _jpn_title="N/A"
_status=$(printf '%s' "$_entry" | sed -nE 's|.*"status":"([^"]*)".*|\1|p')
[ -z "$_status" ] && _status="Unknown"
case "$_status" in
Ongoing) _color="33" ;;
Finished) _color="32" ;;
*) _color="36" ;;
esac
printf "Eng: %s\n" "$_eng_title"
printf "Jpn: %s\n" "$_jpn_title"
printf "Status: \033[1;%sm%s\033[0m\n" "$_color" "$_status"
if [ "$_status" != "Finished" ]; then
_page="$($curl_exe -s "$animeschedule/anime/$_route")"
_sub_ep=$(printf "%s" "$_page" | sed -nE 's|.*release-time-type-subs"[^>]*><span class="release-time-episode-number">Episode ([0-9]+)</span>.*|\1|p' | head -n 1)
_dub_ep=$(printf "%s" "$_page" | sed -nE 's|.*release-time-type-dub"[^>]*><span class="release-time-episode-number">Episode ([0-9]+)</span>.*|\1|p' | head -n 1)
_sub_countdown=$(printf "%s" "$_page" | sed -nE 's|.*class="countdown-time" datetime="([^"]*)".*|\1|p' | head -n 1)
_dub_countdown=$(printf "%s" "$_page" | sed -nE 's|.*class="countdown-time countdown-time-dub" datetime="([^"]*)".*|\1|p' | head -n 1)
[ -n "$_sub_ep" ] && [ -n "$_sub_countdown" ] &&
printf "Next Sub: Episode %s in \033[1;%sm%s\033[0m\n" "$_sub_ep" "$_color" "$_sub_countdown" || printf "Next Sub: N/A\n"
[ -n "$_dub_ep" ] && [ -n "$_dub_countdown" ] &&
printf "Next Dub: Episode %s in \033[1;%sm%s\033[0m\n" "$_dub_ep" "$_color" "$_dub_countdown" || printf "Next Dub: N/A\n"
fi
printf "\n"
done
exit 0
}
# HISTORY
backup_old_hist() {
backupfile="${histfile}.v4"
while IFS=" " read -r ep_no anime_id anime_title; do
case "$anime_id" in
*-*) printf "%s\t%s\t%s\n" "$ep_no" "$anime_id" "$anime_title" >>"${histfile}.new" ;;
*) printf "%s\t%s\t%s\n" "$ep_no" "$anime_id" "$anime_title" >>"$backupfile" ;;
esac
done <"$histfile"
mv "${histfile}.new" "$histfile"
}
process_hist_entry() {
if [ "$source_type" = "anime-sama" ] || [ "$source_type" = "movix" ]; then
ep_list=$(episodes_list "$anime_id")
latest_ep=$(printf "%s\n" "$ep_list" | tail -n 1 | awk '{print $1}')
anime_title=$(printf "%s\n" "$anime_title" | sed "s|[0-9]\+ episodes|${latest_ep} episodes|")
ep_no=$(printf "%s\n" "$ep_list" | grep -A1 "^${ep_no}[[:space:]]" | awk 'NR==2 {print $1}') 2>/dev/null
[ -n "$ep_no" ] && printf "%s\t%s - episode %s\n" "$anime_id" "$anime_title" "$ep_no"
[ -n "$ep_no" ] || printf "%s\t%s - episode %s (up to date)\n" "$anime_id" "$anime_title" "$latest_ep"
else
ep_list=$(anidb_episodes "$anime_id" | cut -f 2)
ep_no=$(printf "%s" "$ep_list" | sed -n "/^${ep_no}$/{n;p;}") 2>/dev/null
[ -n "$ep_no" ] && printf "%s\t%s - episode %s\n" "$anime_id" "$anime_title" "$ep_no"
fi
}
update_history() {
if grep -q " ${anime_id} " "$histfile"; then
# escape sed special chars in replacement: & and \ and delimiter |
safe_title=$(printf "%s" "$anime_title" | sed 's|[&\|]|\\&|g')
sed -E "s|^[^ ]+ ${anime_id} [^ ]+$|${ep_no} ${anime_id} ${safe_title}|" "$histfile" >"${histfile}.new"
else
cp "$histfile" "${histfile}.new"
printf "%s\t%s\t%s\n" "$ep_no" "$anime_id" "$anime_title" >>"${histfile}.new"
fi
mv "${histfile}.new" "$histfile"
}
# PLAYING
# shellcheck disable=SC2086
# $1 = video link, $2 = video name, $3 = extra flags
download() {
command -v "yt-dlp" >/dev/null && yt-dlp "$1" --no-skip-unavailable-fragments --fragment-retries infinite -N 16 -o "$download_dir/$2.mp4" $3 && return 0
ffmpeg -extension_picky 0 -loglevel error -stats -i "$1" -c copy "$download_dir/$2.mp4" $3
}
play_episode() {
if [ "$scans_mode" = "1" ]; then
chapter_data=$(printf "%s\n" "$ep_list" | grep "^${ep_no}[[:space:]]" | head -n 1 | awk -F'\t' '{print $4}')
if [ -z "$chapter_data" ]; then
die "Impossible d'extraire les données du chapitre."
fi
page_urls=$(call_python_scraper "pages" "$chapter_data")
if [ -z "$page_urls" ]; then
die "Aucune image trouvée pour ce chapitre."
fi
sanitized_title=$(printf "%s" "$anime_title" | tr -cd '[:alnum:]_-' | tr ' ' '_')
ch_num=$(printf "%s" "$chapter_data" | cut -d, -f1)
scan_cache_dir="${hist_dir:-$HOME}/scans_${sanitized_title}/chap_${ch_num}"
mkdir -p "$scan_cache_dir"
if [ "$player_function" = "download" ]; then
out_dir="${download_dir}/${sanitized_title}/Chapitre_${ch_num}"
mkdir -p "$out_dir"
printf "\033[1;34mTéléchargement des pages du Chapitre %s dans %s...\033[0m\n" "$ch_num" "$out_dir"
page_idx=1
for img_url in $page_urls; do
padded_idx=$(printf "%03d" "$page_idx")
page_file="$out_dir/page_${padded_idx}.jpg"
if [ ! -s "$page_file" ]; then
curl -sL -A "$agent" "$img_url" -o "${page_file}.tmp" && mv "${page_file}.tmp" "$page_file" &
if [ $((page_idx % 8)) -eq 0 ]; then
wait
fi
fi
page_idx=$((page_idx + 1))
done
wait
printf "\033[1;32mChapitre %s téléchargé avec succès (%s pages).\033[0m\n" "$ch_num" "$((page_idx - 1))"
return
fi
html_file="$scan_cache_dir/chapter.html"
cat <<EOF > "$html_file"
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${anime_title} - Chapitre ${ch_num}</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { background-color: #0d0d11; color: #e2e8f0; font-family: system-ui, -apple-system, sans-serif; display: flex; flex-direction: column; align-items: center; min-height: 100vh; padding-bottom: 50px; }
header { position: sticky; top: 0; width: 100%; background: rgba(13, 13, 17, 0.95); backdrop-filter: blur(10px); padding: 14px 20px; text-align: center; font-size: 1.1rem; font-weight: 700; border-bottom: 1px solid #1e293b; z-index: 100; color: #38bdf8; box-shadow: 0 4px 20px rgba(0,0,0,0.5); }
.container { display: flex; flex-direction: column; align-items: center; width: 100%; max-width: 850px; margin-top: 12px; gap: 0; }
.container img { width: 100%; height: auto; display: block; margin: 0 auto; border-radius: 0; background: #16161e; min-height: 250px; }
</style>
</head>
<body>
<header>${anime_title} — Chapitre ${ch_num}</header>
<div class="container">
EOF
page_idx=1
for img_url in $page_urls; do
printf ' <img src="%s" loading="lazy" alt="Page %s">\n' "$img_url" "$page_idx" >> "$html_file"
page_idx=$((page_idx + 1))
done
cat <<EOF >> "$html_file"
</div>
</body>
</html>
EOF
printf "\033[1;32mOuverture du Chapitre %s (%s pages) dans le lecteur Webtoon...\033[0m\n" "$ch_num" "$((page_idx - 1))"
open_in_browser "$html_file"
return
fi
[ "$log_episode" = 1 ] && [ "$player_function" != "debug" ] && [ "$player_function" != "download" ] && command -v logger >/dev/null && logger -t ani-cli "${anime_title} ${ep_no}"
[ "$skip_intro" = 1 ] && skip_flag="$(ani-skip -i "$mal_id" -e "$canon_ep_no")"
[ -z "$video_link" ] && get_video_link
# wait for the previous player to close (in range selection)
wait
# shellcheck disable=SC2086
case "$player_function" in
debug) printf "All links:\n%s\nSelected link:\n%s\n" "$links" "$video_link" ;;
android_mpv) nohup am start --user 0 -a android.intent.action.VIEW -d "$video_link" -n is.xyz.mpv/.MPVActivity -e "title" "${anime_title} Episode ${ep_no}" $player_extra_flags >/dev/null 2>&1 & ;;
android_vlc) nohup am start --user 0 -a android.intent.action.VIEW -d "$video_link" -n org.videolan.vlc/org.videolan.vlc.gui.video.VideoPlayerActivity -e "title" "${anime_title} Episode ${ep_no}" $player_extra_flags >/dev/null 2>&1 & ;;
*flatpak*mpv*) flatpak run io.mpv.Mpv $skip_flag $player_extra_flags --force-media-title="${anime_title} Episode ${ep_no}" "$video_link" >/dev/null 2>&1 & ;;
*mpv*)
if [ "$no_detach" = 0 ]; then
nohup $player_function $skip_flag $player_extra_flags --force-media-title="${anime_title} Episode ${ep_no}" "$video_link" >/dev/null 2>&1 &
else
$player_function $skip_flag $player_extra_flags --force-media-title="${anime_title} Episode ${ep_no}" "$video_link"
mpv_exitcode=$?
[ "$exit_after_play" = 1 ] && [ -z "$_range" ] && exit "$mpv_exitcode"
fi
;;
*iina*)
if pgrep -f "IINA" >/dev/null 2>&1; then
# omit --keep-running when an IINA instance exists to prevent hanging
nohup $player_function $player_extra_flags --no-stdin --mpv-force-media-title="${anime_title} Episode ${ep_no}" "$video_link" >/dev/null 2>&1 &
else
nohup $player_function $player_extra_flags --no-stdin --keep-running --mpv-force-media-title="${anime_title} Episode ${ep_no}" "$video_link" >/dev/null 2>&1 &
fi
;;
*vlc*) nohup $player_function $player_extra_flags --play-and-exit --meta-title="${anime_title} Episode ${ep_no}" "$video_link" >/dev/null 2>&1 & ;;
*yncpla*) nohup $player_function $player_extra_flags "$video_link" -- --force-media-title="${anime_title} Episode ${ep_no}" >/dev/null 2>&1 & ;;
download) "$player_function" "$video_link" "${anime_title} Episode ${ep_no}" $player_extra_flags ;;
catt) nohup catt cast $player_extra_flags "$video_link" >/dev/null 2>&1 & ;;
iSH)
printf "\033]8;;vlc://%s\a~~~~~~~~~~~~~~~~~~~~\n~ Tap to open VLC ~\n~~~~~~~~~~~~~~~~~~~~\033]8;;\a\n" "$video_link"
sleep 5
;;
*) nohup $player_function $player_extra_flags "$video_link" >/dev/null 2>&1 & ;;
esac
replay="$video_link"
unset video_link
update_history
[ "$menu_program" != "fzf" ] && wait
}
play() {
_start=$(printf "%s" "$ep_no" | grep -Eo '^(-1|[0-9]+(\.[0-9]+)?)')
_end=$(printf "%s" "$ep_no" | grep -Eo '(-1|[0-9]+(\.[0-9]+)?)$')
[ "$_start" = "-1" ] && ep_no=$(printf "%s" "$ep_list" | tail -n 1 | awk '{print $1}') && unset _start
[ -z "$_end" ] || [ "$_end" = "$_start" ] && unset _start _end
[ "$_start" = "0" ] && _start=$(printf "%s" "$ep_list" | head -n 1 | awk '{print $1}')
[ "$_end" = "-1" ] && _end=$(printf "%s" "$ep_list" | tail -n 1 | awk '{print $1}')
_line_count=$(printf "%s\n" "$ep_no" | wc -l | tr -d "[:space:]")
if [ "$_line_count" != 1 ] || [ -n "$_start" ]; then
[ -z "$_start" ] && _start=$(printf "%s\n" "$ep_no" | head -n 1)
[ -z "$_end" ] && _end=$(printf "%s\n" "$ep_no" | tail -n 1)
if [ "$source_type" = "anime-sama" ] || [ "$source_type" = "movix" ]; then
_range=$(printf "%s\n" "$ep_list" | awk -v s="$_start" -v e="$_end" '$1 >= s && $1 <= e {print $1}')
else
_range=$(printf "%s\n" "$ep_list" | sed -nE "/^${_start}\$/,/^${_end}\$/p")
fi
[ -z "$_range" ] && die "Invalid range!"
for i in $_range; do
tput clear
ep_no=$i
canon_ep_no="$(printf "%s" "$ep_list" | sed -n "/^${ep_no}$/=")"
info "Playing episode $ep_no..."
[ "$i" = "$_end" ] && unset _range
play_episode
done
else
if [ "$source_type" = "anime-sama" ] || [ "$source_type" = "movix" ]; then
printf "%s" "$ep_list" | grep -q "^$ep_no[[:space:]]" || die "Invalid episode!"
canon_ep_no="$ep_no"
else
printf "%s" "$ep_list" | grep -q "^$ep_no$" || die "Invalid episode!"
canon_ep_no="$(printf "%s" "$ep_list" | sed -n "/^${ep_no}$/=")"
fi
play_episode
fi
# moves up to stored position and deletes to end
[ "$player_function" != "debug" ] && [ "$player_function" != "download" ] && tput rc && tput ed
}
# MAIN
# setup
base_api="https://anidb.app"
search_api="${base_api}/browse?q=%s"
# search_api_alt="${base_api}/search/suggestions?q=%s"
desc_api="${base_api}/anime/%s"
episodes_api="${base_api}/api/frontend/anime/%s/episodes"
m3u8_api="${base_api}/api/frontend/episode/%s/languages"
ciphers='ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305'
tls13_ciphers='TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256'
agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
mode="${ANI_CLI_MODE:-sub}"
download_dir="${ANI_CLI_DOWNLOAD_DIR:-.}"
log_episode="${ANI_CLI_LOG:-1}"
quality="${ANI_CLI_QUALITY:-best}"
curl_exe=$(dep_ch_failover "curl_firefox135,curl_chrome136,curl_chrome116,curl_ff117,curl") || die "Program curl not found"
case "$(uname -a | cut -d " " -f 1,3-)" in
*Darwin*)
cipher_flag="--ciphers $ciphers --tls13-ciphers $tls13_ciphers"
player_function="${ANI_CLI_PLAYER:-$(dep_ch_failover "iina,/Applications/IINA.app/Contents/MacOS/iina-cli,mpv,vlc")}" || die 'No player found. Looked for iina, mpv and vlc'
;; # mac OS
*ndroid*) player_function="${ANI_CLI_PLAYER:-android_mpv}" ;; # Android OS (termux)
*MINGW* | *WSL2*) player_function="${ANI_CLI_PLAYER:-mpv.exe}" ;; # Windows OS
*ish*)
player_function="${ANI_CLI_PLAYER:-iSH}"
curl_exe=$(dep_ch_failover "curl_safari260_ios,$curl_exe")
;; # iOS (iSH)
*) player_function="${ANI_CLI_PLAYER:-$(dep_ch_failover "mpv,$HOME/.local/share/flatpak/app/io.mpv.Mpv/,/var/lib/flatpak/app/io.mpv.Mpv/,vlc")}" || die 'No player found. Looked for mpv and vlc' ;; # Linux OS
esac
player_extra_flags="${ANI_CLI_PLAYER_FLAGS:-""}"
no_detach="${ANI_CLI_NO_DETACH:-0}"
exit_after_play="${ANI_CLI_EXIT_AFTER_PLAY:-0}"
skip_intro="${ANI_CLI_SKIP_INTRO:-0}"
skip_title="${ANI_CLI_SKIP_TITLE:-}"
menu_program="${ANI_CLI_MENU:-fzf}"
menu_extra_flags="${ANI_CLI_MENU_FLAGS:-""}"
# not opening from a terminal
if [ ! -t 0 ]; then
command -v dmenu >/dev/null && menu_program=dmenu
command -v rofi >/dev/null && menu_program=rofi
fi
hist_dir="${ANI_CLI_HIST_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/ani-cli}"
[ ! -d "$hist_dir" ] && mkdir -p "$hist_dir"
histfile="$hist_dir/ani-hsts"
[ ! -f "$histfile" ] && : >"$histfile"
source="${ANI_CLI_DEFAULT_SOURCE:-search}"
branch="${ANI_CLI_BRANCH:-master}"
while [ $# -gt 0 ]; do
case "$1" in
-v | --vlc)
case "$(uname -a | cut -d " " -f 1,3-)" in
*ndroid*) player_function="android_vlc" ;;
MINGW* | *WSL2*) player_function="vlc.exe" ;;
*ish*) player_function="iSH" ;;
*) player_function="vlc" ;;
esac
;;
-s | --scans)
scans_mode=1
source="anime-sama"
source_type="anime-sama"
;;
--syncplay)
case "$(uname -s)" in
Darwin*) player_function="/Applications/Syncplay.app/Contents/MacOS/syncplay" ;;
MINGW* | *Msys)
export PATH="$PATH":"/c/Program Files (x86)/Syncplay/"
player_function="syncplay.exe"
;;
*) player_function="syncplay" ;;
esac
;;
-q | --quality)
[ $# -lt 2 ] && die "missing argument!"
quality="$2"
shift
;;
-S | --select-nth)
[ $# -lt 2 ] && die "missing argument!"
index="$2"
shift
;;
-c | --continue) source=history ;;
-d | --download)
player_function=download
;;
-D | --delete)
: >"$histfile"
exit 0
;;
-l | --logview)
case "$(uname -s)" in
Darwin*) log show --predicate 'process == "logger"' ;;
Linux*) journalctl -t ani-cli ;;
*) die "Logger not implemented for your platform" ;;
esac
exit 0
;;
-V | --version) version_info ;;
-h | --help) help_info ;;
-e | --episode | -r | --range)
[ $# -lt 2 ] && die "missing argument!"
ep_no="$2"
shift
;;
--dub) mode="dub" ;;
--no-detach) no_detach=1 ;;
--exit-after-play) exit_after_play=1 && no_detach=1 ;;
--rofi) menu_program=rofi ;;
--dmenu) menu_program=dmenu ;;
--skip) skip_intro=1 ;;
--skip-title)
[ $# -lt 2 ] && die "missing argument!"
skip_title="$2"
shift
;;
-N | --nextep-countdown) source=nextep ;;
-a | --anime-sama) source="anime-sama" && source_type="anime-sama" ;;
--source)
[ $# -lt 2 ] && die "missing argument!"
source_server="$2"
shift
;;
-m | --movix) source="movix" && source_type="movix" && movix_content_type="multi" ;;
--movix-anime) source="movix" && source_type="movix" && movix_content_type="anime" ;;
--movix-tv) source="movix" && source_type="movix" && movix_content_type="tv" ;;
--movix-movie) source="movix" && source_type="movix" && movix_content_type="movie" ;;
--season)
[ $# -lt 2 ] && die "missing argument!"
movix_season="$2"
shift
;;
--vf) mode="vf" ;;
-U | --update)
dep_ch "patch"
branch="${2:-$branch}"
update_script
;;
*) query="$(printf "%s" "$query $1" | sed 's|^ ||;s| |+|g')" ;;
esac
shift
done
[ "$menu_program" = "fzf" ] && menu_multi_flag="-m"
[ "$menu_program" = "rofi" ] && menu_multi_flag="-multi-select"
info "Checking dependencies..."
dep_ch "sed"
dep_ch "grep"
[ "$skip_intro" = 1 ] && dep_ch "ani-skip"
dep_ch "$menu_program" # TODO: use dep_ch_failover to iterate through options
case "$player_function" in
debug) ;;
download) dep_ch_failover "yt-dlp,ffmpeg" >/dev/null || die 'Neither yt-dlp nor ffmpeg found' ;;
android*) printf "\33[2K\rChecking of players on Android is disabled.\n" ;;
*iSH*) printf "\33[2K\rChecking of players on iOS is disabled\n" ;;
*flatpak*mpv*) dep_ch "flatpak" ;;
*) dep_ch "$player_function" ;;
esac
# idk where to put this, so I put it here...
trap 'cleanup; exit 1' INT HUP TERM
# searching
case "$source" in
history)
backup_old_hist
anime_list=$(while read -r ep_no anime_id anime_title; do process_hist_entry & done <"$histfile")
wait
[ -z "$anime_list" ] && die "No unwatched series in history!"
[ -z "${index##*[!0-9]*}" ] && anime_id=$(printf "%s" "$anime_list" | nl -w 2 | sed 's/^[[:space:]]//' | nth "Select anime: " | cut -f 1)
[ -z "${index##*[!0-9]*}" ] || anime_id=$(printf "%s" "$anime_list" | sed -n "${index}p" | cut -f 1)
[ -z "$anime_id" ] && exit 1
anime_title=$(printf "%s" "$anime_list" | grep "^$anime_id " | cut -f 2 | sed 's| - episode.*||')
if [ "$source_type" = "anime-sama" ] || [ "$source_type" = "movix" ]; then
ep_list=$(episodes_list "$anime_id")
ep_no=$(printf "%s" "$anime_list" | grep "^$anime_id " | sed -nE 's/.*- episode (.+)$/\1/p')
ep_name_display=$ep_no
else
anidb_desc "$anime_id" || true
episode_maps="$(anidb_episodes "$anime_id")"
ep_list="$(printf "%s" "$episode_maps" | cut -f 2)"
ep_no=$(printf "%s" "$anime_list" | grep "^$anime_id " | sed -nE 's/.*- episode (.+)$/\1/p')
ep_name_display=$ep_no
fi
;;
*)
if [ "$menu_program" = "fzf" ]; then
while [ -z "$query" ]; do
printf "\33[2K\r\033[1;36mSearch anime: \033[0m" && read -r query
done
else
[ -z "$query" ] && query=$(: | menu "Search anime: " "" "$menu_extra_flags")
[ -z "$query" ] && exit 1
fi
# for checking new releases by specifying anime name
[ "$source" = "nextep" ] && time_until_next_ep "$query"
query=$(printf "%s" "$query" | sed 's| |+|g')
if [ "$source_type" = "anime-sama" ] || [ "$source_type" = "movix" ]; then
anime_list=$(search_anime "$query")
[ -z "$anime_list" ] && die "No results found!"
[ "$index" -eq "$index" ] 2>/dev/null && result=$(printf "%s" "$anime_list" | sed -n "${index}p")
[ -z "$index" ] && result=$(printf "%s" "$anime_list" | nl -w 2 | sed 's/^[[:space:]]//' | nth "Select anime: ")
[ -z "$result" ] && die "Invalid anime selection"
anime_title="$(printf "%s" "$result" | cut -f 2)"
anime_id="$(printf "%s" "$result" | cut -f 1)"
ep_list=$(episodes_list "$anime_id")
[ -z "$ep_list" ] && die "Aucun episode disponible pour ce contenu."
# For TV series with movix, allow season selection if not specified
if [ "$source_type" = "movix" ] && [ "$movix_content_type" = "tv" ] && [ -z "$movix_season" ]; then
printf "\033[1;36mSaison (numero) : \033[0m" && read -r movix_season
[ -z "$movix_season" ] && movix_season=1
ep_list=$(episodes_list "$anime_id")
fi
# Select episode
result=$(printf "%s\n" "$ep_list" | nth "Select episode: " "$menu_multi_flag")
ep_no=$(echo "$result" | cut -f 1)
ep_name_display=$(echo "$result" | cut -f 2)
else
anime_list=$(anidb_search "$query")
[ -z "$anime_list" ] && die "No results found!"
[ "$index" -eq "$index" ] 2>/dev/null && result=$(printf "%s" "$anime_list" | sed -n "${index}p")
[ -z "$index" ] && result=$(printf "%s" "$anime_list" | nl -w 2 | sed 's/^[[:space:]]//' | nth "Select anime: ")
[ -z "$result" ] && die "Invalid anime selection"
anime_title="$(printf "%s" "$result" | cut -f 2)"
anime_id="$(printf "%s" "$result" | cut -f 1)"
anidb_desc "$anime_id" || true
episode_maps="$(anidb_episodes "$anime_id")"
ep_list="$(printf '%s' "$episode_maps" | cut -f 2)"
[ -z "$ep_no" ] && ep_no=$(printf "%s" "$ep_list" | nth "Select episode: " "$menu_multi_flag")
[ -z "$ep_no" ] && die "Invalid episode selection"
ep_name_display=$ep_no
fi
;;
esac
[ "$skip_intro" = 1 ] && mal_id="$(ani-skip -q "${skip_title:-${anime_title}}")"
# moves the cursor up one line and clears that line
tput cuu1 && tput el
# stores the position of cursor
tput sc
# playback & loop
play
[ "$player_function" = "download" ] || [ "$player_function" = "debug" ] && exit 0
#shellcheck disable=SC2059
while cmd=$(printf "next\nreplay\nprevious\nselect$seasons_option\nchange_quality\nquit" | nth "Playing episode $ep_name_display of $anime_title... "); do
case "$cmd" in
next)
if [ "$source_type" = "anime-sama" ] || [ "$source_type" = "movix" ]; then
ep_no=$(printf "%s" "$ep_list" | grep -A1 "^${ep_no}[[:space:]]" | awk 'NR==2 {print $1}') 2>/dev/null
ep_name_display=$(printf "%s" "$ep_list" | grep "^${ep_no}[[:space:]]" | head -n 1 | awk -F'\t' '{print $3}')
else
ep_no=$(printf "%s" "$ep_list" | sed -n "/^${ep_no}$/{n;p;}") 2>/dev/null
ep_name_display=$ep_no
fi
;;
replay) video_link="$replay" ;;
previous)
if [ "$source_type" = "anime-sama" ] || [ "$source_type" = "movix" ]; then
ep_no=$(printf "%s" "$ep_list" | grep -B1 "^${ep_no}[[:space:]]" | awk 'NR==1 && $1 != "" {print $1}') 2>/dev/null
[ "$(printf "%s" "$ep_list" | head -n 1 | awk '{print $1}')" = "$ep_no" ] && [ "$cmd" = "previous" ] && ep_no=""
ep_name_display=$(printf "%s" "$ep_list" | grep "^${ep_no}[[:space:]]" | head -n 1 | awk -F'\t' '{print $3}')
else
ep_no=$(printf "%s" "$ep_list" | sed -n "/^${ep_no}$/{g;1!p;};h") 2>/dev/null
ep_name_display=$ep_no
fi
;;
select)
if [ "$source_type" = "anime-sama" ] || [ "$source_type" = "movix" ]; then
result=$(printf "%s" "$ep_list" | nth "Select episode: " "$menu_multi_flag")
ep_no=$(echo "$result" | cut -f 1)
ep_name_display=$(echo "$result" | cut -f 2)
else
ep_no=$(printf "%s" "$ep_list" | nth "Select episode: " "$menu_multi_flag")
ep_name_display=$ep_no
fi
;;
change_season)
new_anime="$(printf "%s" "$seasons" | nl -w 2 | sed 's/^[[:space:]]//' | nth "Select season: ")"
[ -z "$new_anime" ] && die "Invalid season selection"
anime_title="$(printf "%s" "$new_anime" | cut -f 2)"
anime_id="$(printf "%s" "$new_anime" | cut -f 1)"
anidb_desc "$anime_id" || true
episode_maps="$(anidb_episodes "$anime_id")"
ep_list="$(printf '%s' "$episode_maps" | cut -f 2)"
ep_no=$(printf "%s" "$ep_list" | nth "Select episode: " "$menu_multi_flag")
[ -z "$ep_no" ] && die "Invalid episode selection"
ep_name_display=$ep_no
;;
change_quality)
if [ "$source_type" = "anime-sama" ]; then
selected_src="$(printf "sibnet\nvidmoly\nsendvid\nstreamtape\nvk" | menu "Select server: " "" "$menu_extra_flags")"
if [ -n "$selected_src" ]; then
source_server="$selected_src"
ep_list=$(episodes_list "$anime_id")
unset video_link
get_video_link
fi
else
new_quality="$(printf "%s" "$links" | menu "Select Quality: " "" "$menu_extra_flags" | cut -d ">" -f 1)"
[ -z "$new_quality" ] && die "No quality selected"
select_quality "$new_quality"
fi
;;
*)
cleanup
exit 0
;;
esac