diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 176190148..b1c5cfb16 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -14,6 +14,7 @@ - [ ] any anime playing - [ ] bumped version +- [ ] `sh tests/parse.sh` passes --- - [ ] next, prev, replay and select work - [ ] `-c` history and continue work diff --git a/.github/workflows/ani-cli.yml b/.github/workflows/ani-cli.yml index 8951aab4d..e5cf08ae8 100644 --- a/.github/workflows/ani-cli.yml +++ b/.github/workflows/ani-cli.yml @@ -6,6 +6,7 @@ on: pull_request: paths: - "ani-cli" + - "tests/**" jobs: sh-checker: @@ -44,3 +45,11 @@ jobs: - uses: actions/checkout@v4 - name: verify that noone added wget run: '! grep wget "./ani-cli"' + + parse-tests: + name: Parsing Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: run the offline parsing tests + run: sh tests/parse.sh diff --git a/.github/workflows/inverse-ani.yml b/.github/workflows/inverse-ani.yml index 00dd2e920..876530364 100644 --- a/.github/workflows/inverse-ani.yml +++ b/.github/workflows/inverse-ani.yml @@ -6,6 +6,7 @@ on: pull_request: paths-ignore: - "ani-cli" + - "tests/**" jobs: sh-checker: name: Shellcheck + Shfmt @@ -22,3 +23,8 @@ jobs: runs-on: ubuntu-latest steps: - run: 'echo "Not required: did not modify ani-cli"' + parse-tests: + name: Parsing Tests + runs-on: ubuntu-latest + steps: + - run: 'echo "Not required: did not modify ani-cli"' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b84544bff..88f6e2a9c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,6 +4,7 @@ - Appease the linter (run `shfmt -i 4 -ci -d -w ani-cli`) - Appease POSIX (run `shellcheck -s sh -o all -e 2250 ani-cli`) +- Run the offline parsing tests (`sh tests/parse.sh`) and update `tests/` when you change the scraping - Bump the version - Adjust the Readme according to your changes (if applicable) - No extra dependencies unless absolutely necessary diff --git a/README.md b/README.md index 20a7789c2..a4847e807 100644 --- a/README.md +++ b/README.md @@ -557,6 +557,9 @@ Ani-skip uses the external lua script function of mpv and as such – for now * 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`. +* Do I have to type the full name? - No, the search prompt shows what the site finds while you type (fzf 0.25 or newer), pick a result with Enter. Type a name on the command line (`ani-cli dandadan`) to skip the prompt. +* What do the columns in the search results mean? - The type (TV, Movie, OVA, ONA, Special), the number of episodes (`?` while a show is unreleased or still airing) and how many episodes have subtitles and a dub (`-` when there are none). +* What is the pane next to the search results? - The details of the highlighted anime (Japanese name, aired dates, status, score, genres, studios, rating, seasons, synopsis). It exists with fzf only, moves below the list on narrow terminals and can be turned off with `export ANI_CLI_MENU_FLAGS="--no-preview"`. **Note:** All features are documented in `ani-cli --help`. diff --git a/ani-cli b/ani-cli index c6e6299a2..e218195de 100755 --- a/ani-cli +++ b/ani-cli @@ -1,6 +1,6 @@ #!/bin/sh -version_number="5.1.4" +version_number="5.3.0" # UI @@ -8,14 +8,14 @@ version_number="5.1.4" # $1 = prompt, $2 = multi select flag (for fzf/rofi), $3 = extra flags by user menu() { case "$menu_program" in - fzf) fzf --reverse --cycle --prompt "$1" $2 $3 ;; + fzf) fzf --reverse --cycle --prompt "$1" --delimiter '\t' --with-nth 1,4,3 --tabstop 4 ${menu_preview:+--preview "$menu_preview" --preview-window "$preview_window"} ${menu_reload:+--disabled --print-query --bind "change:reload($menu_reload)"} $2 $3 ;; rofi) rofi -sort -dmenu -i -p "$1" $2 $3 ;; dmenu) dmenu -l 20 -p "$1" $3 ;; *) "$menu_program" $3 "$1" ;; esac } -# provide menu. input format is either (ep_list) or (number \t anime_id \t anime_name) +# provide menu. input format is either (ep_list) or (number \t anime_id \t anime_name [\t details]) nth() { _stdin=$(cat -) [ -z "$_stdin" ] && return 1 @@ -23,7 +23,13 @@ nth() { [ "$_line_count" -eq 1 ] && printf "%s" "$_stdin" | cut -f 2,3 && return 0 _prompt="$1" [ $# -ne 1 ] && _multi_flag="$2" - _line=$(printf "%s" "$_stdin" | cut -f 1,3 | tr '\t' ' ' | menu "$_prompt" "$_multi_flag" "$menu_extra_flags" | cut -d " " -f 1) + if [ "$menu_program" = "fzf" ]; then + # fzf gets the raw lines, it hides the id and reorders the columns itself (see menu) + _line=$(printf "%s" "$_stdin" | menu "$_prompt" "$_multi_flag" "$menu_extra_flags" | cut -f 1) + else + # other menus get plain text: number, details (if any), then the name. literal tabs, BSD sed has no \t in brackets + _line=$(printf "%s" "$_stdin" | sed -E 's|^([^ ]*) [^ ]* ([^ ]*) ([^ ]*)$|\1 \3 \2|' | cut -f 1,3 | tr '\t' ' ' | menu "$_prompt" "$_multi_flag" "$menu_extra_flags" | cut -d " " -f 1) + fi _line_start=$(printf "%s" "$_line" | head -n 1) _line_end=$(printf "%s" "$_line" | tail -n 1) [ -n "$_line" ] || return 1 @@ -34,6 +40,27 @@ nth() { fi } +# fzf reloads a list while typing since 0.25 +fzf_reloads() { + _fzf_version=$(fzf --version | cut -d ' ' -f 1) + case "$_fzf_version" in + 0.[0-9].* | 0.1[0-9].* | 0.2[0-4].*) return 1 ;; + *) return 0 ;; + esac +} + +# search while typing: fzf asks the site again after every change and shows what it found, Enter picks a result. +# sets query (what was typed) and result (id \t title of the pick, empty when nothing was picked) +live_search() { + menu_reload="sleep 0.3; sh '$script_path' --search {q}" + _picked=$(: | menu "Search anime: " "" "$menu_extra_flags") + _fzf_status=$? + unset menu_reload + [ "$_fzf_status" -eq 130 ] && exit 1 + query=$(printf "%s\n" "$_picked" | head -n 1) + result=$(printf "%s\n" "$_picked" | sed -n '2p' | cut -f 2,3) +} + die() { printf "\33[2K\r\033[1;31m%s\033[0m\n" "$*" >&2 exit 1 @@ -87,6 +114,10 @@ help_info() { Exit the player, and return the player exit code (useful for non interactive scenarios) -N, --nextep-countdown Display a countdown to the next episode + --info + Show the details of an anime id, this is what the preview pane displays + --search + List what the site finds for a query, this is what the search prompt reloads while typing -U, --update Update the script Some example usages: @@ -195,7 +226,17 @@ deobfuscate_blob() ( printf "%b" "$_output" ) -# search the query and give results. format is (id \t name) +# turns the html entities the site uses for titles and descriptions back into characters +decode_entities() { + sed -e "s|'|'|g" -e 's|"|"|g' -e 's|<|<|g' -e 's|>|>|g' -e "s|&|\&|g" +} + +# appends the first group of the regex $1 as a new tab separated column, "-" when the line has no match +tab_column() { + sed -E -e "s|^(.*$1.*)\$|\\1\\t\\2|" -e t -e 's|$|\t-|' +} + +# search the query and give results. format is (id \t name \t details) hianime_search() { #shellcheck disable=SC2059 _page="$(hianime_curl "$(printf "$search_api" "$1")")" || return 1 @@ -204,9 +245,50 @@ hianime_search() { die "Blocked by cloudflare." fi # 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|
|\n
|g' | - sed -nE 's|.*

[[:space:]]*([A-Za-z]+)' | tab_column 'tick-eps">[[:space:]]*([0-9]+)' | + tab_column 'tick-sub">[[:space:]]*]*>[[:space:]]*([0-9]+)' | tab_column 'tick-dub">[[:space:]]*]*>[[:space:]]*([0-9]+)' | + sed -nE 's|.*

[[:space:]]*/dev/null)" || { + printf "No details available\n" + exit 1 + } + _page="$(printf "%s" "$_page" | tr '\n\t' ' ' | sed 's/ */ /g')" + _title="$(printf "%s" "$_page" | after_first '

' | sed 's| *<.*||')" + # the anisc-info block lists the facts, one item each. an item ends at its first closing div, links become comma separated + _items="$(printf "%s" "$_page" | after_first '
' | sed 's|
.*||; s||,|g' | + sed -nE 's|.*item-head">([^<:]*):(.*)|\1\t\2|p' | sed 's|<[^>]*>| |g; s/ */ /g; s/ *, */, /g; s/ *,* *$//; s/\t /\t/' | decode_entities)" + [ -n "$_rating" ] && _items="$(printf "%s\nRating\t%s" "$_items" "$_rating")" + _seasons="$(printf "%s" "$_page" | sed 's|class="os-item|\n&|g' | sed '1d; s|.*||' | + sed -nE 's|^class="os-item ?(active)?"[^>]*>.*
([^<]*)
.*|\2 \1|p' | sed 's| active$| (this)|; s| $||' | tr '\n' ',' | sed 's|,$||; s|,|, |g' | decode_entities)" + [ -n "$_seasons" ] && _items="$(printf "%s\nSeasons\t%s" "$_items" "$_seasons")" + printf "\033[1m%s\033[0m\n\n" "$_title" + printf "%s\n" "$_items" | grep -v "^Overview" | while IFS=" " read -r _key _value; do + [ -n "$_key" ] && printf "\033[1;34m%-10s\033[0m %s\n" "$_key" "$_value" + done + printf "%s\n" "$_items" | sed -n 's|^Overview\t|\n|p' + exit 0 } # get the episodes list of the selected anime. format is (id \t ep_no) @@ -474,6 +556,11 @@ exit_after_play="${ANI_CLI_EXIT_AFTER_PLAY:-0}" skip_intro="${ANI_CLI_SKIP_INTRO:-0}" menu_program="${ANI_CLI_MENU:-fzf}" menu_extra_flags="${ANI_CLI_MENU_FLAGS:-""}" +# the preview pane runs this script again with --info for the highlighted anime, it goes below the list on narrow terminals +script_path="$(command -v "$0" || printf "%s" "$0")" +preview_window="right:40%:wrap" +_cols="$(tput cols 2>/dev/null)" +[ "${_cols:-100}" -lt 100 ] 2>/dev/null && preview_window="down:40%:wrap" # not opening from a terminal if [ ! -t 0 ]; then @@ -549,6 +636,16 @@ while [ $# -gt 0 ]; do --dmenu) menu_program=dmenu ;; --skip) skip_intro=1 ;; -N | --nextep-countdown) source=nextep ;; + --info) + [ $# -lt 2 ] && die "missing argument!" + hianime_info "$2" + ;; + --search) + [ $# -lt 2 ] && die "missing argument!" + query=$(printf "%s" "$2" | sed 's| |+|g') + [ "${#query}" -ge 2 ] && hianime_search "$query" | nl -w 2 | sed 's/^[[:space:]]//' + exit 0 + ;; -U | --update) dep_ch "patch" branch="${2:-$branch}" @@ -587,7 +684,9 @@ case "$source" in 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!" + menu_preview="sh '$script_path' --info {2}" [ -z "${index##*[!0-9]*}" ] && anime_id=$(printf "%s" "$anime_list" | nl -w 2 | sed 's/^[[:space:]]//' | nth "Select anime: " | cut -f 1) + unset menu_preview [ -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.*||') @@ -597,7 +696,12 @@ case "$source" in ;; *) if [ "$menu_program" = "fzf" ]; then - while [ -z "$query" ]; do + if [ -z "$query" ] && [ -z "$index" ] && [ "$source" != "nextep" ] && fzf_reloads; then + menu_preview="sh '$script_path' --info {2}" + while [ -z "$query" ] && [ -z "$result" ]; do live_search; done + unset menu_preview + fi + while [ -z "$query" ] && [ -z "$result" ]; do printf "\33[2K\r\033[1;36mSearch anime: \033[0m" && read -r query done else @@ -607,12 +711,15 @@ case "$source" in # for checking new releases by specifying anime name [ "$source" = "nextep" ] && time_until_next_ep "$query" - query=$(printf "%s" "$query" | sed 's| |+|g') - - 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: ") + if [ -z "$result" ]; then + query=$(printf "%s" "$query" | sed 's| |+|g') + 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") + menu_preview="sh '$script_path' --info {2}" + [ -z "$index" ] && result=$(printf "%s" "$anime_list" | nl -w 2 | sed 's/^[[:space:]]//' | nth "Select anime: ") + unset menu_preview + fi [ -z "$result" ] && die "Invalid anime selection" anime_title="$(printf "%s" "$result" | cut -f 2)" anime_id="$(printf "%s" "$result" | cut -f 1)" diff --git a/ani-cli.1 b/ani-cli.1 index e4f4e9419..65db3722e 100644 --- a/ani-cli.1 +++ b/ani-cli.1 @@ -1,4 +1,4 @@ -.TH "ANI-CLI" "1" "August 2026" "ani-cli" "User Commands" +.TH "ANI-CLI" "1" "September 2026" "ani-cli" "User Commands" .SH NAME ani-cli \- watch anime from the commandline .SH SYNOPSIS @@ -14,6 +14,10 @@ This tool scrapes the site hianime. .P .PD \f[B]ani-cli\f[R] without options defaults to iina on macOS, flatpak mpv on Steamdeck, mpv apk on android, vlc on iOS and mpv media player everywhere else. +.PD 0 +.P +.PD +Search results list the type, the number of episodes and how many of them have subtitles and a dub next to every title. With fzf (0.25 or newer), the search prompt shows the results while typing and a preview pane shows the details of the highlighted anime. .SH OPTIONS .TP \fB\-e | --episode | -r | --range\fR \fI\,\/\fR @@ -52,6 +56,12 @@ Selects nth entry. \fB\-N | --nextep-countdown\fR Prints a countdown to the next episode as the last episode in the list. If in history is after the current episode. .TP +\fB\--info\fR \fI\,\/\fR +Print the details of an anime id: title, Japanese name, aired dates, status, score, genres, studios, rating, seasons and synopsis. The fzf preview pane uses this. +.TP +\fB\--search\fR \fI\,\/\fR +Print what the site finds for a query, one line per anime: number, id, title and details. The search prompt reloads this while typing. +.TP \fB\--dub\fR Play the dubbed version. Without this flag, it'll always play the subbed version. .TP @@ -97,7 +107,7 @@ Sets the flags for the player ani-cli uses. Can be anything that the player supp Controls the frontend of ani-cli. Can be fzf, rofi, dmenu or any other program. Default is fzf. .TP \fBANI_CLI_MENU_FLAGS\fR -Controls the flags for the frontend. Last flag must take prompt as argument. Default is none +Controls the flags for the frontend. Last flag must take prompt as argument. With fzf, --no-preview hides the details pane. Default is none .TP \fBANI_CLI_LOG\fR Controls the logging feature for playback. Can be 1(logs) or 0(doesn't log). Default is 1. diff --git a/tests/expected/info.txt b/tests/expected/info.txt new file mode 100644 index 000000000..07f9ba7b3 --- /dev/null +++ b/tests/expected/info.txt @@ -0,0 +1,15 @@ +Frieren: Beyond Journey's End + +Japanese 葬送のフリーレン +Synonyms Sousou no Frieren +Aired Sep 29, 2023 to Mar 22, 2024 +Duration 25m +Status Finished Airing +MAL Score 9.36 +Genres Adventure, Fantasy, Shounen, Drama, Award Winning +Studios Madhouse +Producers TOHO animation, Shogakukan +Rating PG-13 +Seasons Season 1 (this), Season 2, Season 3 & Beyond + +During their decade-long quest to defeat the Demon King, the members of the hero's party—Himmel himself, the priest Heiter, the dwarf warrior Eisen, and the elven mage Frieren—forge bonds through adventures and battles, creating unforgettable precious memories for most of them. However, the time that Frieren spends with her comrades is equivalent to merely a fraction of her life, which has lasted over a thousand years. When the party disbands after their victory, Frieren casually returns to her "usual" routine of collecting spells across the continent. Due to her different sense of time, she seemingly holds no strong feelings toward the experiences she went through. As the years pass, Frieren gradually realizes how her days in the hero's party truly impacted her. Witnessing the deaths of two of her former companions, Frieren begins to regret having taken their presence for granted; she vows to better understand humans and create real personal connections. Although the story of that once memorable journey has long ended, a new tale is about to begin. diff --git a/tests/expected/search.tsv b/tests/expected/search.tsv new file mode 100644 index 000000000..1d3a8f214 --- /dev/null +++ b/tests/expected/search.tsv @@ -0,0 +1,12 @@ +frieren-beyond-journeys-end-481 Frieren: Beyond Journey's End TV 28 ep sub 28 dub 28 +oshi-no-ko-final-7206 [Oshi no Ko] Final Season TV ? ep sub - dub - +one-piece-1 One Piece TV ? ep sub 1179 dub 1155 +fatestay-night-heavens-feel-i-presage-flower-1109 Fate/stay night: Heaven's Feel - I. Presage Flower Movie 1 ep sub 1 dub 1 +gintama-season-2-1153 Gintama Season 2 TV 51 ep sub 51 dub - +sousou-no-frieren-2nd-season-9001 葬送のフリーレン 第2期 TV 10 ep sub 10 dub 10 +doupo-cangqiong-nian-fan-9002 斗破苍穹 年番 ONA 52 ep sub 52 dub - +na-honjaman-level-up-9003 나 혼자만 레벨업 TV 12 ep sub 12 dub 12 +special-chars-9004 Tom & Jerry | Pipe \ Backslash "Quoted" 100% [Brackets] a/b Special 1 ep sub 1 dub - +tab-in-title-9005 Tab Inside Title OVA 2 ep sub 2 dub 2 +music-video-9006 Otaku no Uta Music 1 ep sub 1 dub - +dub-only-9007 Dub Only Show TV 6 ep sub - dub 6 diff --git a/tests/fixtures/detail.html b/tests/fixtures/detail.html new file mode 100644 index 000000000..318f21ac8 --- /dev/null +++ b/tests/fixtures/detail.html @@ -0,0 +1,125 @@ + + +Watch Frieren: Beyond Journey's End | HiAnime + +
+

+ Frieren: Beyond Journey's End +

+
+
+
PG-13
+
HD
+
+ 28 +
+
+ 28 +
+
+ 28 +
+ + + TV + + 25m +
+
+
+
+
+
+
+ Overview: +
+ During their decade-long quest to defeat the Demon King, the members of the hero's party—Himmel himself, the priest Heiter, the dwarf warrior Eisen, and the elven mage Frieren—forge bonds through adventures and battles, creating unforgettable precious memories for most of them. However, the time that Frieren spends with her comrades is equivalent to merely a fraction of her life, which has lasted over a thousand years. When the party disbands after their victory, Frieren casually returns to her "usual" routine of collecting spells across the continent. Due to her different sense of time, she seemingly holds no strong feelings toward the experiences she went through. As the years pass, Frieren gradually realizes how her days in the hero's party truly impacted her. Witnessing the deaths of two of her former companions, Frieren begins to regret having taken their presence for granted; she vows to better understand humans and create real personal connections. Although the story of that once memorable journey has long ended, a new tale is about to begin. +
+
+ +
+ Japanese: + 葬送のフリーレン +
+ +
+ Synonyms: + Sousou no Frieren +
+ +
+ Aired: + Sep 29, 2023 to Mar 22, 2024 +
+ +
+ Duration: + 25m +
+ +
+ Status: + Finished Airing +
+ +
+ MAL Score: + 9.36 +
+ + + +
+ Studios: + + Madhouse + +
+ + +
+
+ +
+
This title div must not leak into the seasons
+
R+
+
+ diff --git a/tests/fixtures/search.html b/tests/fixtures/search.html new file mode 100644 index 000000000..1b54e4eab --- /dev/null +++ b/tests/fixtures/search.html @@ -0,0 +1,407 @@ + + +Browse Anime | HiAnime + +
+

Search results for: tests

+
+
+
+
+
28
+
28
+
28
+
+ + Frieren: Beyond Journey's End + + + +
+
+

+ + Frieren: Beyond Journey's End + +

+
+ During their decade-long quest to defeat the Demon King, the members of the hero's party—Himmel himself, the priest Heiter, the dwarf warrior Eisen, and the elven mage Frieren—forge bonds through adve... +
+
+ TV + + 25m +
+
+
+
+
+
+
+
+ + [Oshi no Ko] Final Season + + + +
+
+

+ + [Oshi no Ko] Final Season + +

+
+ The fourth and final season of [Oshi no Ko]. +
+
+ TV (? eps) + + ... +
+
+
+
+
+
+
+
1179
+
1155
+
+ + One Piece + + + +
+
+

+ + One Piece + +

+
+ Gold Roger was known as the "Pirate King," the strongest and most infamous being to have sailed the Grand Line. The capture and execution of Roger by the World Government brought a change throughout t... +
+
+ TV + + 24m +
+
+
+
+
+
+
+
1
+
1
+
1
+
+ + Fate/stay night: Heaven's Feel - I. Presage Flower + + + +
+ +
+
+
+
+
+
51
+
51
+
+ + Gintama Season 2 + + + +
+
+

+ + Gintama Season 2 + +

+
+ After a one-year hiatus, Shinpachi Shimura returns to Edo, only to stumble upon a shocking surprise: Gintoki and Kagura, his fellow Yorozuya members, have become completely different characters! Fleei... +
+
+ TV + + 24m +
+
+
+
+
+
+
+
10
+
10
+
10
+
+ 葬送のフリーレン 第2期 + +
+
+

+ + 葬送のフリーレン 第2期 + +

+
made up entry for the tests
+
+ TV + + 24m +
+
+
+
+
+
+
+
52
+
52
+
+ 斗破苍穹 年番 + +
+
+

+ + 斗破苍穹 年番 + +

+
made up entry for the tests
+
+ ONA + + 22m +
+
+
+
+
+
+
+
12
+
12
+
12
+
+ 나 혼자만 레벨업 + +
+
+

+ + 나 혼자만 레벨업 + +

+
made up entry for the tests
+
+ TV + + 24m +
+
+
+
+
+
+
+
1
+
1
+
+ Tom & Jerry | Pipe \ Backslash "Quoted" <Tag> 100% [Brackets] a/b + +
+
+

+ + Tom & Jerry | Pipe \ Backslash "Quoted" <Tag> 100% [Brackets] a/b + +

+
made up entry for the tests
+
+ SPECIAL + + 5m +
+
+
+
+
+
+
+
2
+
2
+
2
+
+ Tab	Inside Title + +
+
+

+ + Tab Inside Title + +

+
made up entry for the tests
+
+ OVA + + 30m +
+
+
+
+
+
+
+
1
+
1
+
+ Otaku no Uta + +
+
+

+ + Otaku no Uta + +

+
made up entry for the tests
+
+ MUSIC + + 4m +
+
+
+
+
+
+
+
6
+
6
+
+ Dub Only Show + +
+
+

+ + Dub Only Show + +

+
made up entry for the tests
+
+ TV + + 24m +
+
+
+
+
+
+
+
+
1
+

Sidebar

+
TV
+
+
+
+ diff --git a/tests/parse.sh b/tests/parse.sh new file mode 100755 index 000000000..d98ec947f --- /dev/null +++ b/tests/parse.sh @@ -0,0 +1,76 @@ +#!/bin/sh +# Offline tests for the html parsing of ani-cli. They feed saved pages to the +# parsing functions instead of the network. Run: sh tests/parse.sh + +cd "$(dirname "$0")/.." || exit 1 + +# load the functions only: everything before the "# MAIN" marker, which starts the setup and main part +grep -q '^# MAIN$' ani-cli || { + printf 'the "# MAIN" marker is missing in ani-cli, refusing to load it\n' + exit 1 +} +# shellcheck disable=SC2312 +eval "$(sed -n '1,/^# MAIN$/p' ani-cli)" + +# no network: every request returns the fixture named in $fixture +# shellcheck disable=SC2317,SC2329 +hianime_curl() { + cat "tests/fixtures/$fixture" +} +# shellcheck disable=SC2034 +base_api="https://hianime.at" +# shellcheck disable=SC2034 +search_api="${base_api}/search?keyword=%s" +# shellcheck disable=SC2034 +curl_exe="curl" + +failed=0 +esc="$(printf '\033')" + +# $1 = name, $2 = expected file, stdin = actual output +check() { + if _diff="$(diff -u "$2" - 2>&1)"; then + printf 'ok %s\n' "$1" + else + printf 'FAIL %s\n%s\n' "$1" "$_diff" + failed=1 + fi +} + +fixture="search.html" +# the details column is padded with spaces, drop them so the expected file has no trailing whitespace +hianime_search "tests" | sed 's/ *$//' | check "search results carry id, title and details" tests/expected/search.tsv + +fixture="detail.html" +(hianime_info "frieren-beyond-journeys-end-481") | sed "s/${esc}\[[0-9;]*m//g" | check "details of an anime" tests/expected/info.txt + +if (hianime_info "bad/id") 2>&1 | grep -q "Invalid anime id"; then + printf 'ok %s\n' "invalid ids are refused" +else + printf 'FAIL %s\n' "invalid ids are refused" + failed=1 +fi + +# the search prompt reloads its list while typing only with an fzf that can do it +# shellcheck disable=SC2317,SC2329 +fzf() { + printf '%s (test)\n' "$fzf_version" +} +for fzf_version in 0.9.0 0.19.1 0.24.0; do + if fzf_reloads; then + printf 'FAIL fzf %s must not use live search\n' "$fzf_version" + failed=1 + else + printf 'ok fzf %s keeps the plain prompt\n' "$fzf_version" + fi +done +for fzf_version in 0.25.0 0.44.1 1.0.0; do + if fzf_reloads; then + printf 'ok fzf %s searches while typing\n' "$fzf_version" + else + printf 'FAIL fzf %s must use live search\n' "$fzf_version" + failed=1 + fi +done + +exit "$failed"