#!/bin/sh version_number="4.14.0" # UI external_menu() { [ "$use_external_menu" = "1" ] && rofi "$1" -sort -dmenu -i -width 1500 -p "$2" "$3" [ "$use_external_menu" = "2" ] && dmenu -l 20 -p "$2" } launcher() { [ "$use_external_menu" = "0" ] && [ -z "$1" ] && set -- "+m" "$2" [ "$use_external_menu" = "0" ] && fzf "$1" --reverse --cycle --prompt "$2" [ "$use_external_menu" = "1" ] && external_menu "$1" "$2" "$external_menu_args" [ "$use_external_menu" = "2" ] && external_menu "$1" "$2" } 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 -f2,3 && return 0 prompt="$1" multi_flag="" [ $# -ne 1 ] && shift && multi_flag="$1" line=$(printf "%s" "$stdin" | cut -f1,3 | tr '\t' ' ' | launcher "$multi_flag" "$prompt" | cut -d " " -f 1) line_start=$(printf "%s" "$line" | head -n1) line_end=$(printf "%s" "$line" | tail -n1) [ -n "$line" ] || exit 1 if [ "$line_start" = "$line_end" ]; then printf "%s" "$stdin" | grep -E '^'"${line}"'($|[[:space:]])' | cut -f2,3 || exit 1 else printf "%s" "$stdin" | sed -n '/^'"${line_start}"'$/,/^'"${line_end}$"'/p' || exit 1 fi } die() { printf "\33[2K\r\033[1;31m%s\033[0m\n" "$*" >&2 exit 1 } 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, --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, mpv only) --skip-title Use given title as ani-skip query -N, --nextep-countdown Display a countdown to the next episode -U, --update Update the script Some example usages: %s -q 720p banana fish %s --skip --skip-title \"one piece\" -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 } update_script() { update="$(curl -s -A "$agent" "https://raw.githubusercontent.com/pystardust/ani-cli/master/ani-cli")" || die "Connection error" update="$(printf '%s\n' "$update" | diff -u "$0" -)" if [ -z "$update" ]; then printf "Script is up to date :)\n" else if printf '%s\n' "$update" | patch "$0" -; then printf "Script has been updated\n" else die "Can't update for some reason!" fi fi exit 0 } # checks if dependencies are present dep_ch() { for dep; do command -v "${dep%% *}" >/dev/null || die "Program \"${dep%% *}\" not found. Please install it." done } where_iina() { [ -e "/Applications/IINA.app/Contents/MacOS/iina-cli" ] && echo "/Applications/IINA.app/Contents/MacOS/iina-cli" && return 0 printf "%s" "iina" && return 0 } where_mpv() { command -v "flatpak" >/dev/null && flatpak info io.mpv.Mpv >/dev/null 2>&1 && printf "%s" "flatpak_mpv" && return 0 printf "%s" "mpv" && return 0 } # SCRAPING # extract the video links from response of embed urls, extract mp4 links form m3u8 lists get_links() { response="$(curl -e "$allanime_refr" -s "https://${allanime_base}$*" -A "$agent")" episode_link="$(printf '%s' "$response" | sed 's|},{|\ |g' | sed -nE 's|.*link":"([^"]*)".*"resolutionStr":"([^"]*)".*|\2 >\1|p;s|.*hls","url":"([^"]*)".*"hardsub_lang":"en-US".*|\1|p')" case "$episode_link" in *repackager.wixmp.com*) extract_link=$(printf "%s" "$episode_link" | cut -d'>' -f2 | sed 's|repackager.wixmp.com/||g;s|\.urlset.*||g') for j in $(printf "%s" "$episode_link" | sed -nE 's|.*/,([^/]*),/mp4.*|\1|p' | sed 's|,|\ |g'); do printf "%s >%s\n" "$j" "$extract_link" | sed "s|,[^/]*|${j}|g" done | sort -nr ;; *master.m3u8*) m3u8_refr=$(printf '%s' "$response" | sed -nE 's|.*Referer":"([^"]*)".*|\1|p') && printf '%s\n' "m3u8_refr >$m3u8_refr" >"$cache_dir/m3u8_refr" extract_link=$(printf "%s" "$episode_link" | head -1 | cut -d'>' -f2) relative_link=$(printf "%s" "$extract_link" | sed 's|[^/]*$||') m3u8_streams="$(curl -e "$m3u8_refr" -s "$extract_link" -A "$agent")" 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|>|cc>${relative_link}|g" | sort -nr printf '%s' "$response" | sed -nE 's|.*"subtitles":\[\{"lang":"en","label":"English","default":"default","src":"([^"]*)".*|subtitle >\1|p' >"$cache_dir/suburl" ;; *) [ -n "$episode_link" ] && printf "%s\n" "$episode_link" ;; esac printf "%s" "$*" | grep -q "tools.fast4speed.rsvp" && printf "%s\n" "Yt >$*" printf "\033[1;32m%s\033[0m Links Fetched\n" "$provider_name" 1>&2 } # initialises provider_name and provider_id. First argument is the provider name, 2nd is the regex that matches that provider's link provider_init() { provider_name=$1 provider_id=$(printf "%s" "$resp" | sed -n "$2" | head -1 | cut -d':' -f2 | sed 's/../&\ /g' | sed '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/") } # generates links based on given provider generate_link() { case $1 in 1) provider_init "wixmp" "/Default :/p" ;; # wixmp(default)(m3u8)(multi) -> (mp4)(multi) 2) provider_init "youtube" "/Yt-mp4 :/p" ;; # youtube(mp4)(single) 3) provider_init "sharepoint" "/S-mp4 :/p" ;; # sharepoint(mp4)(single) 5) provider_init "filemoon" "/Fm-mp4 :/p" ;; # filemoon(m3u8)(single) *) provider_init "hianime" "/Luf-Mp4 :/p" ;; # hianime(m3u8)(multi) esac if [ "$1" = "5" ] && [ -n "$provider_id" ]; then get_filemoon_links "$provider_id" else [ -n "$provider_id" ] && get_links "$provider_id" fi } select_quality() { # removing urls which have soft subs to avoid playing on android, iSH and vlc (m3u8 streams don't get correct referrer) printf '%s' "$player_function" | cut -f1 -d" " | grep -qE '(android|iSH|vlc)' && links=$(printf '%s' "$links" | sed '/cc>/d;/subtitle >/d;/m3u8_refr >/d') printf '%s' "$player_function" | cut -f1 -d" " | grep -qE '(android|iSH)' && links=$(printf '%s' "$links" | sed '/Yt >/d') case "$1" in best) result=$(printf "%s" "$links" | head -n1) ;; worst) result=$(printf "%s" "$links" | grep -E '^[0-9]{3,4}' | tail -n1) ;; *) result=$(printf "%s" "$links" | grep -m 1 "$1") ;; esac [ -z "$result" ] && printf "Specified quality not found, defaulting to best\n" 1>&2 && result=$(printf "%s" "$links" | head -n1) # add refr,sub flags for m3u8 and refr flag for yt printf '%s' "$result" | grep -q "cc>" && subtitle="$(printf '%s' "$links" | sed -nE 's|subtitle >(.*)|\1|p')" && [ -n "$subtitle" ] && subs_flag="--sub-file=$subtitle" printf '%s' "$result" | grep -q "cc>" && m3u8_refr="$(printf '%s' "$links" | sed -nE 's|m3u8_refr >(.*)|\1|p')" && refr_flag="--referrer=$m3u8_refr" printf "%s" "$result" | grep -q "tools.fast4speed.rsvp" && refr_flag="--referrer=$allanime_refr" ! (printf '%s' "$result" | grep -qE "(cc>|tools.fast4speed.rsvp)") && unset refr_flag ! (printf '%s' "$result" | grep -q "cc>") && unset subs_flag episode=$(printf "%s" "$result" | cut -d'>' -f2) } decode_tobeparsed() { tmp="$(mktemp)" printf '%s' "$1" | base64 -d >"$tmp" file_size="$(wc -c <"$tmp")" iv="$(dd if="$tmp" bs=1 skip=1 count=12 2>/dev/null | od -A n -t x1 | tr -d ' \n')" ctr="${iv}00000002" ct_len=$((file_size - 13 - 16)) plain="$(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)" rm -f "$tmp" printf '%s' "$plain" | tr '{}' '\n' | sed -nE 's|.*"sourceUrl":"--([^"]*)".*"sourceName":"([^"]*)".*|\2 :\1|p' } b64url_to_hex() { _len=$(printf '%s' "$1" | wc -c | tr -d ' ') _mod=$((_len % 4)) case $_mod in 2) _pad="==" ;; 3) _pad="=" ;; *) _pad="" ;; esac printf '%s%s' "$1" "$_pad" | tr -- '-_' '+/' | base64 -d | od -A n -t x1 | tr -d ' \n' } get_filemoon_links() { response="$(curl -e "$allanime_refr" -s "https://${allanime_base}$1" -A "$agent")" _fm_json="$(printf '%s' "$response" | tr -d '\n ' | tr ',' '\n')" iv="$(printf '%s' "$_fm_json" | sed -nE 's|^"iv":"([^"]*)"$|\1|p')" payload="$(printf '%s' "$_fm_json" | sed -nE 's|^"payload":"([^"]*)"$|\1|p')" kp1="$(printf '%s' "$_fm_json" | sed -nE 's|^"key_parts":\["([^"]*)"$|\1|p')" kp2="$(printf '%s' "$_fm_json" | sed -nE 's|^"([A-Za-z0-9_-]+)"\]$|\1|p' | head -1)" _kp1_hex="$(b64url_to_hex "$kp1")" _kp2_hex="$(b64url_to_hex "$kp2")" key_hex="$_kp1_hex$_kp2_hex" iv_hex="$(b64url_to_hex "$iv")00000002" tmp="$(mktemp)" _fm_len=$(printf '%s' "$payload" | wc -c | tr -d ' ') _fm_mod=$((_fm_len % 4)) case $_fm_mod in 2) _fm_pad="==" ;; 3) _fm_pad="=" ;; *) _fm_pad="" ;; esac printf '%s%s' "$payload" "$_fm_pad" | tr -- '-_' '+/' | base64 -d >"$tmp" ct_len=$(($(wc -c <"$tmp") - 16)) 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)" rm -f "$tmp" printf '%s' "$plain" | tr '{}[]' '\n' | sed -nE 's|.*"url":"([^"]*)".*"height":([0-9]+).*|\2 >\1|p;s|.*"height":([0-9]+).*"url":"([^"]*)".*|\1 >\2|p' | sed 's|\\u0026|\&|g;s|\\u003D|=|g' | sort -rn printf "\033[1;32m%s\033[0m Links Fetched\n" "Filemoon" 1>&2 } # gets embed urls, collects direct links into provider files, selects one with desired quality into $episode get_episode_url() { #shellcheck disable=SC2016 episode_embed_gql='query ($showId: String!, $translationType: VaildTranslationTypeEnumType!, $episodeString: String!) { episode( showId: $showId translationType: $translationType episodeString: $episodeString ) { episodeString sourceUrls }}' query_hash="d405d0edd690624b66baba3068e0edc3ac90f1597d898a1ec8db4e5c43c00fec" query_vars="{\"showId\":\"$id\",\"translationType\":\"$mode\",\"episodeString\":\"$ep_no\"}" query_ext="{\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"$query_hash\"}}" encoded_vars=$(printf '%s' "$query_vars" | sed 's/"/%22/g; s/:/%3A/g; s/{/%7B/g; s/}/%7D/g; s/,/%2C/g') encoded_ext=$(printf '%s' "$query_ext" | sed 's/"/%22/g; s/:/%3A/g; s/{/%7B/g; s/}/%7D/g; s/,/%2C/g; s/ /%20/g') api_url="${allanime_api}/api?variables=${encoded_vars}&extensions=${encoded_ext}" api_resp="$(curl -e "https://youtu-chan.com" -s -A "$agent" -H "Origin: https://youtu-chan.com" "$api_url")" if [ -z "$api_resp" ] || ! printf "%s" "$api_resp" | grep -q "tobeparsed"; then 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")" fi if printf "%s" "$api_resp" | grep -q '"tobeparsed"'; then blob="$(printf "%s" "$api_resp" | sed -nE 's|.*"tobeparsed":"([^"]*)".*|\1|p')" resp="$(decode_tobeparsed "$blob")" else resp="$(printf "%s" "$api_resp" | tr '{}' '\n' | sed 's|\\u002F|\/|g;s|\\||g' | sed -nE 's|.*sourceUrl":"--([^"]*)".*sourceName":"([^"]*)".*|\2 :\1|p')" fi # generate links into sequential files cache_dir="$(mktemp -d)" providers="1 2 3 4 5" for provider in $providers; do generate_link "$provider" >"$cache_dir"/"$provider" & done wait # select the link with matching quality links=$(cat "$cache_dir"/* | sort -g -r -s) rm -r "$cache_dir" select_quality "$quality" if printf "%s" "$ep_list" | grep -q "^$ep_no$"; then [ -z "$episode" ] && die "Episode is released, but no valid sources!" else [ -z "$episode" ] && die "Episode not released!" fi } # search the query and give results search_anime() { #shellcheck disable=SC2016 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 } }}' 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|\ | g' | sed -nE "s|.*_id\":\"([^\"]*)\",\"name\":\"(.+)\",.*${mode}\":([1-9][^,]*).*|\1 \2 (\3 episodes)|p" | sed 's/\\"//g' } time_until_next_ep() { animeschedule="https://animeschedule.net" query="$(printf "%s\n" "$*" | tr ' ' '+')" 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 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') status="Ongoing" color="33" printf "%s\n" "$data" ! (printf "%s\n" "$data" | grep -q "Next Raw Release:") && status="Finished" && color="32" printf "Status: \033[1;%sm%s\033[0m\n---\n" "$color" "$status" done exit 0 } # get the episodes list of the selected anime episodes_list() { #shellcheck disable=SC2016 episodes_list_gql='query ($showId: String!) { show( _id: $showId ) { _id availableEpisodesDetail }}' 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|,|\ |g; s|"||g' | sort -n -k 1 } # PLAYING process_hist_entry() { ep_list=$(episodes_list "$id") latest_ep=$(printf "%s\n" "$ep_list" | tail -n1) title=$(printf "%s\n" "$title" | sed "s|[0-9]\+ episodes|${latest_ep} episodes|") 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" "$id" "$title" "$ep_no" } update_history() { if grep -q -- "$id" "$histfile"; then sed -E "s|^[^ ]+ ${id} [^ ]+$|${ep_no} ${id} ${title}|" "$histfile" >"${histfile}.new" else cp "$histfile" "${histfile}.new" printf "%s\t%s\t%s\n" "$ep_no" "$id" "$title" >>"${histfile}.new" fi mv "${histfile}.new" "$histfile" } download() { # download subtitle if it's set [ -n "$subtitle" ] && curl -s "$subtitle" -o "$download_dir/$2.vtt" case $1 in *m3u8*) if command -v "yt-dlp" >/dev/null; then yt-dlp --referer "$m3u8_refr" "$1" --no-skip-unavailable-fragments --fragment-retries infinite -N 16 -o "$download_dir/$2.mp4" else ffmpeg -extension_picky 0 -referer "$m3u8_refr" -loglevel error -stats -i "$1" -c copy "$download_dir/$2.mp4" fi # embed subs into downloads # [ -e "$download_dir/$2.vtt" ] && ffmpeg -i "$download_dir/$2.mp4" -i "$download_dir/$2.vtt" -c copy -c:s mov_text "$download_dir/$2.bak.mp4" && mv "$download_dir/$2.bak.mp4" "$download_dir/$2.mp4" ;; *) # shellcheck disable=SC2086 aria2c --referer="$allanime_refr" --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 ;; esac } play_episode() { [ "$log_episode" = 1 ] && [ "$player_function" != "debug" ] && [ "$player_function" != "download" ] && command -v logger >/dev/null && logger -t ani-cli "${allanime_title}${ep_no}" [ "$skip_intro" = 1 ] && skip_flag="$(ani-skip -q "$mal_id" -e "$ep_no")" [ -z "$episode" ] && get_episode_url # shellcheck disable=SC2086 case "$player_function" in debug) printf "All links:\n%s\nSelected link:\n" "$links" printf "%s\n" "$episode" ;; mpv*) if [ "$no_detach" = 0 ]; then nohup $player_function $skip_flag --force-media-title="${allanime_title}Episode ${ep_no}" "$episode" $subs_flag $refr_flag >/dev/null 2>&1 & else $player_function $skip_flag $subs_flag $refr_flag --force-media-title="${allanime_title}Episode ${ep_no}" "$episode" mpv_exitcode=$? [ "$exit_after_play" = 1 ] && [ -z "$range" ] && exit "$mpv_exitcode" fi ;; android_mpv) nohup am start --user 0 -a android.intent.action.VIEW -d "$episode" -n is.xyz.mpv/.MPVActivity >/dev/null 2>&1 & ;; 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 & ;; *iina*) [ -n "$subs_flag" ] && subs_flag="--mpv-${subs_flag#--}" [ -n "$refr_flag" ] && refr_flag="--mpv-${refr_flag#--}" if pgrep -f "IINA" >/dev/null 2>&1; then # omit --keep-running when an IINA instance exists to prevent hanging nohup $player_function --no-stdin --mpv-force-media-title="${allanime_title}Episode ${ep_no}" $subs_flag $refr_flag "$episode" >/dev/null 2>&1 & else nohup $player_function --no-stdin --keep-running --mpv-force-media-title="${allanime_title}Episode ${ep_no}" $subs_flag $refr_flag "$episode" >/dev/null 2>&1 & fi ;; flatpak_mpv) flatpak run io.mpv.Mpv --force-media-title="${allanime_title}Episode ${ep_no}" "$episode" $subs_flag $refr_flag >/dev/null 2>&1 & ;; vlc*) nohup $player_function --http-referrer="${allanime_refr}" --play-and-exit --meta-title="${allanime_title}Episode ${ep_no}" "$episode" >/dev/null 2>&1 & ;; *yncpla*) nohup $player_function "$episode" -- --force-media-title="${allanime_title}Episode ${ep_no}" $subs_flag $refr_flag >/dev/null 2>&1 & ;; download) "$player_function" "$episode" "${allanime_title}Episode ${ep_no}" "$subtitle" ;; catt) nohup catt cast "$episode" -s "$subtitle" >/dev/null 2>&1 & ;; iSH) printf "\e]8;;vlc://%s\a~~~~~~~~~~~~~~~~~~~~\n~ Tap to open VLC ~\n~~~~~~~~~~~~~~~~~~~~\e]8;;\a\n" "$episode" sleep 5 ;; *) nohup $player_function "$episode" >/dev/null 2>&1 & ;; esac replay="$episode" unset episode update_history [ "$use_external_menu" = "1" ] && wait [ "$use_external_menu" = "2" ] && 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 -n1) && unset start [ -z "$end" ] || [ "$end" = "$start" ] && unset start end [ "$end" = "-1" ] && end=$(printf "%s" "$ep_list" | tail -n1) 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 -n1) [ -z "$end" ] && end=$(printf "%s\n" "$ep_no" | tail -n1) range=$(printf "%s\n" "$ep_list" | sed -nE "/^${start}\$/,/^${end}\$/p") [ -z "$range" ] && die "Invalid range!" for i in $range; do tput clear ep_no=$i printf "\33[2K\r\033[1;34mPlaying episode %s...\033[0m\n" "$ep_no" [ "$i" = "$end" ] && unset range play_episode done else play_episode fi # moves up to stored position and deletes to end [ "$player_function" != "debug" ] && [ "$player_function" != "download" ] && tput rc && tput ed } # MAIN # setup agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/121.0" allanime_refr="https://allmanga.to" allanime_base="allanime.day" allanime_api="https://api.${allanime_base}" allanime_key="$(printf '%s' 'Xot36i3lK3:v1' | openssl dgst -sha256 -binary | od -A n -t x1 | tr -d ' \n')" mode="${ANI_CLI_MODE:-sub}" download_dir="${ANI_CLI_DOWNLOAD_DIR:-.}" log_episode="${ANI_CLI_LOG:-1}" quality="${ANI_CLI_QUALITY:-best}" case "$(uname -a | cut -d " " -f 1,3-)" in *Darwin*) player_function="${ANI_CLI_PLAYER:-$(where_iina)}" ;; # 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}" ;; # iOS (iSH) *) player_function="${ANI_CLI_PLAYER:-$(where_mpv)}" ;; # Linux OS esac no_detach="${ANI_CLI_NO_DETACH:-0}" exit_after_play="${ANI_CLI_EXIT_AFTER_PLAY:-0}" use_external_menu="${ANI_CLI_EXTERNAL_MENU:-0}" external_menu_normal_window="${ANI_CLI_EXTERNAL_MENU_NORMAL_WINDOW:-0}" skip_intro="${ANI_CLI_SKIP_INTRO:-0}" skip_title="${ANI_CLI_SKIP_TITLE:-}" [ -t 0 ] || (command -v dmenu && use_external_menu=2) [ -t 0 ] || (command -v rofi && use_external_menu=1) 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" search="${ANI_CLI_DEFAULT_SOURCE:-scrape}" 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 | --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) search=history ;; -d | --download) [ "$player_function" = "iSH" ] && iSH_DownFix="--async-dns=false" 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) use_external_menu=1 ;; --dmenu) use_external_menu=2 ;; --skip) skip_intro=1 ;; --skip-title) [ $# -lt 2 ] && die "missing argument!" skip_title="$2" shift ;; -N | --nextep-countdown) search=nextep ;; -U | --update) update_script ;; *) query="$(printf "%s" "$query $1" | sed "s|^ ||;s| |+|g")" ;; esac shift done [ "$use_external_menu" = "0" ] && multi_selection_flag="${ANI_CLI_MULTI_SELECTION:-"-m"}" [ "$use_external_menu" = "1" ] && multi_selection_flag="${ANI_CLI_MULTI_SELECTION:-"-multi-select"}" [ "$external_menu_normal_window" = "1" ] && external_menu_args="-normal-window" printf "\33[2K\r\033[1;34mChecking dependencies...\033[0m\n" dep_ch "curl" "sed" "grep" || true case "$(uname -o 2>/dev/null)" in *ndroid*) command -v openssl >/dev/null || die 'Program "openssl" not found. On Termux, install with: pkg install openssl-tool' ;; *) dep_ch "openssl" || true ;; esac [ "$skip_intro" = 1 ] && (dep_ch "ani-skip" || true) dep_ch "fzf" || true case "$player_function" in debug) ;; download) dep_ch "ffmpeg" "aria2c" ;; 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) true ;; # handled out of band in where_mpv *) dep_ch "$player_function" ;; esac # searching case "$search" in history) anime_list=$(while read -r ep_no id title; do process_hist_entry & done <"$histfile") wait [ -z "$anime_list" ] && die "No unwatched series in history!" [ -z "${index##*[!0-9]*}" ] && id=$(printf "%s" "$anime_list" | nl -w 2 | sed 's/^[[:space:]]//' | nth "Select anime: " | cut -f1) [ -z "${index##*[!0-9]*}" ] || id=$(printf "%s" "$anime_list" | sed -n "${index}p" | cut -f1) [ -z "$id" ] && exit 1 title=$(printf "%s" "$anime_list" | grep "$id" | cut -f2 | sed 's/ - episode.*//') ep_list=$(episodes_list "$id") ep_no=$(printf "%s" "$anime_list" | grep "$id" | cut -f2 | sed -nE 's/.*- episode (.+)$/\1/p') allanime_title="$(printf "%s" "$title" | cut -d'(' -f1 | tr -d '[:punct:]')" ;; *) if [ "$use_external_menu" = "0" ]; then while [ -z "$query" ]; do printf "\33[2K\r\033[1;36mSearch anime: \033[0m" && read -r query done else [ -z "$query" ] && query=$(printf "" | external_menu "" "Search anime: " "$external_menu_args") [ -z "$query" ] && exit 1 fi # for checking new releases by specifying anime name [ "$search" = "nextep" ] && time_until_next_ep "$query" query=$(printf "%s" "$query" | sed "s| |+|g") 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" ] && exit 1 title=$(printf "%s" "$result" | cut -f2) allanime_title="$(printf "%s" "$title" | cut -d'(' -f1 | tr -d '[:punct:]')" id=$(printf "%s" "$result" | cut -f1) ep_list=$(episodes_list "$id") [ -z "$ep_no" ] && ep_no=$(printf "%s" "$ep_list" | nth "Select episode: " "$multi_selection_flag") [ -z "$ep_no" ] && exit 1 ;; esac [ "$skip_intro" = 1 ] && mal_id="$(ani-skip -q "${skip_title:-${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 while cmd=$(printf "next\nreplay\nprevious\nselect\nchange_quality\nquit" | nth "Playing episode $ep_no of $title... "); do case "$cmd" in next) ep_no=$(printf "%s" "$ep_list" | sed -n "/^${ep_no}$/{n;p;}") 2>/dev/null ;; replay) episode="$replay" ;; previous) ep_no=$(printf "%s" "$ep_list" | sed -n "/^${ep_no}$/{g;1!p;};h") 2>/dev/null ;; select) ep_no=$(printf "%s" "$ep_list" | nth "Select episode: " "$multi_selection_flag") ;; change_quality) new_quality="$(printf "%s" "$links" | launcher | cut -d\> -f1)" select_quality "$new_quality" ;; *) exit 0 ;; esac [ -z "$ep_no" ] && die "Out of range" play done # ani-cli # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # # Project repository: https://github.com/pystardust/ani-cli