11#! /bin/sh
22
3- version_number=" 5.0.3 "
3+ version_number=" 5.1.0 "
44
55# UI
66
@@ -149,49 +149,114 @@ cleanup() {
149149# SCRAPING
150150
151151# shellcheck disable=SC2086
152- anidb_curl () {
152+ hianime_curl () {
153153 $curl_exe -sL -A " $agent " --max-time 10 $cipher_flag " $@ "
154154}
155155
156+ # Decode one base64 value. GNU, BSD and OpenSSL use different switches.
157+ b64_decode () {
158+ 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
159+ }
160+
161+ # Deobfuscate the XOR blob with key otaku-embed-v1.
162+ # The subshell keeps the temporary positional parameters out of the caller.
163+ deobfuscate_blob () (
164+ _blob=$1
165+ set -- 111 116 97 107 117 45 101 109 98 101 100 45 118 49
166+ _bytes=" $( b64_decode " $_blob " | od -v -An -tu1) "
167+ _output=" "
168+ for _byte in $_bytes ; do
169+ _key=$1
170+ shift
171+ set -- " $@ " " $_key "
172+ _char=$(( _byte ^ _key))
173+ _output=" ${_output} \\ $(( _char / 64 )) $(( (_char / 8 ) % 8 )) $(( _char % 8 )) "
174+ done
175+ printf " %b" " $_output " | sed -n ' s|.*"src":"\([^"]*\.m3u8[^"]*\)".*|\1|p'
176+ )
177+
156178# search the query and give results. format is (id \t name)
157- anidb_search () {
179+ hianime_search () {
158180 # shellcheck disable=SC2059
159- _page=" $( anidb_curl " $( printf " $search_api " " $1 " ) " | tr ' \n ' ' ' | sed ' s|<a href|\n<a href|g ' ) "
181+ _page=" $( hianime_curl " $( printf " $search_api " " $1 " ) " ) "
160182 if printf " %s" " $_page " | grep -qi " Just a moment" ; then
161183 [ " $curl_exe " = " curl" ] && die " Blocked by cloudflare. Try installing curl-impersonate"
162184 die " Blocked by cloudflare."
163185 fi
164- printf " %s" " $_page " | sed -nE ' s|.*anime/([a-z0-9-]+-[0-9]+)".*alt="([^"]+)".*|\1\t\2|p' | sed -e " s|'|'|g" -e ' s|"|"|g' -e " s|&|\&|g"
186+ printf " %s" " $_page " | tr ' \n' ' ' | sed ' s|<div class="film-detail">|\n<div class="film-detail">|g' |
187+ sed -n ' s|.*<h3 class="film-name">[[:space:]]*<a href="https://hianime\.at/\([^"]*\)"[[:space:]]*title="\([^"]*\)".*|\1\t\2|p' |
188+ sed -e " s|'|'|g" -e ' s|"|"|g' -e " s|&|\&|g" -e " s|'|'|g" -e " s|<|<|g" -e " s|>|>|g"
165189}
166190
167191# extract mal_id and seasons metadata
168- anidb_desc () {
192+ hianime_desc () {
169193 # shellcheck disable=SC2059
170- _page=" $( anidb_curl " $( printf " $desc_api " " $1 " ) " | tr ' \n' ' ' | sed ' s|<a href|\n<a href|g' ) "
171- mal_id=" $( printf " %s" " $_page " | sed -nE ' s|.*https://myanimelist.net/anime/([0-9]+)/.*|\1|p' ) "
172- seasons=" $( printf " %s" " $_page " | sed -n ' />Seasons</,/>Details</p' | sed -nE ' s|.*anime/([^"]+-[0-9]+)"[^>]*title="([^"]+)".*|\1\t\2|p' | sed -e " s|'|'|g" ) "
194+ _page=" $( hianime_curl " $( printf " $desc_api " " $1 " ) " ) "
195+ mal_id=" $( printf " %s" " $_page " | sed -n ' s|.*myanimelist\.net/anime/\([0-9][0-9]*\).*|\1|p' ) "
196+ # Related entries are grouped in the season list on HiAnime pages.
197+ 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' ) "
173198 [ -n " $seasons " ] && seasons_option=" \nchange_season"
174199}
175200
176201# get the episodes list of the selected anime. format is (id \t ep_no)
177- anidb_episodes () {
202+ hianime_episodes () {
178203 # shellcheck disable=SC2059
179- _page=" $( anidb_curl " $( printf " $episodes_api " " ${1##* -} " ) " ) "
180- printf " %s" " $_page " | sed ' s|},{|}\n{|g' | sed -nE ' s|.*"id":([0-9]+).*"number":([0-9]+).*|\1\t\2|p'
204+ _page=" $( hianime_curl -H " X-Requested-With: XMLHttpRequest" " $( printf " $episodes_api " " ${1##* -} " ) " ) "
205+ printf " %s" " $_page " | sed ' s|\\\"|"|g; s|ep-item|\nep-item|g' |
206+ sed -n ' s|.*data-number="\([^"]*\)".*data-id="\([0-9][0-9]*\)".*|\2\t\1|p'
181207}
182208
183209# extract the individual m3u8 links from response of embed urls
184- anidb_m3u8 () {
185- _ep_id=$( printf " %s" " $episode_maps " | sed -nE " s|^([0-9]+)\t${1} $|\1|p" )
186- _lang=" jpn"
187- [ " $2 " = " dub" ] && _lang=" eng"
210+ hianime_m3u8 () {
211+ _ep_id=$( printf " %s" " $episode_maps " | sed -n " s|^\([0-9][0-9]*\)\t${1} $|\1|p" )
212+ _lang=" sub"
213+ [ " $2 " = " dub" ] && _lang=" dub"
214+ [ -z " $_ep_id " ] && return 1
215+
216+ # 1. Get servers
188217 # shellcheck disable=SC2059
189- _page=" $( anidb_curl " $( printf " $m3u8_api " " ${_ep_id} " ) " ) "
190- _embed=" $( printf " %s" " $_page " | sed ' s|},{|}\n{|g' | sed -nE ' s|.*' " ${_lang} " ' .*embed_url":"([^"]+)".*|\1|p' | sed ' s|\\/|/|g' | head -n 1) "
191- [ -z " $_embed " ] && return 1
192- _embed_page=" $( anidb_curl " $_embed " ) "
193- _m3u8_master=" $( printf " %s" " $_embed_page " | sed -nE " s|.*file: '([^']*)'.*|\1|p" | head -n 1) "
194- anidb_curl " ${_m3u8_master} " | sed ' s|^#EXT-X-STREAM.*x||g; s|,.*|p|g; /^#/d; $!N; s|\n| >|;/EXT-X-I-FRAME/d'
218+ _servers_json=" $( hianime_curl -H " X-Requested-With: XMLHttpRequest" " $( printf " $servers_api " " ${_ep_id} " ) " ) "
219+
220+ # 2. Prefer HD-1; otherwise use the first server for the selected language.
221+ _servers=" $( printf " %s" " $_servers_json " | sed ' s|\\\"|"|g; s|server-item|\nserver-item|g' ) "
222+ _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) "
223+ [ -z " $_hash " ] && _hash=" $( printf " %s\n" " $_servers " | grep " data-type=\" ${_lang} \" " | sed -n ' s|.*data-hash="\([^"]*\)".*|\1|p' | head -n 1) "
224+
225+ [ -z " $_hash " ] && return 1
226+
227+ # 3. Base64 decode embed URL
228+ _embed_url=" $( b64_decode " $_hash " ) "
229+ [ -z " $_embed_url " ] && return 1
230+
231+ # Extract mal_id for ani-skip
232+ mal_id=" $( printf " %s" " $_embed_url " | sed -n ' s|.*/stream/mal/\([0-9][0-9]*\)/.*|\1|p' ) "
233+
234+ # 4. Fetch embed HTML
235+ _embed_page=" $( hianime_curl -e " ${base_api} /" " $_embed_url " ) "
236+ _blob=" $( printf " %s" " $_embed_page " | sed -n ' s|.*window\.__P="\([^"]*\)".*|\1|p' ) "
237+ [ -z " $_blob " ] && return 1
238+
239+ # 5. Deobfuscate
240+ _m3u8_master=" $( deobfuscate_blob " $_blob " ) "
241+ [ -z " $_m3u8_master " ] && return 1
242+
243+ # 6. Fetch the master playlist and expand quality variants.
244+ _m3u8_content=" $( hianime_curl -e " https://zokoanime.video/" " $_m3u8_master " ) "
245+ if printf " %s" " $_m3u8_content " | grep -q " #EXT-X-STREAM-INF" ; then
246+ _base_url=" ${_m3u8_master%/* } "
247+ _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' ) "
248+ printf " %s\n" " $_parsed " | while IFS= read -r _line || [ -n " $_line " ]; do
249+ [ -z " $_line " ] && continue
250+ _quality=$( printf " %s" " $_line " | cut -d " >" -f 1 | tr -d " " )
251+ _path=$( printf " %s" " $_line " | cut -d " >" -f 2 | tr -d " " )
252+ case " $_path " in
253+ http* ) printf " %s >%s\n" " $_quality " " $_path " ;;
254+ * ) printf " %s >%s/%s\n" " $_quality " " $_base_url " " $_path " ;;
255+ esac
256+ done
257+ else
258+ printf " default >%s\n" " $_m3u8_master "
259+ fi
195260}
196261
197262# select the quality according to the setting
@@ -208,9 +273,9 @@ select_quality() {
208273
209274# gets embed url, collects direct links and selects one with desired quality into $episode
210275get_video_link () {
211- links=$( anidb_m3u8 " $ep_no " " $mode " | sort -g -r -s)
276+ links=$( hianime_m3u8 " $ep_no " " $mode " | sort -g -r -s)
212277 [ -z " $links " ] && die " No sources found for $mode !"
213- info " anidb.app links fetched"
278+ info " hianime.at links fetched"
214279 select_quality " $quality "
215280 # should never trigger but just in case
216281 if printf " %s" " $ep_list " | grep -q " ^$ep_no $" ; then
@@ -265,6 +330,7 @@ time_until_next_ep() {
265330
266331backup_old_hist () {
267332 backupfile=" ${histfile} .v4"
333+ : > " ${histfile} .new"
268334 while IFS=" " read -r ep_no anime_id anime_title; do
269335 case " $anime_id " in
270336 * -* ) printf " %s\t%s\t%s\n" " $ep_no " " $anime_id " " $anime_title " >> " ${histfile} .new" ;;
@@ -275,7 +341,7 @@ backup_old_hist() {
275341}
276342
277343process_hist_entry () {
278- ep_list=$( anidb_episodes " $anime_id " | cut -f 2)
344+ ep_list=$( hianime_episodes " $anime_id " | cut -f 2)
279345 ep_no=$( printf " %s" " $ep_list " | sed -n " /^${ep_no} $/{n;p;}" ) 2> /dev/null
280346 [ -n " $ep_no " ] && printf " %s\t%s - episode %s\n" " $anime_id " " $anime_title " " $ep_no "
281347}
@@ -297,8 +363,8 @@ update_history() {
297363# shellcheck disable=SC2086
298364# $1 = video link, $2 = video name, $3 = extra flags
299365download () {
300- 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
301- ffmpeg -extension_picky 0 -loglevel error -stats -i " $1 " -c copy " $download_dir /$2 .mp4" $3
366+ 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
367+ ffmpeg -extension_picky 0 -referer " https://zokoanime.video/ " - loglevel error -stats -i " $1 " -c copy " $download_dir /$2 .mp4" $3
302368}
303369
304370play_episode () {
@@ -312,27 +378,27 @@ play_episode() {
312378 debug) printf " All links:\n%s\nSelected link:\n%s\n" " $links " " $video_link " ;;
313379 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 & ;;
314380 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 & ;;
315- * 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 & ;;
381+ * 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 & ;;
316382 * mpv* )
317383 if [ " $no_detach " = 0 ]; then
318- nohup $player_function $skip_flag $player_extra_flags --force-media-title=" ${anime_title} Episode ${ep_no} " " $video_link " > /dev/null 2>&1 &
384+ 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 &
319385 else
320- $player_function $skip_flag $player_extra_flags --force-media-title=" ${anime_title} Episode ${ep_no} " " $video_link "
386+ $player_function --http-header-fields= " Referer: https://zokoanime.video/ " $skip_flag $player_extra_flags --force-media-title=" ${anime_title} Episode ${ep_no} " " $video_link "
321387 mpv_exitcode=$?
322388 [ " $exit_after_play " = 1 ] && [ -z " $_range " ] && exit " $mpv_exitcode "
323389 fi
324390 ;;
325391 * iina* )
326392 if pgrep -f " IINA" > /dev/null 2>&1 ; then
327393 # omit --keep-running when an IINA instance exists to prevent hanging
328- nohup $player_function $player_extra_flags --no-stdin --mpv-force-media-title=" ${anime_title} Episode ${ep_no} " " $video_link " > /dev/null 2>&1 &
394+ 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 &
329395 else
330- 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 &
396+ 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 &
331397 fi
332398 ;;
333- * vlc* ) nohup $player_function $player_extra_flags --play-and-exit --meta-title=" ${anime_title} Episode ${ep_no} " " $video_link " > /dev/null 2>&1 & ;;
334- * yncpla* ) nohup $player_function $player_extra_flags " $video_link " -- --force-media-title=" ${anime_title} Episode ${ep_no} " > /dev/null 2>&1 & ;;
335- download) " $player_function " " $video_link " " ${anime_title} Episode ${ep_no} " $player_extra_flags ;;
399+ * 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 & ;;
400+ * 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 & ;;
401+ download) " $player_function " " $video_link " " ${anime_title} Episode ${ep_no} " " $player_extra_flags " ;;
336402 catt) nohup catt cast $player_extra_flags " $video_link " > /dev/null 2>&1 & ;;
337403 iSH)
338404 printf " \033]8;;vlc://%s\a~~~~~~~~~~~~~~~~~~~~\n~ Tap to open VLC ~\n~~~~~~~~~~~~~~~~~~~~\033]8;;\a\n" " $video_link "
@@ -379,13 +445,11 @@ play() {
379445# MAIN
380446
381447# setup
382- base_api=" https://anidb.app"
383- search_api=" ${base_api} /browse?q=%s"
384- # search_api_alt="${base_api}/search/suggestions?q=%s"
385- desc_api=" ${base_api} /anime/%s"
386- episodes_api=" ${base_api} /api/frontend/anime/%s/episodes"
387- m3u8_api=" ${base_api} /api/frontend/episode/%s/languages"
388-
448+ base_api=" https://hianime.at"
449+ search_api=" ${base_api} /search?keyword=%s"
450+ desc_api=" ${base_api} /%s"
451+ episodes_api=" ${base_api} /api/theme/episode/list/%s"
452+ servers_api=" ${base_api} /api/theme/episode/servers?episodeId=%s"
389453ciphers=' 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'
390454tls13_ciphers=' TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256'
391455agent=" Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
@@ -527,15 +591,14 @@ trap 'cleanup; exit 1' INT HUP TERM
527591case " $source " in
528592 history)
529593 backup_old_hist
530- anime_list=$( while read -r ep_no anime_id anime_title; do process_hist_entry & done < " $histfile " )
531- wait
594+ anime_list=$( while read -r ep_no anime_id anime_title; do process_hist_entry & done < " $histfile " ; wait)
532595 [ -z " $anime_list " ] && die " No unwatched series in history!"
533596 [ -z " ${index##* [!0-9]* } " ] && anime_id=$( printf " %s" " $anime_list " | nl -w 2 | sed ' s/^[[:space:]]//' | nth " Select anime: " | cut -f 1)
534597 [ -z " ${index##* [!0-9]* } " ] || anime_id=$( printf " %s" " $anime_list " | sed -n " ${index} p" | cut -f 1)
535598 [ -z " $anime_id " ] && exit 1
536599 anime_title=$( printf " %s" " $anime_list " | grep " ^$anime_id " | cut -f 2 | sed ' s| - episode.*||' )
537- anidb_desc " $anime_id " || true
538- episode_maps=" $( anidb_episodes " $anime_id " ) "
600+ hianime_desc " $anime_id " || true
601+ episode_maps=" $( hianime_episodes " $anime_id " ) "
539602 ep_list=" $( printf " %s" " $episode_maps " | cut -f 2) "
540603 ep_no=$( printf " %s" " $anime_list " | grep " ^$anime_id " | sed -nE ' s/.*- episode (.+)$/\1/p' )
541604 ;;
@@ -553,15 +616,15 @@ case "$source" in
553616
554617 query=$( printf " %s" " $query " | sed ' s| |+|g' )
555618
556- anime_list=$( anidb_search " $query " )
619+ anime_list=$( hianime_search " $query " )
557620 [ -z " $anime_list " ] && die " No results found!"
558621 [ " $index " -eq " $index " ] 2> /dev/null && result=$( printf " %s" " $anime_list " | sed -n " ${index} p" )
559622 [ -z " $index " ] && result=$( printf " %s" " $anime_list " | nl -w 2 | sed ' s/^[[:space:]]//' | nth " Select anime: " )
560623 [ -z " $result " ] && die " Invalid anime selection"
561624 anime_title=" $( printf " %s" " $result " | cut -f 2) "
562625 anime_id=" $( printf " %s" " $result " | cut -f 1) "
563- anidb_desc " $anime_id " || true
564- episode_maps=" $( anidb_episodes " $anime_id " ) "
626+ hianime_desc " $anime_id " || true
627+ episode_maps=" $( hianime_episodes " $anime_id " ) "
565628 ep_list=" $( printf ' %s' " $episode_maps " | cut -f 2) "
566629 [ -z " $ep_no " ] && ep_no=$( printf " %s" " $ep_list " | nth " Select episode: " " $menu_multi_flag " )
567630 [ -z " $ep_no " ] && die " Invalid episode selection"
@@ -589,8 +652,8 @@ while cmd=$(printf "next\nreplay\nprevious\nselect$seasons_option\nchange_qualit
589652 [ -z " $new_anime " ] && die " Invalid season selection"
590653 anime_title=" $( printf " %s" " $new_anime " | cut -f 2) "
591654 anime_id=" $( printf " %s" " $new_anime " | cut -f 1) "
592- anidb_desc " $anime_id " || true
593- episode_maps=" $( anidb_episodes " $anime_id " ) "
655+ hianime_desc " $anime_id " || true
656+ episode_maps=" $( hianime_episodes " $anime_id " ) "
594657 ep_list=" $( printf ' %s' " $episode_maps " | cut -f 2) "
595658 ep_no=$( printf " %s" " $ep_list " | nth " Select episode: " " $menu_multi_flag " )
596659 [ -z " $ep_no " ] && die " Invalid episode selection"
0 commit comments