aniwrapper/ani-cli
ksyasuda 4853dea3d5 add sync database option
Added option to sync the history databases for the same user
across devices on the same network (or the internet if that is set up on
the machines)
2021-11-11 14:13:11 -08:00

733 lines
19 KiB
Bash
Executable File

#!/usr/bin/env bash
# Set config directory if not already set
if [[ -z "$XDG_CONFIG_HOME" ]]; then
XDG_CONFIG_HOME="$HOME/.config"
fi
VERBOSE=0
BASE_URL="https://gogoanime.cm"
CFG_DIR="$XDG_CONFIG_HOME/aniwrapper"
ROFI_CFG="meh.rasi"
HISTORY_DB="$XDG_CONFIG_HOME/aniwrapper/history.sqlite3"
# dependencies: grep, sed, curl, video_player, rofi, sqlite3
# video_player ( needs to be able to play urls )
player_fn="mpv"
prog="ani-cli"
logfile="${XDG_CACHE_HOME:-$HOME/.cache}/ani-hsts"
c_red="\033[1;31m"
c_green="\033[1;32m"
c_yellow="\033[1;33m"
c_blue="\033[1;34m"
c_magenta="\033[1;35m"
c_cyan="\033[1;36m"
c_reset="\033[0m"
help_text() {
while IFS= read line; do
printf "%s\n" "$line"
done <<-EOF
USAGE: $prog <query>
-h show this help text
-d download episode
-H continue where you left off
EOF
}
die() {
printf "$c_red%s$c_reset\n" "$*" >&2
exit 1
}
err() {
printf "$c_red%s$c_reset\n" "$*" >&2
}
search_anime() {
# get anime name along with its id
search=$(printf '%s' "$1" | tr ' ' '-')
curl -s "$BASE_URL//search.html" \
-G \
-d "keyword=$search" |
sed -n -E 's_^[[:space:]]*<a href="/category/([^"]*)" title="([^"]*)".*_\1_p'
}
search_eps() {
# get available episodes for anime_id
anime_id=$1
curl -s "$BASE_URL/category/$anime_id" |
sed -n -E '
/^[[:space:]]*<a href="#" class="active" ep_start/{
s/.* '\''([0-9]*)'\'' ep_end = '\''([0-9]*)'\''.*/\2/p
q
}
'
}
get_dpage_link() {
# get the download page url
anime_id=$1
ep_no=$2
curl -s "$BASE_URL/$anime_id-episode-$ep_no" |
sed -n -E '
/^[[:space:]]*<li class="dowloads">/{
s/.*href="([^"]*)".*/\1/p
q
}'
}
get_links() {
dpage_url="$1"
curl -s "$dpage_url" |
sed -n -E '
/href="([^"]*)" download>Download/{
s/href="([^"]*)" download>Download/\1/p
q
}' | tr -d ' '
}
dep_ch() {
for dep; do
if ! command -v "$dep" >/dev/null; then
die "Program \"$dep\" not found. Please install it."
fi
done
}
check_anime_name() {
# Maybe change to query the db for a similar name
# Check to make sure passed in name is not empty
[ "$VERBOSE" -eq 1 ] && printf "%s\n" "VAR: $1"
if [[ "$1" == "" ]] || [[ "$1" == " " ]] || [[ "$1" == "\n" ]]; then
[ "$VERBOSE" -eq 1 ] &&
printf "%s\n" "Passed in name is nothing"
return 1
fi
return 0
}
run_stmt() {
printf "%s\n" "$1" | sqlite3 -noheader "$HISTORY_DB"
}
#####################
## Database Code ##
#####################
check_db() {
# Return number of matches for anime/episode in db
# echo "$1 $2"
if [[ "$2" == "search" ]]; then
stmt="SELECT DISTINCT COUNT(*) \
FROM search_history \
WHERE anime_name = '$1';"
res=$(run_stmt "$stmt")
return "$res"
else
stmt="SELECT DISTINCT COUNT(*) \
FROM watch_history \
WHERE anime_name = '$1' \
AND episode_number = $2;"
res=$(run_stmt "$stmt")
return "$res"
fi
}
update_date() {
# updates search/watch date for passed in anime
datetime=$(date +'%Y-%m-%d %H:%M:%S')
stmt=""
if [[ "$2" == "search" ]]; then
stmt="UPDATE search_history SET search_date = '$datetime' \
WHERE anime_name = '$1';"
else
stmt="UPDATE watch_history SET watch_date = '$datetime' \
WHERE anime_name = '$1' \
AND episode_number = $2;"
fi
run_stmt "$stmt"
}
insert_history() {
# inserts into search/watch history db
# check the anime_name/id
check_anime_name "$1"
if [[ $? -ne 0 ]]; then
[ "$VERBOSE" -eq 1 ] &&
printf "%s\n" "ERROR: Anime name is none... exiting"
return 1
fi
datetime=$(date +'%Y-%m-%d %H:%M:%S')
check_db "$@"
if [[ $? -gt 0 ]]; then
if [[ "$2" == "search" ]]; then
[ "$VERBOSE" -eq 1 ] &&
printf "%s\n" "Already in search db... Updating search_date"
else
[ "$VERBOSE" -eq 1 ] &&
printf "%s\n" "Already in search db... Updating watch_date"
fi
update_date "$@"
else
if [[ "$2" == "search" ]]; then
stmt="INSERT INTO search_history(anime_name, search_date) \
VALUES('$1', '$datetime');"
run_stmt "$stmt"
else
stmt="INSERT INTO \
watch_history(anime_name, episode_number, watch_date) \
VALUES('$1', '$2', '$datetime');"
run_stmt "$stmt"
fi
fi
}
sync_search_history() {
cnt=0
while read -r line; do
anime_name=$(awk -F '|' '{print $2}' <<<"$line")
res=$(sqlite3 -noheader "$HISTORY_DB" <<<"SELECT anime_name FROM search_history WHERE anime_name = '$anime_name'")
if [[ "${res/ //}" == "" ]]; then
printf "%s\n" "Adding $line to search history..."
search_date=$(awk -F '|' '{print $3}' <<<"$line")
sqlite3 "$HISTORY_DB" <<<"INSERT INTO search_history(anime_name, search_date) VALUES('$anime_name', '$search_date')"
if [[ "$?" -ne 0 ]]; then
err "Error inserting row $line"
fi
((++cnt))
fi
done <<<"$(sqlite3 -noheader "$temp_db" <<<"SELECT DISTINCT * FROM search_history")"
printf "%s\n" "Inserted $cnt rows into search_history table"
}
sync_watch_history() {
cnt=0
while read -r line; do
anime_name=$(awk -F '|' '{print $2}' <<<"$line")
res=$(sqlite3 -noheader "$HISTORY_DB" <<<"SELECT anime_name FROM watch_history WHERE anime_name = '$anime_name'")
if [[ "${res/ //}" == "" ]]; then
printf "%s\n" "Adding $line to watch history..."
episode_num=$(awk -F '|' '{print $3}' <<<"$line")
watch_date=$(awk -F '|' '{print $NF}' <<<"$line")
sqlite3 "$HISTORY_DB" <<<"INSERT INTO watch_history(anime_name, episode_number, watch_date) VALUES('$anime_name', '$episode_num', '$watch_date')"
if [[ "$?" -ne 0 ]]; then
err "Error inserting row $line"
fi
((++cnt))
fi
done <<<"$(sqlite3 -noheader "$temp_db" <<<"SELECT DISTINCT * FROM watch_history")"
printf "%s\n" "Inserted $cnt rows into watch_history table"
}
#####################
## END of db code ##
#####################
# get query
get_search_query() {
# Query the anime to stream/download
# Get search history
# Construct string "<id>. <anime_name>"
stmt="SELECT DISTINCT id || '. ' || anime_name \
FROM search_history \
ORDER BY id DESC;"
hist=$(run_stmt "$stmt")
msg="Choose from list of searched anime below, or enter a unique name of an anime to search for"
span="<span foreground='peachpuff' style='italic' size='small' weight='light'>$msg</span>"
if [ -z "$*" ]; then
query=$(printf "%s\n" "${hist[@]}" |
rofi -dmenu -l 12 -p "Search Anime:" \
-mesg "$span" \
-config "$CFG_DIR/${ROFI_CFG}")
# Strip the list entry number from string
query="${query//[1-9]\. /}"
[ "$VERBOSE" -eq 1 ] && printf "%s\n" "Query: $query"
else
query=$*
fi
}
# create history file
[ -f "$logfile" ] || : >"$logfile"
#####################
## Anime selection ##
#####################
anime_selection() {
# Select anime from query results
search_results=$*
menu_format_string='[%d] %s\n'
menu_format_string_c1="$c_blue[$c_cyan%d$c_blue] $c_reset%s\n"
menu_format_string_c2="$c_blue[$c_cyan%d$c_blue] $c_yellow%s$c_reset\n"
count=1
menu=()
res=()
while read anime_id; do
# alternating colors for menu
[ $((count % 2)) -eq 0 ] &&
menu_format_string=$menu_format_string_c1 ||
menu_format_string=$menu_format_string_c2
# printf "$menu_format_string" "$count" "$anime_id"
menu+="$count. $anime_id\n"
idx=$((count - 1))
res["$idx"]="$anime_id"
count=$((count + 1))
done <<-EOF
$search_results
EOF
searched=""
cnt=0
# Get the comma separated list of indexes of anime that has been searched before
for anime in "${res[@]}"; do
[ "$VERBOSE" -eq 1 ] && printf "%s\n" "ANIME: $anime"
check_db "$anime" "search"
if [[ $? -gt 0 ]]; then
[ "$VERBOSE" -eq 1 ] && printf "%s\n" "SEARCHED BEFORE"
if [[ "$searched" == "" ]]; then
searched="$((cnt++))"
else
searched="$searched, $((cnt++))"
fi
fi
done
[ "$VERBOSE" -eq 1 ] && printf "%s\n" "SEARCHED: $searched"
# get the anime from indexed list
user_input=$(printf "${menu[@]}" |
rofi -dmenu -config "$CFG_DIR/${ROFI_CFG}" \
-a "$searched" \
-l 12 -i -p "Enter number:")
[ "$?" -ne 0 ] && return 1
choice=$(printf '%s\n' "$user_input" | awk '{print $1}')
# Remove period after number
choice="${choice::-1}"
name=$(printf '%s\n' "$user_input" | awk '{print $NF}')
if [[ "$VERBOSE" -eq 1 ]]; then
printf "%s\n" "CHOICE: $name"
printf "%s\n" "NAME: $name"
fi
# check both choice and name are set
if [[ ! "$choice" ]] || [[ ! "$name" ]]; then
die "Invalid choice... committing seppuku"
fi
# Check if input is a number
[ "$choice" -eq "$choice" ] 2>/dev/null || die "Invalid number entered"
insert_history "$name" "search"
printf "$c_reset"
# Select respective anime_id
count=1
while read anime_id; do
if [ $count -eq $choice ]; then
selection_id=$anime_id
break
fi
count=$((count + 1))
done <<-EOF
$search_results
EOF
[ -z "$selection_id" ] && die "Invalid number entered"
read last_ep_number <<-EOF
$(search_eps "$selection_id")
EOF
}
##################
## Ep selection ##
##################
episode_selection() {
# select episode number for anime
[ "$is_download" -eq 1 ] &&
printf "Range of episodes can be specified: start_number end_number\n"
[ "$VERBOSE" -eq 1 ] && printf "%s\n" "Anime ID: $anime_id"
stmt="SELECT DISTINCT episode_number \
FROM watch_history \
WHERE anime_name = '$anime_id';"
hist=$(run_stmt "$stmt")
if [[ "$VERBOSE" -eq 1 ]]; then
printf "%s\n" "HISTORY: ${hist[*]}"
fi
# Get Watch History for $anime_id as comma separated list
watch_history=""
for i in $hist; do
if [[ "$watch_history" == "" ]]; then
watch_history="$((--i))"
else
watch_history="$watch_history, $((--i))"
fi
done
[ "$VERBOSE" -eq 1 ] && printf "WATCH HISTORY: %s\n" "$watch_history"
# get user choice and set the start and end
msg='<span foreground="peachpuff" style="italic" size="small" weight="light">Range of episodes can be provided as: START_EPISODE - END_EPISODE</span>'
choice=$(
seq 1 "$last_ep_number" |
rofi -dmenu -l 12 \
-a "$watch_history" \
-p "Select Episode [1, $last_ep_number]:" \
-mesg "$msg" \
-config "$CFG_DIR/${ROFI_CFG}"
)
ep_choice_start=$(printf '%s\n' "${choice}" | awk '{print $1}')
ep_choice_end=$(printf '%s\n' "${choice}" | awk '{print $NF}')
[ "$VERBOSE" -eq 1 ] && printf "%s\n" "START: $ep_choice_start | END: $ep_choice_end"
if [[ -z "$ep_choice_start" ]] && [[ -z "$ep_choice_end" ]]; then
die "No episode range entered"
fi
# if only one episode was entered, set ep_choice_end to empty string so only selected episode plays
# otherwise plays from ep 1 - ep_choice_start
if [[ "$ep_choice_start" -eq "$ep_choice_end" ]]; then
ep_choice_end=""
fi
# read ep_choice_start ep_choice_end
printf "$c_reset"
}
open_episode() {
anime_id=$1
episode=$2
ddir="$3"
if [[ ! "$is_playlist" ]]; then
# clear the screen
printf '\x1B[2J\x1B[1;1H'
if [[ $episode -lt 1 ]] || [[ $episode -gt $last_ep_number ]]; then
err "Episode out of range"
stmt="SELECT DISTINCT episode_number \
FROM watch_history \
WHERE anime_name = '$anime_id' \
ORDER BY episode_number ASC;"
# hist=$(echo "$stmt" | sqlite3 "$HISTORY_DB" | awk '{ if ( NR > 2 ) { print } }')
hist=$(run_stmt "$stmt")
if [[ "$VERBOSE" -eq 1 ]]; then
echo "HISTORY: ${hist[*]}"
fi
episode=$(printf "%s\n" "${hist[@]}" |
rofi -dmenu -l 12 -p "Choose Episode:" \
-config "$CFG_DIR/${ROFI_CFG}")
printf "$c_reset"
fi
fi
[ "$VERBOSE" -eq 1 ] && printf "Getting data for episode %d\n" $episode
insert_history "$anime_id" "$episode"
dpage_url=$(get_dpage_link "$anime_id" "$episode")
video_url=$(get_links "$dpage_url")
case $video_url in
*streamtape*)
# If direct download not available then scrape streamtape.com
BROWSER=${BROWSER:-firefox}
printf "scraping streamtape.com\n"
video_url=$(curl -s "$video_url" | sed -n -E '
/^<script>document/{
s/^[^"]*"([^"]*)" \+ '\''([^'\'']*).*/https:\1\2\&dl=1/p
q
}
')
;;
esac
if [ $is_download -eq 0 ]; then
# write anime and episode number
# sed -E "
# s/^${selection_id}\t[0-9]+/${selection_id}\t$((episode + 1))/
# " "$logfile" >"${logfile}.new" && mv "${logfile}.new" "$logfile"
setsid -f $player_fn --http-header-fields="Referer: $dpage_url" "$video_url" >/dev/null 2>&1
else
printf "Downloading episode $episode ...\n"
printf "%s\n" "$video_url"
# add 0 padding to the episode name
episode=$(printf "%03d" $episode)
{
cd "${ddir/ //}" || die "Could not enter directory $ddir"
mkdir -p "$anime_id" || die "Could not create directory"
cd "$anime_id" || die "Could not enter subdirectory $ddir/$anime_id"
ffmpeg -headers "Referer: $dpage_url" -i "$video_url" \
-codec copy "${anime_id}-${episode}.mkv" >/dev/null 2>&1 &&
notify-send "Downloaded episode: $episode" ||
notify-send "Download failed episode: $episode"
}
fi
}
############
# Start Up #
############
# to clear the colors when exited using SIGINT
trap "printf '$c_reset'" INT HUP
dep_ch "$player_fn" "curl" "sed" "grep" "sqlite3" "rofi"
# option parsing
is_download=0
list_history=0
scrape=query
download_dir="."
is_playlist=0
playlist_remove=0
playlist_add=0
playlist_file="$CFG_DIR/playlists/playlist.txt"
while getopts 'hd:HlpadP:s' OPT; do
case "$OPT" in
h)
help_text
exit 0
;;
d)
is_download=1
download_dir="$OPTARG"
if [ "$VERBOSE" -eq 1 ]; then
printf "%s\n" "DOWNLOAD DIR: $download_dir"
fi
;;
H)
scrape=history
;;
l)
list_history=1
;;
p)
scrape=playlist
is_playlist=1
;;
a)
is_add=1
scrape=add
;;
d)
is_delete=1
scrape=delete
;;
P)
is_playlist=1
# remove spaces from $OPTARG
playlist_file="${OPTARG/ //}"
[ -z "$playlist_file" ] && die "Enter in path to playlist"
[ "$VERBOSE" -eq 1 ] && printf "%s\n" "$playlist_file"
$player_fn "$playlist_file"
exit 0
;;
s)
printf "%s" "Enter username for remote user: "
read -r username
printf "%s" "Enter host for remote user: "
read -r host
# printf "%s" "Enter connection string for remote user in the form user@host: "
# read -r connection_str
connection_str="$username@$host"
printf "%s" "Enter port to connect to remote host with or leave blank for default (22): "
read -r port
if [[ "${port/ //}" == "" ]]; then
PORT=22
else
PORT="$port"
fi
if [[ ! "@" != *"$connection_str" ]]; then
printf "%s\n" "Enter in full connection string to remote user in the form: user@host"
exit 1
fi
printf "%s\n" "Syncing database with: $connection_str on port $PORT"
temp_db="/tmp/aniwrapper_tmp_history.sqlite3"
scp -P "$PORT" "$connection_str:$XDG_CONFIG_HOME/aniwrapper/history.sqlite3" "$temp_db"
if [[ "$?" -ne 0 ]]; then
printf "%s\n" "Error getting database file from remote host"
exit 1
fi
sync_search_history && sync_watch_history
exit 0
;;
*)
printf "%s\n" "Invalid option"
exit 1
;;
esac
done
shift $((OPTIND - 1))
########
# main #
########
if [[ "$list_history" -eq 1 ]]; then
stmt="SELECT DISTINCT anime_name \
FROM search_history \
ORDER BY search_date DESC"
hist=$(run_stmt "$stmt")
printf "%s\n" "${hist[@]}" |
rofi -config "$CFG_DIR/${ROFI_CFG}" \
-dmenu -l 12 -i -p "Search History"
exit 0
fi
case $scrape in
query)
get_search_query "$*"
search_results=$(search_anime "$query")
[ -z "$search_results" ] && die "No search results found"
anime_selection "$search_results"
[ $? -ne 0 ] && die "No anime selection found"
episode_selection
;;
history)
# search_results=$(sed -n -E 's/\t[0-9]*//p' "$logfile")
stmt="SELECT DISTINCT anime_name FROM watch_history ORDER BY watch_date DESC"
search_results=$(printf "%s\n" "$stmt" | sqlite3 -noheader "$HISTORY_DB")
[ -z "$search_results" ] && die "History is empty"
anime_selection "${search_results[@]}"
[ $? -ne 0 ] && die "No anime selection found"
stmt="SELECT episode_number FROM watch_history ORDER BY watch_date DESC LIMIT 1"
run_stmt "$stmt"
ep_choice_start=$?
echo "EPISODE: $ep_choice_start"
# ep_choice_start=$(sed -n -E "s/${selection_id}\t//p" "$logfile")
;;
playlist)
lines=$(cat "$playlist_file" | wc -l)
[ "$VERBOSE" -eq 1 ] && printf "%s%d\n" "Num lines in playlist: " "$lines"
if [[ "$lines" -eq 0 ]]; then
get_search_query "$*"
search_results=$(search_anime "$query")
[ -z "$search_results" ] && die "No search results found"
anime_selection "$search_results"
[ $? -ne 0 ] && die "No anime selection found"
episode_selection
else
line=($(sed '1q;d' "$playlist_file"))
if [[ "${#line[@]}" -ne 2 ]]; then
die "Something went wrong with the playlist file... exiting"
fi
selection_id="${line[0]}"
episodes=($selection_id)
ep_choice_start="${line[1]}"
ep_choice_end=""
read last_ep_number <<-EOF
$(search_eps "$selection_id")
EOF
[ "$VERBOSE" -eq 1 ] && printf "Anime: %s Episode: %d\n" "$episodes" "$ep_choice_start"
[ "$VERBOSE" -eq 1 ] && printf "Episodes: %s\n" "${episodes[@]}"
fi
;;
add)
get_search_query "$*"
search_results=$(search_anime "$query")
[ -z "$search_results" ] && die "No search results found"
anime_selection "$search_results"
[ $? -ne 0 ] && die "No anime selection found"
episode_selection
;;
delete) ;;
esac
{ # checking input
[ "$ep_choice_start" -eq "$ep_choice_start" ] 2>/dev/null || die "Invalid number entered"
episodes=$ep_choice_start
if [ -n "$ep_choice_end" ]; then
[ "$ep_choice_end" -eq "$ep_choice_end" ] 2>/dev/null || die "Invalid number entered"
# create list of episodes to download/watch
episodes=$(seq $ep_choice_start $ep_choice_end)
fi
}
# add anime to history file
# grep -q -w "${selection_id}" "$logfile" ||
# printf "%s\t%d\n" "$selection_id" $((episode + 1)) >>"$logfile"
for ep in $episodes; do
if [[ "$is_add" -eq 1 ]]; then
if [[ "$VERBOSE" -eq 1 ]]; then
printf "%s\n" "ID: $selection_id"
printf "%s\n" "EPISODES: $episodes"
fi
printf "%s\n" "$selection_id $ep" >>"$playlist_file"
[ "$VERBOSE" -eq 1 ] && printf "%s\n" "Added to playlist file"
exit 0
else
open_episode "$selection_id" "$ep" "$download_dir"
if [[ "$is_playlist" -eq 1 ]]; then
sed -i '1d' "$playlist_file"
fi
fi
done
episode=${ep_choice_end:-$ep_choice_start}
choice=''
while :; do
printf "\n${c_green}Currently playing %s episode ${c_cyan}%d/%d\n" "$selection_id" $episode $last_ep_number
printf "$c_blue[${c_cyan}%s$c_blue] $c_yellow%s$c_reset\n" "n" "next episode"
printf "$c_blue[${c_cyan}%s$c_blue] $c_magenta%s$c_reset\n" "p" "previous episode"
printf "$c_blue[${c_cyan}%s$c_blue] $c_yellow%s$c_reset\n" "s" "select episode"
printf "$c_blue[${c_cyan}%s$c_blue] $c_magenta%s$c_reset\n" "r" "replay current episode"
printf "$c_blue[${c_cyan}%s$c_blue] $c_red%s$c_reset\n" "q" "exit"
printf "${c_blue}Enter choice:${c_green} "
printf "$c_reset"
read choice
printf "$c_reset"
case $choice in
n)
episode=$((episode + 1))
;;
p)
episode=$((episode - 1))
;;
s)
printf "${c_blue}Choose episode $c_cyan[1-%d]$c_reset:$c_green " $last_ep_number
read episode
printf "$c_reset"
[ "$episode" -eq "$episode" ] 2>/dev/null || die "Invalid number entered"
;;
r) ;;
q)
break
;;
*)
die "invalid choice"
;;
esac
open_episode "$selection_id" "$episode" "$download_dir"
done