From 685cb77d422f8e1d6ddd8263dd40e4224d3629c6 Mon Sep 17 00:00:00 2001 From: Dhairya3391 Date: Sat, 5 Sep 2026 19:52:05 +0530 Subject: [PATCH 1/3] fix: replace anidb with hianime provider and bump version to 5.1.0 Co-authored-by: Codex --- ani-cli | 171 ++++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 117 insertions(+), 54 deletions(-) diff --git a/ani-cli b/ani-cli index 57f03100b..a3bba8560 100755 --- a/ani-cli +++ b/ani-cli @@ -1,6 +1,6 @@ #!/bin/sh -version_number="5.0.4" +version_number="5.1.0" # UI @@ -149,7 +149,7 @@ cleanup() { # SCRAPING #shellcheck disable=SC2086 -anidb_curl() { +hianime_curl() { _response="$($curl_exe -sL -A "$agent" --max-time 10 $cipher_flag -w ' %{http_code}' "$@")" || die "Connection error: could not fetch $1 (curl exit $?)" _http_code=${_response##* } @@ -161,48 +161,110 @@ anidb_curl() { printf "%s" "$_response" } +# Decode one base64 value. GNU, BSD and OpenSSL use different switches. +b64_decode() { + printf "%s\n" "$1" | base64 -d 2>/dev/null || printf "%s\n" "$1" | base64 -D 2>/dev/null || printf "%s\n" "$1" | openssl base64 -d -A 2>/dev/null +} + +# Deobfuscate the XOR blob with key otaku-embed-v1. +# The subshell keeps the temporary positional parameters out of the caller. +deobfuscate_blob() ( + _blob=$1 + set -- 111 116 97 107 117 45 101 109 98 101 100 45 118 49 + _bytes="$(b64_decode "$_blob" | od -v -An -tu1)" + _output="" + for _byte in $_bytes; do + _key=$1 + shift + set -- "$@" "$_key" + _char=$((_byte ^ _key)) + _output="${_output}\\$((_char / 64))$(((_char / 8) % 8))$((_char % 8))" + done + printf "%b" "$_output" | sed -n 's|.*"src":"\([^"]*\.m3u8[^"]*\)".*|\1|p' +) + # search the query and give results. format is (id \t name) -anidb_search() { +hianime_search() { #shellcheck disable=SC2059 - _page="$(anidb_curl "$(printf "$search_api" "$1")")" || return 1 - _page="$(printf "%s" "$_page" | tr '\n' ' ' | sed 's||\n
|g' | + sed -n 's|.*

