master ohmu/dihfiles / bin / ani-cli
ohmu  ·  2026-09-26
  1#!/bin/sh
  2
  3version_number="4.14.1"
  4
  5# UI
  6
  7external_menu() {
  8    [ "$use_external_menu" = "1" ] && rofi "$1" -sort -dmenu -i -width 1500 -p "$2" "$3"
  9    [ "$use_external_menu" = "2" ] && dmenu -l 20 -p "$2"
 10}
 11
 12launcher() {
 13    [ "$use_external_menu" = "0" ] && [ -z "$1" ] && set -- "+m" "$2"
 14    [ "$use_external_menu" = "0" ] && fzf "$1" --reverse --cycle --prompt "$2"
 15    [ "$use_external_menu" = "1" ] && external_menu "$1" "$2" "$external_menu_args"
 16    [ "$use_external_menu" = "2" ] && external_menu "$1" "$2"
 17}
 18
 19nth() {
 20    stdin=$(cat -)
 21    [ -z "$stdin" ] && return 1
 22    line_count="$(printf "%s\n" "$stdin" | wc -l | tr -d "[:space:]")"
 23    [ "$line_count" -eq 1 ] && printf "%s" "$stdin" | cut -f2,3 && return 0
 24    prompt="$1"
 25    multi_flag=""
 26    [ $# -ne 1 ] && shift && multi_flag="$1"
 27    line=$(printf "%s" "$stdin" | cut -f1,3 | tr '\t' ' ' | launcher "$multi_flag" "$prompt" | cut -d " " -f 1)
 28    line_start=$(printf "%s" "$line" | head -n1)
 29    line_end=$(printf "%s" "$line" | tail -n1)
 30    [ -n "$line" ] || exit 1
 31    if [ "$line_start" = "$line_end" ]; then
 32        printf "%s" "$stdin" | grep -E '^'"${line}"'($|[[:space:]])' | cut -f2,3 || exit 1
 33    else
 34        printf "%s" "$stdin" | sed -n '/^'"${line_start}"'$/,/^'"${line_end}$"'/p' || exit 1
 35    fi
 36}
 37
 38die() {
 39    printf "\33[2K\r\033[1;31m%s\033[0m\n" "$*" >&2
 40    exit 1
 41}
 42
 43help_info() {
 44    printf "
 45    Usage:
 46    %s [options] [query]
 47    %s [query] [options]
 48    %s [options] [query] [options]
 49
 50    Options:
 51      -c, --continue
 52        Continue watching from history
 53      -d, --download
 54        Download the video instead of playing it
 55      -D, --delete
 56        Delete history
 57      -l, --logview
 58        Show logs
 59      -s, --syncplay
 60        Use Syncplay to watch with friends
 61      -S, --select-nth
 62        Select nth entry
 63      -q, --quality
 64        Specify the video quality
 65      -v, --vlc
 66        Use VLC to play the video
 67      -V, --version
 68        Show the version of the script
 69      -h, --help
 70        Show this help message and exit
 71      -e, --episode, -r, --range
 72        Specify the number of episodes to watch
 73      --dub
 74        Play dubbed version
 75      --rofi
 76        Use rofi instead of fzf for the interactive menu
 77      --dmenu
 78        Use dmenu instead of fzf for the interactive menu
 79      --skip
 80        Use ani-skip to skip the intro of the episode (mpv only)
 81      --no-detach
 82        Don't detach the player (useful for in-terminal playback, mpv only)
 83      --exit-after-play
 84        Exit the player, and return the player exit code (useful for non interactive scenarios, mpv only)
 85      --skip-title <title>
 86        Use given title as ani-skip query
 87      -N, --nextep-countdown
 88        Display a countdown to the next episode
 89      -U, --update
 90        Update the script
 91    Some example usages:
 92      %s -q 720p banana fish
 93      %s --skip --skip-title \"one piece\" -S 2 one piece
 94      %s -d -e 2 cyberpunk edgerunners
 95      %s --vlc cyberpunk edgerunners -q 1080p -e 4
 96      %s blue lock -e 5-6
 97      %s -e \"5 6\" blue lock
 98    \n" "${0##*/}" "${0##*/}" "${0##*/}" "${0##*/}" "${0##*/}" "${0##*/}" "${0##*/}" "${0##*/}" "${0##*/}"
 99    exit 0
100}
101
102version_info() {
103    printf "%s\n" "$version_number"
104    exit 0
105}
106
107update_script() {
108    printf "[branch:%s] Checking for Update..\n" "$branch"
109    update="$(curl --fail-with-body -sL -A "$agent" "https://raw.githubusercontent.com/pystardust/ani-cli/$branch/ani-cli")" || die "[branch:$branch] Connection error: $update"
110    printf '%s' "$update" | grep -q "^version_number" || die "[branch:$branch] Invalid Response."
111    update="$(printf '%s\n' "$update" | diff -u "$0" -)"
112    if [ -z "$update" ]; then
113        printf "[branch:%s] Script is up to date :)\n" "$branch"
114    else
115        if printf '%s\n' "$update" | patch "$0" -; then
116            printf "[branch:%s] Script has been updated\n" "$branch"
117        else
118            die "[branch:$branch] Can't update for some reason!"
119        fi
120    fi
121    exit 0
122}
123
124# checks if dependencies are present
125dep_ch() {
126    for dep; do
127        command -v "${dep%% *}" >/dev/null || die "Program \"${dep%% *}\" not found. Please install it."
128    done
129}
130
131where_iina() {
132    [ -e "/Applications/IINA.app/Contents/MacOS/iina-cli" ] && echo "/Applications/IINA.app/Contents/MacOS/iina-cli" && return 0
133    printf "%s" "iina" && return 0
134}
135
136where_mpv() {
137    command -v "flatpak" >/dev/null && flatpak info io.mpv.Mpv >/dev/null 2>&1 && printf "%s" "flatpak_mpv" && return 0
138    printf "%s" "mpv" && return 0
139}
140
141cleanup() {
142    [ "$player_function" = "android_mpv" ] && : >"/storage/emulated/0/mpv/mpv.config.mp4"
143    printf "\033[0m"        # clears colors
144    rm -f "${histfile}.new" # remove temp logfile
145}
146
147# SCRAPING
148
149# extract the video links from response of embed urls, extract mp4 links form m3u8 lists
150get_links() {
151    case "$*" in
152        *mp4upload*)
153            episode_link=$(curl --max-time 10 -sLk "$*" -A "$agent" -e "$allanime_refr" | sed -nE 's|.*src: "([^"]*)"[[:space:]]*|Mp4Upload >\1|p')
154            ;;
155        *tools.fast4speed.rsvp*)
156            episode_link=$(printf "%s\n" "Yt >$*")
157            ;;
158        *)
159            response="$(curl -e "$allanime_refr" -s "https://${allanime_base}$*" -A "$agent")"
160            episode_link="$(printf '%s' "$response" | sed 's|},{|\
161        |g' | sed -nE 's|.*link":"([^"]*)".*"resolutionStr":"([^"]*)".*|\2 >\1|p;s|.*hls","url":"([^"]*)".*"hardsub_lang":"en-US".*|\1|p')"
162            ;;
163    esac
164
165    case "$episode_link" in
166        *repackager.wixmp.com*)
167            extract_link=$(printf "%s" "$episode_link" | cut -d'>' -f2 | sed 's|repackager.wixmp.com/||g;s|\.urlset.*||g')
168            for j in $(printf "%s" "$episode_link" | sed -nE 's|.*/,([^/]*),/mp4.*|\1|p' | sed 's|,|\
169|g'); do
170                printf "%s >%s\n" "$j" "$extract_link" | sed "s|,[^/]*|${j}|g"
171            done | sort -nr
172            ;;
173        *master.m3u8*)
174            m3u8_refr=$(printf '%s' "$response" | sed -nE 's|.*Referer":"([^"]*)".*|\1|p') && printf '%s\n' "m3u8_refr >$m3u8_refr" >"$cache_dir/m3u8_refr"
175            extract_link=$(printf "%s" "$episode_link" | head -1 | cut -d'>' -f2)
176            relative_link=$(printf "%s" "$extract_link" | sed 's|[^/]*$||')
177            m3u8_streams="$(curl -e "$m3u8_refr" -s "$extract_link" -A "$agent")"
178            printf "%s" "$m3u8_streams" | grep -q "EXTM3U" && printf "%s" "$m3u8_streams" | sed 's|^#EXT-X-STREAM.*x||g; s|,.*|p|g; /^#/d; $!N; s|\n| >|;/EXT-X-I-FRAME/d' | sed "s|>|>${relative_link}|g" | sort -nr
179            ;;
180        *) [ -n "$episode_link" ] && printf "%s\n" "$episode_link" ;;
181    esac
182    printf "\033[1;32m%s\033[0m Links Fetched\n" "$provider_name" 1>&2
183}
184
185# initialises provider_name and provider_id. First argument is the provider name, 2nd is the regex that matches that provider's link
186provider_init() {
187    provider_name=$1
188    provider_id=$(printf "%s" "$resp" | sed -n "$2" | head -1 | cut -d':' -f2-)
189    printf '%s' "$provider_id" | grep -qE "^--" && provider_id=$(printf "%s" "$provider_id" | sed 's/../&\
190/g' | sed 's/^--$/\n/g;s/^79$/A/g;s/^7a$/B/g;s/^7b$/C/g;s/^7c$/D/g;s/^7d$/E/g;s/^7e$/F/g;s/^7f$/G/g;s/^70$/H/g;s/^71$/I/g;s/^72$/J/g;s/^73$/K/g;s/^74$/L/g;s/^75$/M/g;s/^76$/N/g;s/^77$/O/g;s/^68$/P/g;s/^69$/Q/g;s/^6a$/R/g;s/^6b$/S/g;s/^6c$/T/g;s/^6d$/U/g;s/^6e$/V/g;s/^6f$/W/g;s/^60$/X/g;s/^61$/Y/g;s/^62$/Z/g;s/^59$/a/g;s/^5a$/b/g;s/^5b$/c/g;s/^5c$/d/g;s/^5d$/e/g;s/^5e$/f/g;s/^5f$/g/g;s/^50$/h/g;s/^51$/i/g;s/^52$/j/g;s/^53$/k/g;s/^54$/l/g;s/^55$/m/g;s/^56$/n/g;s/^57$/o/g;s/^48$/p/g;s/^49$/q/g;s/^4a$/r/g;s/^4b$/s/g;s/^4c$/t/g;s/^4d$/u/g;s/^4e$/v/g;s/^4f$/w/g;s/^40$/x/g;s/^41$/y/g;s/^42$/z/g;s/^08$/0/g;s/^09$/1/g;s/^0a$/2/g;s/^0b$/3/g;s/^0c$/4/g;s/^0d$/5/g;s/^0e$/6/g;s/^0f$/7/g;s/^00$/8/g;s/^01$/9/g;s/^15$/-/g;s/^16$/./g;s/^67$/_/g;s/^46$/~/g;s/^02$/:/g;s/^17$/\//g;s/^07$/?/g;s/^1b$/#/g;s/^63$/\[/g;s/^65$/\]/g;s/^78$/@/g;s/^19$/!/g;s/^1c$/$/g;s/^1e$/&/g;s/^10$/\(/g;s/^11$/\)/g;s/^12$/*/g;s/^13$/+/g;s/^14$/,/g;s/^03$/;/g;s/^05$/=/g;s/^1d$/%/g' | tr -d '\n' | sed "s/\/clock/\/clock\.json/")
191}
192
193# generates links based on given provider
194generate_link() {
195    case $1 in
196        1) provider_init "wixmp" "/Default :/p" ;;    # wixmp(default)(m3u8)(multi) -> (mp4)(multi)
197        2) provider_init "youtube" "/Yt-mp4 :/p" ;;   # youtube(mp4)(single)
198        3) provider_init "sharepoint" "/S-mp4 :/p" ;; # sharepoint(mp4)(single)
199        4) provider_init "mp4upload" "/Mp4 :/p" ;;    # mp4upload(mp4)(single)
200        *)
201            # provider_init "Filemoon" "/Fm-Hls :/p"
202            # get_filemoon_links "$provider_id"
203            # return 0
204            ;; # filemoon(m3u8)(single)
205    esac
206    [ -n "$provider_id" ] && get_links "$provider_id"
207}
208
209select_quality() {
210    # removing urls which have referrer to avoid playing on iSH and vlc (m3u8 streams don't get correct referrer)
211    printf '%s' "$player_function" | cut -f1 -d" " | grep -qE '(android_vlc|iSH|vlc)' && links=$(printf '%s' "$links" | sed '/m3u8_refr >/d')
212    printf '%s' "$player_function" | cut -f1 -d" " | grep -qE '(android_vlc|iSH)' && links=$(printf '%s' "$links" | sed '/Yt >/d')
213    case "$1" in
214        best) result=$(printf "%s" "$links" | head -n1) ;;
215        worst) result=$(printf "%s" "$links" | grep -E '^[0-9]{3,4}' | tail -n1) ;;
216        *) result=$(printf "%s" "$links" | grep -m 1 "$1") ;;
217    esac
218    [ -z "$result" ] && printf "Specified quality not found, defaulting to best\n" 1>&2 && result=$(printf "%s" "$links" | head -n1)
219
220    # add refr for m3u8 and refr flag for yt
221    case "$result" in
222        *mp4upload*)
223            refr_flag="--referrer=https://www.mp4upload.com"
224            ;;
225        *sharepoint*)
226            unset refr_flag
227            ;;
228        *)
229            refr_flag="--referrer=$allanime_refr"
230            ;;
231    esac
232
233    episode=$(printf "%s" "$result" | cut -d'>' -f2)
234}
235
236process_response() {
237    if ! printf '%s' "$1" | grep -q '"tobeparsed"'; then
238        printf '%s' "$1"
239        return 0
240    fi
241
242    tmp="$(mktemp)"
243    printf '%s' "$1" | sed -nE 's|.*"tobeparsed":"([^"]*)".*|\1|p' | base64 -d >"$tmp"
244    file_size="$(wc -c <"$tmp")"
245    iv="$(dd if="$tmp" bs=1 skip=1 count=12 2>/dev/null | od -A n -t x1 | tr -d ' \n')"
246    ctr="${iv}00000002"
247    ct_len=$((file_size - 13 - 16))
248    dd if="$tmp" bs=1 skip=13 count="$ct_len" 2>/dev/null | openssl enc -d -aes-256-ctr -K "$allanime_key" -iv "$ctr" -nosalt -nopad 2>/dev/null
249    rm -f "$tmp"
250}
251
252b64url_to_hex() {
253    _len=$(printf '%s' "$1" | wc -c | tr -d ' ')
254    _mod=$((_len % 4))
255    case $_mod in
256        2) _pad="==" ;;
257        3) _pad="=" ;;
258        *) _pad="" ;;
259    esac
260    printf '%s%s' "$1" "$_pad" | tr -- '-_' '+/' | base64 -d | od -A n -t x1 | tr -d ' \n'
261}
262
263get_filemoon_links() {
264    response="$(curl -e "$allanime_refr" -s "https://${allanime_base}$1" -A "$agent")"
265    _fm_json="$(printf '%s' "$response" | tr -d '\n ' | tr ',' '\n')"
266    iv="$(printf '%s' "$_fm_json" | sed -nE 's|^"iv":"([^"]*)"$|\1|p')"
267    payload="$(printf '%s' "$_fm_json" | sed -nE 's|^"payload":"([^"]*)"$|\1|p')"
268    kp1="$(printf '%s' "$_fm_json" | sed -nE 's|^"key_parts":\["([^"]*)"$|\1|p')"
269    kp2="$(printf '%s' "$_fm_json" | sed -nE 's|^"([A-Za-z0-9_-]+)"\]$|\1|p' | head -1)"
270    _kp1_hex="$(b64url_to_hex "$kp1")"
271    _kp2_hex="$(b64url_to_hex "$kp2")"
272    key_hex="$_kp1_hex$_kp2_hex"
273    iv_hex="$(b64url_to_hex "$iv")00000002"
274    tmp="$(mktemp)"
275    _fm_len=$(printf '%s' "$payload" | wc -c | tr -d ' ')
276    _fm_mod=$((_fm_len % 4))
277    case $_fm_mod in
278        2) _fm_pad="==" ;;
279        3) _fm_pad="=" ;;
280        *) _fm_pad="" ;;
281    esac
282    printf '%s%s' "$payload" "$_fm_pad" | tr -- '-_' '+/' | base64 -d >"$tmp"
283    ct_len=$(($(wc -c <"$tmp") - 16))
284    plain="$(dd if="$tmp" bs=1 count="$ct_len" 2>/dev/null | openssl enc -d -aes-256-ctr -K "$key_hex" -iv "$iv_hex" -nosalt -nopad 2>/dev/null)"
285    rm -f "$tmp"
286    printf '%s' "$plain" | tr '{}[]' '\n' |
287        sed -nE 's|.*"url":"([^"]*)".*"height":([0-9]+).*|\2 >\1|p;s|.*"height":([0-9]+).*"url":"([^"]*)".*|\1 >\2|p' |
288        sed 's|\\u0026|\&|g;s|\\u003D|=|g' | sort -rn
289    printf "\033[1;32m%s\033[0m Links Fetched\n" "Filemoon" 1>&2
290}
291
292# gets embed urls, collects direct links into provider files, selects one with desired quality into $episode
293get_episode_url() {
294    #shellcheck disable=SC2016
295    episode_embed_gql='query ($showId: String!, $translationType: VaildTranslationTypeEnumType!, $episodeString: String!) { episode( showId: $showId translationType: $translationType episodeString: $episodeString ) { episodeString sourceUrls }}'
296
297    query_hash="d405d0edd690624b66baba3068e0edc3ac90f1597d898a1ec8db4e5c43c00fec"
298    query_vars="{\"showId\":\"$id\",\"translationType\":\"$mode\",\"episodeString\":\"$ep_no\"}"
299    query_ext="{\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"$query_hash\"}}"
300
301    api_resp="$(curl -e "$allanime_refr" -sG -A "$agent" -H "Origin: ${allanime_refr}" "${allanime_api}/api" --data-urlencode "variables=${query_vars}" --data-urlencode "extensions=${query_ext}")"
302
303    if [ -z "$api_resp" ] || ! printf "%s" "$api_resp" | grep -q "tobeparsed"; then
304        api_resp="$(curl -e "$allanime_refr" -s -H "Content-Type: application/json" -X POST "${allanime_api}/api" --data "{\"variables\":{\"showId\":\"$id\",\"translationType\":\"$mode\",\"episodeString\":\"$ep_no\"},\"query\":\"$episode_embed_gql\"}" -A "$agent")"
305    fi
306
307    resp="$(process_response "$api_resp" | tr '{}' '\n' | sed 's|\\u002F|\/|g;s|\\||g' | sed -nE 's|.*sourceUrl":"([^"]*)".*sourceName":"([^"]*)".*|\2 :\1|p')"
308    # generate links into sequential files
309    cache_dir="$(mktemp -d)"
310    #providers="1 2 3 4 5"
311    providers="1 2 3 4"
312    for provider in $providers; do
313        generate_link "$provider" >"$cache_dir"/"$provider" &
314    done
315    wait
316    # select the link with matching quality
317    links=$(cat "$cache_dir"/* | sort -g -r -s)
318    rm -r "$cache_dir"
319    select_quality "$quality"
320    if printf "%s" "$ep_list" | grep -q "^$ep_no$"; then
321        [ -z "$episode" ] && die "Episode is released, but no valid sources!"
322    else
323        [ -z "$episode" ] && die "Episode not released!"
324    fi
325}
326
327# search the query and give results
328search_anime() {
329    #shellcheck disable=SC2016
330    search_gql='query( $search: SearchInput $limit: Int $page: Int $translationType: VaildTranslationTypeEnumType $countryOrigin: VaildCountryOriginEnumType ) { shows( search: $search limit: $limit page: $page translationType: $translationType countryOrigin: $countryOrigin ) { edges { _id name availableEpisodes __typename } }}'
331
332    curl -e "$allanime_refr" -s -H "Content-Type: application/json" -X POST "${allanime_api}/api" --data "{\"variables\":{\"search\":{\"allowAdult\":false,\"allowUnknown\":false,\"query\":\"$1\"},\"limit\":40,\"page\":1,\"translationType\":\"$mode\",\"countryOrigin\":\"ALL\"},\"query\":\"$search_gql\"}" -A "$agent" | sed 's|Show|\
333| g' | sed -nE "s|.*_id\":\"([^\"]*)\",\"name\":\"(.+)\",.*${mode}\":([1-9][^,]*).*|\1	\2 (\3 episodes)|p" | sed 's/\\"//g'
334}
335
336time_until_next_ep() {
337    animeschedule="https://animeschedule.net"
338    query="$(printf "%s\n" "$*" | tr ' ' '+')"
339    curl -s -G "$animeschedule/api/v3/anime" --data "q=${query}" | sed 's|"id"|\n|g' | sed -nE 's|.*,"route":"([^"]*)","premier.*|\1|p' | while read -r anime; do
340        data=$(curl -s "$animeschedule/anime/$anime" | sed '1,/"anime-header-list-buttons-wrapper"/d' | sed -nE 's|.*countdown-time-raw" datetime="([^"]*)">.*|Next Raw Release: \1|p;s|.*countdown-time" datetime="([^"]*)">.*|Next Sub Release: \1|p;s|.*english-title">([^<]*)<.*|English Title: \1|p;s|.*main-title".*>([^<]*)<.*|Japanese Title: \1|p')
341        status="Ongoing"
342        color="33"
343        printf "%s\n" "$data"
344        ! (printf "%s\n" "$data" | grep -q "Next Raw Release:") && status="Finished" && color="32"
345        printf "Status:  \033[1;%sm%s\033[0m\n---\n" "$color" "$status"
346    done
347    exit 0
348}
349
350# get the episodes list of the selected anime
351episodes_list() {
352    #shellcheck disable=SC2016
353    episodes_list_gql='query ($showId: String!) { show( _id: $showId ) { _id availableEpisodesDetail }}'
354
355    curl -e "$allanime_refr" -s -H "Content-Type: application/json" -X POST "${allanime_api}/api" --data "{\"variables\":{\"showId\":\"$*\"},\"query\":\"$episodes_list_gql\"}" -A "$agent" | sed -nE "s|.*$mode\":\[([0-9.\",]*)\].*|\1|p" | sed 's|,|\
356|g; s|"||g' | sort -n -k 1
357}
358
359# PLAYING
360
361process_hist_entry() {
362    ep_list=$(episodes_list "$id")
363    latest_ep=$(printf "%s\n" "$ep_list" | tail -n1)
364    title=$(printf "%s\n" "$title" | sed "s|[0-9]\+ episodes|${latest_ep} episodes|")
365    ep_no=$(printf "%s" "$ep_list" | sed -n "/^${ep_no}$/{n;p;}") 2>/dev/null
366    [ -n "$ep_no" ] && printf "%s\t%s - episode %s\n" "$id" "$title" "$ep_no"
367}
368
369update_history() {
370    if grep -q -- "$id" "$histfile"; then
371        sed -E "s|^[^	]+	${id}	[^	]+$|${ep_no}	${id}	${title}|" "$histfile" >"${histfile}.new"
372    else
373        cp "$histfile" "${histfile}.new"
374        printf "%s\t%s\t%s\n" "$ep_no" "$id" "$title" >>"${histfile}.new"
375    fi
376    mv "${histfile}.new" "$histfile"
377}
378
379download() {
380    case $1 in
381        *m3u8*)
382            if command -v "yt-dlp" >/dev/null; then
383                yt-dlp --referer "$m3u8_refr" "$1" --no-skip-unavailable-fragments --fragment-retries infinite -N 16 -o "$download_dir/$2.mp4"
384            else
385                ffmpeg -extension_picky 0 -referer "$m3u8_refr" -loglevel error -stats -i "$1" -c copy "$download_dir/$2.mp4"
386            fi
387            ;;
388        *)
389            # shellcheck disable=SC2086
390            aria2c --referer="${refr_flag#--referrer=}" --enable-rpc=false --check-certificate=false --continue $iSH_DownFix --summary-interval=0 -x 16 -s 16 "$1" --dir="$download_dir" -o "$2.mp4" --download-result=hide
391            ;;
392    esac
393}
394
395android_mpv() {
396    dir="/storage/emulated/0/mpv"
397    file="$dir/mpv.config.mp4"
398    if [ -w "${dir%*/mpv}" ]; then
399        [ -d "${dir}" ] || mkdir "${dir}"
400        printf "%s" "$3" | sed 's| --|\n|g; s|^--||g' >"$file"
401    fi
402    am start --user 0 -a android.intent.action.VIEW -d "$1" -n is.xyz.mpv/.MPVActivity -e "title" "$2" >/dev/null 2>&1
403    unset dir file
404}
405
406play_episode() {
407    [ "$log_episode" = 1 ] && [ "$player_function" != "debug" ] && [ "$player_function" != "download" ] && command -v logger >/dev/null && logger -t ani-cli "${allanime_title}${ep_no}"
408    [ "$skip_intro" = 1 ] && skip_flag="$(ani-skip -q "$mal_id" -e "$ep_no")"
409    [ -z "$episode" ] && get_episode_url
410    # shellcheck disable=SC2086
411    case "$player_function" in
412        debug)
413            printf "All links:\n%s\nSelected link:\n" "$links"
414            printf "%s\n" "$episode"
415            ;;
416        mpv*)
417            if [ "$no_detach" = 0 ]; then
418                nohup $player_function $skip_flag --tls-verify=no --force-media-title="${allanime_title}Episode ${ep_no}" "$episode" $refr_flag >/dev/null 2>&1 &
419            else
420                $player_function $skip_flag $refr_flag --tls-verify=no --force-media-title="${allanime_title}Episode ${ep_no}" "$episode"
421                mpv_exitcode=$?
422                [ "$exit_after_play" = 1 ] && [ -z "$range" ] && exit "$mpv_exitcode"
423            fi
424            ;;
425        android_mpv) "$player_function" "$episode" "${allanime_title}Episode ${ep_no}" "--tls-verify=no $refr_flag" 2>&1 & ;;
426        android_vlc) nohup am start --user 0 -a android.intent.action.VIEW -d "$episode" -n org.videolan.vlc/org.videolan.vlc.gui.video.VideoPlayerActivity -e "title" "${allanime_title}Episode ${ep_no}" >/dev/null 2>&1 & ;;
427        *iina*)
428            [ -n "$refr_flag" ] && refr_flag="--mpv-${refr_flag#--}"
429            if pgrep -f "IINA" >/dev/null 2>&1; then
430                # omit --keep-running when an IINA instance exists to prevent hanging
431                nohup $player_function --no-stdin --mpv-tls-verify=no --mpv-force-media-title="${allanime_title}Episode ${ep_no}" $refr_flag "$episode" >/dev/null 2>&1 &
432            else
433                nohup $player_function --no-stdin --mpv-tls-verify=no --keep-running --mpv-force-media-title="${allanime_title}Episode ${ep_no}" $refr_flag "$episode" >/dev/null 2>&1 &
434            fi
435            ;;
436        flatpak_mpv) flatpak run io.mpv.Mpv --tls-verify=no --force-media-title="${allanime_title}Episode ${ep_no}" "$episode" $refr_flag >/dev/null 2>&1 & ;;
437        vlc*) nohup $player_function --http-referrer="${refr_flag#--referrer=}" --play-and-exit --meta-title="${allanime_title}Episode ${ep_no}" "$episode" >/dev/null 2>&1 & ;;
438        *yncpla*) nohup $player_function "$episode" -- --tls-verify=no --force-media-title="${allanime_title}Episode ${ep_no}" $refr_flag >/dev/null 2>&1 & ;;
439        download) "$player_function" "$episode" "${allanime_title}Episode ${ep_no}" ;;
440        catt) nohup catt cast "$episode" >/dev/null 2>&1 & ;;
441        iSH)
442            printf "\e]8;;vlc://%s\a~~~~~~~~~~~~~~~~~~~~\n~ Tap to open VLC ~\n~~~~~~~~~~~~~~~~~~~~\e]8;;\a\n" "$episode"
443            sleep 5
444            ;;
445        *) nohup $player_function "$episode" >/dev/null 2>&1 & ;;
446    esac
447    replay="$episode"
448    unset episode
449    update_history
450    [ "$use_external_menu" = "1" ] && wait
451    [ "$use_external_menu" = "2" ] && wait
452}
453
454play() {
455    start=$(printf "%s" "$ep_no" | grep -Eo '^(-1|[0-9]+(\.[0-9]+)?)')
456    end=$(printf "%s" "$ep_no" | grep -Eo '(-1|[0-9]+(\.[0-9]+)?)$')
457    [ "$start" = "-1" ] && ep_no=$(printf "%s" "$ep_list" | tail -n1) && unset start
458    [ -z "$end" ] || [ "$end" = "$start" ] && unset start end
459    [ "$end" = "-1" ] && end=$(printf "%s" "$ep_list" | tail -n1)
460    line_count=$(printf "%s\n" "$ep_no" | wc -l | tr -d "[:space:]")
461    if [ "$line_count" != 1 ] || [ -n "$start" ]; then
462        [ -z "$start" ] && start=$(printf "%s\n" "$ep_no" | head -n1)
463        [ -z "$end" ] && end=$(printf "%s\n" "$ep_no" | tail -n1)
464        range=$(printf "%s\n" "$ep_list" | sed -nE "/^${start}\$/,/^${end}\$/p")
465        [ -z "$range" ] && die "Invalid range!"
466        for i in $range; do
467            tput clear
468            ep_no=$i
469            printf "\33[2K\r\033[1;34mPlaying episode %s...\033[0m\n" "$ep_no"
470            [ "$i" = "$end" ] && unset range
471            play_episode
472        done
473    else
474        play_episode
475    fi
476    # moves up to stored position and deletes to end
477    [ "$player_function" != "debug" ] && [ "$player_function" != "download" ] && tput rc && tput ed
478}
479
480# MAIN
481
482# setup
483agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0"
484allanime_refr="https://youtu-chan.com"
485allanime_base="allanime.day"
486allanime_api="https://api.${allanime_base}"
487allanime_key="$(printf '%s' 'Xot36i3lK3:v1' | openssl dgst -sha256 -binary | od -A n -t x1 | tr -d ' \n')"
488mode="${ANI_CLI_MODE:-sub}"
489download_dir="${ANI_CLI_DOWNLOAD_DIR:-.}"
490log_episode="${ANI_CLI_LOG:-1}"
491quality="${ANI_CLI_QUALITY:-best}"
492case "$(uname -a | cut -d " " -f 1,3-)" in
493    *Darwin*) player_function="${ANI_CLI_PLAYER:-$(where_iina)}" ;;   # mac OS
494    *ndroid*) player_function="${ANI_CLI_PLAYER:-android_mpv}" ;;     # Android OS (termux)
495    *MINGW* | *WSL2*) player_function="${ANI_CLI_PLAYER:-mpv.exe}" ;; # Windows OS
496    *ish*) player_function="${ANI_CLI_PLAYER:-iSH}" ;;                # iOS (iSH)
497    *) player_function="${ANI_CLI_PLAYER:-$(where_mpv)}" ;;           # Linux OS
498esac
499
500no_detach="${ANI_CLI_NO_DETACH:-0}"
501exit_after_play="${ANI_CLI_EXIT_AFTER_PLAY:-0}"
502use_external_menu="${ANI_CLI_EXTERNAL_MENU:-0}"
503external_menu_normal_window="${ANI_CLI_EXTERNAL_MENU_NORMAL_WINDOW:-0}"
504skip_intro="${ANI_CLI_SKIP_INTRO:-0}"
505skip_title="${ANI_CLI_SKIP_TITLE:-}"
506[ -t 0 ] || (command -v dmenu && use_external_menu=2)
507[ -t 0 ] || (command -v rofi && use_external_menu=1)
508hist_dir="${ANI_CLI_HIST_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/ani-cli}"
509[ ! -d "$hist_dir" ] && mkdir -p "$hist_dir"
510histfile="$hist_dir/ani-hsts"
511[ ! -f "$histfile" ] && : >"$histfile"
512search="${ANI_CLI_DEFAULT_SOURCE:-scrape}"
513branch="${ANI_CLI_BRANCH:-master}"
514
515while [ $# -gt 0 ]; do
516    case "$1" in
517        -v | --vlc)
518            case "$(uname -a | cut -d " " -f 1,3-)" in
519                *ndroid*) player_function="android_vlc" ;;
520                MINGW* | *WSL2*) player_function="vlc.exe" ;;
521                *ish*) player_function="iSH" ;;
522                *) player_function="vlc" ;;
523            esac
524            ;;
525        -s | --syncplay)
526            case "$(uname -s)" in
527                Darwin*) player_function="/Applications/Syncplay.app/Contents/MacOS/syncplay" ;;
528                MINGW* | *Msys)
529                    export PATH="$PATH":"/c/Program Files (x86)/Syncplay/"
530                    player_function="syncplay.exe"
531                    ;;
532                *) player_function="syncplay" ;;
533            esac
534            ;;
535        -q | --quality)
536            [ $# -lt 2 ] && die "missing argument!"
537            quality="$2"
538            shift
539            ;;
540        -S | --select-nth)
541            [ $# -lt 2 ] && die "missing argument!"
542            index="$2"
543            shift
544            ;;
545        -c | --continue) search=history ;;
546        -d | --download)
547            [ "$player_function" = "iSH" ] && iSH_DownFix="--async-dns=false"
548            player_function=download
549            ;;
550        -D | --delete)
551            : >"$histfile"
552            exit 0
553            ;;
554        -l | --logview)
555            case "$(uname -s)" in
556                Darwin*) log show --predicate 'process == "logger"' ;;
557                Linux*) journalctl -t ani-cli ;;
558                *) die "Logger not implemented for your platform" ;;
559            esac
560            exit 0
561            ;;
562        -V | --version) version_info ;;
563        -h | --help) help_info ;;
564        -e | --episode | -r | --range)
565            [ $# -lt 2 ] && die "missing argument!"
566            ep_no="$2"
567            shift
568            ;;
569        --dub) mode="dub" ;;
570        --no-detach) no_detach=1 ;;
571        --exit-after-play) exit_after_play=1 && no_detach=1 ;;
572        --rofi) use_external_menu=1 ;;
573        --dmenu) use_external_menu=2 ;;
574        --skip) skip_intro=1 ;;
575        --skip-title)
576            [ $# -lt 2 ] && die "missing argument!"
577            skip_title="$2"
578            shift
579            ;;
580        -N | --nextep-countdown) search=nextep ;;
581        -U | --update)
582            branch="${2:-$branch}"
583            update_script
584            ;;
585        *) query="$(printf "%s" "$query $1" | sed "s|^ ||;s| |+|g")" ;;
586    esac
587    shift
588done
589[ "$use_external_menu" = "0" ] && multi_selection_flag="${ANI_CLI_MULTI_SELECTION:-"-m"}"
590[ "$use_external_menu" = "1" ] && multi_selection_flag="${ANI_CLI_MULTI_SELECTION:-"-multi-select"}"
591[ "$external_menu_normal_window" = "1" ] && external_menu_args="-normal-window"
592printf "\33[2K\r\033[1;34mChecking dependencies...\033[0m\n"
593dep_ch "curl" "sed" "grep" || true
594case "$(uname -o 2>/dev/null)" in
595    *ndroid*) command -v openssl >/dev/null || die 'Program "openssl" not found. On Termux, install with: pkg install openssl-tool' ;;
596    *) dep_ch "openssl" || true ;;
597esac
598[ "$skip_intro" = 1 ] && (dep_ch "ani-skip" || true)
599dep_ch "fzf" || true
600case "$player_function" in
601    debug) ;;
602    download) dep_ch "ffmpeg" "aria2c" ;;
603    android*) printf "\33[2K\rChecking of players on Android is disabled.\nANI-CLI recommends Android MPV for full compatibility.\nAdd:\n include='/storage/emulated/0/mpv/mpv.config.mp4'\nto:\n MPV > Settings > Advanced > mpv.conf\nfor full functionality.\nIgnore, If Done.\n" ;;
604    *iSH*) printf "\33[2K\rChecking of players on iOS is disabled\n" ;;
605    flatpak_mpv) true ;; # handled out of band in where_mpv
606    *) dep_ch "$player_function" ;;
607esac
608
609# idk where to put this, so I put it here...
610trap 'cleanup; exit 1' INT HUP
611
612# searching
613case "$search" in
614    history)
615        anime_list=$(while read -r ep_no id title; do process_hist_entry & done <"$histfile")
616        wait
617        [ -z "$anime_list" ] && die "No unwatched series in history!"
618        [ -z "${index##*[!0-9]*}" ] && id=$(printf "%s" "$anime_list" | nl -w 2 | sed 's/^[[:space:]]//' | nth "Select anime: " | cut -f1)
619        [ -z "${index##*[!0-9]*}" ] || id=$(printf "%s" "$anime_list" | sed -n "${index}p" | cut -f1)
620        [ -z "$id" ] && exit 1
621        title=$(printf "%s" "$anime_list" | grep "$id" | cut -f2 | sed 's/ - episode.*//')
622        ep_list=$(episodes_list "$id")
623        ep_no=$(printf "%s" "$anime_list" | grep "$id" | cut -f2 | sed -nE 's/.*- episode (.+)$/\1/p')
624        allanime_title="$(printf "%s" "$title" | cut -d'(' -f1 | tr -d '[:punct:]')"
625        ;;
626    *)
627        if [ "$use_external_menu" = "0" ]; then
628            while [ -z "$query" ]; do
629                printf "\33[2K\r\033[1;36mSearch anime: \033[0m" && read -r query
630            done
631        else
632            [ -z "$query" ] && query=$(printf "" | external_menu "" "Search anime: " "$external_menu_args")
633            [ -z "$query" ] && exit 1
634        fi
635        # for checking new releases by specifying anime name
636        [ "$search" = "nextep" ] && time_until_next_ep "$query"
637
638        query=$(printf "%s" "$query" | sed "s| |+|g")
639        anime_list=$(search_anime "$query")
640        [ -z "$anime_list" ] && die "No results found!"
641        [ "$index" -eq "$index" ] 2>/dev/null && result=$(printf "%s" "$anime_list" | sed -n "${index}p")
642        [ -z "$index" ] && result=$(printf "%s" "$anime_list" | nl -w 2 | sed 's/^[[:space:]]//' | nth "Select anime: ")
643        [ -z "$result" ] && exit 1
644        title=$(printf "%s" "$result" | cut -f2)
645        allanime_title="$(printf "%s" "$title" | cut -d'(' -f1 | tr -d '[:punct:]')"
646        id=$(printf "%s" "$result" | cut -f1)
647        ep_list=$(episodes_list "$id")
648        [ -z "$ep_no" ] && ep_no=$(printf "%s" "$ep_list" | nth "Select episode: " "$multi_selection_flag")
649        [ -z "$ep_no" ] && exit 1
650        ;;
651esac
652[ "$skip_intro" = 1 ] && mal_id="$(ani-skip -q "${skip_title:-${title}}")"
653
654# moves the cursor up one line and clears that line
655tput cuu1 && tput el
656# stores the position of cursor
657tput sc
658
659# playback & loop
660play
661[ "$player_function" = "download" ] || [ "$player_function" = "debug" ] && exit 0
662
663while cmd=$(printf "next\nreplay\nprevious\nselect\nchange_quality\nquit" | nth "Playing episode $ep_no of $title... "); do
664    case "$cmd" in
665        next) ep_no=$(printf "%s" "$ep_list" | sed -n "/^${ep_no}$/{n;p;}") 2>/dev/null ;;
666        replay) episode="$replay" ;;
667        previous) ep_no=$(printf "%s" "$ep_list" | sed -n "/^${ep_no}$/{g;1!p;};h") 2>/dev/null ;;
668        select) ep_no=$(printf "%s" "$ep_list" | nth "Select episode: " "$multi_selection_flag") ;;
669        change_quality)
670            new_quality="$(printf "%s" "$links" | launcher | cut -d\> -f1)"
671            select_quality "$new_quality"
672            ;;
673        *)
674            cleanup
675            exit 0
676            ;;
677    esac
678    [ -z "$ep_no" ] && die "Out of range"
679    play
680done
681
682# ani-cli
683#
684# This program is free software: you can redistribute it and/or modify
685# it under the terms of the GNU General Public License as published by
686# the Free Software Foundation, either version 3 of the License, or
687# (at your option) any later version.
688#
689# This program is distributed in the hope that it will be useful,
690# but WITHOUT ANY WARRANTY; without even the implied warranty of
691# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
692# GNU General Public License for more details.
693#
694# You should have received a copy of the GNU General Public License
695# along with this program. If not, see <http://www.gnu.org/licenses/>.
696#
697# Project repository: https://github.com/pystardust/ani-cli