diff --git a/README.md b/README.md index 67b471ab8..7f5bc6981 100644 --- a/README.md +++ b/README.md @@ -546,7 +546,8 @@ Ani-skip uses the external lua script function of mpv and as such – for now * Can I change subtitle language or turn them off? - No, the subtitles are baked into the video. * 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 change media source? - This fork adds **simindad.top** alongside allanime. Search covers both by default and each result is labelled with its provider (`[allanime]` / `[simindad]`). Use `--source allanime|simindad|all` (or `ANI_CLI_SOURCE`) to scope it. +* How do I use simindad? - simindad requires a (free) account. Run `ani-cli --login-simindad` once to log in (it shows the captcha and stores the session), or set `ANI_CLI_SIMINDAD_USER` / `ANI_CLI_SIMINDAD_PASS`; a login prompt also appears automatically the first time you pick a simindad title. It covers films, series and anime (movies play as a single item). * 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`. * How can I download? - Use `-d`, it will download into your working directory. diff --git a/ani-cli b/ani-cli index 37a18368c..0666dfedf 100755 --- a/ani-cli +++ b/ani-cli @@ -1,6 +1,6 @@ #!/bin/sh -version_number="4.14.1" +version_number="4.14.1-simindad" # UI @@ -86,6 +86,10 @@ help_info() { Use given title as ani-skip query -N, --nextep-countdown Display a countdown to the next episode + --source + Choose which source(s) to search (default: all). Results are labelled per provider. + --login-simindad + Log in to simindad.top (prompts for credentials + captcha) and store the session -U, --update Update the script Some example usages: @@ -291,17 +295,27 @@ get_filemoon_links() { # gets embed urls, collects direct links into provider files, selects one with desired quality into $episode get_episode_url() { + # dispatch to the simindad source when the id is tagged with the sd: prefix + case "$id" in + sd:*) + get_episode_url_simindad + return + ;; + *) ;; + esac + showid="${id#aa:}" # strip the allanime source tag before querying + #shellcheck disable=SC2016 episode_embed_gql='query ($showId: String!, $translationType: VaildTranslationTypeEnumType!, $episodeString: String!) { episode( showId: $showId translationType: $translationType episodeString: $episodeString ) { episodeString sourceUrls }}' query_hash="d405d0edd690624b66baba3068e0edc3ac90f1597d898a1ec8db4e5c43c00fec" - query_vars="{\"showId\":\"$id\",\"translationType\":\"$mode\",\"episodeString\":\"$ep_no\"}" + query_vars="{\"showId\":\"$showid\",\"translationType\":\"$mode\",\"episodeString\":\"$ep_no\"}" query_ext="{\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"$query_hash\"}}" api_resp="$(curl -e "$allanime_refr" -sG -A "$agent" -H "Origin: ${allanime_refr}" "${allanime_api}/api" --data-urlencode "variables=${query_vars}" --data-urlencode "extensions=${query_ext}")" if [ -z "$api_resp" ] || ! printf "%s" "$api_resp" | grep -q "tobeparsed"; then - api_resp="$(curl -e "$allanime_refr" -s -H "Content-Type: application/json" -X POST "${allanime_api}/api" --data "{\"variables\":{\"showId\":\"$id\",\"translationType\":\"$mode\",\"episodeString\":\"$ep_no\"},\"query\":\"$episode_embed_gql\"}" -A "$agent")" + api_resp="$(curl -e "$allanime_refr" -s -H "Content-Type: application/json" -X POST "${allanime_api}/api" --data "{\"variables\":{\"showId\":\"$showid\",\"translationType\":\"$mode\",\"episodeString\":\"$ep_no\"},\"query\":\"$episode_embed_gql\"}" -A "$agent")" fi resp="$(process_response "$api_resp" | tr '{}' '\n' | sed 's|\\u002F|\/|g;s|\\||g' | sed -nE 's|.*sourceUrl":"([^"]*)".*sourceName":"([^"]*)".*|\2 :\1|p')" @@ -333,6 +347,177 @@ search_anime() { | g' | sed -nE "s|.*_id\":\"([^\"]*)\",\"name\":\"(.+)\",.*${mode}\":([1-9][^,]*).*|\1 \2 (\3 episodes)|p" | sed 's/\\"//g' } +# ---- simindad.top source ---------------------------------------------------- +# simindad is a login-gated WordPress site (film2media theme). Search is open, +# but content pages sit behind an SSO login and expose direct .mp4/.mkv links. + +# render the login captcha: inline terminal image if possible, else open it +simindad_show_captcha() { + if command -v chafa >/dev/null 2>&1; then + chafa -s 60x14 "$1" >&2 + elif command -v imgcat >/dev/null 2>&1; then + imgcat "$1" >&2 + elif command -v catimg >/dev/null 2>&1; then + catimg "$1" >&2 + elif command -v viu >/dev/null 2>&1; then + viu -h 12 "$1" >&2 + else + case "$(uname -s)" in + Darwin*) open "$1" >/dev/null 2>&1 ;; + *) + if command -v termux-open >/dev/null 2>&1; then + termux-open "$1" >/dev/null 2>&1 + elif command -v xdg-open >/dev/null 2>&1; then + xdg-open "$1" >/dev/null 2>&1 + else + printf "Open this image to read the captcha: %s\n" "$1" >&2 + fi + ;; + esac + fi +} + +# interactive login; persists the WordPress session into $simindad_jar +simindad_login() { + printf "\33[2K\r\033[1;34mLogging in to simindad...\033[0m\n" >&2 + profile_page="$(curl -sL -A "$agent" -c "$simindad_jar" -b "$simindad_jar" "$simindad_base/profile/")" + login_nonce="$(printf '%s' "$profile_page" | sed -nE 's/.*id="authbutton"[^>]*value="([^"]+)".*/\1/p' | head -n1)" + captcha_img="$(mktemp).png" + curl -s -A "$agent" -c "$simindad_jar" -b "$simindad_jar" -e "$simindad_base/profile/" "$simindad_base/captcha.php?w=150&h=50" -o "$captcha_img" + simindad_show_captcha "$captcha_img" + + login_user="${ANI_CLI_SIMINDAD_USER:-}" + login_pass="${ANI_CLI_SIMINDAD_PASS:-}" + if [ -z "$login_user" ]; then + printf "\033[1;36mSimindad username: \033[0m" >&2 + read -r login_user + fi + if [ -z "$login_pass" ]; then + printf "\033[1;36mSimindad password: \033[0m" >&2 + stty -echo 2>/dev/null + read -r login_pass + stty echo 2>/dev/null + printf "\n" >&2 + fi + printf "\033[1;36mCaptcha (5 digits, type the shown digits as 0-9): \033[0m" >&2 + read -r login_code + rm -f "$captcha_img" + + login_hdr="$(curl -s -A "$agent" -c "$simindad_jar" -b "$simindad_jar" -e "$simindad_base/profile/" -D - -o /dev/null \ + --data-urlencode "username=$login_user" --data-urlencode "password=$login_pass" \ + --data-urlencode "challenge_captcha=$login_code" --data-urlencode "native=" \ + --data-urlencode "submit=$login_nonce" "$simindad_base/profile/")" + if printf '%s' "$login_hdr" | grep -qi 'wordpress_logged_in'; then + printf "\033[1;32mSimindad login successful\033[0m\n" >&2 + else + die "Simindad login failed (wrong username/password or captcha)." + fi +} + +# fetch a simindad path (following the SSO redirect chain); auto-login if needed +simindad_page() { + page="$(curl -sL -A "$agent" -c "$simindad_jar" -b "$simindad_jar" -e "$simindad_base/" "$simindad_base$1")" + if printf '%s' "$page" | grep -q 'id="authform"'; then + simindad_login + page="$(curl -sL -A "$agent" -c "$simindad_jar" -b "$simindad_jar" -e "$simindad_base/" "$simindad_base$1")" + fi + printf '%s' "$page" +} + +# extract direct media links from a fetched page into "epnomodequalityurl" +# lines. $2 is the title type (series|movie). Uses a handful of whole-stream passes +# (not one fork per link) so it stays fast even on series pages with 1000+ links. +simindad_extract_links() { + ex_dir="$(mktemp -d)" + printf '%s' "$1" | grep -oE 'https?://[^"'\'' <>]+\.(mkv|mp4)[^"'\'' <>]*' | + grep -viE 'trailer' | sort -u >"$ex_dir/url" + if [ "$2" = series ]; then + # episode number: prefer SxxExx, else the number just before the resolution + # (e.g. One.Piece.1156.1080p), else fall back to 1 + sed -E 's/.*[Ss][0-9]+[Ee]0*([0-9]+).*/\1/; t + s/.*[._ ]0*([0-9]{1,4})[._ ][0-9]{3,4}p.*/\1/; t + s/.*/1/' "$ex_dir/url" >"$ex_dir/ep" + else + # movies are a single item; never mistake a year for an episode number + sed 's/.*/1/' "$ex_dir/url" >"$ex_dir/ep" + fi + # dubbed vs subbed, inferred from the filename + sed -E 's/.*([Dd]ubbed|[Dd]ubla|دوبله).*/dub/; t; s/.*/sub/' "$ex_dir/url" >"$ex_dir/mode" + # numeric quality (resolution) from the filename, else 0 + sed -E 's/.*[^0-9]([0-9]{3,4})p.*/\1/; t; s/.*/0/' "$ex_dir/url" >"$ex_dir/q" + paste "$ex_dir/ep" "$ex_dir/mode" "$ex_dir/q" "$ex_dir/url" + rm -rf "$ex_dir" +} + +# search simindad via its open /quick-search endpoint; emit "sd:[simindad] (<type>)" +search_simindad() { + search_q="$(printf '%s' "$1" | tr '+' ' ')" + # Parse the per-hit "_formatted" objects: they mirror the top-level fields + # (_kind/title/url) and never contain nested braces, so they survive even the + # occasional "_db_error" prefix the endpoint returns. Strip <em> highlight tags. + curl -s -A "$agent" -e "$simindad_base/" -X POST "$simindad_base/quick-search" \ + --data-urlencode "q=$search_q" --data "sort=modified_at:desc" | + grep -oE '"_formatted":\{[^}]*\}' | sed 's/<[^>]*>//g' | + sed -nE 's/.*"_kind":"([^"]*)".*"title":"([^"]*)".*"url":"([^"]*)".*/\1\t\2\t\3/p' | + grep -v "^term${T}" | cut -f2- | while IFS="${T}" read -r sd_title sd_url; do + [ -z "$sd_url" ] && continue + case "$sd_url" in + *"/series/"*) sd_type=series ;; + *) sd_type=movie ;; + esac + printf 'sd:%s\t[simindad] %s (%s)\n' "$sd_url" "$sd_title" "$sd_type" + done +} + +# episode list for a simindad title; caches extracted links in $simindad_links_file +episodes_list_simindad() { + simindad_links_file="$(mktemp)" + case "$1" in + *"/series/"*) simindad_type=series ;; + *) simindad_type=movie ;; + esac + simindad_html="$(simindad_page "${1#sd:}")" + simindad_extract_links "$simindad_html" "$simindad_type" >"$simindad_links_file" + cut -f1 "$simindad_links_file" | sort -n -u +} + +# select the media url for the current $ep_no from the cached simindad links +get_episode_url_simindad() { + [ -f "$simindad_links_file" ] || episodes_list_simindad "$id" >/dev/null + sd_lines="$(grep "^${ep_no}${T}" "$simindad_links_file")" + sd_mode_lines="$(printf '%s\n' "$sd_lines" | grep "${T}${mode}${T}")" + [ -n "$sd_mode_lines" ] && sd_lines="$sd_mode_lines" + links="$(printf '%s\n' "$sd_lines" | while IFS="${T}" read -r _ _ lq lu; do + [ -n "$lu" ] && printf '%s >%s\n' "$lq" "$lu" + done | sort -nr -s)" + if printf "%s" "$ep_list" | grep -q "^$ep_no$"; then + [ -z "$links" ] && die "Episode is released, but no valid sources!" + else + [ -z "$links" ] && die "Episode not released!" + fi + select_quality "$quality" + refr_flag="--referrer=$simindad_refr" +} +# ----------------------------------------------------------------------------- + +# run the search across the configured sources and tag each row with its provider +search_sources() { + case "$source_provider" in + allanime) + search_anime "$1" | while IFS="$T" read -r aa_id aa_title; do + [ -n "$aa_id" ] && printf 'aa:%s\t[allanime] %s\n' "$aa_id" "$aa_title" + done + ;; + simindad) search_simindad "$1" ;; + *) + search_anime "$1" | while IFS="$T" read -r aa_id aa_title; do + [ -n "$aa_id" ] && printf 'aa:%s\t[allanime] %s\n' "$aa_id" "$aa_title" + done + search_simindad "$1" + ;; + esac +} + time_until_next_ep() { animeschedule="https://animeschedule.net" query="$(printf "%s\n" "$*" | tr ' ' '+')" @@ -349,6 +534,16 @@ time_until_next_ep() { # get the episodes list of the selected anime episodes_list() { + # dispatch to the simindad source when the id is tagged with the sd: prefix + case "$1" in + sd:*) + episodes_list_simindad "$1" + return + ;; + *) ;; + esac + set -- "${1#aa:}" # strip the allanime source tag before querying + #shellcheck disable=SC2016 episodes_list_gql='query ($showId: String!) { show( _id: $showId ) { _id availableEpisodesDetail }}' @@ -511,6 +706,13 @@ histfile="$hist_dir/ani-hsts" [ ! -f "$histfile" ] && : >"$histfile" search="${ANI_CLI_DEFAULT_SOURCE:-scrape}" branch="${ANI_CLI_BRANCH:-master}" +# simindad.top source configuration +simindad_base="https://www.simindad.top" +simindad_refr="https://www.simindad.top" +simindad_jar="$hist_dir/simindad_cookies.txt" +source_provider="${ANI_CLI_SOURCE:-all}" # all | allanime | simindad +# literal tab, used as a field separator +T="$(printf '\t')" while [ $# -gt 0 ]; do case "$1" in @@ -578,6 +780,12 @@ while [ $# -gt 0 ]; do shift ;; -N | --nextep-countdown) search=nextep ;; + --source) + [ $# -lt 2 ] && die "missing argument!" + source_provider="$2" + shift + ;; + --login-simindad) simindad_do_login=1 ;; -U | --update) branch="${2:-$branch}" update_script @@ -609,6 +817,9 @@ esac # idk where to put this, so I put it here... trap 'cleanup; exit 1' INT HUP +# explicit simindad login refresh +[ "$simindad_do_login" = 1 ] && simindad_login && exit 0 + # searching case "$search" in history) @@ -636,12 +847,12 @@ case "$search" in [ "$search" = "nextep" ] && time_until_next_ep "$query" query=$(printf "%s" "$query" | sed "s| |+|g") - anime_list=$(search_anime "$query") + anime_list=$(search_sources "$query") [ -z "$anime_list" ] && die "No results found!" [ "$index" -eq "$index" ] 2>/dev/null && result=$(printf "%s" "$anime_list" | sed -n "${index}p") [ -z "$index" ] && result=$(printf "%s" "$anime_list" | nl -w 2 | sed 's/^[[:space:]]//' | nth "Select anime: ") [ -z "$result" ] && exit 1 - title=$(printf "%s" "$result" | cut -f2) + title=$(printf "%s" "$result" | cut -f2 | sed 's/^\[[^]]*\] //') allanime_title="$(printf "%s" "$title" | cut -d'(' -f1 | tr -d '[:punct:]')" id=$(printf "%s" "$result" | cut -f1) ep_list=$(episodes_list "$id") diff --git a/ani-cli.1 b/ani-cli.1 index ae0a1cc8e..db9ee83fb 100644 --- a/ani-cli.1 +++ b/ani-cli.1 @@ -69,6 +69,12 @@ Exit the player, and return the player exit code (useful for non interactive sce .TP \fB\--skip-title\fR \fI\,<title>\/\fR Specify the title to use for ani-skip +.TP +\fB\--source\fR \fI\,<all|allanime|simindad>\/\fR +Choose which source(s) to search. Results are labelled per provider. Default is all. +.TP +\fB\--login-simindad\fR +Log in to simindad.top (prompts for username, password and captcha) and store the session, then exit. .PP .SH ENVIRONMENT VARIABLES @@ -113,6 +119,15 @@ Controls if mpv is detached from the main process for playback, which can be use .TP \fBANI_CLI_SKIP_TITLE\fR Overrides the anime title to query for skip times. Can be any string value. Default is empty, resolving to the anime title as given by ani-cli. +.TP +\fBANI_CLI_SOURCE\fR +Controls which source(s) are searched. Valid options are all, allanime or simindad. Default is all. +.TP +\fBANI_CLI_SIMINDAD_USER\fR +Username for simindad.top. If set (together with ANI_CLI_SIMINDAD_PASS), login is non-interactive except for the captcha. +.TP +\fBANI_CLI_SIMINDAD_PASS\fR +Password for simindad.top. See ANI_CLI_SIMINDAD_USER. .PP .SH EPISODE SELECTION .PP diff --git a/packaging/aur/PKGBUILD b/packaging/aur/PKGBUILD new file mode 100644 index 000000000..e6cee1c84 --- /dev/null +++ b/packaging/aur/PKGBUILD @@ -0,0 +1,37 @@ +# Maintainer: mgh12453 <algorier.otp@gmail.com> +_pkgname=awesome-ani-cli +pkgname=awesome-ani-cli-git +pkgver=r1 +pkgrel=1 +pkgdesc="ani-cli fork that adds the simindad.top source (films, series, anime) alongside allanime" +arch=('any') +url="https://github.com/mgh12453/awesome-ani-cli" +license=('GPL3') +depends=('grep' 'sed' 'curl' 'openssl') +optdepends=( + 'mpv: default video player' + 'fzf: interactive menu (required for interactive use)' + 'vlc: alternative video player (--vlc)' + 'aria2: downloading (-d)' + 'ffmpeg: downloading m3u8 streams (-d)' + 'yt-dlp: downloading m3u8 streams (-d)' + 'ani-skip: skip intros (--skip)' + 'chafa: render the simindad login captcha inline in the terminal' +) +provides=('ani-cli') +conflicts=('ani-cli') +makedepends=('git') +source=("$_pkgname::git+https://github.com/mgh12453/awesome-ani-cli.git") +sha256sums=('SKIP') + +pkgver() { + cd "$srcdir/$_pkgname" + printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)" +} + +package() { + cd "$srcdir/$_pkgname" + install -Dm755 ani-cli "$pkgdir/usr/bin/ani-cli" + install -Dm644 ani-cli.1 "$pkgdir/usr/share/man/man1/ani-cli.1" + install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE" +} diff --git a/packaging/aur/README.md b/packaging/aur/README.md new file mode 100644 index 000000000..de7c1cf4f --- /dev/null +++ b/packaging/aur/README.md @@ -0,0 +1,56 @@ +# Publishing `awesome-ani-cli-git` to the AUR + +Run these on your Arch machine (they need `pacman`/`makepkg`, which this repo's CI box does not have). + +## 1. Create an AUR account + SSH key (one time) + +1. Register at https://aur.archlinux.org/register (any username, e.g. `mgh12453`). +2. Generate an SSH key if you don't have one: + ```sh + ssh-keygen -t ed25519 -C "aur" -f ~/.ssh/aur + ``` +3. Copy the **public** key and paste it into AUR → *My Account* → *SSH Public Key*: + ```sh + cat ~/.ssh/aur.pub + ``` +4. Tell SSH to use it for the AUR host — add to `~/.ssh/config`: + ``` + Host aur.archlinux.org + IdentityFile ~/.ssh/aur + User aur + ``` + +## 2. Create and push the package + +```sh +# clone the (empty) AUR repo for the package name +git clone ssh://aur@aur.archlinux.org/awesome-ani-cli-git.git +cd awesome-ani-cli-git + +# copy the PKGBUILD from this repo +cp /path/to/awesome-ani-cli/packaging/aur/PKGBUILD . + +# build + install locally to verify it works +makepkg -si + +# generate the metadata AUR requires +makepkg --printsrcinfo > .SRCINFO + +git add PKGBUILD .SRCINFO +git commit -m "Initial import of awesome-ani-cli-git" +git push +``` + +After the push it is installable with: + +```sh +yay -S awesome-ani-cli-git +``` + +## Notes + +- The package installs the binary as `ani-cli` and declares `provides=ani-cli` / + `conflicts=ani-cli`, so it cannot be co-installed with the upstream `ani-cli` + package (it is a superset). +- It is a `-git` package: `pkgver()` is derived from the git history, so bumping is + automatic — re-run `makepkg --printsrcinfo > .SRCINFO` and push when you cut changes.