[[:space:]]*|g" } # extract mal_id and seasons metadata -anidb_desc() { +hianime_desc() { #shellcheck disable=SC2059 - _page="$(anidb_curl "$(printf "$desc_api" "$1")" | tr '\n' ' ' | sed 's|SeasonsDetails]*title="([^"]+)".*|\1\t\2|p' | sed -e "s|'|'|g")" + _page="$(hianime_curl "$(printf "$desc_api" "$1")")" + mal_id="$(printf "%s" "$_page" | sed -n 's|.*myanimelist\.net/anime/\([0-9][0-9]*\).*|\1|p')" + # Related entries are grouped in the season list on HiAnime pages. + seasons="$(printf "%s" "$_page" | tr '\n' ' ' | sed 's|]*title="\([^"]*\)".*|\1\t\2|p' | sed -e "s|'|'|g" -e 's|"|"|g' -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() { +hianime_episodes() { #shellcheck disable=SC2059 - _page="$(anidb_curl "$(printf "$episodes_api" "${1##*-}")")" || return 1 - [ -z "$_page" ] && die "Connection error: no response from $base_api" - printf "%s" "$_page" | sed 's|},{|}\n{|g' | sed -nE 's|.*"id":([0-9]+).*"number":([0-9]+).*|\1\t\2|p' + _page="$(hianime_curl -H "X-Requested-With: XMLHttpRequest" "$(printf "$episodes_api" "${1##*-}")")" || return 1 + printf "%s" "$_page" | sed 's|\\\"|"|g; s|ep-item|\nep-item|g' | + sed -n 's|.*data-number="\([^"]*\)".*data-id="\([0-9][0-9]*\)".*|\2\t\1|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" +hianime_m3u8() { + _ep_id=$(printf "%s" "$episode_maps" | sed -n "s|^\([0-9][0-9]*\)\t${1}$|\1|p") + _lang="sub" + [ "$2" = "dub" ] && _lang="dub" + [ -z "$_ep_id" ] && return 1 + + # 1. Get servers #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' + _servers_json="$(hianime_curl -H "X-Requested-With: XMLHttpRequest" "$(printf "$servers_api" "${_ep_id}")")" + + # 2. Prefer HD-1; otherwise use the first server for the selected language. + _servers="$(printf "%s" "$_servers_json" | sed 's|\\\"|"|g; s|server-item|\nserver-item|g')" + _hash="$(printf "%s\n" "$_servers" | grep "data-type=\"${_lang}\"" | grep "data-server-name=\"HD-1\"" | sed -n 's|.*data-hash="\([^"]*\)".*|\1|p' | head -n 1)" + [ -z "$_hash" ] && _hash="$(printf "%s\n" "$_servers" | grep "data-type=\"${_lang}\"" | sed -n 's|.*data-hash="\([^"]*\)".*|\1|p' | head -n 1)" + + [ -z "$_hash" ] && return 1 + + # 3. Base64 decode embed URL + _embed_url="$(b64_decode "$_hash")" + [ -z "$_embed_url" ] && return 1 + + # Extract mal_id for ani-skip + mal_id="$(printf "%s" "$_embed_url" | sed -n 's|.*/stream/mal/\([0-9][0-9]*\)/.*|\1|p')" + + # 4. Fetch embed HTML + _embed_page="$(hianime_curl -e "${base_api}/" "$_embed_url")" + _blob="$(printf "%s" "$_embed_page" | sed -n 's|.*window\.__P="\([^"]*\)".*|\1|p')" + [ -z "$_blob" ] && return 1 + + # 5. Deobfuscate + _m3u8_master="$(deobfuscate_blob "$_blob")" + [ -z "$_m3u8_master" ] && return 1 + + # 6. Fetch the master playlist and expand quality variants. + _m3u8_content="$(hianime_curl -e "https://zokoanime.video/" "$_m3u8_master")" + if printf "%s" "$_m3u8_content" | grep -q "#EXT-X-STREAM-INF"; then + _base_url="${_m3u8_master%/*}" + _parsed="$(printf "%s\n" "$_m3u8_content" | sed 's|^#EXT-X-STREAM.*x||g; s|,.*|p|g; /^#/d; $!N; s|\n| >|;/EXT-X-I-FRAME/d')" + printf "%s\n" "$_parsed" | while IFS= read -r _line || [ -n "$_line" ]; do + [ -z "$_line" ] && continue + _quality=$(printf "%s" "$_line" | cut -d ">" -f 1 | tr -d " ") + _path=$(printf "%s" "$_line" | cut -d ">" -f 2 | tr -d " ") + case "$_path" in + http*) printf "%s >%s\n" "$_quality" "$_path" ;; + *) printf "%s >%s/%s\n" "$_quality" "$_base_url" "$_path" ;; + esac + done + else + printf "default >%s\n" "$_m3u8_master" + fi } # select the quality according to the setting @@ -219,9 +281,9 @@ select_quality() { # gets embed url, collects direct links and selects one with desired quality into $episode get_video_link() { - links=$(anidb_m3u8 "$ep_no" "$mode" | sort -g -r -s) + links=$(hianime_m3u8 "$ep_no" "$mode" | sort -g -r -s) [ -z "$links" ] && die "No sources found for $mode!" - info "anidb.app links fetched" + info "hianime.at links fetched" select_quality "$quality" # should never trigger but just in case if printf "%s" "$ep_list" | grep -q "^$ep_no$"; then @@ -276,6 +338,7 @@ time_until_next_ep() { backup_old_hist() { backupfile="${histfile}.v4" + : >"${histfile}.new" 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" ;; @@ -286,7 +349,7 @@ backup_old_hist() { } process_hist_entry() { - ep_list=$(anidb_episodes "$anime_id" | cut -f 2) + ep_list=$(hianime_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" } @@ -308,8 +371,8 @@ update_history() { # 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 + command -v "yt-dlp" >/dev/null && yt-dlp --referer "https://zokoanime.video/" "$1" --no-skip-unavailable-fragments --fragment-retries infinite -N 16 -o "$download_dir/$2.mp4" $3 && return 0 + ffmpeg -extension_picky 0 -referer "https://zokoanime.video/" -loglevel error -stats -i "$1" -c copy "$download_dir/$2.mp4" $3 } play_episode() { @@ -323,12 +386,12 @@ play_episode() { 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 & ;; + *flatpak*mpv*) flatpak run io.mpv.Mpv --http-header-fields="Referer: https://zokoanime.video/" $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 & + nohup $player_function --http-header-fields="Referer: https://zokoanime.video/" $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" + $player_function --http-header-fields="Referer: https://zokoanime.video/" $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 @@ -336,14 +399,14 @@ play_episode() { *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 & + nohup $player_function --mpv-http-header-fields="Referer: https://zokoanime.video/" $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 & + nohup $player_function --mpv-http-header-fields="Referer: https://zokoanime.video/" $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 ;; + *vlc*) nohup $player_function --http-referrer="https://zokoanime.video/" $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" -- --http-header-fields="Referer: https://zokoanime.video/" --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" @@ -390,13 +453,11 @@ play() { # 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" - +base_api="https://hianime.at" +search_api="${base_api}/search?keyword=%s" +desc_api="${base_api}/%s" +episodes_api="${base_api}/api/theme/episode/list/%s" +servers_api="${base_api}/api/theme/episode/servers?episodeId=%s" 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" @@ -538,15 +599,17 @@ trap 'cleanup; exit 1' INT HUP TERM 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 + 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.*||') - anidb_desc "$anime_id" || true - episode_maps="$(anidb_episodes "$anime_id")" || exit 1 + hianime_desc "$anime_id" || true + episode_maps="$(hianime_episodes "$anime_id")" || exit 1 ep_list="$(printf "%s" "$episode_maps" | cut -f 2)" ep_no=$(printf "%s" "$anime_list" | grep "^$anime_id " | sed -nE 's/.*- episode (.+)$/\1/p') ;; @@ -564,15 +627,15 @@ case "$source" in query=$(printf "%s" "$query" | sed 's| |+|g') - anime_list=$(anidb_search "$query") || exit 1 + anime_list=$(hianime_search "$query") || exit 1 [ -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")" || exit 1 + hianime_desc "$anime_id" || true + episode_maps="$(hianime_episodes "$anime_id")" || exit 1 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" @@ -600,8 +663,8 @@ while cmd=$(printf "next\nreplay\nprevious\nselect$seasons_option\nchange_qualit [ -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")" || exit 1 + hianime_desc "$anime_id" || true + episode_maps="$(hianime_episodes "$anime_id")" || exit 1 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" From e8941ee81af11fa1727d7441c38cd7cf68efcbe2 Mon Sep 17 00:00:00 2001 From: Dhairya3391 Date: Sun, 6 Sep 2026 13:58:29 +0530 Subject: [PATCH 2/3] fix: wire external .vtt subtitles to mpv/iina/vlc/syncplay - deobfuscate_blob extracts both m3u8 and subtitles .vtt src (proper newline handling for printf/sed quirk) - hianime_m3u8 persists .vtt to ${histfile}.subs, cleans on failures - get_video_link downloads VTT locally with Referer, fallback to remote URL - play_episode passes --sub-file for mpv/flatpak, --mpv-sub-files for iina (alias fix), :sub-file for vlc, --sub-file via syncplay mpv passthrough - cleanup removes temp subs/vtt, download fetches vtt, replay preserves subs Fixes subs not loading where master.m3u8 has no #EXT-X-MEDIA tracks (verified mpv --sid=1 external webvtt) --- ani-cli | 106 ++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 88 insertions(+), 18 deletions(-) diff --git a/ani-cli b/ani-cli index a3bba8560..40ddd2199 100755 --- a/ani-cli +++ b/ani-cli @@ -142,8 +142,10 @@ dep_ch_failover() { } cleanup() { - tput sgr0 # clears colors - rm -f "${histfile}.new" # remove temp logfile + tput sgr0 # clears colors + rm -f "${histfile}.new" # remove temp logfile + rm -f "${histfile}.subs" # remove temp subtitle url + rm -f "${histfile}.vtt" # remove temp subtitle file } # SCRAPING @@ -180,7 +182,8 @@ deobfuscate_blob() ( _char=$((_byte ^ _key)) _output="${_output}\\$((_char / 64))$(((_char / 8) % 8))$((_char % 8))" done - printf "%b" "$_output" | sed -n 's|.*"src":"\([^"]*\.m3u8[^"]*\)".*|\1|p' + printf "%s\n" "$(printf "%b" "$_output" | sed -n 's|.*"src":"\([^"]*\.m3u8[^"]*\)".*|\1|p')" + printf "%s\n" "$(printf "%b" "$_output" | sed -n 's|.*"subtitles":\[[^]]*"src":"\([^"]*\.vtt[^"]*\)".*|\1|p')" ) # search the query and give results. format is (id \t name) @@ -219,7 +222,10 @@ hianime_m3u8() { _ep_id=$(printf "%s" "$episode_maps" | sed -n "s|^\([0-9][0-9]*\)\t${1}$|\1|p") _lang="sub" [ "$2" = "dub" ] && _lang="dub" - [ -z "$_ep_id" ] && return 1 + [ -z "$_ep_id" ] && { + rm -f "${histfile}.subs" + return 1 + } # 1. Get servers #shellcheck disable=SC2059 @@ -230,11 +236,17 @@ hianime_m3u8() { _hash="$(printf "%s\n" "$_servers" | grep "data-type=\"${_lang}\"" | grep "data-server-name=\"HD-1\"" | sed -n 's|.*data-hash="\([^"]*\)".*|\1|p' | head -n 1)" [ -z "$_hash" ] && _hash="$(printf "%s\n" "$_servers" | grep "data-type=\"${_lang}\"" | sed -n 's|.*data-hash="\([^"]*\)".*|\1|p' | head -n 1)" - [ -z "$_hash" ] && return 1 + [ -z "$_hash" ] && { + rm -f "${histfile}.subs" + return 1 + } # 3. Base64 decode embed URL _embed_url="$(b64_decode "$_hash")" - [ -z "$_embed_url" ] && return 1 + [ -z "$_embed_url" ] && { + rm -f "${histfile}.subs" + return 1 + } # Extract mal_id for ani-skip mal_id="$(printf "%s" "$_embed_url" | sed -n 's|.*/stream/mal/\([0-9][0-9]*\)/.*|\1|p')" @@ -242,11 +254,24 @@ hianime_m3u8() { # 4. Fetch embed HTML _embed_page="$(hianime_curl -e "${base_api}/" "$_embed_url")" _blob="$(printf "%s" "$_embed_page" | sed -n 's|.*window\.__P="\([^"]*\)".*|\1|p')" - [ -z "$_blob" ] && return 1 + [ -z "$_blob" ] && { + rm -f "${histfile}.subs" + return 1 + } # 5. Deobfuscate - _m3u8_master="$(deobfuscate_blob "$_blob")" - [ -z "$_m3u8_master" ] && return 1 + _deobfuscated="$(printf "%s\n" "$(deobfuscate_blob "$_blob")" | sed '/^$/d')" + _m3u8_master="$(printf "%s\n" "$_deobfuscated" | sed -n '1p')" + _subs="$(printf "%s\n" "$_deobfuscated" | sed -n '2p')" + [ -z "$_m3u8_master" ] && { + rm -f "${histfile}.subs" + return 1 + } + if [ -n "$_subs" ]; then + printf "%s\n" "$_subs" >"${histfile}.subs" + else + rm -f "${histfile}.subs" + fi # 6. Fetch the master playlist and expand quality variants. _m3u8_content="$(hianime_curl -e "https://zokoanime.video/" "$_m3u8_master")" @@ -284,6 +309,19 @@ get_video_link() { links=$(hianime_m3u8 "$ep_no" "$mode" | sort -g -r -s) [ -z "$links" ] && die "No sources found for $mode!" info "hianime.at links fetched" + sub_link="$(cat "${histfile}.subs" 2>/dev/null)" + if [ -n "$sub_link" ]; then + # Download VTT locally for players that can use file paths; fallback to remote URL if download fails + if $curl_exe -sL -A "$agent" --max-time 10 $cipher_flag -e "https://zokoanime.video/" "$sub_link" -o "${histfile}.vtt" 2>/dev/null && [ -s "${histfile}.vtt" ]; then + sub_file="${histfile}.vtt" + else + rm -f "${histfile}.vtt" + sub_file="" + fi + else + rm -f "${histfile}.vtt" + sub_file="" + fi select_quality "$quality" # should never trigger but just in case if printf "%s" "$ep_list" | grep -q "^$ep_no$"; then @@ -371,6 +409,7 @@ update_history() { # shellcheck disable=SC2086 # $1 = video link, $2 = video name, $3 = extra flags download() { + [ -n "$sub_link" ] && $curl_exe -sL -A "$agent" --max-time 10 $cipher_flag -e "https://zokoanime.video/" "$sub_link" -o "$download_dir/$2.vtt" 2>/dev/null & command -v "yt-dlp" >/dev/null && yt-dlp --referer "https://zokoanime.video/" "$1" --no-skip-unavailable-fragments --fragment-retries infinite -N 16 -o "$download_dir/$2.mp4" $3 && return 0 ffmpeg -extension_picky 0 -referer "https://zokoanime.video/" -loglevel error -stats -i "$1" -c copy "$download_dir/$2.mp4" $3 } @@ -379,19 +418,42 @@ play_episode() { [ "$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 + # Resolve subtitle source: prefer local VTT, fallback to remote URL; flatpak can't see host tempfile + _sub_src="" + if [ -n "$sub_file" ] && [ -s "$sub_file" ]; then + _sub_src="$sub_file" + elif [ -n "$sub_link" ]; then + _sub_src="$sub_link" + fi + case "$player_function" in *flatpak*mpv*) [ -n "$sub_link" ] && _sub_src="$sub_link" ;; esac + if [ -n "$_sub_src" ]; then + sub_file_flag="--sub-file=$_sub_src" + # VLC per-input slave must be attached after URL via :sub-file (colon syntax) — global --sub-file is not reliably picked up for HLS + sub_vlc_flag=":sub-file=$_sub_src" + sub_sync_flag="--sub-file=$_sub_src" + else + sub_file_flag=""; sub_vlc_flag=""; sub_sync_flag="" + fi + # IINA: --sub-file is CLI alias (maps to --sub-files-append) not recognized by iina-cli; + # must use --mpv-sub-files (plural). Remote URLs not fetched by mpv via IINA, so only local file works. + if [ -n "$sub_file" ] && [ -s "$sub_file" ]; then + sub_iina_flag="--mpv-sub-files=$sub_file" + else + sub_iina_flag="" + fi # 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" ;; + debug) printf "All links:\n%s\nSelected link:\n%s\nSubtitles:\n%s\n" "$links" "$video_link" "$sub_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 --http-header-fields="Referer: https://zokoanime.video/" $skip_flag $player_extra_flags --force-media-title="${anime_title} Episode ${ep_no}" "$video_link" >/dev/null 2>&1 & ;; + *flatpak*mpv*) flatpak run io.mpv.Mpv --http-header-fields="Referer: https://zokoanime.video/" $sub_file_flag $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 --http-header-fields="Referer: https://zokoanime.video/" $skip_flag $player_extra_flags --force-media-title="${anime_title} Episode ${ep_no}" "$video_link" >/dev/null 2>&1 & + nohup $player_function --http-header-fields="Referer: https://zokoanime.video/" $sub_file_flag $skip_flag $player_extra_flags --force-media-title="${anime_title} Episode ${ep_no}" "$video_link" >/dev/null 2>&1 & else - $player_function --http-header-fields="Referer: https://zokoanime.video/" $skip_flag $player_extra_flags --force-media-title="${anime_title} Episode ${ep_no}" "$video_link" + $player_function --http-header-fields="Referer: https://zokoanime.video/" $sub_file_flag $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 @@ -399,13 +461,13 @@ play_episode() { *iina*) if pgrep -f "IINA" >/dev/null 2>&1; then # omit --keep-running when an IINA instance exists to prevent hanging - nohup $player_function --mpv-http-header-fields="Referer: https://zokoanime.video/" $player_extra_flags --no-stdin --mpv-force-media-title="${anime_title} Episode ${ep_no}" "$video_link" >/dev/null 2>&1 & + nohup $player_function --mpv-http-header-fields="Referer: https://zokoanime.video/" $sub_iina_flag $player_extra_flags --no-stdin --mpv-force-media-title="${anime_title} Episode ${ep_no}" "$video_link" >/dev/null 2>&1 & else - nohup $player_function --mpv-http-header-fields="Referer: https://zokoanime.video/" $player_extra_flags --no-stdin --keep-running --mpv-force-media-title="${anime_title} Episode ${ep_no}" "$video_link" >/dev/null 2>&1 & + nohup $player_function --mpv-http-header-fields="Referer: https://zokoanime.video/" $sub_iina_flag $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 --http-referrer="https://zokoanime.video/" $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" -- --http-header-fields="Referer: https://zokoanime.video/" --force-media-title="${anime_title} Episode ${ep_no}" >/dev/null 2>&1 & ;; + *vlc*) nohup $player_function --http-referrer="https://zokoanime.video/" $player_extra_flags --play-and-exit --meta-title="${anime_title} Episode ${ep_no}" "$video_link" $sub_vlc_flag >/dev/null 2>&1 & ;; + *yncpla*) nohup $player_function $player_extra_flags "$video_link" -- --http-header-fields="Referer: https://zokoanime.video/" $sub_sync_flag --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) @@ -415,7 +477,11 @@ play_episode() { *) nohup $player_function $player_extra_flags "$video_link" >/dev/null 2>&1 & ;; esac replay="$video_link" + replay_sub="$sub_link" + replay_sub_file="$sub_file" unset video_link + unset sub_link + unset sub_file update_history [ "$menu_program" != "fzf" ] && wait } @@ -655,7 +721,11 @@ play while cmd=$(printf "next\nreplay\nprevious\nselect$seasons_option\nchange_quality\nquit" | nth "Playing episode $ep_no of $anime_title... "); do case "$cmd" in next) ep_no=$(printf "%s" "$ep_list" | sed -n "/^${ep_no}$/{n;p;}") 2>/dev/null ;; - replay) video_link="$replay" ;; + replay) + video_link="$replay" + sub_link="$replay_sub" + sub_file="$replay_sub_file" + ;; 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: " "$menu_multi_flag") ;; change_season) From 159c5c785079d92f1d6afdc8a6736b562b46b68e Mon Sep 17 00:00:00 2001 From: Dhairya3391 Date: Mon, 7 Sep 2026 22:36:02 +0530 Subject: [PATCH 3/3] fix: resolve hianime server changes and player issues - Dynamically select ZokoAnime server, adapting to HiAnime's server rename - Filter out Top 10 sidebar items from search results so invalid searches show no results - Support subtitle language preference (ANI_CLI_SUB_LANG, default en) and avoid greedy regex - Stream subtitles directly to players without temporary files - Escape subtitle URLs for IINA and use :input-slave for VLC - Forward skip_flag to syncplay and ensure mal_id is set before ani-skip runs - Prevent history collisions between v5.0 and HiAnime by checking full slug - Avoid global variable races in concurrent history processing - Use fixed-string matching for episode numbers to handle decimals like 12.5 - Optimize string parsing with POSIX parameter expansion and remove hianime_desc - Update documentation and man page references to hianime --- README.md | 6 +- ani-cli | 271 +++++++++++++++++++------------------------------- ani-cli.1 | 8 +- disclaimer.md | 2 +- hacking.md | 2 +- 5 files changed, 112 insertions(+), 177 deletions(-) diff --git a/README.md b/README.md index e0304dbde..88be4f26d 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@

-A cli to browse and watch anime (alone AND with friends). This tool scrapes the site anidb. +A cli to browse and watch anime (alone AND with friends). This tool scrapes the site hianime.

@@ -534,12 +534,12 @@ Ani-skip uses the external lua script function of mpv and as such – for now ## FAQ
-* Can I change subtitle language or turn them off? - No, the subtitles are baked into the video. +* Can I change subtitle language or turn them off? - Subtitles are a separate english track handed to the player, so you can toggle them there (mpv: press `v`). Other languages are not offered. * Can I watch dub? - Yes, use `--dub`. * Can I change dub language? - No. * Can I change media source? - No (unless you can scrape that source yourself). * Can I use vlc? - Yes, use `--vlc` or `export ANI_CLI_PLAYER=vlc`. -* Can I adjust resolution? - Yes, use `-q resolution`, for example `ani-cli -q 1080`. +* Can I adjust resolution? - Yes, use `-q resolution`, for example `ani-cli -q 1080`. The provider currently serves a single 1080p stream, other values fall back to best. * How can I download? - Use `-d`, it will download into your working directory. * Can i change download folder? - Yes, set the `ANI_CLI_DOWNLOAD_DIR` to your desired location. * How can I bulk download? - `Use -d -e firstepisode-lastepisode`, for example `ani-cli onepiece -d -e 1-1000`. diff --git a/ani-cli b/ani-cli index 40ddd2199..06f41d847 100755 --- a/ani-cli +++ b/ani-cli @@ -142,23 +142,26 @@ dep_ch_failover() { } cleanup() { - tput sgr0 # clears colors - rm -f "${histfile}.new" # remove temp logfile - rm -f "${histfile}.subs" # remove temp subtitle url - rm -f "${histfile}.vtt" # remove temp subtitle file + tput sgr0 # clears colors + rm -f "${histfile}.new" # remove temp logfile } # SCRAPING #shellcheck disable=SC2086 hianime_curl() { - _response="$($curl_exe -sL -A "$agent" --max-time 10 $cipher_flag -w ' %{http_code}' "$@")" || - die "Connection error: could not fetch $1 (curl exit $?)" + _url="" + for _arg; do + case "$_arg" in http*) _url="$_arg" ;; *) ;; esac + done + [ -z "$_url" ] && _url="$1" + _response="$($curl_exe -sL -A "$agent" --max-time 15 $cipher_flag -w ' %{http_code}' "$@")" || + die "Connection error: could not fetch $_url (curl exit $?)" _http_code=${_response##* } _response=${_response% *} case "$_http_code" in 2??) ;; - *) printf "%s" "$_response" | grep -qi "Just a moment" || die "Request failed: HTTP $_http_code from $1" ;; + *) printf "%s" "$_response" | grep -qi "Just a moment" || die "Request failed: HTTP $_http_code from $_url" ;; esac printf "%s" "$_response" } @@ -168,128 +171,98 @@ b64_decode() { printf "%s\n" "$1" | base64 -d 2>/dev/null || printf "%s\n" "$1" | base64 -D 2>/dev/null || printf "%s\n" "$1" | openssl base64 -d -A 2>/dev/null } -# Deobfuscate the XOR blob with key otaku-embed-v1. -# The subshell keeps the temporary positional parameters out of the caller. +# The embed page ships its config as base64(json XOR "otaku-embed-v1"), this prints the json. +# Runs in a subshell so the key bytes can live in the positional parameters and rotate with shift. deobfuscate_blob() ( - _blob=$1 + _bytes="$(b64_decode "$1" | od -v -An -tu1)" set -- 111 116 97 107 117 45 101 109 98 101 100 45 118 49 - _bytes="$(b64_decode "$_blob" | od -v -An -tu1)" _output="" for _byte in $_bytes; do - _key=$1 + _char=$((_byte ^ $1)) + _output="${_output}\\0$((_char / 64))$(((_char / 8) % 8))$((_char % 8))" + set -- "$@" "$1" shift - set -- "$@" "$_key" - _char=$((_byte ^ _key)) - _output="${_output}\\$((_char / 64))$(((_char / 8) % 8))$((_char % 8))" done - printf "%s\n" "$(printf "%b" "$_output" | sed -n 's|.*"src":"\([^"]*\.m3u8[^"]*\)".*|\1|p')" - printf "%s\n" "$(printf "%b" "$_output" | sed -n 's|.*"subtitles":\[[^]]*"src":"\([^"]*\.vtt[^"]*\)".*|\1|p')" + printf "%b" "$_output" ) # search the query and give results. format is (id \t name) hianime_search() { #shellcheck disable=SC2059 _page="$(hianime_curl "$(printf "$search_api" "$1")")" || return 1 - if printf "%s" "$_page" | grep -qi "Just a moment"; then + 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" | tr '\n' ' ' | sed 's|<div class="film-detail">|\n<div class="film-detail">|g' | - sed -n 's|.*<h3 class="film-name">[[:space:]]*<a href="https://hianime\.at/\([^"]*\)"[[:space:]]*title="\([^"]*\)".*|\1\t\2|p' | + # the top 10 sidebar repeats the result markup, cut it off before flattening the page + printf "%s" "$_page" | sed '/id="main-sidebar"/,$d' | tr '\n' ' ' | sed 's|<div class="film-detail">|\n<div class="film-detail">|g' | + sed -nE 's|.*<h3 class="film-name">[[:space:]]*<a href="[^"]*/([^"/]*)"[[:space:]]*title="([^"]*)".*|\1\t\2|p' | sed -e "s|'|'|g" -e 's|"|"|g' -e "s|&|\&|g" -e "s|'|'|g" -e "s|<|<|g" -e "s|>|>|g" } -# extract mal_id and seasons metadata -hianime_desc() { - #shellcheck disable=SC2059 - _page="$(hianime_curl "$(printf "$desc_api" "$1")")" - mal_id="$(printf "%s" "$_page" | sed -n 's|.*myanimelist\.net/anime/\([0-9][0-9]*\).*|\1|p')" - # Related entries are grouped in the season list on HiAnime pages. - seasons="$(printf "%s" "$_page" | tr '\n' ' ' | sed 's|<a href|\n<a href|g' | sed -n 's|.*href="https://hianime\.at/\([^"?][^"]*\)"[^>]*title="\([^"]*\)".*|\1\t\2|p' | sed -e "s|'|'|g" -e 's|"|"|g' -e 's|&|\&|g')" - [ -n "$seasons" ] && seasons_option="\nchange_season" -} - # get the episodes list of the selected anime. format is (id \t ep_no) hianime_episodes() { #shellcheck disable=SC2059 - _page="$(hianime_curl -H "X-Requested-With: XMLHttpRequest" "$(printf "$episodes_api" "${1##*-}")")" || return 1 - printf "%s" "$_page" | sed 's|\\\"|"|g; s|ep-item|\nep-item|g' | - sed -n 's|.*data-number="\([^"]*\)".*data-id="\([0-9][0-9]*\)".*|\2\t\1|p' + _page="$(hianime_curl "$(printf "$episodes_api" "${1##*-}")")" || return 1 + # ids from the v5.0 provider have the same shape, so check the slug in the episode links too + printf "%s" "$_page" | sed 's|\\||g; s|ep-item|\nep-item|g' | + sed -nE "s|.*data-number=\"([^\"]*)\".*data-id=\"([0-9]+)\".*/watch/${1}[?]ep=.*|\\2\\t\\1|p" } -# extract the individual m3u8 links from response of embed urls +# resolves episode $1 in mode $2 (sub/dub): sets links (quality >url per line), sub_link, mal_id and refr hianime_m3u8() { - _ep_id=$(printf "%s" "$episode_maps" | sed -n "s|^\([0-9][0-9]*\)\t${1}$|\1|p") - _lang="sub" - [ "$2" = "dub" ] && _lang="dub" - [ -z "$_ep_id" ] && { - rm -f "${histfile}.subs" - return 1 - } - - # 1. Get servers + _ep_id=$(printf "%s" "$episode_maps" | sed -nE "s|^([0-9]+)\t${1}$|\1|p") #shellcheck disable=SC2059 - _servers_json="$(hianime_curl -H "X-Requested-With: XMLHttpRequest" "$(printf "$servers_api" "${_ep_id}")")" - - # 2. Prefer HD-1; otherwise use the first server for the selected language. - _servers="$(printf "%s" "$_servers_json" | sed 's|\\\"|"|g; s|server-item|\nserver-item|g')" - _hash="$(printf "%s\n" "$_servers" | grep "data-type=\"${_lang}\"" | grep "data-server-name=\"HD-1\"" | sed -n 's|.*data-hash="\([^"]*\)".*|\1|p' | head -n 1)" - [ -z "$_hash" ] && _hash="$(printf "%s\n" "$_servers" | grep "data-type=\"${_lang}\"" | sed -n 's|.*data-hash="\([^"]*\)".*|\1|p' | head -n 1)" - - [ -z "$_hash" ] && { - rm -f "${histfile}.subs" - return 1 - } - - # 3. Base64 decode embed URL - _embed_url="$(b64_decode "$_hash")" - [ -z "$_embed_url" ] && { - rm -f "${histfile}.subs" - return 1 - } - - # Extract mal_id for ani-skip - mal_id="$(printf "%s" "$_embed_url" | sed -n 's|.*/stream/mal/\([0-9][0-9]*\)/.*|\1|p')" - - # 4. Fetch embed HTML - _embed_page="$(hianime_curl -e "${base_api}/" "$_embed_url")" - _blob="$(printf "%s" "$_embed_page" | sed -n 's|.*window\.__P="\([^"]*\)".*|\1|p')" - [ -z "$_blob" ] && { - rm -f "${histfile}.subs" - return 1 - } - - # 5. Deobfuscate - _deobfuscated="$(printf "%s\n" "$(deobfuscate_blob "$_blob")" | sed '/^$/d')" - _m3u8_master="$(printf "%s\n" "$_deobfuscated" | sed -n '1p')" - _subs="$(printf "%s\n" "$_deobfuscated" | sed -n '2p')" - [ -z "$_m3u8_master" ] && { - rm -f "${histfile}.subs" - return 1 - } - if [ -n "$_subs" ]; then - printf "%s\n" "$_subs" >"${histfile}.subs" + _servers="$(hianime_curl "$(printf "$servers_api" "$_ep_id")" | sed 's|\\"|"|g; s|server-item|\nserver-item|g')" + # prefer ZokoAnime; fallback to any server whose embed contains zokoanime, then HD-1 + _hash="$(printf "%s\n" "$_servers" | sed -nE 's|.*data-type="'"$2"'".*data-server-name="ZokoAnime".*data-hash="([^"]*)".*|\1|p' | head -n 1)" + if [ -z "$_hash" ]; then + for _h in $(printf "%s\n" "$_servers" | sed -nE 's|.*data-type="'"$2"'".*data-hash="([^"]*)".*|\1|p'); do + case "$(b64_decode "$_h")" in + *zokoanime*) + _hash="$_h" + break + ;; + *) ;; + esac + done + fi + [ -z "$_hash" ] && _hash="$(printf "%s\n" "$_servers" | sed -nE 's|.*data-type="'"$2"'".*data-server-name="HD-1".*data-hash="([^"]*)".*|\1|p' | head -n 1)" + [ -z "$_hash" ] && _hash="$(printf "%s\n" "$_servers" | sed -nE 's|.*data-type="'"$2"'".*data-hash="([^"]*)".*|\1|p' | head -n 1)" + _embed="$(b64_decode "$_hash")" + [ -z "$_embed" ] && return 1 + # the stream host wants the embed site as referer, and the embed url carries the mal id for ani-skip + refr="${_embed%%://*}:" + _tmp_host="${_embed#*://}" + refr="${refr}//${_tmp_host%%/*}/" + _tmp_mal="${_embed#*/mal/}" + if [ "$_tmp_mal" != "$_embed" ]; then + mal_id="${_tmp_mal%%/*}" else - rm -f "${histfile}.subs" + mal_id="" fi - - # 6. Fetch the master playlist and expand quality variants. - _m3u8_content="$(hianime_curl -e "https://zokoanime.video/" "$_m3u8_master")" + _blob="$(hianime_curl "$_embed" | sed -nE 's|.*window\.__P="([^"]*)".*|\1|p')" + [ -z "$_blob" ] && return 1 + _json="$(deobfuscate_blob "$_blob")" + _tmp_src="${_json#*\"src\":\"}" + _m3u8_master="${_tmp_src%%\"*}" + case "$_m3u8_master" in + *.m3u8*) ;; + *) _m3u8_master="" ;; + esac + [ -z "$_m3u8_master" ] && return 1 + _sub_entries="$(printf "%s" "$_json" | tr '}' '\n' | sed -n 's|.*"lang":"\([^"]*\)".*"src":"\([^"]*\.vtt[^"]*\)".*|\1\t\2|p')" + _sub_langs="${ANI_CLI_SUB_LANG:-en}" + _sub_pattern="$(printf "%s" "$_sub_langs" | tr ',:' '|')" + sub_link="$(printf "%s\n" "$_sub_entries" | grep -iE "^(${_sub_pattern})[[:space:]]" | head -n 1 | cut -f 2)" + [ -z "$sub_link" ] && sub_link="$(printf "%s\n" "$_sub_entries" | head -n 1 | cut -f 2)" + _m3u8_content="$(hianime_curl "$_m3u8_master" -e "$refr")" if printf "%s" "$_m3u8_content" | grep -q "#EXT-X-STREAM-INF"; then - _base_url="${_m3u8_master%/*}" - _parsed="$(printf "%s\n" "$_m3u8_content" | sed 's|^#EXT-X-STREAM.*x||g; s|,.*|p|g; /^#/d; $!N; s|\n| >|;/EXT-X-I-FRAME/d')" - printf "%s\n" "$_parsed" | while IFS= read -r _line || [ -n "$_line" ]; do - [ -z "$_line" ] && continue - _quality=$(printf "%s" "$_line" | cut -d ">" -f 1 | tr -d " ") - _path=$(printf "%s" "$_line" | cut -d ">" -f 2 | tr -d " ") - case "$_path" in - http*) printf "%s >%s\n" "$_quality" "$_path" ;; - *) printf "%s >%s/%s\n" "$_quality" "$_base_url" "$_path" ;; - esac - done + links="$(printf "%s\n" "$_m3u8_content" | sed 's|^#EXT-X-STREAM.*x||g; s|,.*|p|g; /^#/d; $!N; s|\n| >|;/EXT-X-I-FRAME/d' | sed "\|>https*://|!s|>|>${_m3u8_master%/*}/|" | sort -g -r -s)" else - printf "default >%s\n" "$_m3u8_master" + links="default >$_m3u8_master" fi + [ -n "$links" ] } # select the quality according to the setting @@ -301,30 +274,16 @@ select_quality() { 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) + video_link="${_result#*>}" } # gets embed url, collects direct links and selects one with desired quality into $episode get_video_link() { - links=$(hianime_m3u8 "$ep_no" "$mode" | sort -g -r -s) - [ -z "$links" ] && die "No sources found for $mode!" + hianime_m3u8 "$ep_no" "$mode" || die "No sources found for $mode!" info "hianime.at links fetched" - sub_link="$(cat "${histfile}.subs" 2>/dev/null)" - if [ -n "$sub_link" ]; then - # Download VTT locally for players that can use file paths; fallback to remote URL if download fails - if $curl_exe -sL -A "$agent" --max-time 10 $cipher_flag -e "https://zokoanime.video/" "$sub_link" -o "${histfile}.vtt" 2>/dev/null && [ -s "${histfile}.vtt" ]; then - sub_file="${histfile}.vtt" - else - rm -f "${histfile}.vtt" - sub_file="" - fi - else - rm -f "${histfile}.vtt" - sub_file="" - fi select_quality "$quality" # should never trigger but just in case - if printf "%s" "$ep_list" | grep -q "^$ep_no$"; then + if printf "%s\n" "$ep_list" | grep -Fqx "$ep_no"; then [ -z "$video_link" ] && die "Episode is released, but no valid sources!" else [ -z "$video_link" ] && die "Episode not released!" @@ -387,9 +346,12 @@ backup_old_hist() { } process_hist_entry() { - ep_list=$(hianime_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" + _entry_ep="$1" + _entry_id="$2" + _entry_title="$3" + _entry_list=$(hianime_episodes "$_entry_id" | cut -f 2) + _next_ep=$(printf "%s" "$_entry_list" | sed -n "/^${_entry_ep}$/{n;p;}") 2>/dev/null + [ -n "$_next_ep" ] && printf "%s\t%s - episode %s\n" "$_entry_id" "$_entry_title" "$_next_ep" } update_history() { @@ -409,38 +371,15 @@ update_history() { # shellcheck disable=SC2086 # $1 = video link, $2 = video name, $3 = extra flags download() { - [ -n "$sub_link" ] && $curl_exe -sL -A "$agent" --max-time 10 $cipher_flag -e "https://zokoanime.video/" "$sub_link" -o "$download_dir/$2.vtt" 2>/dev/null & - command -v "yt-dlp" >/dev/null && yt-dlp --referer "https://zokoanime.video/" "$1" --no-skip-unavailable-fragments --fragment-retries infinite -N 16 -o "$download_dir/$2.mp4" $3 && return 0 - ffmpeg -extension_picky 0 -referer "https://zokoanime.video/" -loglevel error -stats -i "$1" -c copy "$download_dir/$2.mp4" $3 + [ -n "$sub_link" ] && $curl_exe -sL -A "$agent" --max-time 10 $cipher_flag -e "$refr" "$sub_link" -o "$download_dir/$2.vtt" 2>/dev/null & + command -v "yt-dlp" >/dev/null && yt-dlp --referer "$refr" "$1" --no-skip-unavailable-fragments --fragment-retries infinite -N 16 -o "$download_dir/$2.mp4" $3 && return 0 + ffmpeg -extension_picky 0 -referer "$refr" -loglevel error -stats -i "$1" -c copy "$download_dir/$2.mp4" $3 } play_episode() { [ "$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 - # Resolve subtitle source: prefer local VTT, fallback to remote URL; flatpak can't see host tempfile - _sub_src="" - if [ -n "$sub_file" ] && [ -s "$sub_file" ]; then - _sub_src="$sub_file" - elif [ -n "$sub_link" ]; then - _sub_src="$sub_link" - fi - case "$player_function" in *flatpak*mpv*) [ -n "$sub_link" ] && _sub_src="$sub_link" ;; esac - if [ -n "$_sub_src" ]; then - sub_file_flag="--sub-file=$_sub_src" - # VLC per-input slave must be attached after URL via :sub-file (colon syntax) — global --sub-file is not reliably picked up for HLS - sub_vlc_flag=":sub-file=$_sub_src" - sub_sync_flag="--sub-file=$_sub_src" - else - sub_file_flag=""; sub_vlc_flag=""; sub_sync_flag="" - fi - # IINA: --sub-file is CLI alias (maps to --sub-files-append) not recognized by iina-cli; - # must use --mpv-sub-files (plural). Remote URLs not fetched by mpv via IINA, so only local file works. - if [ -n "$sub_file" ] && [ -s "$sub_file" ]; then - sub_iina_flag="--mpv-sub-files=$sub_file" - else - sub_iina_flag="" - fi + [ "$skip_intro" = 1 ] && skip_flag="$(ani-skip -i "$mal_id" -e "$canon_ep_no")" # wait for the previous player to close (in range selection) wait # shellcheck disable=SC2086 @@ -448,27 +387,29 @@ play_episode() { debug) printf "All links:\n%s\nSelected link:\n%s\nSubtitles:\n%s\n" "$links" "$video_link" "$sub_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 --http-header-fields="Referer: https://zokoanime.video/" $sub_file_flag $skip_flag $player_extra_flags --force-media-title="${anime_title} Episode ${ep_no}" "$video_link" >/dev/null 2>&1 & ;; + *flatpak*mpv*) flatpak run io.mpv.Mpv --referrer="$refr" ${sub_link:+--sub-file="$sub_link"} $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 --http-header-fields="Referer: https://zokoanime.video/" $sub_file_flag $skip_flag $player_extra_flags --force-media-title="${anime_title} Episode ${ep_no}" "$video_link" >/dev/null 2>&1 & + nohup $player_function --referrer="$refr" ${sub_link:+--sub-file="$sub_link"} $skip_flag $player_extra_flags --force-media-title="${anime_title} Episode ${ep_no}" "$video_link" >/dev/null 2>&1 & else - $player_function --http-header-fields="Referer: https://zokoanime.video/" $sub_file_flag $skip_flag $player_extra_flags --force-media-title="${anime_title} Episode ${ep_no}" "$video_link" + $player_function --referrer="$refr" ${sub_link:+--sub-file="$sub_link"} $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*) + # mpv splits sub-files on ':', so the url has to be escaped for iina + _sub_iina="$(printf "%s" "$sub_link" | sed 's|:|\\:|g')" if pgrep -f "IINA" >/dev/null 2>&1; then # omit --keep-running when an IINA instance exists to prevent hanging - nohup $player_function --mpv-http-header-fields="Referer: https://zokoanime.video/" $sub_iina_flag $player_extra_flags --no-stdin --mpv-force-media-title="${anime_title} Episode ${ep_no}" "$video_link" >/dev/null 2>&1 & + nohup $player_function --mpv-referrer="$refr" ${_sub_iina:+--mpv-sub-files="$_sub_iina"} $player_extra_flags --no-stdin --mpv-force-media-title="${anime_title} Episode ${ep_no}" "$video_link" >/dev/null 2>&1 & else - nohup $player_function --mpv-http-header-fields="Referer: https://zokoanime.video/" $sub_iina_flag $player_extra_flags --no-stdin --keep-running --mpv-force-media-title="${anime_title} Episode ${ep_no}" "$video_link" >/dev/null 2>&1 & + nohup $player_function --mpv-referrer="$refr" ${_sub_iina:+--mpv-sub-files="$_sub_iina"} $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 --http-referrer="https://zokoanime.video/" $player_extra_flags --play-and-exit --meta-title="${anime_title} Episode ${ep_no}" "$video_link" $sub_vlc_flag >/dev/null 2>&1 & ;; - *yncpla*) nohup $player_function $player_extra_flags "$video_link" -- --http-header-fields="Referer: https://zokoanime.video/" $sub_sync_flag --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" ;; + *vlc*) nohup $player_function --http-referrer="$refr" $player_extra_flags --play-and-exit --meta-title="${anime_title} Episode ${ep_no}" "$video_link" ${sub_link:+:input-slave="$sub_link"} >/dev/null 2>&1 & ;; + *yncpla*) nohup $player_function $player_extra_flags "$video_link" -- --referrer="$refr" ${sub_link:+--sub-file="$sub_link"} $skip_flag --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" @@ -478,10 +419,8 @@ play_episode() { esac replay="$video_link" replay_sub="$sub_link" - replay_sub_file="$sub_file" unset video_link unset sub_link - unset sub_file update_history [ "$menu_program" != "fzf" ] && wait } @@ -502,14 +441,14 @@ play() { for i in $_range; do tput clear ep_no=$i - canon_ep_no="$(printf "%s" "$ep_list" | sed -n "/^${ep_no}$/=")" + canon_ep_no="$(printf "%s\n" "$ep_list" | grep -Fnx "$ep_no" | cut -d: -f1)" info "Playing episode $ep_no..." [ "$i" = "$_end" ] && unset _range play_episode done else - printf "%s" "$ep_list" | grep -q "^$ep_no$" || die "Invalid episode!" - canon_ep_no="$(printf "%s" "$ep_list" | sed -n "/^${ep_no}$/=")" + printf "%s\n" "$ep_list" | grep -Fqx "$ep_no" || die "Invalid episode!" + canon_ep_no="$(printf "%s\n" "$ep_list" | grep -Fnx "$ep_no" | cut -d: -f1)" play_episode fi # moves up to stored position and deletes to end @@ -521,7 +460,6 @@ play() { # setup base_api="https://hianime.at" search_api="${base_api}/search?keyword=%s" -desc_api="${base_api}/%s" episodes_api="${base_api}/api/theme/episode/list/%s" servers_api="${base_api}/api/theme/episode/servers?episodeId=%s" 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' @@ -666,7 +604,7 @@ case "$source" in history) backup_old_hist anime_list=$( - while read -r ep_no anime_id anime_title; do process_hist_entry & done <"$histfile" + while read -r ep_no anime_id anime_title; do process_hist_entry "$ep_no" "$anime_id" "$anime_title" & done <"$histfile" wait ) [ -z "$anime_list" ] && die "No unwatched series in history!" @@ -674,7 +612,6 @@ case "$source" in [ -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.*||') - hianime_desc "$anime_id" || true episode_maps="$(hianime_episodes "$anime_id")" || exit 1 ep_list="$(printf "%s" "$episode_maps" | cut -f 2)" ep_no=$(printf "%s" "$anime_list" | grep "^$anime_id " | sed -nE 's/.*- episode (.+)$/\1/p') @@ -700,7 +637,6 @@ case "$source" in [ -z "$result" ] && die "Invalid anime selection" anime_title="$(printf "%s" "$result" | cut -f 2)" anime_id="$(printf "%s" "$result" | cut -f 1)" - hianime_desc "$anime_id" || true episode_maps="$(hianime_episodes "$anime_id")" || exit 1 ep_list="$(printf '%s' "$episode_maps" | cut -f 2)" [ -z "$ep_no" ] && ep_no=$(printf "%s" "$ep_list" | nth "Select episode: " "$menu_multi_flag") @@ -717,23 +653,22 @@ tput sc 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_no of $anime_title... "); do +while cmd=$(printf "next\nreplay\nprevious\nselect\nchange_season\nchange_quality\nquit" | nth "Playing episode $ep_no of $anime_title... "); do case "$cmd" in next) ep_no=$(printf "%s" "$ep_list" | sed -n "/^${ep_no}$/{n;p;}") 2>/dev/null ;; replay) video_link="$replay" sub_link="$replay_sub" - sub_file="$replay_sub_file" ;; 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: " "$menu_multi_flag") ;; change_season) - new_anime="$(printf "%s" "$seasons" | nl -w 2 | sed 's/^[[:space:]]//' | nth "Select season: ")" + # hianime has no season list, related entries are found by searching the title + _query=$(printf "%s" "$anime_title" | sed 's| |+|g; s|&|%26|g; s|#|%23|g') + new_anime="$(hianime_search "$_query" | 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)" - hianime_desc "$anime_id" || true episode_maps="$(hianime_episodes "$anime_id")" || exit 1 ep_list="$(printf '%s' "$episode_maps" | cut -f 2)" ep_no=$(printf "%s" "$ep_list" | nth "Select episode: " "$menu_multi_flag") diff --git a/ani-cli.1 b/ani-cli.1 index 450bdde01..1b770ada2 100644 --- a/ani-cli.1 +++ b/ani-cli.1 @@ -9,7 +9,7 @@ A shell script to browse and search anime from the command-line. .PD 0 .P .PD -This tool scrapes the site anidb. +This tool scrapes the site hianime. .PD 0 .P .PD @@ -34,8 +34,8 @@ Show logs. \fB\-h | --help\fR Show summary of options. .TP -\fB\-q | --quality\fR \fI\,<best|worst|360|480|720|1080>\/\fR -Set the video quality. Default quality is best. +\fB\-q | --quality\fR \fI\,<best|worst|resolution>\/\fR +Set the video quality. Default quality is best. The provider currently serves a single 1080p stream, other values fall back to best. .TP \fB\-s | --syncplay\fR Watch anime together with friends, using Syncplay (works with mpv only). @@ -85,7 +85,7 @@ Controls the scraped media's mode, valid options are sub or dub. Default is sub. Controls the directory where files are downloaded. Default is the current dir. .TP \fBANI_CLI_QUALITY\fR -Controls the scraped media's quality, check anidb for valid options or set to worst/best. Default is best. +Controls the scraped media's quality, a resolution such as 1080, or worst/best. Default is best. .TP \fBANI_CLI_PLAYER\fR Sets the player ani-cli uses. Can be debug (print links), download (equivalent to -d), android_mpv (apk and am start), android_vlc (apk and am start), catt (for streaming to tv), or any player that can play urls. For defaults see working without arguments. diff --git a/disclaimer.md b/disclaimer.md index de35faf3d..face5590d 100644 --- a/disclaimer.md +++ b/disclaimer.md @@ -33,7 +33,7 @@ This project has no control on the content it is serving, using copyrighted cont A browser is a tool, and the maliciousness of the tool is directly based on the user. </b> -This project uses client-side content access mechanisms. Hence, the copyright infrigements or DMCA in this project's regards are to be forwarded to the associated site by the associated notifier of any such claims. As of writing this is [anidb.app](https://anidb.app/) +This project uses client-side content access mechanisms. Hence, the copyright infrigements or DMCA in this project's regards are to be forwarded to the associated site by the associated notifier of any such claims. As of writing this is [hianime.at](https://hianime.at/) <b> Do not harass the maintainer. </b> diff --git a/hacking.md b/hacking.md index e291a2e4e..46ff13fdb 100644 --- a/hacking.md +++ b/hacking.md @@ -1,5 +1,5 @@ # Hacking ani-cli -Ani-cli is set up to scrape one platform - currently allanime. Supporting multiple sources at a time would require more changes than we (the maintainers) find worth doing, for this reason any feature request asking for a new site is rejected. +Ani-cli is set up to scrape one platform - currently hianime. Supporting multiple sources at a time would require more changes than we (the maintainers) find worth doing, for this reason any feature request asking for a new site is rejected. However ani-cli being open-source and the pirate anime streaming sites being so similar you can hack ani-cli to support any site that follows a few conventions.