Created
July 25, 2022 18:59
-
-
Save carterprince/8c742233e05988b4c6bb125802842f7e to your computer and use it in GitHub Desktop.
libby rewrite by stewie410 w/ modifications
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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