Skip to content

Instantly share code, notes, and snippets.

@carterprince
Created July 25, 2022 18:59
Show Gist options
  • Save carterprince/8c742233e05988b4c6bb125802842f7e to your computer and use it in GitHub Desktop.
Save carterprince/8c742233e05988b4c6bb125802842f7e to your computer and use it in GitHub Desktop.
libby rewrite by stewie410 w/ modifications
#!/usr/bin/env bash
#
# A simple CLI tool to quickly download books from Library Genesis
cleanup() {
rm --recursive --force "${stage}"
unset stage
}
show_help() {
cat << EOF
A simple CLI tool to quickly download books from Library Genesis
USAGE: ${0##*/} [OPTIONS] QUERY
OPTIONS:
-h, --help Show this help message
-f, --fzf Use fzf to select a book (default)
-r, --rofi Use rofi to select a book
-d, --dmenu Use dmenu to select a book
-m, --mirror NUM Use an alternative libgen mirror (default: 3)
-V, --no-view Don't view the result
-k, --to-kindle Convert file to .mobi and send to \$LIBBY_KINDLE_EMAIL via neo-/mutt (implies -V)
-u, --to-usb Copy file to root of \$LIBBY_USB via rsync (impies -V)
-o, --outdir PATH Output book to PATH instead of \$LIBBY_OUTPUT_DIR
EOF
}
trim() {
sed 's/^\s*//;s/\s*$//' | awk '{$1=$1};1' | xargs
}
require() {
command -v "${*}" &>/dev/null && return
printf '%s\n' "Missing required utility: '${*}'" >&2
return 1
}
getRawResults() {
local uri
uri="https://libgen.gs/index.php?req="
uri+="$(jq --slurp --raw-input --raw-output '@uri' <<< "${*}")"
uri+='&columns%5B%5D=t&columns%5B%5D=a&columns%5B%5D=s&columns%5B%5D=y&columns%5B%5D=i&objects%5B%5D=f&topics%5B%5D=l&topics%5B%5D=f&res=25'
curl --user-agent "${curlAgent}" --silent --fail --location "${uri}" | pup 'table:nth-last-of-type(1)'
}
getBookField() {
local ln filter
filter="tr:nth-of-type(${1}) td:nth-of-type(${2})"
case "${2}" in
1 | 2 )
(($1 == 1)) && ln="8"
pup --file "${3}" "${filter} text{}" | \
sed '/^\s*$/d' | \
awk --assign "num=${ln:-1}" '
NR == num {
print gensub(/^\s*|\s*$/, "", "G", $0)
}
'
;;
9 )
pup --file "${3}" "${filter} a:nth-of-type(${mirrornum}) attr{href}" | \
trim
;;
* )
pup --file "${3}" "${filter} text{}" | \
trim
;;
esac
}
parseRawResults() {
local i j idx
local -a book
idx="1"
for ((i = 1; i > 0; i++)); do
book[0]="$(getBookField "${i}" "1" "${*}")"
[[ -n "${book[0]}" ]] || return
for j in {2..5} {7..9}; do
book+=("$(getBookField "${i}" "${j}" "${*}")")
done
if ! [[ "${book[6],,}" =~ (epub|pdf) ]]; then
((idx++))
unset book
continue
fi
printf '%s;' "0" "${idx}"
for ((j = 0; j < ${#book[@]}; j++)); do
printf '%s;' "$((j + 1))" "${book[${j}]}"
done
printf '%s\n' ""
unset line
((idx++))
done
}
getUserSelection() {
local options userSelection
options="${stage}/userSelectionOptions"
# I made the separator in the style of ';2;' because I wanted a string that I was pretty sure could not appear in a title. A standalone semicolon may (?) appear in a title somewhere, so it might be best to make this two semicolons if possible. But thank you for the awk suggestion, that's definitely better.
awk --field-separator ";" '{
line = $2 ": " $4 " - " $6 " (" $8 ", " $10 ") [" $12 "]"
print gensub(/^\s*|\s*$/, "", "G", line)
}' "${*}" | \
recode --quiet "html..ascii" > "${options}"
# this used to say "record" instead of "recode", probably a mistake
case "${mode,,}" in
rofi )
userSelection="$(rofi \
-i \
-hover-select \
-me-select-entry "" \
-me-accept-entry "MousePrimary" \
-dmenu \
-p "Select book: " \
-theme-str '* {font: "monospace 12"; width:80%;}' \
-l "26" < "${options}")" || \
return
;;
dmenu )
userSelection="$(dmenu -l 26 -fn "monospace" -p "Select book: " < "${options}")" || \
return
;;
fzf )
userSelection="$(fzf < "${options}")" || \
return
;;
esac
echo "\n\n${userSelection}\n\n" > wow.txt
# this used to be a ';', but the string for prompting the user usually won't contain any semicolons. So I replaced this with a colon.
sed --quiet "$(cut --fields='1' --delimiter=':' <<< "${userSelection}")p" "${*}"
}
sendKindleEmail() {
local mobi emlCmd
mobi="${stage}/$(basename "${*%.*}").mobi"
require "ebook-converter" || return
if ! ebook-convert "${*}" "${mobi}"; then
printf '%s\n' "Failed to convert book: '${*}' -> '${mobi}'" >&2
return 1
fi
for i in "mutt" "neomutt" "mailx" "mail"; do
if command -v "${i}" &>/dev/null; then
emlCmd="${i}"
break
fi
done
if [[ -z "${emlCmd}" ]]; then
printf '%s\n' "Missing application to send file as email attachment: '${mobi}'"
return 1
fi
"${emlCmd}" -s "wow" -a "${mobi}" -- "${kindleEmail}" <<< ""
}
sendToUSB() {
require "rsync" || return
if ! mount | grep --quiet "${usb}"; then
printf '%s\n' "Usb is not mounted: '${usb}'" >&2
return 1
fi
mkdir --parents "${usb}"
rsync \
--times \
--executability \
--recursive \
--archive \
--verbose \
--human-readable \
--perms \
--progress \
"${*}" "${usb}/${*##*/}" || return
sync
}
getSelectedField() {
local num
case "${1,,}" in
title ) num="4";;
author ) num="6";;
publisher ) num="8";;
year ) num="10";;
size ) num="12";;
filetype ) num="16";; # offset by 2, originally 14
mirror ) num="18";; # offset by 2, originally 16
* )
printf '%s\n' "Unknown selected field: '${1,,}'" >&2
return 1
;;
esac
case "${1,,}" in
4 )
awk --field-separator ";" '{
title = gensub(/[0-9]{6,}\s?,?;?/, "", "G", $4)
title = gensub(/^\s|\s[1Xb]$/, "", "G", title)
title = gensub(/\s+/, "-", "G", title)
title = gensub(/[^a-zA-Z0-9._-]/, "", "G", title)
print gensub(/-+/, "-", "G", title)
}' <<< "${2,,}" | recode --quiet "html..ascii"
;;
* )
cut --fields="${num}" --delimiter=";" <<< "${2}"
;;
esac
}
download() {
local mirror tmp
tmp="${stage}/$(getSelectedField "title" "${*}").$(getSelectedField "filetype" "${*}")"
mirror="$(getSelectedField "mirror" "${*}")"
if [[ -z "${mirror}" ]]; then
printf '%s\n' "No mirror found!" >&2
return 1
fi
printf '%s\n' "Fetching mirror URI..."
# --agent is not an option in curl, I think you meant --user-agent
mirror="$(curl --silent --fail --user-agent "${curlAgent}" --location "${mirror}" | \
pup "#download a attr{href}" | \
sed 1q)"
if [[ -z "${mirror}" ]]; then
printf '%s\n' "Failed to retrieve mirror!" >&2
return 1
fi
printf '\n\n%s\n\n' "Downloading book from mirror: '${mirror}' -> '${tmp}'"
if ! curl --progress-bar --fail --location "${mirror}" > "${tmp}"; then
printf '%s\n' "Failed to download file!" >&2
return 1
fi
mkdir --parents "${outdir}"
mv --force "${tmp}" "${outdir}/${tmp##*/}"
printf '%s\n' "${outdir}/${tmp##*/}"
}
process() {
local raw books selected outfile err
raw="${stage}/raw.html"
books="${stage}/books.list"
printf '%s\n' "Fetching data from query..."
getRawResults "${*}" > "${raw}"
parseRawResults "${raw}" > "${books}"
if ! [[ -s "${books}" ]]; then
printf '%s\n' "No results found for query: '${*}'" >&2
return 1
fi
printf '%s\n' "Prompting user for selection..."
selected="$(getUserSelection "${books}")" || return
# nothing in the download function gets printed/shown to the user because the whole thing is called in a command subtitution
outfile="$(download "${selected}")" || return
if [[ -n "${toKindle}" ]] && ! sendKindleEmail "${outfile}"; then
printf '%s\n' "Failed to send kindle email" >&2
err+="1"
fi
if [[ -n "${toUSB}" ]] && ! sendToUSB "${outfile}"; then
printf '%s\n' "Failed to send to USB" >&2
err+="2"
fi
# Many of the function calls along the way include some print statements, and this interferes with the output of the download function.
# The $outfile variable includes the entire program output, not just the path to the outfile.
# As a hacky workaround, I just get the last line of output.
outfile="$(tail -1 <<< ${outfile})"
[[ -n "${view}" ]] && "${view}" "${outfile}"
return "${err:-0}"
}
main() {
local opts outdir usb kindleEmail mode mirrornum view toKindle toUSB curlAgent
outdir="${LIBBY_OUTPUT_DIR:-${HOME}/books}"
usb="${LIBBY_USB_DIR}"
kindleEmail="${LIBBY_KINDLE_EMAIL}"
mode="fzf"
mirrornum="3"
view="${LIBBY_VIEWER:-xdg-open}"
curlAgent='Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) QtWebEngine/5.15.5 Chrome/87.0.4280.144 Safari/537.36'
opts="$(getopt --options hfrdm:Vkuo: --longoptions help,fzf,rofi,dmenu,mirror:,no-view,to-kindle,to-usb,outdir: --name "${0##*/}" -- "${@}")"
eval set -- "${opts}"
while true; do
case "${1}" in
-h | --help ) show_help; return 0;;
-f | --fzf ) mode="fzf";;
-r | --rofi ) mode="rofi";;
-d | --dmenu ) mode="dmenu";;
-m | --mirror ) mirrornum="${2}"; shift;;
-V | --no-view ) unset view;;
-k | --to-kindle ) toKindle="1"; unset view;;
-u | --to-usb ) toUSB="1"; unset view;;
-o | --outdir ) outdir="${2}"; shift;;
-- ) shift; break;;
* ) break;;
esac
shift
done
if [[ -z "${*}" ]]; then
printf '%s\n' "No query specified" >&2
show_help
return 1
fi
require "pup" || return
require "curl" || return
require "${mode,,}" || return
process "${*}"
}
declare stage
stage="$(mktemp --directory)"
trap cleanup EXIT
main "${@}"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment