#!/bin/sh
version_number="4.14.5"
allanime_key="43724f7d46135c6cdb2824f00c4ee272a0fff52f89681213140c6c2b80af8d21"
allanime_query_hash="c66a5306b7ab6cf4701e766cb352b25b70198e873c4e917f9388da75db5cdca2"
allanime_epoch="2957"
allanime_buildid="162"
allanime_lane="k7"
# 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 <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() {
printf "[branch:%s] Checking for Update..\n" "$branch"
update="$(curl --fail-with-body -sL -A "$agent" "https://raw.githubusercontent.com/pystardust/ani-cli/$branch/ani-cli")" || die "[branch:$branch] Connection error: $update"
printf '%s' "$update" | grep -q "^version_number" || die "[branch:$branch] Invalid Response."
update="$(printf '%s\n' "$update" | diff -u "$0" -)"
if [ -z "$update" ]; then
printf "[branch:%s] Script is up to date :)\n" "$branch"
else
if printf '%s\n' "$update" | patch "$0" -; then
printf "[branch:%s] Script has been updated\n" "$branch"
else
die "[branch:$branch] Can't update for some reason!"
fi
fi
exit 0
}
# checks if dependencies are present
dep_ch() {
command -v "$1" >/dev/null || die "Program $1 not found. Please install it."
}
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"
}
cleanup() {
[ "$player_function" = "android_mpv" ] && : >"/storage/emulated/0/mpv/mpv.config.mp4"
printf "\033[0m" # clears colors
rm -f "${histfile}.new" # remove temp logfile
}
# SCRAPING
# extract the video links from response of embed urls, extract mp4 links form m3u8 lists
get_links() {
case "$*" in
*mp4upload*)
episode_link=$(curl --max-time 10 -sLk "$*" -A "$agent" -e "$allanime_refr" | sed -nE 's|.*src: "([^"]*)"[[:space:]]*|Mp4Upload >\1|p')
;;
*tools.fast4speed.rsvp*)
episode_link=$(printf "%s\n" "Yt >$*")
;;
*)
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')"
;;
esac
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|>|>${relative_link}|g" | sort -nr
;;
*) [ -n "$episode_link" ] && printf "%s\n" "$episode_link" ;;
esac
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-)
printf '%s' "$provider_id" | grep -qE "^--" && provider_id=$(printf "%s" "$provider_id" | sed 's/../&\
/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/")
}
# 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)
4) provider_init "mp4upload" "/Mp4 :/p" ;; # mp4upload(mp4)(single)
*) true ;;
esac
[ -n "$provider_id" ] && get_links "$provider_id"
}
select_quality() {
# removing urls which have referrer to avoid playing on iSH and vlc (m3u8 streams don't get correct referrer)
printf '%s' "$player_function" | cut -f1 -d" " | grep -qE '(android_vlc|iSH|vlc)' && links=$(printf '%s' "$links" | sed '/m3u8_refr >/d')
printf '%s' "$player_function" | cut -f1 -d" " | grep -qE '(android_vlc|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 for m3u8 and refr flag for yt
case "$result" in
*mp4upload*)
refr_flag="--referrer=https://www.mp4upload.com"
;;
*sharepoint*)
unset refr_flag
;;
*)
refr_flag="--referrer=$allanime_refr"
;;
esac
episode=$(printf "%s" "$result" | cut -d'>' -f2)
}
process_tobeparsed() {
if ! printf '%s' "$1" | grep -q '"tobeparsed"'; then
printf '%s' "$1"
return 0
fi
tmp="$(mktemp)"
printf '%s' "$1" | sed -nE 's|.*"tobeparsed":"([^"]*)".*|\1|p' | 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')"
gcm_len=$((file_size - 13))
if [ "$botan_version" -eq 3 ]; then
dd if="$tmp" bs=1 skip=13 count="$gcm_len" 2>/dev/null | "$botan_exe" cipher --decrypt --cipher='AES-256/GCM' --key="$allanime_key" --nonce="$iv" - 2>/dev/null
else
dd if="$tmp" bs=1 skip=13 count="$gcm_len" 2>/dev/null | "$botan_exe" encryption --decrypt --mode='aes-256-gcm' --key="$allanime_key" --iv="$iv" 2>/dev/null
fi
rm -f "$tmp"
}
get_aa_req() {
ts="$(($(date +%s) / 300 * 300 * 1000))"
payload_iv="$allanime_epoch:$allanime_buildid:$allanime_query_hash:$ts:$allanime_lane"
payload="{\"v\":1,\"ts\":$ts,\"epoch\":$allanime_epoch,\"buildId\":\"$allanime_buildid\",\"qh\":\"$allanime_query_hash\",\"k\":\"$allanime_lane\"}"
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT INT
printf '%s' "$payload_iv" | "$botan_exe" hash --no-fsname | "$botan_exe" hex_dec - | dd bs=1 count=12 of="$tmpdir/iv.bin" 2>/dev/null
iv_hex="$(od -An -t x1 "$tmpdir/iv.bin" | tr -d ' \n')"
if [ "$botan_version" -eq 3 ]; then
printf "%s" "$payload" | $botan_exe cipher --cipher=AES-256/GCM --key="$allanime_key" --nonce="$iv_hex" - >"$tmpdir/gcm_out.bin"
else
printf "%s" "$payload" | $botan_exe encryption --mode=aes-256-gcm --key="$allanime_key" --iv="$iv_hex" >"$tmpdir/gcm_out.bin"
fi
file_size="$(wc -c <"$tmpdir/gcm_out.bin")"
ct_len=$((file_size - 16))
dd if="$tmpdir/gcm_out.bin" of="$tmpdir/ct.bin" bs=1 skip=0 count="$ct_len" 2>/dev/null
dd if="$tmpdir/gcm_out.bin" of="$tmpdir/tag.bin" bs=1 skip="$ct_len" count=16 2>/dev/null
(
printf '\001'
cat "$tmpdir/iv.bin" "$tmpdir/ct.bin" "$tmpdir/tag.bin"
) | base64 -w 0
}
# gets embed urls, collects direct links into provider files, selects one with desired quality into $episode
get_episode_url() {
query_vars="{\"showId\":\"$id\",\"translationType\":\"$mode\",\"episodeString\":\"$ep_no\"}"
query_ext="{\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"$allanime_query_hash\"}, \"k\":\"$allanime_lane\", \"aaReq\":\"$(get_aa_req)\"}"
api_resp="$(curl -e "$allanime_refr" -sG -A "$agent" -H "Origin: $allanime_refr" -H "x-build-id: $allanime_buildid" "${allanime_api}/api" --data-urlencode "variables=${query_vars}" --data-urlencode "extensions=${query_ext}")"
resp="$(process_tobeparsed "$api_resp" | tr '{}' '\n' | sed 's|\\u002F|\/|g;s|\\||g' | sed -nE 's|.*sourceUrl":"([^"]*)".*sourceName":"([^"]*)".*|\2 :\1|p')"
# generate links into sequential files
cache_dir="$(mktemp -d)"
providers="1 2 3 4"
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 airedStart __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 -e 's#\"airedStart\":{}#\"airedStart\":{"year":0,}#' | sed -nrE "s|.*_id\":\"([^\"]*)\",\"name\":\"([^\"]*)\",.*${mode}\":([1-9][^,]*).*(year\":([0-9][^,]*)).*|\1 \2 (\3 episodes) (\5)|p" | sed -e 's#(0)##' | 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"
[ -n "$ep_no" ] || printf "%s\t%s - episode %s (up to date)\n" "$id" "$title" "$latest_ep"
}
update_history() {
if grep -q -- "$id" "$histfile"; then
# escape sed special chars in replacement: & and \ and delimiter |
safe_title=$(printf '%s' "$title" | sed 's/[&\|]/\\&/g')
sed -E "s|^[^ ]+ ${id} [^ ]+$|${ep_no} ${id} ${safe_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() {
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
;;
*)
# shellcheck disable=SC2086
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
;;
esac
}
android_mpv() {
dir="/storage/emulated/0/mpv"
file="$dir/mpv.config.mp4"
if [ -w "${dir%*/mpv}" ]; then
[ -d "${dir}" ] || mkdir "${dir}"
printf "%s" "$3" | sed 's| --|\n|g; s|^--||g' >"$file"
fi
am start --user 0 -a android.intent.action.VIEW -d "$1" -n is.xyz.mpv/.MPVActivity -e "title" "$2" >/dev/null 2>&1
unset dir file
}
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"
;;
android_mpv) "$player_function" "$episode" "${allanime_title}Episode ${ep_no}" "--tls-verify=no $refr_flag" 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 & ;;
"$HOME"/.local/share/flatpak/app/io.mpv/Mpv/) flatpak run io.mpv.Mpv --tls-verify=no --force-media-title="${allanime_title}Episode ${ep_no}" "$episode" $refr_flag >/dev/null 2>&1 & ;;
*mpv*)
if [ "$no_detach" = 0 ]; then
nohup $player_function $skip_flag --tls-verify=no --force-media-title="${allanime_title}Episode ${ep_no}" "$episode" $refr_flag >/dev/null 2>&1 &
else
$player_function $skip_flag $refr_flag --tls-verify=no --force-media-title="${allanime_title}Episode ${ep_no}" "$episode"
mpv_exitcode=$?
[ "$exit_after_play" = 1 ] && [ -z "$range" ] && exit "$mpv_exitcode"
fi
;;
*iina*)
[ -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-tls-verify=no --mpv-force-media-title="${allanime_title}Episode ${ep_no}" $refr_flag "$episode" >/dev/null 2>&1 &
else
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 &
fi
;;
*vlc*) nohup $player_function --http-referrer="${refr_flag#--referrer=}" --play-and-exit --meta-title="${allanime_title}Episode ${ep_no}" "$episode" >/dev/null 2>&1 & ;;
*yncpla*) nohup $player_function "$episode" -- --tls-verify=no --force-media-title="${allanime_title}Episode ${ep_no}" $refr_flag >/dev/null 2>&1 & ;;
download) "$player_function" "$episode" "${allanime_title}Episode ${ep_no}" ;;
catt) nohup catt cast "$episode" >/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:150.0) Gecko/20100101 Firefox/150.0"
allanime_refr="https://mkissa.to"
allanime_base="allanime.day"
allanime_api="https://api.mkissa.net"
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:-$(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
#TODO out of band mpv and vlc
*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:-$(dep_ch_failover "mpv,$HOME/.local/share/flatpak/app/io.mpv/Mpv/,vlc")}" || die 'No player found. Looked for mpv and vlc' ;; # 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}"
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 | --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 && dep_ch rofi ;;
--dmenu) use_external_menu=2 && dep_ch dmenu ;;
--skip) skip_intro=1 ;;
--skip-title)
[ $# -lt 2 ] && die "missing argument!"
skip_title="$2"
shift
;;
-N | --nextep-countdown) search=nextep ;;
-U | --update)
branch="${2:-$branch}"
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"
dep_ch "sed"
dep_ch "grep"
botan_exe=$(dep_ch_failover "botan3,botan,botan-cli") || die 'Program "botan" not found.'
botan_version=$("$botan_exe" --version | cut -c1)
[ "$skip_intro" = 1 ] && (dep_ch "ani-skip")
external_helper=$(dep_ch_failover "fzf,rofi,dmenu") || die 'No menu found. Install fzf, rofi or dmenu'
case "$external_helper" in
fzf) true ;;
rofi) [ "$use_external_menu" = "0" ] && use_external_menu=1 ;;
dmenu) use_external_menu=2 ;;
*) die 'No menu found. Install fzf, rofi or dmenu' ;;
esac
case "$player_function" in
debug) ;;
download)
dep_ch_failover "yt-dlp,ffmpeg" >/dev/null || die 'Neither yt-dlp nor ffmpeg found'
dep_ch "aria2c"
;;
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" ;;
*iSH*) printf "\33[2K\rChecking of players on iOS is disabled\n" ;;
*) dep_ch "$player_function" ;;
esac
# idk where to put this, so I put it here...
trap 'cleanup; exit 1' INT HUP
# 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:]')"
printf "%s" "$anime_list" | grep "$id" | cut -f2 | grep -q "(up to date)" && time_until_next_ep "$allanime_title"
;;
*)
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"
;;
*)
cleanup
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
Just copy this and save to ani-cli or whatever you like