├── releases ├── books-0.6.tar.gz ├── books-0.6.1.tar.gz ├── books-0.6.2.tar.gz ├── books-0.6.3.tar.gz ├── books-0.6.4.tar.gz ├── books-0.6.5.tar.gz ├── books-0.7.1.tar.gz └── books-0.7.2.tar.gz ├── screenshots ├── screenshot_region-20162728012748.png └── screenshot_region-20162828002811.png ├── books_functions ├── tm ├── import_metadata.sh ├── import_metadata ├── refresh_libgen ├── classify ├── update_libgen ├── LICENSE ├── README.md └── books /releases/books-0.6.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yetangitu/books/HEAD/releases/books-0.6.tar.gz -------------------------------------------------------------------------------- /releases/books-0.6.1.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yetangitu/books/HEAD/releases/books-0.6.1.tar.gz -------------------------------------------------------------------------------- /releases/books-0.6.2.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yetangitu/books/HEAD/releases/books-0.6.2.tar.gz -------------------------------------------------------------------------------- /releases/books-0.6.3.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yetangitu/books/HEAD/releases/books-0.6.3.tar.gz -------------------------------------------------------------------------------- /releases/books-0.6.4.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yetangitu/books/HEAD/releases/books-0.6.4.tar.gz -------------------------------------------------------------------------------- /releases/books-0.6.5.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yetangitu/books/HEAD/releases/books-0.6.5.tar.gz -------------------------------------------------------------------------------- /releases/books-0.7.1.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yetangitu/books/HEAD/releases/books-0.7.1.tar.gz -------------------------------------------------------------------------------- /releases/books-0.7.2.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yetangitu/books/HEAD/releases/books-0.7.2.tar.gz -------------------------------------------------------------------------------- /screenshots/screenshot_region-20162728012748.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yetangitu/books/HEAD/screenshots/screenshot_region-20162728012748.png -------------------------------------------------------------------------------- /screenshots/screenshot_region-20162828002811.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yetangitu/books/HEAD/screenshots/screenshot_region-20162828002811.png -------------------------------------------------------------------------------- /books_functions: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash disable=SC2154 2 | 3 | # UTITLITIES 4 | 5 | # find tool, returns the first|one|found, exit with error message if none found 6 | find_tool () { 7 | IFS='|' read -ra tools <<< "$*" 8 | 9 | found=0 10 | 11 | for tool in "${tools[@]}"; do 12 | if [[ -n $(which "$tool") ]]; then 13 | found=1 14 | break 15 | fi 16 | done 17 | 18 | if [[ "$found" -eq 0 ]]; then 19 | if [[ ${#tools[@]} -gt 1 ]]; then 20 | exit_with_error "missing programs: $*; install at least one of these: ${tools[*]} and try again" 21 | else 22 | exit_with_error "missing program: $1; please install and try again" 23 | fi 24 | fi 25 | 26 | echo "$tool" 27 | } 28 | 29 | url_available () { 30 | url="$1" 31 | dl_tool=$(find_tool "curl|wget") 32 | 33 | case "$dl_tool" in 34 | curl) 35 | ${torsocks:-} curl --output /dev/null --silent --fail -r 0-0 "$url" 36 | ;; 37 | wget) 38 | ${torsocks:-} wget -q --spider "$url" 39 | ;; 40 | *) 41 | exit_with_error "unknown download tool ${dl_tool}" 42 | ;; 43 | esac 44 | } 45 | 46 | add_cron_job () { 47 | job="$*" 48 | 49 | (crontab -l ; echo "*/1 * * * * $job") 2>/dev/null | sort | uniq | crontab - 50 | } 51 | 52 | # leave
and
 to enable some simple formatting tasks
 53 | strip_html () {
 54 |         #echo "$*"|sed -e 's/
/\n/g;s/<[^>]*>//g;s/\n/
/g' 55 | echo "$*" 56 | } 57 | 58 | is_true () { 59 | val="${1,,}" 60 | if [[ "${val:0:1}" == "y" || "$val" -gt 0 ]]; then 61 | true 62 | else 63 | false 64 | fi 65 | } 66 | 67 | # dummmy cleanup function 68 | cleanup () { 69 | true 70 | } 71 | 72 | # echo error message to stderr and terminate main 73 | exit_with_error () { 74 | echo -e "$(basename "$0"): $*" >&2 75 | 76 | kill -s TERM "$TOP_PID" 77 | } 78 | 79 | trap_error () { 80 | cleanup 81 | 82 | exit 1 83 | } 84 | 85 | trap_clean () { 86 | cleanup 87 | 88 | exit 89 | } 90 | 91 | _log () { 92 | msg="$*" 93 | logdir="${XDG_STATE_HOME:-$HOME/.state}/books" 94 | logfile=$(basename "$0").log 95 | mkdir -p "$logdir" 96 | echo "$(date -Iseconds): $msg" >> "$logdir/$logfile" 97 | } 98 | 99 | log_err () { 100 | _log "E: $*" 101 | } 102 | 103 | log_warn () { 104 | _log "W: $*" 105 | } 106 | 107 | log_info () { 108 | _log "I: $*" 109 | } 110 | 111 | log_debug () { 112 | _log "D: $*" 113 | } 114 | 115 | # DATABASE 116 | dbx () { 117 | db="$1" 118 | shift 119 | 120 | mysql=$(find_tool "mysql") 121 | 122 | if [ $# -gt 0 ]; then 123 | "$mysql" -N -Bsss -h "$dbhost" -P "$dbport" -u "$dbuser" "$db" -e "$*" 124 | else 125 | "$mysql" -N -Bsss -h "$dbhost" -P "$dbport" -u "$dbuser" "$db" 126 | fi 127 | } 128 | 129 | # LOCKING 130 | 131 | exlock () { 132 | cmd="$1" 133 | 134 | lockfile="/var/lock/$(basename "$0")" 135 | lockfd=99 136 | 137 | flock=$(find_tool "flock") 138 | 139 | case "$cmd" in 140 | prepare) 141 | eval "exec $lockfd<>\"$lockfile\"" 142 | trap 'exlock nolock' EXIT 143 | ;; 144 | 145 | now) 146 | $flock -xn $lockfd 147 | ;; 148 | 149 | lock) 150 | $flock -x $lockfd 151 | ;; 152 | 153 | shlock) 154 | $flock -s $lockfd 155 | ;; 156 | 157 | unlock) 158 | $flock -u $lockfd 159 | ;; 160 | 161 | nolock) 162 | $flock -u $lockfd 163 | $flock -xn $lockfd && rm -f "$lockfile" 164 | trap_clean 165 | ;; 166 | 167 | *) 168 | exit_with_error "unknown lock command: $cmd" 169 | ;; 170 | esac 171 | } 172 | -------------------------------------------------------------------------------- /tm: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # shellcheck disable=SC2034,SC1090,SC2207 3 | 4 | shopt -s extglob 5 | 6 | export TOP_PID=$$ 7 | 8 | version="0.1.2" 9 | release="20210512" 10 | 11 | main () { 12 | config=${XDG_CONFIG_HOME:-$HOME/.config}/tm.conf 13 | netrc=~/.tm-netrc 14 | tm_host="localhost:4081" 15 | # source config file if it exists 16 | [[ -f ${config} ]] && source "${config}" 17 | 18 | declare -a commands=($(declare -F|grep tm_|sed -e 's/.*tm_\(\S\+\).*/\1/')) 19 | declare -a programs=($(declare -F|grep tm_|sed -e 's/.*\(tm_\S\+\).*/\1/;s/_/-/g')) 20 | 21 | 22 | while getopts "a:chH:kln:" OPTION 23 | do 24 | case $OPTION in 25 | k) 26 | create_symlinks 27 | exit 28 | ;; 29 | a) 30 | tm_add "${OPTARG}" 31 | exit 32 | ;; 33 | l) 34 | tm_ls 35 | exit 36 | ;; 37 | n) 38 | netrc="${OPTARG}" 39 | ;; 40 | H) 41 | tm_host="${OPTARG}" 42 | ;; 43 | c) 44 | if [[ ! -f "${config}" ]]; then 45 | cat <<-EOT > "${config}" 46 | netrc="$netrc" 47 | tm_host="$tm_host" 48 | EOT 49 | else 50 | exit_with_error "-c: config file ${config} exists, either remove it or edit it directly" 51 | fi 52 | ;; 53 | h) 54 | help 55 | exit 56 | ;; 57 | *) 58 | exit_with_error "unknown option $OPTION" 59 | ;; 60 | esac 61 | done 62 | 63 | # shift out options 64 | shift $((OPTIND-1)) 65 | 66 | cmd="${1//-/_}" 67 | program=$(basename "$0") 68 | 69 | IFS='|' 70 | if [[ $cmd =~ ${commands[*]} ]]; then 71 | unset IFS 72 | shift 73 | tm_"$cmd" "$@" 74 | elif [[ $program =~ ${programs[*]} ]]; then 75 | unset IFS 76 | ${program//-/_} "$@" 77 | else 78 | unset IFS 79 | exit_with_error "no such command: $cmd\navailable commands: ${commands[*]//_/-}" 80 | fi 81 | } 82 | 83 | # commands 84 | 85 | tm_cmd () { 86 | if [[ -n $(which transmission-remote) ]]; then 87 | transmission-remote "$tm_host" -N "$netrc" "$@" 88 | else 89 | exit_with_error "transmission-remote not found, please install it first (apt install transmission-cli)" 90 | fi 91 | } 92 | 93 | tm_help () { 94 | tm_cmd -h 95 | } 96 | 97 | tm_add () { 98 | tm_cmd -a "$@" 99 | } 100 | 101 | tm_add_selective () { 102 | torrent="$1" 103 | shift 104 | files="${*,,}" 105 | 106 | keep=0 107 | count=0 108 | 109 | if [[ -z "$torrent" || -z "$files" ]]; then 110 | echo 'use: tm-add-selective [,file2,file3,file4...]' 111 | exit 1 112 | fi 113 | 114 | check_torrent "$torrent" 115 | 116 | tm_cmd --start-paused 117 | btih=$(tm_torrent_hash "$torrent") 118 | 119 | # check if torrent is already downloading 120 | if tm_active "$btih"; then 121 | running=1 122 | tm_stop "$btih" 123 | else 124 | tm_add "$torrent" 125 | fi 126 | 127 | if tm_info "$btih" > /dev/null; then 128 | count=$(tm_file_count "$btih") 129 | # if the torrent only has 1 file it does not make sense to do a selective download... 130 | if [[ $count -gt 1 ]]; then 131 | if [[ $running -eq 0 ]]; then 132 | # need to keep at least 1 file active, otherwise transmission removes the torrent 133 | tm_cmd -t "$btih" -G 1-$((count-1)) 134 | fi 135 | while read -r id; do 136 | [[ $id -eq 0 ]] && keep=1 137 | tm_cmd -t "$btih" -g "$id" 138 | done < <(tm_cmd -t "$btih" -f|grep -E "${files/,/|}"|cut -d ':' -f 1) 139 | [[ $keep -eq 0 && $running -eq 0 ]] && tm_cmd -t "$btih" -G 0 140 | fi 141 | else 142 | echo "error adding torrent" 143 | exit 1 144 | fi 145 | tm_cmd --no-start-paused 146 | tm_start "$btih" 147 | } 148 | 149 | tm_remove () { 150 | tm_cmd -t "$@" -r 151 | } 152 | 153 | tm_start () { 154 | if tm_active "$@"; then 155 | tm_cmd -t "$@" -s 156 | fi 157 | } 158 | 159 | tm_stop () { 160 | tm_cmd -t "$@" -S 161 | } 162 | 163 | tm_info () { 164 | tm_cmd -t "$@" -i 165 | } 166 | 167 | tm_files () { 168 | tm_cmd -t "$@" -f 169 | } 170 | 171 | tm_ls () { 172 | tm_cmd -l 173 | } 174 | 175 | tm_file_count () { 176 | tm_files "$1"|head -1|sed 's/.* (\([[:digit:]]\+\) files):/\1/' 177 | } 178 | 179 | tm_active () { 180 | tm_cmd -t "$@" -ip|grep -q Address 181 | } 182 | 183 | # torrent file related commands 184 | 185 | tm_torrent_show () { 186 | check_torrent "$@" 187 | transmission-show "$@" 188 | } 189 | 190 | tm_torrent_files () { 191 | tm_torrent_show "$@"|awk '/^FILES/ {start=1}; NF>1 && start==1 {print $0}'|sed -e 's/^\s\+\(.*\) ([0-9.]\+ .B)$/\1/' 192 | } 193 | 194 | tm_torrent_hash () { 195 | tm_torrent_show "$@"|awk ' /^\s+Hash:/ {print $2}' 196 | } 197 | 198 | # helper functions 199 | 200 | exit_with_error () { 201 | echo -e "$(basename "$0"): $*" >&2 202 | 203 | kill -s TERM $TOP_PID 204 | } 205 | 206 | check_torrent () { 207 | if ! (file "$1"|grep -i bittorrent)>/dev/null; then 208 | exit_with_error "$1 is not a torrent file" 209 | fi 210 | } 211 | 212 | create_symlinks () { 213 | basedir="$(dirname "$0")" 214 | sourcefile="$(readlink -e "$0")" 215 | prefix=$(basename "$sourcefile") 216 | for cmd in "${commands[@]}"; do 217 | name="${prefix}-${cmd//_/-}" 218 | if [[ ! -e "$basedir/$name" ]]; then 219 | ln -s "$sourcefile" "$basedir/$name" 220 | fi 221 | done 222 | 223 | exit 224 | } 225 | 226 | help () { 227 | sourcefile="$(readlink -e "$0")" 228 | prefix=$(basename "$sourcefile") 229 | echo "$(basename "$(readlink -f "$0")")" "version $version" 230 | cat <<- EOF 231 | 232 | Use: $prefix COMMAND OPTIONS [parameters] 233 | $prefix-COMMAND OPTIONS [parameters] 234 | 235 | A helper script for transmission-remote and related tools, adding some 236 | functionality like selective download etc. 237 | 238 | PROGRAMS/COMMANDS 239 | 240 | EOF 241 | 242 | for cmd in "${programs[@]}"; do 243 | echo -e " $cmd\r\t\t\t${cmd/$prefix-}" 244 | done 245 | 246 | cat <<- EOF 247 | 248 | OPTIONS 249 | 250 | -k create symbolic links 251 | creates links to all supported commands 252 | e.g. $prefix-cmd, $prefix-ls, $prefix-add, ... 253 | links are created in the directory where $prefix resides 254 | 255 | -n NETRC set netrc ($netrc) 256 | 257 | -H HOST set host ($tm_host) 258 | 259 | -c create a config file using current settings (see -n, -H) 260 | 261 | -l execute command 'ls' 262 | 263 | -a TORR execute command 'add' 264 | 265 | -h this help message 266 | 267 | EXAMPLES 268 | 269 | In all cases it is possible to replace $prefix-COMMAND with $prefix COMMAND 270 | 271 | show info about running torrents: 272 | 273 | $ $prefix-ls 274 | 275 | add a torrent or a magnet link: 276 | 277 | $prefix-add /path/to/torrent/file.torrent 278 | $prefix-add 'magnet:?xt=urn:btih:123...' 279 | 280 | add a torrent and selectivly download two files 281 | this only works with torrent files (i.e. not magnet links) for now 282 | 283 | $prefix-add-selective /path/to/torrent/file.torrent filename1,filename2 284 | 285 | show information about a running torrent, using its btih or ID: 286 | 287 | $prefix-show f0a7524fe95910da462a0d1b11919ffb7e57d34a 288 | $prefix-show 21 289 | 290 | show files for a running torrent identified by btih (can also use ID) 291 | 292 | $prefix-files f0a7524fe95910da462a0d1b11919ffb7e57d34a 293 | 294 | stop a running torrent, using its ID (can also use btih) 295 | 296 | $prefix-stop 21 297 | 298 | get btih for a torrent file 299 | 300 | $prefix-torrent-hash /path/to/torrent/file.torrent 301 | 302 | remove a torrent from transmission 303 | 304 | $prefix-remove 21 305 | 306 | execute any transmission-remote command - notice the double dash 307 | see man transmission-remote for more info on supported commands 308 | 309 | 310 | $prefix-cmd -- -h 311 | $prefix cmd -h 312 | 313 | 314 | CONFIGURATION FILES 315 | 316 | $config 317 | 318 | $prefix can be configured by editing the script itself or the configuration file: 319 | 320 | netrc=~/.tm-netrc 321 | tm_host="transmission-host.example.org:4081" 322 | 323 | values set in the configuration file override those in the script 324 | 325 | EOF 326 | } 327 | 328 | main "$@" 329 | -------------------------------------------------------------------------------- /import_metadata.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #shellcheck disable=SC2034,SC1090 3 | # 4 | # import_metadata - import metadata to libgen/libgen_fiction 5 | # 6 | # input: a [single line of|file containg] CSV-ordered metadata 7 | 8 | shopt -s extglob 9 | trap "trap_error" TERM 10 | trap "trap_clean" EXIT 11 | export TOP_PID=$$ 12 | 13 | version="0.1.0" 14 | release="20210518" 15 | 16 | functions="$(dirname "$0")/books_functions" 17 | if [ -f "$functions" ]; then 18 | source "$functions" 19 | else 20 | echo "$functions not found" 21 | exit 1 22 | fi 23 | 24 | main () { 25 | 26 | exlock now || exit 1 27 | 28 | coproc coproc_ddc { coproc_ddc; } 29 | coproc coproc_fast { coproc_fast; } 30 | 31 | # PREFERENCES 32 | config=${XDG_CONFIG_HOME:-$HOME/.config}/books.conf 33 | 34 | dbhost="localhost" 35 | dbport="3306" 36 | db="libgen" 37 | dbuser="libgen" 38 | 39 | tmpdir=$(mktemp -d '/tmp/import_metadata.XXXXXX') 40 | update_sql="${tmpdir}/update_sql" 41 | 42 | # input field filters 43 | declare -A filter=( 44 | [md5]=filter_md5 45 | [ddc]=filter_ddc 46 | [lcc]=filter_ddc 47 | [nlm]=filter_ddc 48 | [fast]=filter_fast 49 | [author]=filter_fast 50 | [title]=filter_fast 51 | ) 52 | 53 | # redirect OCLC [key] to field 54 | declare -A redirect=( 55 | [fast]="tags" 56 | ) 57 | 58 | # used to get index for field / field for index 59 | keys="md5 ddc lcc nlm fast author title" 60 | declare -A headers 61 | index=0 62 | for key in $keys;do 63 | headers["$key"]=$index 64 | ((index++)) 65 | done 66 | 67 | declare -A tables=( 68 | [libgen]="updated" 69 | [libgen_fiction]="fiction" 70 | ) 71 | 72 | # source config file if it exists 73 | [[ -f ${config} ]] && source "${config}" 74 | 75 | declare -a csvdata 76 | declare -a csv 77 | 78 | while getopts "d:f:F:ns:vh" OPTION; do 79 | case $OPTION in 80 | d) 81 | if [ -n "${tables[$OPTARG]}" ]; then 82 | db="$OPTARG" 83 | else 84 | exit_with_error "-d $OPTARG: no such database" 85 | fi 86 | ;; 87 | f) 88 | for n in $OPTARG; do 89 | if [ -n "${headers[$n]}" ]; then 90 | fields+="${fields:+ }$n" 91 | else 92 | exit_with_error "no such field: $n" 93 | fi 94 | done 95 | ;; 96 | F) 97 | if [ -f "$OPTARG" ]; then 98 | csvfile="$OPTARG" 99 | else 100 | exit_with_error "-f $OPTARG: no such file" 101 | fi 102 | ;; 103 | s) 104 | sqlfile="$OPTARG" 105 | if ! touch "$sqlfile"; then 106 | exit_with_error "-s $OPTARG: can not write to file" 107 | fi 108 | ;; 109 | n) 110 | dry_run=1 111 | ;; 112 | v) 113 | ((verbose++)) 114 | ;; 115 | h) 116 | help 117 | exit 118 | ;; 119 | *) 120 | exit_with_error "unknown option: -$OPTION" 121 | ;; 122 | esac 123 | done 124 | 125 | shift $((OPTIND-1)) 126 | 127 | [[ -z "$db" ]] && exit_with_error "no database defined, use -d database" 128 | [[ -z "$fields" ]] && exit_with_error "no fields defined, use -f 'field1 field2' or -f field1 -f field2" 129 | 130 | if [ -z "$dry_run" ]; then 131 | declare -A current_fields='('$(get_current_fields "$db")')' 132 | for field in $fields; do 133 | [[ -n "${redirect[$field]}" ]] && field="${redirect[$field]}" 134 | if [[ ! "${!current_fields[*]}" =~ "${field,,}" ]]; then 135 | exit_with_error "field $field not in database $db" 136 | fi 137 | done 138 | fi 139 | 140 | if [[ -n "$csvfile" ]]; then 141 | readarray -t csvdata < <(cat "$csvfile") 142 | else 143 | readarray -t csvdata <<< "$*" 144 | fi 145 | 146 | printf "start transaction;\n" > "${update_sql}" 147 | 148 | for line in "${csvdata[@]}"; do 149 | readarray -d',' -t csv <<< "$line" 150 | 151 | if [[ "$verbose" -ge 2 ]]; then 152 | index=0 153 | for key in $keys; do 154 | echo "${key^^}: $(${filter[$key]} "$key")" 155 | ((index++)) 156 | done 157 | fi 158 | 159 | sql="$(build_sql)" 160 | 161 | printf "$sql\n" >> "$update_sql" 162 | 163 | if [[ "$verbose" -ge 3 ]]; then 164 | echo "$sql" 165 | fi 166 | 167 | [[ -n "$sqlfile" ]] && printf "$sql\n" >> "$sqlfile" 168 | 169 | unset key 170 | unset sql 171 | csv=() 172 | done 173 | 174 | printf "commit;\n" >> "$update_sql" 175 | [[ -z "$dry_run" ]] && dbx "$db" < "$update_sql" 176 | } 177 | 178 | filter_md5 () { 179 | field="$1" 180 | printf "${csv[${headers[$field]}]}" 181 | } 182 | 183 | filter_ddc () { 184 | field="$1" 185 | 186 | # without coprocess 187 | # echo "${csv[${headers[$field]}]}"|sed 's/"//g;s/[[:blank:]]\+/,/g' 188 | 189 | # with coprocess (30% faster) 190 | printf "${csv[${headers[$field]}]}\n" >&${coproc_ddc[1]} 191 | IFS= read -ru ${coproc_ddc[0]} value 192 | printf "$value" 193 | } 194 | 195 | coproc_ddc () { 196 | sed -u 's/"//g;s/[[:blank:]]\+/,/g' 197 | } 198 | 199 | 200 | filter_fast () { 201 | field="$1" 202 | 203 | # without coprocess 204 | # echo "${csv[${headers[$field]}]}"|base64 -d|sed -u 's/\(["\\'\'']\)/\\\1/g;s/\r/\\r/g;s/\n/\\n/g;s/\t/\\t/g' 205 | 206 | # with coprocess (30% faster) 207 | # base64 can not be used as a coprocess due to its uncurable buffering addiction 208 | value=$(printf "${csv[${headers[$field]}]}"|base64 -d) 209 | printf "$value\n" >&${coproc_fast[1]} 210 | IFS= read -ru ${coproc_fast[0]} value 211 | printf "$value" 212 | } 213 | 214 | coproc_fast () { 215 | sed -u 's/\(["\\'\'']\)/\\\1/g;s/\r/\\r/g;s/\n/\\n/g;s/\t/\\t/g' 216 | } 217 | 218 | 219 | get_field () { 220 | field="$1" 221 | value="${csv[${headers[$field]}]}" 222 | if [ -n "${filters[$field]}" ]; then 223 | printf "$value"|eval "${filters[$field]}" 224 | else 225 | printf "$value" 226 | fi 227 | } 228 | 229 | get_current_fields () { 230 | db="$1" 231 | for table in "${tables[$db]}"; do 232 | dbx "$db" "describe $table;"|awk '{printf "[%s]=%s ",tolower($1),"'$table'"}' 233 | done 234 | } 235 | 236 | build_sql () { 237 | sql="" 238 | for field in $fields; do 239 | data=$(${filter[$field]} "$field") 240 | if [ -n "$data" ]; then 241 | [[ -n "${redirect[$field]}" ]] && field="${redirect[$field]}" 242 | sql+="${sql:+,}${field^^}='${data}'" 243 | fi 244 | done 245 | 246 | if [ -n "$sql" ]; then 247 | printf "update ${tables[$db]} set $sql where MD5='$(${filter['md5']} md5)';" 248 | fi 249 | } 250 | 251 | cleanup () { 252 | rm -rf "${tmpdir}" 253 | } 254 | 255 | # HELP 256 | 257 | help () { 258 | echo "$(basename "$(readlink -f "$0")")" "version $version" 259 | cat <<- EOHELP 260 | 261 | Use: import_metadata [OPTIONS] -d database -f "field1 field2" [-F CSVDATAFILE | single line of csv data ] 262 | 263 | Taking either a single line of CSV-formatted data or a file containing 264 | such data, this tool can be used to update a libgen / libgen_fiction 265 | database with fresh metadata. It can also be used to produce SQL (using 266 | the -s sqlfile option) which can be used to update multiple database 267 | instances. 268 | 269 | CSV data format: 270 | 271 | $(hkeys=${keys^^};echo ${hkeys// /,}) 272 | 273 | CSV field names are subject to redirection to database field names, 274 | currently these redirections are active (CSV -> DB): 275 | 276 | $(for field in "${!redirect[@]}";do echo " ${field^^} -> ${redirect[$field]^^}";done) 277 | 278 | OPTIONS: 279 | 280 | -d DB define which database to use (libgen/libgen_fiction) 281 | 282 | -f 'field1 field2' 283 | -f field1 -f field2 284 | 285 | define which fields to update 286 | 287 | -F CSVFILE 288 | 289 | define CSV input file 290 | 291 | -s SQLFILE 292 | 293 | write SQL to SQLFILE 294 | 295 | -n do not update database 296 | use with -s SQLFILE to produce SQL for later use 297 | use with -vv to see data from CSVFILE 298 | use with -vvv to see SQL 299 | 300 | -v verbosity 301 | repeat to increase verbosity 302 | 303 | -h this help message 304 | 305 | Examples 306 | 307 | $ import_metadata -d libgen -F csv/update-0000 -f 'ddc lcc fast' 308 | 309 | update database 'libgen' using data from CSV file csv/update-0000, 310 | fields DDC, LCC and FAST (which is redirected to libgen.Tags) 311 | 312 | $ for f in csv/update-*;do 313 | import_metadata -d libgen -s sql/metadata.sql -n -f 'ddc lcc fast' -F "\$f" 314 | done 315 | 316 | create SQL (-s sql/metadata.sql) to update database using fields 317 | DDC, LCC and FAST from all files matching glob csv/update-*, 318 | do not update database (-n option) 319 | 320 | 321 | EOHELP 322 | } 323 | 324 | exlock prepare || exit 1 325 | 326 | main "$@" 327 | -------------------------------------------------------------------------------- /import_metadata: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # import_metadata - import metadata to libgen/libgen_fiction 4 | 5 | import base64 6 | import csv 7 | import getopt 8 | import os 9 | import pymysql 10 | import re 11 | import sys 12 | 13 | 14 | version="0.1.0" 15 | release="20210521" 16 | 17 | def exit_with_error(msg): 18 | sys.exit(os.path.basename(sys.argv[0])+" "+msg) 19 | 20 | def try_file(f,p): 21 | try: 22 | fp=open(f,p) 23 | fp.close() 24 | return True 25 | except IOError as x: 26 | exit_with_error(str(x)) 27 | 28 | def main(): 29 | 30 | config = { 31 | 'dbhost': 'base.unternet.org', 32 | 'dbport': '3306', 33 | 'db': '', 34 | 'dbuser': 'libgen' 35 | } 36 | 37 | verbose = 0 38 | dry_run = False 39 | sqlfile = None 40 | csvfile = None 41 | use_fields=[] 42 | sql=[] 43 | 44 | re_csv=re.compile('(\s+)') 45 | 46 | # read books config file (a bash source file) and interpret it 47 | # works only for single-line static declarations (no shell code) 48 | def read_conf(conf): 49 | if 'APPDATA' in os.environ: 50 | confdir = os.environ['APPDATA'] 51 | elif 'XDG_CONFIG_HOME' in os.environ: 52 | confdir = os.environ['XDG_CONFIG_HOME'] 53 | else: 54 | confdir = os.path.join(os.environ['HOME'], '.config') 55 | 56 | conffile = os.path.join(confdir, 'books.conf') 57 | 58 | if try_file(conffile,'r'): 59 | line_re = re.compile('(?:export )?(?P\w+)(?:\s*\=\s*)(?P.+)') 60 | value_re = re.compile('(?P^[^#]+)(?P#.*)?$') 61 | for line in open(conffile): 62 | m = line_re.match(line) 63 | if m: 64 | name = m.group('name') 65 | value = '' 66 | if m.group('value'): 67 | value = m.group('value') 68 | m = value_re.match(value) 69 | if m: 70 | value=m.group('value') 71 | 72 | conf[name]=value.strip('\"').strip("\'") 73 | 74 | return conf 75 | 76 | config=read_conf(config) 77 | 78 | def to_itself(field): 79 | return field 80 | 81 | def to_csv(field): 82 | return re_csv.sub(',', field) 83 | 84 | def to_sqlescape(field): 85 | return pymysql.escape_string(base64.b64decode(field).decode().rstrip()) 86 | 87 | fields=['md5','ddc','lcc','nlm','fast','author','title'] 88 | 89 | filters = { 90 | 'md5': to_itself, 91 | 'ddc': to_csv, 92 | 'lcc': to_csv, 93 | 'nlm': to_csv, 94 | 'fast': to_sqlescape, 95 | 'author': to_sqlescape, 96 | 'title': to_sqlescape 97 | } 98 | 99 | redirects = { 100 | 'fast': 'tags' 101 | } 102 | 103 | tables = { 104 | 'libgen': 'updated', 105 | 'libgen_fiction': 'fiction' 106 | } 107 | 108 | 109 | def redirect(field): 110 | if field in redirects: 111 | return redirects[field] 112 | else: 113 | return field 114 | 115 | def usage(): 116 | msg=[] 117 | def fmt_dict(lst): 118 | for key in lst: 119 | msg.append(str(key+" -> "+lst[key]).upper()) 120 | return msg 121 | 122 | print(helpmsg.format( 123 | progname=os.path.basename(sys.argv[0]), 124 | version="v."+version, 125 | csvfields=','.join(fields).upper(), 126 | redirects=fmt_dict(redirects) 127 | )) 128 | sys.exit() 129 | 130 | try: 131 | opts, args = getopt.getopt(sys.argv[1:], "d:f:F:H:u:U:ns:vh") 132 | except getopt.GetoptError as err: 133 | print(str(err)) 134 | usage() 135 | 136 | for o, a in opts: 137 | if o == "-v": 138 | verbose+=1 139 | elif o in ("-h"): 140 | usage() 141 | elif o in ("-d"): 142 | config['db'] = a 143 | elif o in ("-f"): 144 | for f in a.split(','): 145 | if f in fields: 146 | use_fields.append(f) 147 | else: 148 | exit_with_error("-f "+f+" : no such field") 149 | elif o in ("-F"): 150 | if try_file(a,'r'): 151 | csvfile = a 152 | elif o in ("-H"): 153 | config['dbhost'] = a 154 | elif o in ("-U"): 155 | config['dbuser'] = a 156 | elif o in ("-n"): 157 | dry_run = True 158 | elif o in ("-s"): 159 | if try_file(a,'w'): 160 | sqlfile = a 161 | else: 162 | exit_with_error("unhandled option") 163 | 164 | if len(sys.argv) <= 2: 165 | exit_with_error("needs at least 3 parameters: -d database -f field1,field2 -F csvfile") 166 | 167 | if not config['db'] or config['db'] not in tables: 168 | exit_with_error("-d "+config['db']+": no such database") 169 | 170 | if not use_fields: 171 | exit_with_error("no fields defined, use -f field1 -f field2") 172 | 173 | with open(csvfile) as cf: 174 | reader = csv.DictReader(cf, fieldnames=fields) 175 | 176 | if verbose >= 1: 177 | sys.stdout.writelines(['\n#----DATA----------------------\n\n']) 178 | 179 | for row in reader: 180 | if verbose >= 1: 181 | for field in fields: 182 | print(field.upper()+": "+filters[field](row[field])) 183 | print("") 184 | 185 | updates="" 186 | comma="" 187 | for field in use_fields: 188 | value=filters[field](row[field]) 189 | if value: 190 | if updates: 191 | comma="," 192 | updates+=comma+redirect(field).upper()+"='"+value+"'" 193 | 194 | if updates: 195 | sql.append("update updated set "+updates+" where md5='"+row['md5']+"';\n") 196 | else: 197 | if verbose: 198 | print("-- fields "+str(use_fields)+" not defined for md5:"+row['md5']) 199 | 200 | if sql: 201 | if sqlfile: 202 | fp=open(sqlfile,'a') 203 | fp.writelines([ 204 | '-- csvfile: '+csvfile+'\n', 205 | '-- database: '+config['db']+'\n', 206 | '-- fields: '+str(use_fields)+'\n', 207 | '-- command: '+' '.join(sys.argv)+'\n', 208 | 'start transaction;\n' 209 | ]) 210 | fp.writelines(sql) 211 | fp.writelines(['commit;\n']) 212 | fp.close() 213 | 214 | if verbose >= 2: 215 | sys.stdout.writelines(['\n#----SQL-----------------------\n\n']) 216 | sys.stdout.writelines(sql) 217 | 218 | if not dry_run: 219 | conn=pymysql.connect( 220 | read_default_file='~/.my.cnf', 221 | host=config['dbhost'], 222 | port=config['dbport'], 223 | user=config['dbuser'], 224 | database=config['db'] 225 | ) 226 | 227 | with conn: 228 | with conn.cursor() as cursor: 229 | for line in sql: 230 | cursor.execute(line) 231 | 232 | conn.commit() 233 | 234 | helpmsg = """ 235 | {progname} {version} 236 | 237 | Use: {progname} [OPTIONS] -d database -f "field1,field2" -F CSVDATAFILE 238 | 239 | Taking a file containing lines of CSV-formatted data, this tool can be 240 | used to update a libgen / libgen_fiction database with fresh metadata. 241 | It can also be used to produce SQL (using the -s sqlfile option) which 242 | can be used to update multiple database instances. 243 | 244 | CSV data format: 245 | 246 | {csvfields} 247 | 248 | Fields FAST, AUTHOR and TITLE should be base64-encoded. 249 | 250 | CSV field names are subject to redirection to database field names, 251 | currently these redirections are active (CSV -> DB): 252 | 253 | {redirects} 254 | 255 | OPTIONS: 256 | 257 | -d DB define which database to use (libgen/libgen_fiction) 258 | 259 | -f field1,field2 260 | -f field1 -f field2 261 | define which fields to update 262 | 263 | -F CSVFILE 264 | define CSV input file 265 | 266 | -s SQLFILE 267 | write SQL to SQLFILE 268 | 269 | -n do not update database 270 | use with -s SQLFILE to produce SQL for later use 271 | use with -v to see data from CSVFILE 272 | use with -vv to see SQL 273 | 274 | -v verbosity 275 | repeat to increase verbosity 276 | 277 | -h this help message 278 | 279 | Examples 280 | 281 | $ import_metadata -d libgen -F csv/update-0000 -f 'ddc lcc fast' 282 | 283 | update database 'libgen' using data from CSV file csv/update-0000, 284 | fields DDC, LCC and FAST (which is redirected to libgen.Tags) 285 | 286 | $ for f in csv/update-*;do 287 | {progname} -d libgen -s "$f.sql" -n -f 'ddc,lcc,fast' -F "$f" 288 | done 289 | 290 | create SQL (-s "$f.sql") to update database using fields 291 | DDC, LCC and FAST from all files matching glob csv/update-*, 292 | do not update database (-n option) 293 | """ 294 | 295 | if __name__ == "__main__": 296 | main() 297 | 298 | -------------------------------------------------------------------------------- /refresh_libgen: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # shellcheck disable=SC2034,SC1090 3 | # 4 | # refresh libgen databases from dump files 5 | 6 | version="0.6.2" 7 | release="20210601" 8 | 9 | trap "trap_error" TERM 10 | trap "trap_clean" EXIT 11 | export TOP_PID=$$ 12 | 13 | functions="$(dirname "$0")/books_functions" 14 | if [ -f "$functions" ]; then 15 | source "$functions" 16 | else 17 | echo "$functions not found" 18 | exit 1 19 | fi 20 | 21 | main () { 22 | 23 | exlock now || exit 1 24 | 25 | # PREFERENCES 26 | config=${XDG_CONFIG_HOME:-$HOME/.config}/books.conf 27 | 28 | # maximum age (in days) of database dump file to use 29 | max_age=5 30 | 31 | # database server to use 32 | dbhost="localhost" 33 | dbport="3306" 34 | dbuser="libgen" 35 | 36 | # where to get updates. A change here probably necessitates a change in the urls array 37 | # as dump file names can be site-specific. 38 | base="http://libgen.rs/dbdumps/" 39 | 40 | # database names 41 | declare -A databases=( 42 | [libgen]=libgen 43 | [compact]=libgen_compact 44 | [fiction]=libgen_fiction 45 | ) 46 | 47 | # source config file if it exists 48 | [[ -f ${config} ]] && source "${config}" 49 | 50 | # (mostly) END OF PREFERENCES 51 | 52 | # urls for dump files (minus datestamp and extension) 53 | declare -A urls=( 54 | [libgen]="${base}/libgen" 55 | [compact]="${base}/libgen_compact" 56 | [fiction]="${base}/fiction" 57 | ) 58 | 59 | # sql to get time last modified for database 60 | declare -A lastmodified=( 61 | [libgen]="select max(timelastmodified) from updated;" 62 | [compact]="select max(timelastmodified) from updated;" 63 | [fiction]="select max(timelastmodified) from fiction;" 64 | ) 65 | 66 | declare -A filter=( 67 | [libgen]='s/DEFINER[ ]*=[ ]*[^*]*\*/\*/;s/DEFINER[ ]*=[ ]*[^*]*PROCEDURE/PROCEDURE/;s/DEFINER[ ]*=[ ]*[^*]*FUNCTION/FUNCTION/' 68 | [compact]='s/DEFINER[ ]*=[ ]*[^*]*\*/\*/;s/DEFINER[ ]*=[ ]*[^*]*PROCEDURE/PROCEDURE/;s/DEFINER[ ]*=[ ]*[^*]*FUNCTION/FUNCTION/' 69 | ) 70 | 71 | # sql to run BEFORE update 72 | declare -A before_update=( 73 | ) 74 | 75 | # sql to run AFTER update 76 | declare -A after_update=( 77 | [compact]="drop trigger updated_edited;create table description (id int(11) not null auto_increment, md5 varchar(32) not null default '', descr varchar(20000) not null default '', toc mediumtext not null, TimeLastModified timestamp not null default current_timestamp on update current_timestamp, primary key (id), unique key md5_unique (md5) using btree, key time (timelastmodified) using btree, key md5_hash (md5) using hash);" 78 | ) 79 | 80 | declare -A options=( 81 | [wget]="-nv" 82 | [wget_verbose]="" 83 | [unrar]="-inul" 84 | [unrar_verbose]="" 85 | ) 86 | 87 | 88 | tmpdir=$(mktemp -d /var/tmp/libgen.XXXXXX) 89 | 90 | unrar=$(find_tool "unrar") 91 | wget=$(find_tool "wget") 92 | w3m=$(find_tool "w3m") 93 | 94 | while getopts "a:cd:efhH:knP:u:U:v@" OPTION 95 | do 96 | case $OPTION in 97 | n) 98 | no_action=1 99 | ;; 100 | f) 101 | force_refresh=1 102 | ;; 103 | d) 104 | max_age=${OPTARG} 105 | ;; 106 | u) 107 | if [[ -v "databases[${OPTARG}]" ]]; then 108 | dbs+=" ${OPTARG}" 109 | else 110 | exit_with_error "-u ${OPTARG}: no such database" 111 | fi 112 | ;; 113 | v) 114 | pv=$(find_tool "pv") 115 | verbose="_verbose" 116 | ;; 117 | H) 118 | dbhost="${OPTARG}" 119 | ;; 120 | P) 121 | dbport="${OPTARG}" 122 | ;; 123 | U) 124 | dbuser="${OPTARG}" 125 | ;; 126 | c) 127 | if [[ ! -f "${config}" ]]; then 128 | cat <<-EOT > "${config}" 129 | dbhost=${dbhost} 130 | dbport=${dbport} 131 | dbuser=${dbuser} 132 | base=${base} 133 | EOT 134 | else 135 | exit_with_error "-c: config file ${config} exists, either remove it or edit it directly" 136 | fi 137 | exit 138 | ;; 139 | e) 140 | if [[ -f "$config" ]]; then 141 | if [[ "$VISUAL" ]]; then "$VISUAL" "$config"; 142 | elif [[ "$EDITOR" ]]; then "$EDITOR" "$config"; 143 | else exit_with_error "-e: no editor configured, can not edit $config" 144 | fi 145 | else 146 | exit_with_error "-e: config file does not exist, create is first (see -c)" 147 | fi 148 | exit 149 | ;; 150 | a) 151 | if url_available "${OPTARG}"; then 152 | base="${OPTARG}" 153 | else 154 | exit_with_error "-a ${OPTARG}: repository not available" 155 | fi 156 | ;; 157 | @) 158 | torsocks=$(find_tool "torsocks") 159 | export TORSOCKS_TOR_PORT=$OPTARG 160 | ;; 161 | k) 162 | keep_downloaded_files=1 163 | ;; 164 | h) 165 | help 166 | exit 167 | ;; 168 | *) 169 | exit_with_error "unknown option: $OPTION" 170 | ;; 171 | esac 172 | done 173 | 174 | [[ -z ${dbs} ]] && dbs="${!databases[*]}" 175 | 176 | pushd "$tmpdir" >/dev/null || exit_with_error "can not change directory to $tmpdir" 177 | for db in ${dbs}; do 178 | database=${databases[$db]} 179 | if [[ $(db_exists "$database") ]]; then 180 | db_dump=$(is_available "${db}" "${max_age}") 181 | if [[ -n $db_dump ]]; then 182 | [[ -n $verbose ]] && echo "update available for ${db}: ${db_dump}" 183 | if [[ -z ${no_action} ]]; then 184 | $torsocks "$wget" "${options[$wget${verbose}]}" "${db_dump}" 185 | $unrar "${options[$unrar${verbose}]}" x "$(basename "${db_dump}")" 186 | [[ -n "${filter[$db]}" ]] && run_filter "$($unrar lb "$(basename "${db_dump}")")" "${filter[$db]}" 187 | drop_tables=$(drop_table_sql "${database}") 188 | [[ -n $drop_tables ]] && dbx "${database}" "${drop_tables}" 189 | [[ -n ${before_update[$db]} ]] && dbx "${database}" "${before_update[$db]}" 190 | [[ -n ${filter[$db]} ]] && filter_command="|sed -e '${filter[$db]}'" 191 | if [[ -n $verbose ]]; then 192 | echo "importing $(basename "${db_dump}") into ${database}" 193 | $pv "$($unrar lb "$(basename "${db_dump}")")" | dbx "${database}" 194 | else 195 | dbx "${database}" < "$($unrar lb "$(basename "${db_dump}")")" 196 | fi 197 | [[ -n ${after_update[$db]} ]] && dbx "${database}" "${after_update[$db]}" 198 | fi 199 | else 200 | [[ -n $verbose ]] && echo "no update available for ${db}" 201 | fi 202 | else 203 | echo "database '$database' does not exist, please create it before attempting to refresh" >&2 204 | fi 205 | done 206 | popd >/dev/null || exit_with_error "popd failed?" 207 | } 208 | 209 | # check whether there is a dump file which is more recent than the current database and no older 210 | # than $max_age 211 | is_available () { 212 | db="$1" 213 | max_age="$2" 214 | 215 | db_age=$(db_age "$db") 216 | 217 | age=0 218 | 219 | while [[ $age -lt $db_age && $age -lt $max_age ]]; do 220 | timestamp=$(date -d "@$(($(date +%s) - $((60*60*24*age))))" +%Y-%m-%d) 221 | result=$($w3m -dump "${base}" | awk '{ print $1 }'|grep "$(basename "${urls[$db]}_${timestamp}.rar")") 222 | [[ -n $result ]] && break 223 | ((age++)) 224 | done 225 | 226 | [[ -n $result ]] && echo "$(dirname "${urls[$db]}")"/"${result}" 227 | } 228 | 229 | # drop tables to prepare database for refresh 230 | drop_table_sql () { 231 | database="$1" 232 | dbx "$database" "SELECT concat('DROP TABLE IF EXISTS ', table_name, ';') FROM information_schema.tables WHERE table_schema = '$database';" 233 | } 234 | 235 | # returns database name if it exists, nothing otherwise 236 | db_exists () { 237 | database="$1" 238 | dbx "$database" "select schema_name from information_schema.schemata where schema_name='$database';" 2>/dev/null 239 | } 240 | 241 | # return database age in days 242 | db_age () { 243 | db="$1" 244 | now=$(date +%s) 245 | age=0 246 | if [[ "$force_refresh" -gt 0 ]]; then 247 | age=$max_age 248 | else 249 | db_last_modified=$(date -d "$(dbx "$database" "${lastmodified[$db]}")" +%s) 250 | age=$(((now-db_last_modified)/60/60/24)) 251 | fi 252 | echo -n $age 253 | } 254 | 255 | # run filter on dump 256 | run_filter () { 257 | dump_file="$1" 258 | flt="$2" 259 | if [[ -n $verbose ]]; then 260 | echo "running '$flt' on '$dump_file'" 261 | fi 262 | sed -i -e "$flt" "$dump_file" 263 | } 264 | 265 | check_credentials () { 266 | if [[ ! $(dbx "" "select true;" 2>/dev/null) ]]; then 267 | exit_with_error "database connection error, bad username or password?" 268 | fi 269 | } 270 | 271 | url_available () { 272 | url="$1" 273 | $torsocks "$wget" -q --spider "$url" 274 | } 275 | 276 | cleanup () { 277 | if [[ ! -v keep_downloaded_files ]]; then 278 | rm -rf "${tmpdir}" 279 | else 280 | echo "-k option active, temporary directory ${tmpdir} not removed" 281 | fi 282 | } 283 | 284 | help () { 285 | echo "$(basename "$(readlink -f "$0")")" "version $version" 286 | cat <<- EOT 287 | 288 | Usage: refresh_libgen OPTIONS 289 | 290 | Performs a refresh from a database dump file for the chosen libgen databases. 291 | 292 | Make sure the database credentials are configured (in \$HOME/.my.cnf) before 293 | using this tool. 294 | 295 | -n do not refresh database 296 | use together with '-v' to check if recent dumps are available 297 | -f force refresh, use this on first install 298 | -v be verbose about what is being updated 299 | -d DAYS only use database dump files no older than DAYS days (default: ${max_age}) 300 | -u DBS refresh DBS databases (default: ${!databases[@]}) 301 | 302 | -H DBHOST database host (${dbhost}) 303 | -P DBPORT database port (${dbport}) 304 | -U DBUSER database user (${dbuser}) 305 | -a REPO dump repository (${base}) 306 | -c create a config file using current settings (see -H, -P, -U, -R) 307 | -e edit config file 308 | 309 | -@ TORPORT use tor (through torsocks) to connect to libgen server 310 | -k keep downloaded files after exit 311 | -h this help message 312 | 313 | EOT 314 | } 315 | 316 | exlock prepare || exit 1 317 | 318 | main "$@" 319 | -------------------------------------------------------------------------------- /classify: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #shellcheck disable=SC2034,SC1090 3 | # 4 | # classify - return classification data for ISBN (etc.) or MD5 (from libgen/libgen_fiction) 5 | 6 | shopt -s extglob 7 | trap "trap_error" TERM 8 | trap "trap_clean" EXIT 9 | export TOP_PID=$$ 10 | 11 | version="0.5.1" 12 | release="20210601" 13 | 14 | functions="$(dirname "$0")/books_functions" 15 | if [ -f "$functions" ]; then 16 | source "$functions" 17 | else 18 | echo "$functions not found" 19 | exit 1 20 | fi 21 | 22 | main () { 23 | # PREFERENCES 24 | config=${XDG_CONFIG_HOME:-$HOME/.config}/books.conf 25 | # OCLC classify API 26 | oclc="http://classify.oclc.org/classify2/Classify" 27 | 28 | declare -A API=( 29 | [response]='/classify/response/@code' 30 | [owi]='/classify/works/work[1]/@owi' 31 | [wi]='/classify/works/work[1]/@wi' 32 | [fast]='join(/classify/recommendations/fast/headings/heading,",")' 33 | [ddc]='join(/classify/recommendations/ddc/mostPopular/@nsfa)' 34 | [lcc]='join(/classify/recommendations/lcc/mostPopular/@nsfa)' 35 | [nlm]='join(/classify/recommendations/nlm/mostPopular/@sfa)' 36 | [author]='/classify/work/@author' 37 | [authors]='join(/classify/authors/author," | ")' 38 | [title]='/classify/work/@title' 39 | ) 40 | 41 | declare -A filters=( 42 | [filename]="sed -e 's/[^-[:alnum:]:;?!.,+@#%]/_/g;s/^\([-_]\)*//'" 43 | ) 44 | 45 | declare -A tables=( 46 | [libgen]="updated" 47 | [libgen_fiction]="fiction" 48 | ) 49 | 50 | xidel=$(find_tool "xidel") 51 | curl=$(find_tool "curl") 52 | xq="$xidel -s" 53 | 54 | request="" 55 | 56 | TMPDIR="/tmp" 57 | xml=$(mktemp -p $TMPDIR classify.XXXXX) 58 | 59 | # source config file if it exists 60 | [[ -f ${config} ]] && source "${config}" 61 | 62 | while getopts "owdlnfatVAD:C:X:G@:h" OPTION; do 63 | case $OPTION in 64 | o) 65 | request="$request owi" 66 | ;; 67 | w) 68 | request="$request wi" 69 | ;; 70 | d) 71 | request="$request ddc" 72 | ;; 73 | l) 74 | request="$request lcc" 75 | ;; 76 | n) 77 | request="$request nlm" 78 | ;; 79 | f) 80 | request="$request fast" 81 | ;; 82 | a) 83 | request="$request author" 84 | ;; 85 | t) 86 | request="$request title" 87 | ;; 88 | V) 89 | verbose=1 90 | ;; 91 | D) 92 | db="$OPTARG" 93 | ;; 94 | C) 95 | [ -z "$db" ] && exit_with_error "use -D to define which database to use" 96 | build_csv=1 97 | md5="$OPTARG" 98 | idents=$(get_identifiers "$db" "$md5") 99 | [ -z "$idents" ] && exit_with_error "no identifier found in $db for MD5 = $md5" 100 | ;; 101 | X) 102 | save_xml="$OPTARG" 103 | [[ ! -d "$save_xml" ]] && exit_with_error "Save XML (-X $OPTARG): directory does not exist?" 104 | ;; 105 | A) 106 | request="author title fast owi wi ddc lcc nlm" 107 | verbose=1 108 | ;; 109 | G) 110 | ((debug++)) 111 | ;; 112 | @) 113 | torsocks=$(find_tool "torsocks") 114 | export TORSOCKS_TOR_PORT=${OPTARG} 115 | ;; 116 | h) 117 | help 118 | exit 119 | ;; 120 | 121 | *) 122 | exit_with_error "unknown option: $OPTION" 123 | ;; 124 | esac 125 | done 126 | 127 | shift $((OPTIND-1)) 128 | [ -z "$idents" ] && idents="$1" 129 | 130 | IFS=',' read -ra idarr <<< "$idents" 131 | 132 | for ident in "${idarr[@]}"; do 133 | 134 | [[ -n "$debug" ]] && echo "trying $ident..." 135 | 136 | get_xml "$xml" "stdnbr=${ident// }" 137 | response=$(get "response" "$xml") 138 | 139 | case "$response" in 140 | 0) 141 | success=1 142 | break 143 | ;; 144 | 2) 145 | success=1 146 | break 147 | ;; 148 | 4) 149 | wi=$(get "wi" "$xml") 150 | get_xml "$xml" "wi=$wi" 151 | if [[ $(get "response" "$xml") =~ 0|2 ]]; then 152 | success=1 153 | break 154 | else 155 | continue 156 | fi 157 | ;; 158 | *) 159 | continue 160 | ;; 161 | esac 162 | done 163 | 164 | [[ -z "$success" ]] && exit_with_error "no valid response for identifier(s) $idents" 165 | 166 | if [[ -n "$save_xml" ]]; then 167 | [[ -z "$md5" ]] && exit_with_error "Save XML (-X) only works with a defined MD5 (-C MD5)" 168 | cp "$xml" "$save_xml/$md5.xml" 169 | fi 170 | 171 | if [[ -n "$debug" ]]; then 172 | cat "$xml" 173 | fi 174 | 175 | if [[ -n "$build_csv" ]]; then 176 | build_csv "$db" "$md5" "$xml" 177 | else 178 | show_data "$request" 179 | fi 180 | } 181 | 182 | get_xml () { 183 | xml="$1" 184 | shift 185 | query="$*" 186 | $torsocks "$curl" -s "${oclc}?summary=false&${query}" --output "$xml" 187 | } 188 | 189 | get () { 190 | parameter="$1" 191 | xml="$2" 192 | shift 2 193 | filter="$*" 194 | [[ -z "$filter" ]] && filter='cat -' 195 | $xq "$xml" -e "${API[$parameter]}"|eval "$filter" 196 | } 197 | 198 | get_identifiers () { 199 | db="$1" 200 | md5="$2" 201 | 202 | declare -A sql_identifier=( 203 | [libgen]="select IdentifierWODash from updated where md5='${md5}';" 204 | [libgen_fiction]="select Identifier from fiction where md5='${md5}';" 205 | ) 206 | 207 | sql="${sql_identifier[$db]}" 208 | dbx "$db" "$sql" 209 | } 210 | 211 | show_data () { 212 | request="$*" 213 | 214 | for parameter in $request; do 215 | data=$(get "$parameter" "$xml") 216 | [[ -n "$verbose" ]] && legend="${parameter^^}: " 217 | [[ -n "$data" ]] && echo "${legend}${data}" 218 | done 219 | } 220 | 221 | build_csv () { 222 | db="$1" 223 | md5="$2" 224 | xml="$3" 225 | 226 | updates="${md5}" 227 | 228 | for parameter in ddc lcc nlm; do 229 | data=$(get "$parameter" "$xml") 230 | updates+=",\"${data}\"" 231 | done 232 | 233 | for parameter in fast author title; do 234 | data=$(get "$parameter" "$xml" "base64 -w0") 235 | updates+=",${data}" 236 | done 237 | 238 | echo "$updates" 239 | } 240 | 241 | cleanup () { 242 | base=$(basename "$xml") 243 | rm -f "$TMPDIR/$base" 244 | } 245 | 246 | help () { 247 | cat <<-EOHELP 248 | $(basename "$(readlink -f "$0")") "version $version" 249 | 250 | Use: classify [OPTIONS] identifier[,identifier...] 251 | 252 | Queries OCLC classification service for available data 253 | Supports: DDC, LCC, NLM, Author and Title 254 | 255 | Valid identifiers are ISBN, ISSN, UPC and OCLC/OWI 256 | 257 | OPTIONS: 258 | 259 | -d show DDC 260 | -l show LCC 261 | -n show NLM 262 | -f show FAST 263 | -a show Author 264 | -t show Title 265 | 266 | -o show OWI (OCLC works identifier) 267 | -w show WI (OCLC works number) 268 | 269 | -C md5 create CSV (MD5,DDC,LCC,NLM,FAST,AUTHOR,TITLE) 270 | use -D libgen/-D libgen_fiction to indicate database 271 | 272 | -X dir save OCLC XML response to \$dir/\$md5.xml 273 | only works with a defined MD5 (-C MD5) 274 | 275 | -D db define which database to use (libgen/libgen_fiction) 276 | 277 | -A show all available data for identifier 278 | 279 | -V show labels 280 | 281 | -@ PORT use torsocks to connect to the OCLC classify service. 282 | use this to avoid getting your IP blocked by OCLC 283 | 284 | -h show this help message 285 | 286 | Examples 287 | 288 | $ classify -A 0199535760 289 | AUTHOR: Plato | Jowett, Benjamin, 1817-1893 Translator; Editor; Other] ... 290 | TITLE: The republic 291 | DDC: 321.07 292 | LCC: JC71 293 | 294 | $ classify -D libgen -C 25b8ce971343e85dbdc3fa375804b538 295 | 25b8ce971343e85dbdc3fa375804b538,"321.07","JC71","",UG9saXRpY2FsI\ 296 | HNjaWVuY2UsVXRvcGlhcyxKdXN0aWNlLEV0aGljcyxQb2xpdGljYWwgZXRoaWNzLFB\ 297 | oaWxvc29waHksRW5nbGlzaCBsYW5ndWFnZSxUaGVzYXVyaQo=,UGxhdG8gfCBKb3dl\ 298 | dHQsIEJlbmphbWluLCAxODE3LTE4OTMgW1RyYW5zbGF0b3I7IEVkaXRvcjsgT3RoZX\ 299 | JdIHwgV2F0ZXJmaWVsZCwgUm9iaW4sIDE5NTItIFtUcmFuc2xhdG9yOyBXcml0ZXIg\ 300 | b2YgYWRkZWQgdGV4dDsgRWRpdG9yOyBPdGhlcl0gfCBMZWUsIEguIEQuIFAuIDE5MD\ 301 | gtMTk5MyBbVHJhbnNsYXRvcjsgRWRpdG9yOyBBdXRob3Igb2YgaW50cm9kdWN0aW9u\ 302 | XSB8IFNob3JleSwgUGF1bCwgMTg1Ny0xOTM0IFtUcmFuc2xhdG9yOyBBdXRob3I7IE\ 303 | 90aGVyXSB8IFJlZXZlLCBDLiBELiBDLiwgMTk0OC0gW1RyYW5zbGF0b3I7IEVkaXRv\ 304 | cjsgT3RoZXJdCg==,VGhlIHJlcHVibGljCg== 305 | 306 | 307 | Classifying libgen/libgen_fiction 308 | 309 | This tool can be used to add classification data to libgen and 310 | libgen_fiction databases. It does not directy modify the database, 311 | instead producing CSV which can be used to apply the modifications. 312 | The best way to do this is to produce a list of md5 hashes for 313 | publications which do have Identifier values but lack values for DDC 314 | and/or LCC. Such lists can be produced by the following SQL: 315 | 316 | libgen: select md5 from updated where IdentifierWODash<>"" and DDC=""; 317 | libgen_fiction: select md5 from fiction where Identifier<>"" and DDC=""; 318 | 319 | Run these as batch jobs (mysql -B .... -e 'sql_code_here;' > md5_list), split 320 | the resulting file in ~1000 line sections and feed these to this tool, 321 | preferably with a random pause between requests to keep OCLC's intrusion 322 | detection systems from triggering too early. It is advisable to use 323 | this tool through Tor (using -@ TORPORT to enable torsocks, make sure it 324 | is configured correctly for your Tor instance) to avoid having too 325 | many requests from your IP to be registered, this again to avoid 326 | your IP being blocked. The OCLC classification service is not 327 | run as a production service (I asked them). 328 | 329 | Return values are stored in the following order: 330 | 331 | MD5,DDC,LCC,NLM,FAST,AUTHOR,TITLE 332 | 333 | DDC, LCC and NLM are enclosed within double quotes and can contain 334 | multiple space-separated values. FAST, AUTHOR and TITLE are base64 encoded 335 | since these fields can contain a whole host of unwholesome characters 336 | which can mess up CSV. The AUTHOR field currentlydecodes to a pipe ('|') 337 | separated list of authors in the format: 338 | 339 | LAST_NAME, NAME_OR_INITIALS, DATE_OF_BIRTH-[DATE_OF_DEATH] [[ROLE[[;ROLE]...]]] 340 | 341 | This format could change depending on what OCLC does with the 342 | (experimental) service. 343 | 344 | EOHELP 345 | } 346 | 347 | main "$@" 348 | -------------------------------------------------------------------------------- /update_libgen: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # shellcheck disable=SC2034,SC1090,SC2155,SC2207 3 | 4 | version="0.6.1" 5 | release="20210512" 6 | 7 | trap "trap_error" TERM 8 | trap "trap_clean" EXIT 9 | export TOP_PID=$$ 10 | 11 | LC_ALL=C 12 | 13 | functions="$(dirname "$0")/books_functions" 14 | if [ -f "$functions" ]; then 15 | source "$functions" 16 | else 17 | echo "$functions not found" 18 | exit 1 19 | fi 20 | 21 | main () { 22 | 23 | exlock now || exit 1 24 | 25 | # PREFERENCES 26 | config=${XDG_CONFIG_HOME:-$HOME/.config}/books.conf 27 | 28 | dbhost="localhost" 29 | dbport="3306" 30 | db="libgen" 31 | dbuser="libgen" 32 | limit=1000 33 | 34 | api="http://libgen.rs/json.php" 35 | 36 | # source config file if it exists 37 | [[ -f ${config} ]] && source "${config}" 38 | 39 | # (more or less) END OF PREFERENCES 40 | 41 | jq=$(find_tool "jq") 42 | curl=$(find_tool "curl") 43 | 44 | tmpdir=$(mktemp -d '/tmp/update_libgen.XXXXXX') 45 | updates="${tmpdir}/updates" 46 | update_count="${tmpdir}/update_count" 47 | update_sql="${tmpdir}/update_sql" 48 | update_last_modified="${tmpdir}/update_last_modified" 49 | update_last_id="${tmpdir}/update_last_id" 50 | update_newer="${tmpdir}/update_newer" 51 | 52 | verbose=0 53 | no_action=0 54 | unknown_fields="" 55 | 56 | re_type='[a-z]+' 57 | re_int='[0-9]+' 58 | re_year='[0-9]{4}' 59 | re_timestamp='[0-9]{4}-[0-9]{2}-[0-9]{2} [0-2][0-9]:[0-5][0-9]:[0-5][0-9]' 60 | 61 | declare -a tables="(description hashes updated)" 62 | declare -A current_fields="($(get_current_fields))" 63 | declare -A field_types="($(get_field_types))" 64 | declare -A field_sizes="($(get_field_sizes))" 65 | declare -A columns=() 66 | declare -A values=() 67 | declare -A upsert=() 68 | 69 | while getopts "a:D:j:hH:i:l:nP:U:qs:ct:u:v@:" OPTION 70 | do 71 | case $OPTION in 72 | j) 73 | json_dump="${OPTARG}" 74 | ;; 75 | s) 76 | sql_dump="${OPTARG}" 77 | ;; 78 | c) 79 | classify=$(find_tool "classify") 80 | import_metadata=$(find_tool "import_metadata") 81 | classifile="${tmpdir}/classifile" 82 | ;; 83 | v) 84 | ((verbose++)) 85 | ;; 86 | n) 87 | no_action=1 88 | ;; 89 | l) 90 | limit="${OPTARG}" 91 | if [[ $limit -le 1 ]]; then 92 | exit_wit_error "limit too low (-l ${limit}), minimum is 2" 93 | fi 94 | ;; 95 | t) 96 | startdatetime="${OPTARG}" 97 | ;; 98 | i) 99 | echo "${OPTARG}" > "${update_last_id}" 100 | ;; 101 | u) 102 | api="${OPTARG}" 103 | ;; 104 | H) 105 | dbhost="${OPTARG}" 106 | ;; 107 | P) 108 | dbport="${OPTARG}" 109 | ;; 110 | U) 111 | dbuser="${OPTARG}" 112 | ;; 113 | D) 114 | db="${OPTARG}" 115 | ;; 116 | a) 117 | if url_available "${OPTARG}?fields=id&ids=0"; then 118 | api="${OPTARG}" 119 | else 120 | exit_with_error "-a ${OPTARG}: API endpoint not available" 121 | fi 122 | ;; 123 | @) 124 | torsocks=$(find_tool "torsocks") 125 | export TORSOCKS_TOR_PORT=${OPTARG} 126 | ;; 127 | q) 128 | quiet=1 129 | ;; 130 | h) 131 | help 132 | exit 133 | ;; 134 | *) 135 | exit_with_error "unknown option $OPTION" 136 | ;; 137 | 138 | esac 139 | done 140 | 141 | check_fields 142 | 143 | while ( 144 | if [[ -s ${update_last_modified} ]]; then 145 | last_update="$(cat "${update_last_modified}")" 146 | last_update_in_db="$(get_time_last_modified)" 147 | if [[ $last_update != "$last_update_in_db" && $no_action == 0 ]]; then 148 | exit_with_error "uh oh... something went wrong, last update in db does not equal last update from api response..." 149 | fi 150 | elif [[ -n $startdatetime ]]; then 151 | last_update="${startdatetime}" 152 | else 153 | last_update="$(get_time_last_modified)" 154 | fi 155 | 156 | last_id=$([[ -s ${update_last_id} ]] && cat "${update_last_id}" || get_max_id) 157 | 158 | get_updates "$last_id" "$limit" "$last_update" 159 | 160 | updcnt=$(get_update_count) 161 | 162 | [[ -n $json_dump ]] && cat "${updates}" >> "${json_dump}" 163 | 164 | if [[ $verbose -ge 1 ]]; then 165 | echo "database last modified: $last_update"; 166 | # update counter is 0-based, humans prefer 1-based notation 167 | if [[ ${updcnt} -gt 0 ]]; then 168 | echo "$((updcnt+1)) updates"; 169 | else 170 | more=$([[ -s $update_last_id ]] && echo "more " || echo "") 171 | echo "no ${more}updates" 172 | fi 173 | echo ; 174 | fi 175 | 176 | test "$updcnt" -gt 0 177 | ); do 178 | 179 | updcnt=$(get_update_count) 180 | count=0 181 | echo "start transaction;" > "${update_sql}" 182 | 183 | while [[ $count -le $updcnt ]]; do 184 | declare -A record 185 | while IFS="=" read -r key value; do 186 | # drop unknown fields 187 | if [[ ! $unknown_fields =~ ${key,,} ]]; then 188 | # limit field size to avoid choking jq on overly long strings 189 | [[ ${#value} -gt 1000 ]] && value="${value:0:997}..." 190 | record[${key,,}]="$value" 191 | fi 192 | done < <($jq -r ".[$count]"'|to_entries|map("\(.key)=\(.value|tostring|.[0:4000]|gsub("\n";"\\n"))")|.[]' "${updates}") 193 | 194 | # record current position 195 | echo "${record['id']}" > "${update_last_id}" 196 | echo "${record['timelastmodified']}" > "${update_last_modified}" 197 | 198 | if [[ $verbose -ge 2 ]]; then 199 | echo "ID: ${record['id']}"; 200 | echo "Author: ${record['author']}"; 201 | echo "Title: ${record['title']}"; 202 | echo "Modified: ${record['timelastmodified']}"; 203 | echo 204 | fi 205 | 206 | if [[ -n "$classifile" && -n "${record['identifierwodash']}" ]]; then 207 | echo "${record['md5']}" >> "$classifile" 208 | fi 209 | 210 | keys=${!record[*]} 211 | 212 | md5="${record[md5]}" 213 | 214 | # split fields between tables 215 | for key in "${!record[@]}"; do 216 | table=${current_fields[$key]} 217 | columns[$table]+="${key}," 218 | value=${record[$key]} 219 | if [ -n "$value" ]; then 220 | value=$(sanitize_field "$key" "$value") 221 | fi 222 | values[$table]+="'$value'," 223 | upsert[$table]+="${key} = values(${key})," 224 | done 225 | 226 | # add md5 to secondary tables (all but the last) 227 | for n in $(seq 0 $((${#tables[@]}-2))); do 228 | table="${tables[$n]}" 229 | if [[ -n "${columns[$table]}" ]]; then 230 | columns[$table]+="md5," 231 | values[$table]+="'$md5'," 232 | upsert[$table]+="md5 = values(md5)," 233 | fi 234 | done 235 | 236 | # main table (last in tables array) first 237 | for n in $(seq $((${#tables[@]}-1)) -1 0); do 238 | table="${tables[$n]}" 239 | if [[ -n "${columns[$table]}" ]]; then 240 | sql+="insert into $table (${columns[$table]%?}) values(${values[$table]%?}) on duplicate key update ${upsert[$table]%?};" 241 | fi 242 | done 243 | 244 | echo "${sql}" >> "${update_sql}" 245 | [[ -n $sql_dump ]] && echo "${sql}" >> "${sql_dump}" 246 | 247 | unset record 248 | unset keys 249 | unset key 250 | unset value 251 | unset sql 252 | columns=() 253 | values=() 254 | upsert=() 255 | 256 | ((count++)) 257 | done 258 | 259 | echo "commit;" >> "${update_sql}" 260 | 261 | [[ $no_action == 0 ]] && dbx "$db" < "${update_sql}" 262 | done 263 | 264 | # optionally add classification data to new records 265 | # this will use tor and round-robin through TOR ports if these are 266 | # defined in classify_tor_ports in the config file 267 | if [[ -n "$classifile" && -f $classifile ]]; then 268 | now=$(date +%Y%m%d%H%M) 269 | csvfile="${classify_csv:+$classify_csv/}${now}.csv" 270 | IFS=',' read -ra torports <<< "$classify_tor_ports" 271 | if [[ ${#torports[*]} -gt 0 ]]; then 272 | torpc=${#torports[*]} 273 | fi 274 | upc=0 275 | while read md5;do 276 | $classify ${torpc:+-@ ${torports[$upc%$torpc]}} -D "$db" ${classify_xml:+-X $classify_xml} -C "$md5" >> "${csvfile}" 277 | ((upc++)) 278 | done < <(cat "$classifile") 279 | 280 | if [[ -f ${csvfile} ]]; then 281 | $import_metadata -d "$db" -f "${classify_fields:-ddc,lcc,fast}" ${classify_sql:+-s $classify_sql/$now.sql} -F "${csvfile}" 282 | fi 283 | fi 284 | 285 | } 286 | 287 | get_current_fields () { 288 | for table in "${tables[@]}"; do 289 | dbx "$db" "describe $table;"|awk '{print "["tolower($1)"]='"$table"'"}' 290 | done 291 | } 292 | 293 | get_field_type () { 294 | field="$1" 295 | table="${current_fields[$field]}" 296 | dbx "$db" "show fields from $table where field=\"$field\";"|awk '{print $2}' 297 | } 298 | 299 | get_field_types () { 300 | for field in "${!current_fields[@]}"; do 301 | fieldtype=$(get_field_type "$field") 302 | [[ "$fieldtype" =~ $re_type ]] 303 | echo -n "[$field]=${BASH_REMATCH[0]} " 304 | done 305 | } 306 | 307 | get_field_sizes () { 308 | for field in "${!current_fields[@]}"; do 309 | fieldtype=$(get_field_type "$field") 310 | [[ "$fieldtype" =~ $re_int ]] 311 | if [[ "${BASH_REMATCH[0]}" -gt 0 ]]; then 312 | echo -n "[$field]=${BASH_REMATCH[0]} " 313 | fi 314 | done 315 | } 316 | 317 | # sanitize_field FIELD VALUE 318 | sanitize_field () { 319 | field=$1 320 | shift 321 | value="$*" 322 | 323 | # quote values for SQL 324 | value=${value//\\/\\\\} 325 | value=${value//\'/\\\'} 326 | 327 | # field-type specific filters 328 | case "${field_types[$field]}" in 329 | int|bigint) 330 | [[ "$value" =~ $re_int ]] 331 | value=${BASH_REMATCH[0]} 332 | value=${value:0:${field_sizes[$field]}} 333 | ;; 334 | char|varchar) 335 | value=${value:0:${field_sizes[$field]}} 336 | ;; 337 | timestamp) 338 | [[ "$value" =~ $re_timestamp ]] 339 | value=${BASH_REMATCH[0]} 340 | ;; 341 | esac 342 | 343 | # field-specific filters 344 | case "$field" in 345 | year) 346 | # filter out Chinese date stamps 347 | [[ "$value" =~ $re_year ]] 348 | value=${BASH_REMATCH[0]} 349 | ;; 350 | esac 351 | 352 | echo -n "$value" 353 | } 354 | 355 | # libgen_api ID LIMIT TIME_LAST_MODIFIED 356 | libgen_api () { 357 | id="$1" 358 | shift 359 | limit="$1" 360 | shift 361 | if ! newer=$(date -d "$*" +'%Y-%m-%d%%20%H:%M:%S'); then 362 | exit_with_error "date error: $* is not a valid date" 363 | fi 364 | 365 | echo "$newer" > "$update_newer" 366 | 367 | $torsocks "$curl" -s "${api}?"'fields=*&idnewer='"${id}"'&mode=newer&limit1='"${limit}"'&timenewer='"${newer}" 368 | } 369 | 370 | # get_updates ID LIMIT TIME_LAST_MODIFIED 371 | get_updates () { 372 | id="$1" 373 | shift 374 | limit="$1" 375 | shift 376 | last="$*" 377 | libgen_api "$id" "$limit" "$last" > "${updates}" 378 | $jq '.|length' "${updates}" > "${update_count}" 379 | } 380 | 381 | 382 | get_time_last_modified () { 383 | dbx "$db" 'select MAX(TimeLastModified) FROM updated;'|tail -1 384 | } 385 | 386 | get_max_id () { 387 | dbx "$db" 'select MAX(id) FROM updated;'|tail -1 388 | } 389 | 390 | get_update_count () { 391 | echo $(($(cat "${update_count}")-1)) 392 | } 393 | 394 | check_fields () { 395 | updates_fields=($(libgen_api 1 2 '2000-01-01'|$jq -r '.[0]|keys|@sh')) 396 | db_fields="${!current_fields[*]}" 397 | db_fields="${db_fields,,}" 398 | 399 | # check for extra fields in api response 400 | for index in "${!updates_fields[@]}"; do 401 | field="${updates_fields[$index]%\'}" 402 | field="${field#\'}" 403 | if [[ ! $db_fields =~ ${field,,} ]]; then 404 | if [[ ! -v quiet ]]; then 405 | echo "unknown field in api response: ${field} (consider refreshing database from dump)" 406 | fi 407 | unknown_fields+="${field,,} " 408 | else 409 | : 410 | fi 411 | done 412 | 413 | # check for missing fields in api reponse 414 | [[ $verbose -ge 1 ]] && { 415 | for field in "${!current_fields[@]}"; do 416 | if [[ ! -v quiet && ! ${updates_fields[*],,} =~ ${field,,} ]]; then 417 | echo "missing field in api response: $field" 418 | fi 419 | done 420 | } 421 | } 422 | 423 | cleanup () { 424 | rm -rf "${tmpdir}" 425 | } 426 | 427 | help () { 428 | echo "$(basename "$(readlink -f "$0")")" "version $version" 429 | cat <<- 'EOT' 430 | 431 | Usage: update_libgen OPTIONS 432 | 433 | -l LIMIT get updates in blocks of LIMIT entries 434 | -v be verbose about what is being updated; repeat for more verbosity: 435 | -v: show basic info (number of updates, etc) 436 | -vv: show ID, Title and TimeLastModified for each update 437 | -n do not update database. Use together with -v or -vv to show 438 | how many (-v) and which (-vv) titles would be updated. 439 | -j FILE dump (append) json to FILE 440 | -s FILE dump (append) sql to FILE 441 | -u URL use URL to access the libgen API (overrides default) 442 | -t DATETIME get updates since DATETIME (ignoring TimeLastModified in database) 443 | use this option together with -s to create an sql update file to update 444 | non-networked machines 445 | -i ID get updates from ID 446 | 447 | -H DBHOST database host 448 | -P DBPORT database port 449 | -U DBUSER database user 450 | -D DATABASE database name 451 | 452 | -a APIHOST use APIHOST as API server 453 | -@ TORPORT use tor (through torsocks) to connect to libgen API server 454 | -c run classify over new records to get classification data 455 | -q don't warn about missing fields in database or api response 456 | -h this help message 457 | 458 | EOT 459 | } 460 | 461 | exlock prepare || exit 1 462 | 463 | main "$@" 464 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | Copyright © 2007 Free Software Foundation, Inc. 4 | 5 | Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. 6 | 7 | Preamble 8 | 9 | The GNU General Public License is a free, copyleft license for software and other kinds of works. 10 | 11 | The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. 12 | 13 | When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. 14 | 15 | To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. 16 | 17 | For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. 18 | 19 | Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. 20 | 21 | For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. 22 | 23 | Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. 24 | 25 | Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. 26 | 27 | The precise terms and conditions for copying, distribution and modification follow. 28 | 29 | TERMS AND CONDITIONS 30 | 31 | 0. Definitions. 32 | 33 | “This License” refers to version 3 of the GNU General Public License. 34 | 35 | “Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. 36 | 37 | “The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals or organizations. 38 | 39 | To “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work “based on” the earlier work. 40 | 41 | A “covered work” means either the unmodified Program or a work based on the Program. 42 | 43 | To “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. 44 | 45 | To “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. 46 | 47 | An interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 48 | 49 | 1. Source Code. 50 | The “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means any non-source form of a work. 51 | 52 | A “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. 53 | 54 | The “System Libraries” of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Component”, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. 55 | 56 | The “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. 57 | 58 | The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. 59 | 60 | The Corresponding Source for a work in source code form is that same work. 61 | 62 | 2. Basic Permissions. 63 | All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. 64 | 65 | You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. 66 | 67 | Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 68 | 69 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 70 | No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. 71 | 72 | When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 73 | 74 | 4. Conveying Verbatim Copies. 75 | You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. 76 | 77 | You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 78 | 79 | 5. Conveying Modified Source Versions. 80 | You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: 81 | 82 | a) The work must carry prominent notices stating that you modified it, and giving a relevant date. 83 | 84 | b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”. 85 | 86 | c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. 87 | 88 | d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. 89 | 90 | A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 91 | 92 | 6. Conveying Non-Source Forms. 93 | You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: 94 | 95 | a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. 96 | 97 | b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. 98 | 99 | c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. 100 | 101 | d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. 102 | 103 | e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. 104 | 105 | A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. 106 | 107 | A “User Product” is either (1) a “consumer product”, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used” refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. 108 | 109 | “Installation Information” for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. 110 | 111 | If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). 112 | 113 | The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. 114 | 115 | Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 116 | 117 | 7. Additional Terms. 118 | “Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. 119 | 120 | When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. 121 | 122 | Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: 123 | 124 | a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or 125 | 126 | b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or 127 | 128 | c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or 129 | 130 | d) Limiting the use for publicity purposes of names of licensors or authors of the material; or 131 | 132 | e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or 133 | 134 | f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. 135 | 136 | All other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. 137 | 138 | If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. 139 | 140 | Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 141 | 142 | 8. Termination. 143 | You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). 144 | 145 | However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. 146 | 147 | Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. 148 | 149 | Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 150 | 151 | 9. Acceptance Not Required for Having Copies. 152 | You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 153 | 154 | 10. Automatic Licensing of Downstream Recipients. 155 | Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. 156 | 157 | An “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. 158 | 159 | You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 160 | 161 | 11. Patents. 162 | A “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's “contributor version”. 163 | 164 | A contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. 165 | 166 | Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. 167 | 168 | In the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. 169 | 170 | If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. 171 | 172 | If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. 173 | 174 | A patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. 175 | 176 | Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 177 | 178 | 12. No Surrender of Others' Freedom. 179 | If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 180 | 181 | 13. Use with the GNU Affero General Public License. 182 | Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 183 | 184 | 14. Revised Versions of this License. 185 | The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. 186 | 187 | Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. 188 | 189 | If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. 190 | 191 | Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 192 | 193 | 15. Disclaimer of Warranty. 194 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 195 | 196 | 16. Limitation of Liability. 197 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 198 | 199 | 17. Interpretation of Sections 15 and 16. 200 | If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. 201 | 202 | END OF TERMS AND CONDITIONS 203 | 204 | How to Apply These Terms to Your New Programs 205 | 206 | If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. 207 | 208 | To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found. 209 | 210 | 211 | Copyright (C) 212 | 213 | This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. 214 | 215 | This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 216 | 217 | You should have received a copy of the GNU General Public License along with this program. If not, see . 218 | 219 | Also add information on how to contact you by electronic and paper mail. 220 | 221 | If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: 222 | 223 | Copyright (C) 224 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 225 | This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. 226 | 227 | The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”. 228 | 229 | You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . 230 | 231 | The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . 232 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # books 2 | 3 | [B]ooks - which is only one of the names this program goes by - is a front-end for accessing a locally accessible libgen / libgen_fiction database instance, offering versatile search and download directly from the command line. The included `update_libgen` tool is used to keep the database up to date - if the database is older than a user-defined value it is updated before the query is executed. This generally only takes a few seconds, but it might take longer on a slow connection or after a long update interval. Updating can be temporarily disabled by using the '-x' command line option. To refresh the database(s) from a dump file use the included `refresh_libgen program`, see 'update_libgen vs refresh_libgen' below for more information on which tool to use. 4 | 5 | Books comes in three main flavours: 6 | 7 | * `books` / `books-all` / `fiction`: CLI search interface which dumps results to the terminal, download through MD5 8 | * `nbook` / `nfiction`: text-based browser offering limited preview and download 9 | * `xbook` / `xfiction`: gui-based browser offering preview and download 10 | 11 | 12 | The *book* tools are based on the *libgen* database, the *fiction* tools use the *libgen_fiction* database. Apart from the fact that the *fiction* tools do not support all the search criteria offered by the 'book' tools due to differences in the database layout, all programs share the same interface. 13 | 14 | The database can be searched in two modes, per-field (the default) and fulltext (which, of course, only searches book metadata, not the actual book contents). The current implementation for fulltext search is actually a pattern match search on a number of concatenated database columns, it does not use MySQL's native fulltext search. The advantage of this implementation is that it does not need a full-text index (which is not part of the libgen dump and would need to be generated locally), the disadvantage is that it does not offer more advanced natural language search options. Given the limited amount of 'natural language' available in the database the latter does not seem to be much of a disadvantage and the implementation performs well. 15 | 16 | In the (default) per-field search mode the database can be searched for patterns (SQL 'like' operator with leading and trailing wildcards) using lower-case options and/or exact matches using upper-case options. The fulltext search by necessity always uses pattern matching over the indicated fields ('title' and 'author' if no other fields are specified). 17 | 18 | Publications can be downloaded using IPFS, through torrents or from libgen download mirror servers by selecting them in the result list or by using the 'Download' button in the preview window, the `books` and `fiction` tools can be used to download publications based on their MD5 hash (use `-J ...`). When using the gui-based tools in combination with the 'yad' tool, double-clicking a row in the result list shows a preview, the other tools generate previews for selected publications using the '-w' command line option. 19 | 20 | See [Installation](#installation) for information on how to install *books*. 21 | 22 | ## How to use *books* et al. 23 | 24 | I'll let the programs themselves do the talking: 25 | 26 | ```txt 27 | $ books -h 28 | books version 0.7 29 | 30 | Use: books OPTIONS [like] [] 31 | 32 | (...) 33 | 34 | SEARCH BY FIELD: 35 | 36 | This is the default search mode. If no field options are given this searches 37 | the Title field for the PATTERN. Capital options (-A, -T, etc) for exact match, 38 | lower-case (-a, -t, etc) for pattern match. 39 | 40 | FULLTEXT SEARCH (-f): 41 | 42 | Performs a pattern match search over all fields indicated by the options. If no 43 | field options are given, perform a pattern match search over the Author and 44 | Title fields. 45 | 46 | Depending on which name this program is executed under it behaves differently: 47 | 48 | books: query database and show results, direct download with md5 49 | books-all: query database and show results (exhaustive search over all tables, slow) 50 | 51 | nbook: select publications for download from list (terminal-based) 52 | xbook: select publications for download from list (GUI) 53 | 54 | fiction: query database and show results (using 'fiction' database), direct download with md5 55 | 56 | nfiction: select publications for download from list (terminal-based, use 'fiction' database) 57 | xfiction: select publications for download from list (GUI, use 'fiction' database) 58 | 59 | OPTIONS 60 | 61 | -z, -Z search on LOCATOR 62 | -y, -Y search on YEAR 63 | -v, -V search on VOLUMEINFO 64 | -t, -T search on TITLE 65 | -s, -S search on SERIES 66 | -r, -R search on PERIODICAL 67 | -q, -Q search on OPENLIBRARYID 68 | -p, -P search on PUBLISHER 69 | -o, -O search on TOPIC_DESCR 70 | -n, -N search on ASIN 71 | -m search on MD5 72 | -l, -L search on LANGUAGE 73 | -i, -I search on ISSN 74 | -g, -G search on TAGS 75 | -e, -E search on EXTENSION 76 | -d, -D search on EDITION 77 | -c, -C search on CITY 78 | -b, -B search on IDENTIFIERWODASH 79 | -a, -A search on AUTHOR 80 | 81 | -f fulltext search 82 | searches for the given words in the fields indicated by the other options. 83 | when no other options are given this will perform a pattern match search 84 | for the given words over the Author and Title fields. 85 | 86 | -w preview publication info before downloading (cover preview only in GUI tools) 87 | select one or more publication to preview and press enter/click OK. 88 | 89 | double-clicking a result row also shows a preview irrespective of this option, 90 | but this only works when using the yad gui tool 91 | 92 | -= DIR set download location to DIR 93 | 94 | -$ use extended path when downloading: 95 | nonfiction/[topic/]author[/series]/title 96 | fiction/language/author[/series]/title 97 | 98 | -u BOOL use bittorrent (-u 1 or -u y) or direct download (-u 0 or -u n) 99 | this parameter overrides the default download method 100 | bittorrent download depends on an external helper script 101 | to interface with a bittorrent client 102 | 103 | -I BOOL use ipfs (-I 1 or -I y) or direct download (-I 0 or -I n) 104 | this parameter overrides the default download method 105 | ipfs download depends on a functioning ipfs gateway. 106 | default gateway is hosted by Cloudfront, see https://ipfs.io/ 107 | for instructions on how to run a local gateway 108 | 109 | -U MD5 print torrent path (torrent#/md5) for given MD5 110 | 111 | -j MD5 print filename for given MD5 112 | 113 | -J MD5 download file for given MD5 114 | can be combined with -u to download with bittorrent 115 | 116 | -M MD5 fast path search on md5, only works in _books_ and _fiction_ 117 | can be combined with -F FIELDS to select fields to be shown 118 | output goes directly to the terminal (no pager) 119 | 120 | -F FIELDS select which fields to show in pager output 121 | 122 | -# LIMIT limit search to LIMIT hits (default: 1000) 123 | 124 | -x skip database update 125 | (currently only the 'libgen' database can be updated) 126 | 127 | -@ TORPORT use torsocks to connect to the libgen server(s). You'll need to install 128 | torsocks before using this option; try this in case your ISP 129 | (or a transit provider somewhere en-route) blocks access to libgen 130 | 131 | -k install symlinks for all program invocations 132 | 133 | -h show this help message 134 | 135 | EXAMPLES 136 | 137 | Do a pattern match search on the Title field for 'ilias' and show the results in the terminal 138 | 139 | $ books like ilias 140 | 141 | 142 | Do an exact search on the Title field for 'The Odyssey' and show the results in the terminal 143 | 144 | $ books 'the odyssey' 145 | 146 | 147 | Do an exact search on the Title field for 'The Odyssey' and the Author field for 'Homer', showing 148 | the result in the terminal 149 | 150 | $ books -T 'The Odyssey' -A 'Homer' 151 | 152 | 153 | Do the same search as above, showing the results in a list on the terminal with checkboxes to select 154 | one or more publications for download 155 | 156 | $ nbook -T 'The Odyssey' -A 'Homer' 157 | 158 | 159 | A case-insensitive pattern search using an X11-based interface; use bittorrent (-u y or -u 1) when downloading files 160 | 161 | $ xbook -u y -t 'the odyssey' -a 'homer' 162 | 163 | 164 | Do a fulltext search over the Title, Author, Series, Periodical and Publisher fields, showing the 165 | results in a terminal-based checklist for download after preview (-w) 166 | 167 | $ nbook -w -f -t -a -s -r -p 'odyssey' 168 | 169 | 170 | Walk over a directory of publications, compute md5 and use this to generate file names: 171 | 172 | $ find /path/to/publications -type f|while read f; do books -j $(md5sum "$f"|awk '{print $1}');done 173 | 174 | 175 | As above, but print torrent number and path in torrent file 176 | 177 | $ find /path/to/publications -type f|while read f; do books -U $(md5sum "$f"|awk '{print $1}');done 178 | 179 | 180 | Find publications by author 'thucydides' and show their md5,title and year in the terminal 181 | 182 | $ books -a thucydides -F md5,title,year 183 | 184 | 185 | Get data on a single publication using fast path MD5 search, show author, title and extension 186 | 187 | $ books -M 51b4ee7bc7eeb6ed7f164830d5d904ae -F author,title,extension 188 | 189 | 190 | Download a publication using its MD5 (-J MD5), using bittorrent (-u y or -u 1) to download 191 | 192 | $ books -u y -J 51b4ee7bc7eeb6ed7f164830d5d904ae 193 | 194 | ``` 195 | 196 | ```txt 197 | $ update_libgen -h 198 | update_libgen version 0.6 199 | 200 | Usage: update_libgen OPTIONS 201 | 202 | -l LIMIT get updates in blocks of LIMIT entries 203 | -v be verbose about what is being updated; repeat for more verbosity: 204 | -v: show basic info (number of updates, etc) 205 | -vv: show ID, Title and TimeLastModified for each update 206 | -n do not update database. Use together with -v or -vv to show 207 | how many (-v) and which (-vv) titles would be updated. 208 | -j FILE dump (append) json to FILE 209 | -s FILE dump (append) sql to FILE 210 | -u URL use URL to access the libgen API (overrides default) 211 | -t DATETIME get updates since DATETIME (ignoring TimeLastModified in database) 212 | use this option together with -s to create an sql update file to update 213 | non-networked machines 214 | -i ID get updates from ID 215 | 216 | -H DBHOST database host 217 | -P DBPORT database port 218 | -U DBUSER database user 219 | -D DATABASE database name 220 | 221 | -a APIHOST use APIHOST as API server 222 | -@ TORPORT use tor (through torsocks) to connect to libgen API server 223 | -c run classify over new records to get classification data 224 | -q don't warn about missing fields in database or api response 225 | -h this help message 226 | ``` 227 | 228 | ```txt 229 | $ refresh_libgen -h 230 | refresh_libgen version 0.6.1 231 | 232 | Usage: refresh_libgen OPTIONS 233 | 234 | Performs a refresh from a database dump file for the chosen libgen databases. 235 | 236 | -n do not refresh database 237 | use together with '-v' to check if recent dumps are available 238 | -f force refresh, use this on first install 239 | -v be verbose about what is being updated 240 | -d DAYS only use database dump files no older than DAYS days (default: 5) 241 | -u DBS refresh DBS databases (default: compact fiction libgen) 242 | 243 | -H DBHOST database host (localhost) 244 | -P DBPORT database port (3306) 245 | -U DBUSER database user (libgen) 246 | -R REPO dump repository (http://gen.lib.rus.ec/dbdumps/) 247 | -c create a config file using current settings (see -H, -P, -U, -R) 248 | -e edit config file 249 | 250 | -@ TORPORT use tor (through torsocks) to connect to libgen server 251 | -k keep downloaded files after exit 252 | -h this help message 253 | ``` 254 | 255 | ## IPFS, Torrents, direct download... 256 | 257 | *Books* (et al) can download files either through IPFS (using `-I 1` or `-I y`), from torrents (using `-u y` or `-u 1`) or from one of the libgen download mirrors (default, use `-I n`/`-u n` or `-I 0`/`-u 0` in case IPFS or torrent download is set as default). To limit the load on the download servers it is best to use IPFS or torrents whenever possible. The latest publications are not yet available through IPFS or torrents since those are only created for batches of 1000 publications. The feasibility of torrent download also depends on whether the needed torrents are seeded while for IPFS download a working IPFS gateway is needed. Publications which can not be downloaded through IPFS or torrents can be downloaded directly. 258 | 259 | ### IPFS download process 260 | IPFS download makes use of an IPFS gateway, by default this is set to Cloudflare's gateway: 261 | 262 | ``` 263 | # ipfs gateway 264 | ipfs_gw="https://cloudflare-ipfs.com" 265 | ``` 266 | 267 | This can be changed in the config file (usually `$HOME/.config/books.conf`) 268 | 269 | The actual download works exactly the same as the direct download, only the source is changed from a direct download server to the IPFS gateway. Download speed depends on whether the gateway has the file in cache or not, in the latter case it can take a bit more time - be patient. 270 | 271 | ### Torrent download process 272 | Torrent download works by selecting individual files for download from the 'official' torrents, i.e. it is *not* necessary to download the whole torrent for a single publication. This process is automated by means of a helper script which is used to interface *books* with a torrent client. Currently the only torrent client for which a helper script is available is *transmission-daemon*, the script uses the related *transmission-remote* program to interface with the daemon. Writing a helper script should not be that hard for other torrent clients as long as these can be controlled through the command line or via an API. 273 | 274 | When downloading through torrents *books* first tries to download the related torrent file from the 'official' repository, if this fails it gives up and suggests using direct download instead. Once the torrent file has been downloaded it is checked to see whether it contains the required file. If this check passes the torrent is submitted to the torrent client with only the required file selected for download. A job script is created which can be used to control the torrent job, if the `torrent_cron_job` parameter in the PREFERENCES section or the config file is set to `1` it is submitted as a cron job. The task of this script is to copy the downloaded file from the torrent client download directory (`torrent_download_directory` in books.conf or the PREFERENCES section) to the target directory (preference `target_directory`) under the correct name. Once the torrent has finished downloading the job script will copy the file to that location and remove the cron job. If `torrent_cron_job` is not set (or is set to `0`) the job script can be called 'by hand' to copy the file, it can also be used to perform other tasks like retrying the download from a libgen download mirror server (use `-D`, this will cancel the torrent and cron job for this file) or to retry the torrent download (use `-R`). The script has the following options: 275 | 276 | ```txt 277 | $ XYZ.job -h 278 | Use: bash jobid.job [-s] -[i] [-r] [-R] [-D] [-h] [torrent_download_directory] 279 | 280 | Copies file from libgen/libgen_fiction torrent to correct location and name 281 | 282 | -S show job status 283 | -s show torrent status (short) 284 | -i show torrent info (long) 285 | -I show target file name 286 | -r remove torrent and cron jobs 287 | -R restart torrent download (does not restart cron job) 288 | -D direct download (removes torrent and cron jobs) 289 | -h show this help message 290 | ``` 291 | 292 | ### The torrent helper script interface 293 | The torrent helper script (here named `ttool`) needs to support the following commands: 294 | 295 | * `ttool add-selective ` 296 | download file `` from torrent `` 297 | * `ttool torrent-hash ` 298 | get btih (info-hash) for `` 299 | * `ttool torrent-files ` 300 | list files in `` 301 | * `ttool remove ` 302 | remove active torrent with info-hash `` 303 | * `ttool ls ` 304 | show download status for active torrent with info-hash `` 305 | * `ttool info ` 306 | show extensive info (files, peers, etc) for torrent with info-hash `` 307 | * `ttool active ` 308 | return `true` if the torrent is active, `false` otherwise 309 | 310 | Output should be the requested data without any headers or other embellishments. Here is an example using the (included) `tm` helper script for the *transmission-daemon* torrent client, showing all required commands: 311 | 312 | ```txt 313 | $ tm torrent-files r_2412000.torrent 314 | 2412000/00b3c21460499dbd80bb3a118974c879 315 | 2412000/00b64be1207c374e8719ee1186a33c4d 316 | 2412000/00c4f3a075d3af0813479754f010c491 317 | ... 318 | ... (994 files omitted for brevity) 319 | ... 320 | 2412000/ff2473a3b8ec1439cc459711fb2a4b97 321 | 2412000/ff913204c002f19ed2ee1e2bdfd236d4 322 | 2412000/ffb249ae5d148639d38f2af2dba6c681 323 | 324 | $ tm torrent-hash r_2412000.torrent 325 | e73d4bc21d0f91088c174834840f7da232330b4d 326 | 327 | $ tm add-selective r_2412000.torrent 00c4f3a075d3af0813479754f010c491 328 | ... (torrent client output omitted) 329 | 330 | $ tm ls 6934f632c06a91572b4401e5b4c96eec89d311d7 331 | ID Done Have ETA Up Down Ratio Status Name 332 | 25 0% None Unknown 0.0 0.0 None Idle 762000 333 | Sum: None 0.0 0.0 334 | 335 | (output from transmission-daemon, format is client-dependent) 336 | 337 | $ tm info 6934f632c06a91572b4401e5b4c96eec89d311d7 338 | ... (torrent client output omitted) 339 | 340 | $ tm active 6934f632c06a91572b4401e5b4c96eec89d311d7; echo "torrent is $([[ $? -gt 0 ]] && echo "not ")active" 341 | torrent is active 342 | 343 | $ if tm active 6934f632c06a91572b4401e5b4c96eec89d311d7; then echo "torrent is active"; fi 344 | torrent is active 345 | 346 | $ tm active d34db33f; echo "torrent is $([[ $? -gt 0 ]] && echo "not ")active" 347 | torrent is not active 348 | ``` 349 | 350 | #### The `tm` torrent helper script 351 | The `tm` torrent helper script supports the following options: 352 | ```txt 353 | $ tm -h 354 | tm version 0.1 355 | 356 | Use: tm COMMAND OPTIONS [parameters] 357 | tm-COMMAND OPTIONS [parameters] 358 | 359 | A helper script for transmission-remote and related tools, adding some 360 | functionality like selective download etc. 361 | 362 | PROGRAMS/COMMANDS 363 | 364 | tm-active active 365 | tm-add add 366 | tm-add-selective add-selective 367 | tm-cmd cmd 368 | tm-file-count file-count 369 | tm-files files 370 | tm-help help 371 | tm-info info 372 | tm-ls ls 373 | tm-remove remove 374 | tm-start start 375 | tm-stop stop 376 | tm-torrent-files torrent-files 377 | tm-torrent-hash torrent-hash 378 | tm-torrent-show torrent-show 379 | 380 | OPTIONS 381 | 382 | -k create symbolic links 383 | creates links to all supported commands 384 | e.g. tm-cmd, tm-ls, tm-add, ... 385 | links are created in the directory where tm resides 386 | 387 | -n NETRC set netrc (/home/frank/.tm-netrc) 388 | 389 | -H HOST set host (p2p:4081) 390 | 391 | -c create a config file using current settings (see -n, -H) 392 | 393 | -l execute command 'ls' 394 | 395 | -a TORR execute command 'add' 396 | 397 | -h this help message 398 | 399 | EXAMPLES 400 | 401 | In all cases it is possible to replace tm-COMMAND with tm COMMAND 402 | 403 | show info about running torrents: 404 | 405 | $ tm-ls 406 | 407 | add a torrent or a magnet link: 408 | 409 | tm-add /path/to/torrent/file.torrent 410 | tm-add 'magnet:?xt=urn:btih:123...' 411 | 412 | add a torrent and selectivly download two files 413 | this only works with torrent files (i.e. not magnet links) for now 414 | 415 | tm-add-selective /path/to/torrent/file.torrent filename1,filename2 416 | 417 | show information about a running torrent, using its btih or ID: 418 | 419 | tm-show f0a7524fe95910da462a0d1b11919ffb7e57d34a 420 | tm-show 21 421 | 422 | show files for a running torrent identified by btih (can also use ID) 423 | 424 | tm-files f0a7524fe95910da462a0d1b11919ffb7e57d34a 425 | 426 | stop a running torrent, using its ID (can also use btih) 427 | 428 | tm-stop 21 429 | 430 | get btih for a torrent file 431 | 432 | tm-torrent-hash /path/to/torrent/file.torrent 433 | 434 | remove a torrent from transmission 435 | 436 | tm-remove 21 437 | 438 | execute any transmission-remote command - notice the double dash 439 | see man transmission-remote for more info on supported commands 440 | 441 | 442 | tm-cmd -- -h 443 | tm cmd -h 444 | 445 | 446 | CONFIGURATION FILES 447 | 448 | /home/username/.config/tm.conf 449 | 450 | tm can be configured by editing the script itself or the configuration file: 451 | 452 | netrc=~/.tm-netrc 453 | tm_host="transmission-host.example.org:4081" 454 | 455 | values set in the configuration file override those in the script 456 | ``` 457 | 458 | 459 | ## Classify 460 | Classify is a tool which, when fed an *identifier* (ISBN or ISSN, it also works 461 | with UPC and OCLC OWI/WI but these are not in the database) [i]or[/i] a 462 | database name and MD5 can be used to extract classification data from the OCLC 463 | classifier. Depending on what OCLC returns it can be used to add or update the 464 | following fields: 465 | 466 | ### Always present: 467 | - Author 468 | - Title 469 | 470 | ### One or more of: 471 | - [DDC](https://en.wikipedia.org/wiki/Dewey_Decimal_Classification) 472 | - [LCC](https://en.wikipedia.org/wiki/Library_of_Congress_Classification) 473 | - [NLM](https://en.wikipedia.org/wiki/National_Library_of_Medicine_classification) 474 | - [FAST](https://www.oclc.org/research/areas/data-science/fast.html) (Faceted Application of Subject Terminology, basically a list of subject keywords derived from the Library of Congress Subject Headings (LCSH)) 475 | 476 | The *classify* tool stores these fields in CSV files which can be fed to the 477 | *import_metadata* tool (see below)to update the database and/or produce SQL 478 | code. It can also store all XML data as returned by the OCLC classifier for 479 | later use, this offloads the OCLC classifier service which is marked as 480 | 'experimental' and 'not built for production use' and as such can change or 481 | disappear at any moment. 482 | 483 | The *classify* helper script supports the following options: 484 | 485 | ``` 486 | $ classify -h 487 | classify "version 0.5.0" 488 | 489 | Use: classify [OPTIONS] identifier[,identifier...] 490 | 491 | Queries OCLC classification service for available data 492 | Supports: DDC, LCC, NLM, FAST, Author and Title 493 | 494 | Valid identifiers are ISBN, ISSN, UPC and OCLC/OWI 495 | 496 | OPTIONS: 497 | 498 | -d show DDC 499 | -l show LCC 500 | -n show NLM 501 | -f show FAST 502 | -a show Author 503 | -t show Title 504 | 505 | -o show OWI (OCLC works identifier) 506 | -w show WI (OCLC works number) 507 | 508 | -C md5 create CSV (MD5,DDC,LCC,NLM,FAST,AUTHOR,TITLE) 509 | use -D libgen/-D libgen_fiction to indicate database 510 | 511 | -X dir save OCLC XML response to $dir/$md5.xml 512 | only works with a defined MD5 (-C MD5) 513 | 514 | -D db define which database to use (libgen/libgen_fiction) 515 | 516 | -A show all available data for identifier 517 | 518 | -V show labels 519 | 520 | -@ PORT use torsocks to connect to the OCLC classify service. 521 | use this to avoid getting your IP blocked by OCLC 522 | 523 | -h show this help message 524 | 525 | Examples 526 | 527 | $ classify -A 0199535760 528 | AUTHOR: Plato | Jowett, Benjamin, 1817-1893 Translator; Editor; Other] ... 529 | TITLE: The republic 530 | DDC: 321.07 531 | LCC: JC71 532 | 533 | $ classify -D libgen -C 25b8ce971343e85dbdc3fa375804b538 534 | 25b8ce971343e85dbdc3fa375804b538,"321.07","JC71","",UG9saXRpY2FsI\ 535 | HNjaWVuY2UsVXRvcGlhcyxKdXN0aWNlLEV0aGljcyxQb2xpdGljYWwgZXRoaWNzLFB\ 536 | oaWxvc29waHksRW5nbGlzaCBsYW5ndWFnZSxUaGVzYXVyaQo=,UGxhdG8gfCBKb3dl\ 537 | dHQsIEJlbmphbWluLCAxODE3LTE4OTMgW1RyYW5zbGF0b3I7IEVkaXRvcjsgT3RoZX\ 538 | JdIHwgV2F0ZXJmaWVsZCwgUm9iaW4sIDE5NTItIFtUcmFuc2xhdG9yOyBXcml0ZXIg\ 539 | b2YgYWRkZWQgdGV4dDsgRWRpdG9yOyBPdGhlcl0gfCBMZWUsIEguIEQuIFAuIDE5MD\ 540 | gtMTk5MyBbVHJhbnNsYXRvcjsgRWRpdG9yOyBBdXRob3Igb2YgaW50cm9kdWN0aW9u\ 541 | XSB8IFNob3JleSwgUGF1bCwgMTg1Ny0xOTM0IFtUcmFuc2xhdG9yOyBBdXRob3I7IE\ 542 | 90aGVyXSB8IFJlZXZlLCBDLiBELiBDLiwgMTk0OC0gW1RyYW5zbGF0b3I7IEVkaXRv\ 543 | cjsgT3RoZXJdCg==,VGhlIHJlcHVibGljCg== 544 | 545 | 546 | Classifying libgen/libgen_fiction 547 | 548 | This tool can be used to add classification data to libgen and 549 | libgen_fiction databases. It does not directy modify the database, 550 | instead producing CSV which can be used to apply the modifications. 551 | The best way to do this is to produce a list of md5 hashes for 552 | publications which do have Identifier values but lack values for DDC 553 | and/or LCC. Such lists can be produced by the following SQL: 554 | 555 | libgen: select md5 from updated where IdentifierWODash<>"" and DDC=""; 556 | libgen_fiction: select md5 from fiction where Identifier<>"" and DDC=""; 557 | 558 | Run these as batch jobs (mysql -B .... -e 'sql_code_here;' > md5_list), split 559 | the resulting file in ~1000 line sections and feed these to this tool, 560 | preferably with a random pause between requests to keep OCLC's intrusion 561 | detection systems from triggering too early. It is advisable to use 562 | this tool through Tor (using -@ TORPORT to enable torsocks, make sure it 563 | is configured correctly for your Tor instance) to avoid having too 564 | many requests from your IP to be registered, this again to avoid 565 | your IP being blocked. The OCLC classification service is not 566 | run as a production service (I asked them). 567 | 568 | Return values are stored in the following order: 569 | 570 | MD5,DDC,LCC,NLM,FAST,AUTHOR,TITLE 571 | 572 | DDC, LCC and NLM are enclosed within double quotes and can contain 573 | multiple space-separated values. FAST, AUTHOR and TITLE are base64 encoded 574 | since these fields can contain a whole host of unwholesome characters 575 | which can mess up CSV. The AUTHOR field currentlydecodes to a pipe ('|') 576 | separated list of authors in the format: 577 | 578 | LAST_NAME, NAME_OR_INITIALS, DATE_OF_BIRTH-[DATE_OF_DEATH] [[ROLE[[;ROLE]...]]] 579 | 580 | This format could change depending on what OCLC does with the 581 | (experimental) service. 582 | ``` 583 | 584 | ## import_metadata 585 | Taking a file containing lines of CSV-formatted data, this tool can be used to 586 | update a libgen / libgen_fiction database with fresh metadata. It can also be 587 | used to produce SQL (using the -s sqlfile option) which can be used to update 588 | multiple database instances. 589 | 590 | In contrast to the other *books* tools *import_metadata* is a Python (version 591 | 3) script using the *pymysql* "pure python" driver (*python3-pymysql* on Debian) 592 | and as such should run on any device where Python is available. The 593 | distribution file contains a Bash script (*import_metadata.sh*) with the same 594 | interface and options which can be used where Python is not available. 595 | 596 | 597 | ``` 598 | $ import_metadata -h 599 | 600 | import_metadata v.0.1.0 601 | 602 | Use: import_metadata [OPTIONS] -d database -f "field1,field2" -F CSVDATAFILE 603 | 604 | Taking a file containing lines of CSV-formatted data, this tool can be 605 | used to update a libgen / libgen_fiction database with fresh metadata. 606 | It can also be used to produce SQL (using the -s sqlfile option) which 607 | can be used to update multiple database instances. 608 | 609 | CSV data format: 610 | 611 | MD5,DDC,LCC,NLM,FAST,AUTHOR,TITLE 612 | 613 | Fields FAST, AUTHOR and TITLE should be base64-encoded. 614 | 615 | CSV field names are subject to redirection to database field names, 616 | currently these redirections are active (CSV -> DB): 617 | 618 | ['FAST -> TAGS'] 619 | 620 | OPTIONS: 621 | 622 | -d DB define which database to use (libgen/libgen_fiction) 623 | 624 | -f field1,field2 625 | -f field1 -f field2 626 | define which fields to update 627 | 628 | -F CSVFILE 629 | define CSV input file 630 | 631 | -s SQLFILE 632 | write SQL to SQLFILE 633 | 634 | -n do not update database 635 | use with -s SQLFILE to produce SQL for later use 636 | use with -v to see data from CSVFILE 637 | use with -vv to see SQL 638 | 639 | -v verbosity 640 | repeat to increase verbosity 641 | 642 | -h this help message 643 | 644 | Examples 645 | 646 | $ import_metadata -d libgen -F csv/update-0000 -f 'ddc lcc fast' 647 | 648 | update database 'libgen' using data from CSV file csv/update-0000, 649 | fields DDC, LCC and FAST (which is redirected to libgen.Tags) 650 | 651 | $ for f in csv/update-*;do 652 | import_metadata -d libgen -s "$f.sql" -n -f 'ddc,lcc,fast' -F "$f" 653 | done 654 | 655 | create SQL (-s "$f.sql") to update database using fields 656 | DDC, LCC and FAST from all files matching glob csv/update-*, 657 | do not update database (-n option) 658 | ``` 659 | 660 | 661 | 662 | ## Installation 663 | Download this repository (or a tarball) and copy the four scripts - `books`, `update_libgen`, `refresh_libgen` and `tm` (only needed when using the transmission-daemon torrent client) - into a directory which is somewhere on your $PATH ($HOME/bin would be a good spot). Run `books -k`to create symlinks to the various names under which the program can be run: 664 | 665 | * `books` 666 | * `books-all` 667 | * `fiction` 668 | * `nbook` 669 | * `xbook` 670 | * `nfiction` 671 | * `xfiction` 672 | 673 | Create a database on a mysql server somewhere within reach of the intended host. Either open *books* in an editor to configure the database details (look for `CONFIGURE ME` below) and anything else (eg. `target_directory` for downloaded books, `max_age` before update, `language` for topics, MD5 in filenames, tools, etc) or add these settings to the (optional) config file `books.conf` in $XDG_CONFIG_HOME (usually $HOME/.config). The easiest way to create the config file is to run `refresh_libgen` with the required options. As an example, the following command sets the database server to `base.example.org`, the database port to `3306` and the database username to `genesis`: 674 | 675 | ```bash 676 | $ refresh_libgen -H base.example.org -P 3306 -U genesis -c 677 | ``` 678 | 679 | Make sure to add the `-c` option *at the end* of the command or it won't work. Once the config file has been created it can be edited 680 | 681 | 682 | ```bash 683 | main () { 684 | # PREFERENCES 685 | config=${XDG_CONFIG_HOME:-$HOME/.config}/books.conf 686 | 687 | # target directory for downloaded publications 688 | target_directory="${HOME}/Books" <<<<<< ... CONFIGURE ME ... >>>>>> 689 | # when defined, subdirectory of $target_directory) for torrents 690 | torrent_directory="torrents" 691 | # when defined, location where files downloaded with torrent client end up 692 | # torrent_download_directory="/net/p2p/incoming" <<<<<< ... ENABLE/CONFIGURE ME ... >>>>>> 693 | # when true, launch cron jobs to copy files from torrent download directory 694 | # to target directory using the correct name 695 | torrent_cron_job=1 696 | # default limit on queries 697 | limit=1000 698 | # maximum database age (in minutes) before attempting update 699 | max_age=120 700 | # topics are searched/displayed in this language ("en" or "ru") 701 | language="en" <<<<<<<<<<<< ... CONFIGURE ME ..... >>>>>>>>>>>>>>>> 702 | # database host 703 | dbhost="localhost" <<<<<<<<<<<< ... CONFIGURE ME ..... >>>>>>>>>>>>>>>> 704 | # database port 705 | dbport="3306" <<<<<<<<<<<< ... CONFIGURE ME ..... >>>>>>>>>>>>>>>> 706 | # database user 707 | dbuser="libgen" <<<<<<<<<<<< ... CONFIGURE ME ..... >>>>>>>>>>>>>>>> 708 | # default fields for fulltext search 709 | default_fields="author,title" 710 | # window/dialog heading for dialog and yad/zenity 711 | list_heading="Select publication(s) for download:" 712 | 713 | # add md5 to filename? Possibly superfluous as it can be derived from the file contents but a good guard against file corruption 714 | filename_add_md5=0 715 | 716 | # tool preferences, list preferred tool first 717 | gui_tools="yad|zenity" 718 | tui_tools="dialog|whiptail" 719 | dl_tools="curl|wget" 720 | parser_tools="xidel|hxwls" 721 | pager_tools="less|more" 722 | 723 | # torrent helper tools need to support the following commands: 724 | # ttool add-selective # downloads file from torrent 725 | # ttool torrent-hash # gets btih for 726 | # ttool torrent-files # lists files in 727 | torrent_tools="tm" <<<<<<<<<<<< ... CONFIGURE ME ..... >>>>>>>>>>>>>>>> 728 | 729 | # database names to use: 730 | # books, books-all, nbook, xbook and xbook-all use the main libgen database 731 | # fiction, nfiction and xfiction use the 'fiction' database 732 | declare -A programs=( 733 | [books]=libgen <<<<<<<<<<<<<<<< ... CONFIGURE ME ..... >>>>>>>>>>>>>>>> 734 | [books-all]=libgen <<<<<<<<<<<<<<<< ... CONFIGURE ME ..... >>>>>>>>>>>>>>>> 735 | [nbook]=libgen <<<<<<<<<<<<<<<< ... CONFIGURE ME ..... >>>>>>>>>>>>>>>> 736 | [xbook]=libgen <<<<<<<<<<<<<<<< ... CONFIGURE ME ..... >>>>>>>>>>>>>>>> 737 | [fiction]=libgen_fiction <<<<<<< ... CONFIGURE ME ..... >>>>>>>>>>>>>>>> 738 | [nfiction]=libgen_fiction <<<<<<<< ... CONFIGURE ME ..... >>>>>>>>>>>>>>>> 739 | [xfiction]=libgen_fiction <<<<<<<< ... CONFIGURE ME ..... >>>>>>>>>>>>>>>> 740 | [libgen_preview]=libgen # the actual database to use for preview is passed as a command line option 741 | ) 742 | ``` 743 | 744 | The same goes for the 'PREFERENCES' sections in `update_libgen` and `refresh_libgen`. In most cases the only parameters which might need change are `dbhost`, `dbuser`, `ipfs_gw` (if you don't want to use the default hosted by Cloudfront), `torrent_download_directory` and possibly `torrent_tools`. Since all programs use a common `books.conf` config file it is usually sufficient to add these parameters there: 745 | 746 | ```bash 747 | $ cat $HOME/.config/books.conf 748 | dbhost="base.example.org" 749 | dbuser="exampleuser" 750 | ipfs_gw="http://ipfs.example.org" 751 | torrent_download_directory="/net/p2p/incoming" 752 | torrent_tools="tm" 753 | ``` 754 | 755 | Please note that there is no option to enter a database password as that would be rather insecure. Either use a read-only, password-free mysql user to access the database or enter your database details in $HOME/.my.cnf, like so: 756 | 757 | ```ini 758 | [mysql] 759 | user=exampleuser 760 | password=zooperzeekret 761 | ``` 762 | 763 | Make sure the permissions on $HOME/.my.cnf are sane (eg. mode 640 or 600), see http://dev.mysql.com/doc/refman/5.7/en/ ... files.html for more info on this subject. 764 | 765 | Install symlinks to all tools by calling books with the -k option: 766 | 767 | ``` 768 | $ books -k 769 | ``` 770 | 771 | ## configuration file 772 | The configuration file is *source*d by all shell scripts, it is parsed and interpreted by import_metadata. There are some of the more useful parameters which can be set in this file: 773 | 774 | ``` 775 | dbhost="base.example.org" 776 | dbport="3306" 777 | dbuser="libgen" 778 | ``` 779 | Use these to set the database server hostname, port and username. 780 | ``` 781 | torrent_download_directory="/net/incoming" 782 | torrent_cron_job=1 783 | torrent_tools="tm" 784 | ``` 785 | Set torrent download directory (where the torrent client places downloaded files), whether a cron job should be created to copy downloaded publications to their final name and location, which torrent helper tool to use 786 | ``` 787 | use_deep_path=1 788 | ``` 789 | Add section, language, author and subject to path name (e.g. nonfication/German/Physics/Einstein, Albert./Die Evolution der Physik) 790 | ``` 791 | use_ipfs=1 792 | ``` 793 | Try to use IPFS when downloading, reverts to direct download for files which do not have a defined ipfs_cid 794 | ``` 795 | gui_tools="yad|zenity" 796 | tui_tools="dialog|whiptail" 797 | parser_tools="xidel|hxwls" 798 | dl_tools="wget|curl" 799 | pager_tools="less|more|cat" 800 | ``` 801 | Tools to be used, in|order|of|preference - the first available is used 802 | ``` 803 | api=http://libgen.rs/json.php 804 | base=http://libgen.rs/dbdumps/ 805 | ipfs_gw=https://cloudflare-ipfs.com 806 | #ipfs_gw=http://your_own_ipfs_node.example.org:8080 807 | ``` 808 | Defines which resources to use for API, dumps, IPFS etc 809 | ``` 810 | classify_xml="/home/username/Project/libgen_classify/xml" 811 | classify_csv="/home/username/Project/libgen_classify/csv" 812 | classify_sql="/home/username/Project/libgen_classify/sql" 813 | classify_fields="ddc,lcc,nlm,fast,title,author" 814 | classify_tor_ports="9100,9102,9104,9106,9108" 815 | ``` 816 | Used by update_libgen to configure *classify* and *import_metadata*, defines whether files are saved and where they are saved, which fields to update in the database and whether Tor is used and if so on which port(s). It is advisable to use more than one port to spread the traffic over several exit nodes, this reduces the risk of OCLC blocking the Tor exit node. 817 | 818 | There are far more configurable parameters, check the script source for more possibilities. 819 | 820 | ## *update_libgen* vs. *refresh_libgen* 821 | 822 | If you regularly use books, nbook and/or xbook, the main (or compact) database should be kept up to date automatically. In that case it is only necessary to use *refresh_libgen* to refresh the database when you get a warning from *update_libgen* about unknown columns in the API response. 823 | 824 | If you have not used any of these tools for a while it can take a long time - and a lot of data transfer - to update the database through the API (which is what *update_libgen* does). Especially when using the compact database it can be quicker to use *refresh_libgen* to just pull the latest dump instead of waiting for *update_libgen* to do its job. 825 | 826 | The *fiction* database can not be updated through the API (yet), so for that databases *refresh_libgen* is currently the canonical way to get the latest version. 827 | 828 | ## Dependencies 829 | 830 | These tools have the following dependencies (apart from a locally available libgen/libgen_fiction instance on MySQL/MariaDB), sorted in order of preference: 831 | 832 | * all: bash 4.x or higher - the script relies on quite a number of bashisms 833 | 834 | 835 | * `books`/`fiction`: less | more (use less!) 836 | * `nbook`/`nfiction`: dialog | whiptail (whiptail is buggy, use dialog!) 837 | * `xbook`/`xfiction`: yad | zenity (more functionality with yad, but make sure your yad supports --html - you might have to build it yourself (use --enable-html during ./configure). If in doubt about the how and why of this, just use Zenity) 838 | 839 | 840 | Preview/Download has these dependencies: 841 | 842 | * awk (tested with mawk, nawk and gawk) 843 | * stdbuf (part of GNU coreutils) 844 | * xidel | hxwls (html parser tools, used for link extraction) 845 | * curl | wget 846 | 847 | 848 | `update_libgen` has the following dependencies: 849 | 850 | * jq (CLI json parser/mangler) 851 | * awk (tested with mawk, nawk and gawk) 852 | 853 | 854 | `refresh_libgen` has these dependencies: 855 | 856 | * w3m 857 | * wget 858 | * unrar 859 | * pv (only needed when using the verbose (-v) option 860 | 861 | `tm` has these dependencies: 862 | 863 | * transmission-remote 864 | 865 | 866 | -------------------------------------------------------------------------------- /books: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # shellcheck disable=SC2034,SC1090,SC2254 4 | 5 | shopt -s extglob 6 | trap "trap_error" TERM 7 | trap "trap_clean" EXIT 8 | export TOP_PID=$$ 9 | 10 | version="0.7.2" 11 | release="20210601" 12 | 13 | functions="$(dirname "$0")/books_functions" 14 | if [ -f "$functions" ]; then 15 | source "$functions" 16 | else 17 | echo "$functions not found" 18 | exit 1 19 | fi 20 | 21 | main () { 22 | # PREFERENCES 23 | config=${XDG_CONFIG_HOME:-$HOME/.config}/books.conf 24 | 25 | # target directory for downloaded publications 26 | target_directory="${HOME}/Books" 27 | # when defined, subdirectory of $target_directory) for torrents 28 | torrent_directory="torrents" 29 | # when defined, location where files downloaded with torrent client end up 30 | torrent_download_directory="/net/media/incoming" 31 | # when true, launch cron jobs to copy files from torrent download directory 32 | # to target directory using the correct name 33 | torrent_cron_job=1 34 | # default limit on queries 35 | limit=1000 36 | # maximum database age (in minutes) before attempting update 37 | max_age=120 38 | # topics are searched/displayed in this language ("en" or "ru") 39 | language="en" 40 | # database host 41 | dbhost="localhost" 42 | # database port 43 | dbport="3306" 44 | # database user 45 | dbuser="libgen" 46 | # default fields for fulltext search 47 | default_fields="author,title" 48 | # window/dialog heading for dialog and yad/zenity 49 | list_heading="Select publication(s) for download:" 50 | 51 | 52 | # add md5 to filename? Possibly superfluous as it can be derived from the file contents but a good guard against file corruption 53 | filename_add_md5=0 54 | 55 | # tool preferences, list preferred tool first 56 | gui_tools="zenity|yad" 57 | tui_tools="dialog|whiptail" 58 | dl_tools="curl|wget" 59 | parser_tools="xidel|hxwls" 60 | pager_tools="less|more|cat" 61 | 62 | # torrent helper tools need to support the following commands: 63 | # add-selective # downloads file from torrent 64 | # torrent-hash # gets btih for 65 | # torrent-files # lists files in 66 | # remove # remove active torrent with info-hash 67 | # ls # show download status for active torrent with info-hash 68 | # info # show extensive info (files, peers, etc) for torrent with info-hash 69 | # active # return `true` if the torrent is active, `false` otherwise 70 | torrent_tools="tm" 71 | 72 | # don't use a pager when not running in a tty 73 | [ ! -t 1 ] && pager_tools="cat" 74 | 75 | 76 | # database names to use: 77 | # books, books-all, nbook, xbook and xbook-all use the main libgen database 78 | # fiction, nfiction and xfiction use the 'fiction' database 79 | declare -A programs=( 80 | [books]=libgen 81 | [books-all]=libgen 82 | [nbook]=libgen 83 | [xbook]=libgen 84 | [fiction]=libgen_fiction 85 | [nfiction]=libgen_fiction 86 | [xfiction]=libgen_fiction 87 | [libgen_preview]=libgen # the actual database to use for preview is passed as a command line option 88 | ) 89 | 90 | declare -A tables=( 91 | [libgen]="(updated topics description hashes)" 92 | [libgen_fiction]="(fiction fiction_description fiction_hashes)" 93 | ) 94 | 95 | # searchable database fields 96 | declare -A schema=( 97 | [a]=author 98 | [t]=title 99 | [d]=edition 100 | [e]=extension 101 | [l]=language 102 | [y]=year 103 | [s]=series 104 | [p]=publisher 105 | [c]=city 106 | [o]=topic_descr 107 | [v]=volumeinfo 108 | [r]=periodical 109 | [g]=tags 110 | [z]=locator 111 | [i]=issn 112 | [n]=asin 113 | [q]=openlibraryid 114 | [b]=identifierwodash 115 | [m]=md5 116 | [D]=ddc 117 | [L]=lcc 118 | ) 119 | 120 | # base url for covers 121 | declare -A coverurl=( 122 | [libgen]='http://93.174.95.29/covers' 123 | [libgen_fiction]='http://93.174.95.29/fictioncovers' 124 | ) 125 | 126 | # download archives 127 | declare -A downloadurl=( 128 | [libgen]="http://93.174.95.29/main" 129 | [libgen_fiction]="http://93.174.95.29/fiction" 130 | ) 131 | 132 | # torrent archives 133 | declare -A torrenturl=( 134 | [libgen]='http://libgen.rs/repository_torrent' 135 | [libgen_fiction]='http://libgen.rs/fiction/repository_torrent' 136 | ) 137 | 138 | # torrent file name prefixes 139 | declare -A torrentprefix=( 140 | [libgen]='r' 141 | [libgen_fiction]='f' 142 | ) 143 | 144 | # directory name prefixes (used with deep path (-$)) 145 | declare -A dirprefix=( 146 | [libgen]="nonfiction" 147 | [libgen_fiction]="fiction" 148 | ) 149 | 150 | # ipfs gateway 151 | ipfs_gw="https://cloudflare-ipfs.com" 152 | 153 | # source config file if it exists 154 | [[ -f ${config} ]] && source "${config}" 155 | 156 | # (mostly) END OF PREFERENCES 157 | 158 | # user agent string generator 159 | declare -a adjective=(white red blue pink green brown dark light big small tiny earth glass air space forest lake sea ground fire crunchy spicy boring zonked blasted stoned fried flattened stretched smelly ugly obnoxious irritating whiny lazy) 160 | declare -a critter=(goat frog hound fish lizard gator moose monkey whale hippo fox bird weasel owl cow pig hog donkey duck chicken dino sloth snake iguana gecko) 161 | user_agent="Mozilla/5.0 ($(uname -s)) ${adjective[$((RANDOM%${#adjective[*]}))]^}${critter[$((RANDOM%${#critter[*]}))]^}/${RANDOM:0:3}.${RANDOM:0:1}.${RANDOM:0:4}.${RANDOM:0:2}" 162 | 163 | # display columns for yad, zenity and pager 164 | declare -A zenity_columns=( 165 | [libgen]="--column Download --column MD5 --column Title --column Author --column Year --column Edition --column Publisher --column Language --column Topic --column Size --column Format --hide-column=2" 166 | [libgen_fiction]="--column Download --column MD5 --column Title --column Author --column Year --column Edition --column Publisher --column Language --column Commentary --column Size --column Format --hide-column=2" 167 | ) 168 | 169 | declare -A yad_columns=( 170 | [libgen]="--column Download --column MD5:HD --column Title --column Author --column Year:NUM --column Edition --column Publisher --column Language --column Topic --column Size:NUM --column Format --column Info:HD --search-column=3 --print-column=2 --tooltip-column=12" 171 | [libgen_fiction]="--column Download --column MD5:HD --column Title --column Author --column Year:NUM --column Edition --column Publisher --column Language --column Commentary --column Size:NUM --column Format --column Info:HD --search-column=3 --print-column=2 --tooltip-column=12" 172 | ) 173 | 174 | # defines which columns are shown by default in a command line (i.e. pager) search 175 | declare -A pager_columns=( 176 | [libgen]="Title,Author,Year,Edition,Publisher,Language,Topic_descr,Filesize,Extension,Series,MD5" 177 | [libgen_fiction]="Title,Author,Year,Edition,Publisher,Language,Commentary,Filesize,Extension,Series,MD5" 178 | ) 179 | 180 | # used to select table prefix in command line search, contains regex patterns 181 | declare -A attribute_columns=( 182 | [topics]="topic_descr|topic_id" 183 | [hashes]="crc32|edonkey|aich|sha1|tth|torrent|btih|sha256" 184 | [description]="^descr$|toc" 185 | ) 186 | 187 | # query output filter for different purposes 188 | declare -A filters=( 189 | [search]="cat" 190 | [xsearch]="sed -e 's/&/&/g'" 191 | [filename]="sed -e 's/[^-[:alnum:]:;?!.,+@#%]/_/g;s/^\([-_]\)*//'" 192 | [dirname]="sed -e 's/[^-[:alnum:]:;?!/.,+@#%]/_/g;s/^\([-_]\)*//'" 193 | [id]="cat" 194 | [ipfs_cid]="cat" 195 | [extension]="cat" 196 | [attributes]="cat" 197 | [preview_dialog]="cat" 198 | [preview_whiptail]="cat" 199 | [preview_yad]="sed -e 's/&/&/g'" 200 | [preview_zenity]="sed -e 's/&/&/g'" 201 | ) 202 | 203 | # GETOPT config 204 | search_options='@('$(echo "${!schema[@]}"|tr ' ' '|')')' 205 | search_getopts="$(echo "${!schema[@]}"|tr ' ' ':'):" 206 | 207 | # X11-related config 208 | if xset q &>/dev/null; then 209 | # used to size yad/zenity windows 210 | min_screenres=$(xrandr|grep '\*'|sort -n|head -1|awk '{print $1}') 211 | x_size=$(($(echo "$min_screenres"|cut -d 'x' -f 1) - 50)) 212 | y_size=$(($(echo "$min_screenres"|cut -d 'x' -f 2) - 30)) 213 | fi 214 | 215 | # defines program behaviour to a large extent 216 | program=$(basename "$0") 217 | db="${programs[$program]}" 218 | 219 | # arrays 220 | declare -a query_result 221 | 222 | # refs 223 | declare -n sql_source 224 | 225 | # misc 226 | clauses="" 227 | fields="" 228 | opt_fields="" 229 | show_fields="" 230 | query="" 231 | no_update=0 232 | 233 | # this contains the columns in the current database, used to filter out unsupported search fields 234 | current_fields="$(get_current_fields)" 235 | 236 | # set ui_tool in order of preference (first found wins) 237 | case "$program" in 238 | xbook|xfiction|libgen_preview) 239 | ui_tool=$(find_tool "$gui_tools") 240 | ;; 241 | nbook|nfiction) 242 | ui_tool=$(find_tool "$tui_tools") 243 | ;; 244 | *) 245 | ui_tool="pager" 246 | ;; 247 | esac 248 | 249 | check_settings 250 | 251 | # PROCESS OPTIONS AND BUILD QUERY 252 | 253 | while getopts ":${search_getopts}fF:hkxX#:@wu:I:U:=:j:J:M:$" OPTION 254 | do 255 | case $OPTION in 256 | $search_options) 257 | add_clause "${OPTION}" "${OPTARG}" 258 | ;; 259 | 260 | h) 261 | help 262 | exit 263 | ;; 264 | k) 265 | create_symlinks 266 | exit 267 | ;; 268 | f) 269 | fulltext=1 270 | ;; 271 | F) 272 | if [[ "$program" =~ ^books|^fiction ]]; then 273 | show_fields="${OPTARG}" 274 | # test for non-existent fields 275 | get_fields > /dev/null 276 | else 277 | exit_with_error "-F FIELDS only works with _books_ and _fiction_" 278 | fi 279 | ;; 280 | x) 281 | no_update=1 282 | ;; 283 | X) 284 | ((exact_match++)) 285 | ;; 286 | '#') 287 | limit="${OPTARG}" 288 | ;; 289 | w) 290 | preview=1 291 | ;; 292 | u) 293 | if is_true "$OPTARG"; then 294 | if [[ -n "$torrent_tools" ]]; then 295 | if find_tool $torrent_tools >/dev/null;then 296 | use_torrent=1 297 | unset use_ipfs 298 | else 299 | exit_with_error "-u: torrent helper script ($torrent_tools) not found" 300 | fi 301 | else 302 | exit_with_error "-u needs torrent helper script, see \$torrent_tools" 303 | fi 304 | else 305 | unset use_torrent 306 | fi 307 | ;; 308 | 309 | I) 310 | if [[ "$OPTARG" == "show" ]]; then 311 | get_ipfs_cid 312 | exit 313 | elif is_true "$OPTARG"; then 314 | use_ipfs=1 315 | unset use_torrent 316 | else 317 | unset use_ipfs 318 | fi 319 | ;; 320 | 321 | U) 322 | get_torrentpath "${OPTARG}" 323 | exit 324 | ;; 325 | j) 326 | get_filename "${OPTARG}" 327 | exit 328 | ;; 329 | J) 330 | md5_download=1 331 | md5="${OPTARG}" 332 | ;; 333 | M) 334 | md5_fast_path=1 335 | md5="${OPTARG}" 336 | ;; 337 | @) 338 | source "$(which torsocks)" on 339 | export TORSOCKS_TOR_PORT=$OPTARG 340 | ;; 341 | =) 342 | if [ -d "${OPTARG}" ]; then 343 | target_directory="${OPTARG}" 344 | else 345 | exit_with_error "Directory ${OPTARG} does not exist" 346 | fi 347 | ;; 348 | $) 349 | use_deep_path=1 350 | ;; 351 | 352 | esac 353 | done 354 | 355 | # direct download - download single publication using MD5 356 | if [[ -n "$md5_download" ]]; then 357 | ui_tool="none" 358 | db=$(get_db_for_md5 "$md5") 359 | if [[ -n "$db" ]]; then 360 | download "$db" "$md5" 361 | else 362 | exit_with_error "unknown md5, can not download" 363 | fi 364 | exit 365 | fi 366 | 367 | # direct info - get data on a single publication using MD5 368 | if [[ -n "$md5_fast_path" ]]; then 369 | case "$program" in 370 | books|fiction) 371 | pager_tools="cat" 372 | query="and md5='${md5}'" 373 | sql=$(prepare_sql "$db" "attributes") 374 | run_query "$db" "attributes" "$sql" 375 | list_${ui_tool} $preview 376 | exit 377 | ;; 378 | *) 379 | exit_with_error "-M MD5 (fast path search) only works in _books_ and _fiction_" 380 | esac 381 | fi 382 | 383 | # shift out options 384 | shift $((OPTIND-1)) 385 | 386 | # process rest of command line 387 | 388 | # this enables a simple 'books [like] ...' search pattern 389 | operator=$1 390 | 391 | if [[ $operator == like ]]; then 392 | percent='%' 393 | operator=' like ' 394 | shift 395 | else 396 | operator='=' 397 | fi 398 | 399 | [[ $limit -gt 0 ]] && sql_limit="limit $limit" 400 | 401 | [[ -z $opt_fields ]] && fields="$default_fields" || fields="${opt_fields%?}" 402 | 403 | [[ $fulltext -gt 0 && ! $pattern =~ $opt_pattern ]] && pattern+="$opt_pattern" 404 | 405 | if [[ $fulltext -gt 0 ]]; then 406 | if [[ -z $* && -z $opt_pattern ]]; then 407 | echo "can not perform a fulltext search without search words" 408 | exit 1 409 | else 410 | unset clauses 411 | query="and concat($fields) like '%${opt_pattern}${*}%'" 412 | fi 413 | else 414 | if [[ -n $* ]]; then 415 | query="and title${operator}'${percent}${*}${percent}'" 416 | fi 417 | fi 418 | 419 | [[ -z ${query} && -z ${opt_pattern} ]] && exit_with_error "Empty query" 420 | 421 | window_title="$query $clauses" 422 | window_title=${window_title/and /} 423 | 424 | # RUN QUERY AND PROCESS RESULTS 425 | 426 | # update database before performing query 427 | case "$program" in 428 | books|books-all|nbook|xbook) 429 | # this depends on the external 'update_libgen' script 430 | # set no_update to 1 in case that script is not available, or when this script is run on a 431 | # system without internet access 432 | if [[ -z $no_update ]]; then 433 | update_database 434 | fi 435 | ;; 436 | *) 437 | # no database update for other databases (yet) 438 | ;; 439 | esac 440 | 441 | case "$program" in 442 | 443 | # this searches both the normal 'updated' table as well as the 'updated_edited' table 444 | books-all) 445 | sql=$(prepare_sql "$db" "search_all" $ui_tool) 446 | run_query "$db" "search" "$sql" 447 | list_${ui_tool} $preview 448 | ;; 449 | 450 | books|nbook|xbook|fiction|nfiction|xfiction) 451 | if [[ ${program:0:1} == "x" ]]; then 452 | filter="xsearch" 453 | else 454 | filter="search" 455 | fi 456 | sql=$(prepare_sql "$db" "search" $ui_tool) 457 | run_query "$db" "$filter" "$sql" 458 | list_${ui_tool} $preview 459 | ;; 460 | 461 | # preview info on publication 462 | libgen_preview) 463 | db="$1" 464 | shift 465 | md5="$2" # preview gets fed the whole row of data by yad/zenity; md5 is the second item in this row 466 | 467 | if [[ -n $md5 ]]; then 468 | preview "$db" "$md5" 469 | fi 470 | ;; 471 | 472 | # default 473 | *) 474 | exit_with_error "unknown program: $program" 475 | ;; 476 | esac 477 | 478 | } 479 | 480 | # DOWNLOAD 481 | 482 | # feed this a list of hashes to attempt to download the related publications 483 | download () { 484 | db="$1" 485 | shift 486 | for md5 in "$@"; do 487 | 488 | filename=$(get_filename "$md5" "$use_deep_path") 489 | 490 | if [[ -n "$filename" ]]; then 491 | 492 | if is_true "$use_torrent"; then 493 | dl_src_torrent "$db" "$md5" "$filename" 494 | elif is_true "$use_ipfs"; then 495 | dl_src_ipfs "$db" "$md5" "$filename" 496 | else 497 | dl_src_direct "$db" "$md5" "$filename" 498 | fi 499 | fi 500 | 501 | log_info "downloaded: $filename" 502 | done 503 | } 504 | 505 | # this attempts to download the actual publication, using one of several download tools 506 | # and reporting through one of several progress monitors 507 | get_file () { 508 | filename="$1" 509 | shift 510 | url="$*" 511 | 512 | # strip quotes 513 | filename=${filename%\'} 514 | filename=${filename#\'} 515 | 516 | dl_tool=$(find_tool "$dl_tools") 517 | stdbuf=$(find_tool "stdbuf") 518 | 519 | tmpdir=$(mktemp -d /tmp/libgen_dl.XXXXXX) 520 | touch "${tmpdir}/progress" 521 | 522 | case $dl_tool in 523 | curl) 524 | curl --user-agent "$user_agent" -L -o "$target_directory/$filename" "${url}" 2>"${tmpdir}/progress" & 525 | echo $! >"${tmpdir}/dl_pid" 526 | ;; 527 | 528 | wget) 529 | wget --no-use-server-timestamps --user-agent "$user_agent" -O "$target_directory/$filename" "${url}" -o "${tmpdir}/progress" --progress=bar:force & 530 | echo $! >"${tmpdir}/dl_pid" 531 | ;; 532 | *) 533 | exit 534 | ;; 535 | esac 536 | 537 | # mawk does not support pattern repetition, hence the funny patterns 538 | case $ui_tool in 539 | dialog) 540 | "$stdbuf" -oL tail -f "${tmpdir}/progress"|stdbuf -oL tr '\r' '\n'|awk -W posix_space -W interactive 'NF==12 { print "XXX\n" $(NF-11) "\nDownloading:\n'"$filename"'\n" $(NF-8) " of " $(NF-10) " at " $(NF) "B/s (" $(NF-1) " left)"; system(""); } /^.......................%\[....................\]/ { split($2,A,/\%/); print "XXX\n" A[1]"\nDownloading:\n'"$filename"'\n"$4 " at " $5 " (" $7 " left)"; system("");}' 2>/dev/null| dialog --backtitle "Download: $filename" --gauge "Starting download..." 10 120 2>/dev/null & 541 | echo $! >"${tmpdir}/pager_pid" 542 | ;; 543 | whiptail) 544 | "$stdbuf" -oL tail -f "${tmpdir}/progress"|stdbuf -oL tr '\r' '\n'|awk -W posix_space -W interactive 'NF==12 { print "XXX\n" $(NF-11) "\nDownloading:\n'"$filename"'\n" $(NF-8) " of " $(NF-10) " at " $(NF) "B/s (" $(NF-1) " left)"; system(""); } /^.......................%\[....................\]/ { split($2,A,/\%/); print "XXX\n" A[1]"\nDownloading:\n'"$filename"'\n"$4 " at " $5 " (" $7 " left)"; system("");}' 2>/dev/null| whiptail --clear --backtitle "Download: $filename" --gauge "Starting download..." 10 0 0 2>/dev/null & 545 | echo $! >"${tmpdir}/pager_pid" 546 | ;; 547 | yad) 548 | "$stdbuf" -oL tail -f "${tmpdir}/progress"|stdbuf -oL tr '\r' '\n'|awk -W posix_space -W interactive 'NF==12 { print $(NF-11) "\n#'"$filename"' (" $(NF) "B/s)"; system(""); } /^.......................%\[....................\]/ { split($2,A,/\%/); print A[1]"\n#'"$filename"' (" $5 ")"; system("");}' 2>/dev/null| yad --window-icon='gtk-save' --title='Downloading' --progress --progress-text="$filename" --auto-close 2>/dev/null & 549 | echo $! >"${tmpdir}/pager_pid" 550 | ;; 551 | zenity) 552 | "$stdbuf" -oL tail -f "${tmpdir}/progress"|stdbuf -oL tr '\r' '\n'|awk -W posix_space -W interactive 'NF=12 { print $(NF-11) "\n#'"$filename"' (" $(NF) "B/s)"; system(""); } /^.......................%\[....................\]/ { split($2,A,/\%/); print A[1]"\n#'"$filename"' (" $5 ")"; system("");}' 2>/dev/null| zenity --window-icon='gtk-save' --title='Downloading' --progress --auto-close 2>/dev/null & 553 | echo $! >"${tmpdir}/pager_pid" 554 | ;; 555 | *) 556 | "$stdbuf" -oL tail -f "${tmpdir}/progress" & 557 | echo $! >"${tmpdir}/pager_pid" 558 | ;; 559 | esac 560 | 561 | trap 'kill $(<"${tmpdir}"/dl_pid) $(<"${tmpdir}"/pager_pid) 2>/dev/null; rm -rf "${tmpdir}";' EXIT 562 | 563 | # wait for the pager to finish (or be closed by the user) and/or the download to finish 564 | # this replaces the (buggy) auto-kill functionality of yad and zenity (dialog does not have any auto-kill) 565 | 566 | while (kill -0 "$(<"${tmpdir}/pager_pid")" 2>/dev/null); do 567 | if (kill -0 "$(<"${tmpdir}/dl_pid")" 2>/dev/null); then 568 | sleep 1 569 | else 570 | break 571 | fi 572 | done 573 | } 574 | 575 | 576 | dl_src_direct () { 577 | db="$1" 578 | shift 579 | md5="$1" 580 | shift 581 | filename="$*" 582 | 583 | case "$db" in 584 | libgen) 585 | url="${downloadurl[$db]}/$(get_torrent "${db}" "${md5}")/${md5,,}/placeholder" 586 | ;; 587 | libgen_fiction) 588 | extension=$(get_attr 'extension' "${db}" "${md5}") 589 | url="${downloadurl[$db]}/$(get_torrent "${db}" "${md5}")/${md5,,}.${extension,,}/placeholder" 590 | ;; 591 | *) 592 | exit_with_error "no direct download available for $db" 593 | ;; 594 | esac 595 | 596 | if ! url_available "$url"; then 597 | 598 | url="" 599 | parser=$(find_tool "$parser_tools") 600 | 601 | if [[ $parser == "xidel" ]]; then 602 | url=$(xidel --user-agent "$user_agent" -s "${downloadurl[$db]}/${md5}" -e '//td[@id="info"]/h2/a/@href'); 603 | elif [[ $parser == "hxwls" ]]; then 604 | url=$(hxwls "${downloadurl[$db]}/${md5}"|head -2|tail -1) 605 | fi 606 | fi 607 | 608 | [[ -n "$url" ]] && get_file "'${filename}'" "${url}" 609 | } 610 | 611 | dl_src_ipfs () { 612 | db="$1" 613 | shift 614 | md5="$1" 615 | shift 616 | filename="$*" 617 | 618 | ipfs_cid=$(get_attr 'ipfs_cid' "${db}" "${md5}") 619 | 620 | if [[ -z $ipfs_cid ]]; then 621 | echo "ipfs_cid not found, trying direct download..." 622 | dl_src_direct "${db}" "${md5}" "${filename}" 623 | else 624 | url="${ipfs_gw}/ipfs/${ipfs_cid}" 625 | get_file "'${filename}'" "${url}" 626 | fi 627 | } 628 | 629 | dl_src_torrent () { 630 | db="$1" 631 | shift 632 | md5="$1" 633 | shift 634 | dest_filename="$*" 635 | 636 | torrent_abspath=$(get_torrent_filename "${db}" "${md5}" 1) 637 | ttool=$(find_tool "$torrent_tools") 638 | 639 | if dl_torrent "$db" "$md5"; then 640 | 641 | torrent_filepath=$($ttool torrent-files "$torrent_abspath"|grep -i "$md5") 642 | 643 | if [[ -f "$torrent_abspath" ]]; then 644 | torrent_job=$(date +%Y%m%d-%H%M%S)-$(get_torrent "$db" "$md5")"-$md5.job" 645 | torrent_log="$target_directory/${torrent_directory:+$torrent_directory/}${torrent_job}" 646 | torrent_btih=$($ttool torrent-hash "$torrent_abspath") 647 | 648 | cat <<- EOT > "$torrent_log" 649 | #!/usr/bin/env bash 650 | # command: "$ttool add-selective $torrent_abspath $md5" 651 | tdir="$torrent_download_directory" 652 | tpath="$torrent_filepath" 653 | dest="$target_directory/$dest_filename" 654 | btih="$torrent_btih" 655 | ttool="$ttool" 656 | cronjob_remove () { 657 | if cronjob_active; then 658 | (crontab -l) 2>/dev/null | grep -v "$torrent_log" | sort | uniq | crontab - 659 | fi 660 | } 661 | torrent_restart () { $ttool add-selective "$torrent_abspath" "$md5"; } 662 | direct_download () { exec $program -u n -J "$md5"; } 663 | cronjob_active () { crontab -l|grep -q "$torrent_log"; } 664 | EOT 665 | 666 | cat <<- 'EOT' >> "$torrent_log" 667 | torrent_remove () { $ttool remove "$btih"; } 668 | torrent_status () { $ttool ls "$btih"; } 669 | torrent_info () { $ttool info "$btih"; } 670 | torrent_active () { $ttool active "$btih"; } 671 | job_status () { 672 | if [[ -f "$dest" ]]; then echo -e "job finished, file copied to destination:\n$dest"; 673 | elif [[ -f "$tdir/$tpath" ]]; then echo "torrent downloaded"; 674 | if cronjob_active; then echo "cronjob active, wait for file to be copied to destination"; 675 | else echo "run this job without options to copy file to destination"; 676 | fi 677 | elif torrent_active; then echo "torrent active:";torrent_status; 678 | else echo "job inactive, file not downloaded, -R or -D to retry"; 679 | fi 680 | } 681 | check_md5 () { 682 | md5_real=$(md5sum "$1"|awk '{print $1}') 683 | md5=$(basename "$tpath") 684 | [[ "${md5_real,,}" == "${md5,,}" ]] 685 | } 686 | copy_file () { 687 | if [[ -f "$tdir/$tpath" ]]; then 688 | if check_md5 "$tdir/$tpath"; then 689 | install -D "$tdir/$tpath" "$dest" 690 | else 691 | false 692 | fi 693 | else 694 | false 695 | fi 696 | } 697 | show_help () { 698 | cat <<-EOHELP 699 | Use: bash jobid.job [-s] -[i] [-r] [-R] [-D] [-h] [torrent_download_directory] 700 | 701 | Copies file from libgen/libgen_fiction torrent to correct location and name 702 | 703 | -S show job status 704 | -s show torrent status (short) 705 | -i show torrent info (long) 706 | -I show target file name 707 | -r remove torrent and cron jobs 708 | -R restart torrent download (does not restart cron job) 709 | -D direct download (removes torrent and cron jobs) 710 | -h show this help message 711 | EOHELP 712 | } 713 | if [[ "$1" == "-r" ]]; then torrent_remove; cronjob_remove; exit; fi 714 | if [[ "$1" == "-R" ]]; then torrent_restart; exit; fi 715 | if [[ "$1" == "-D" ]]; then torrent_remove; direct_download; exit; fi 716 | if [[ "$1" == "-s" ]]; then torrent_status; exit; fi 717 | if [[ "$1" == "-S" ]]; then job_status; exit; fi 718 | if [[ "$1" == "-i" ]]; then torrent_info; exit; fi 719 | if [[ "$1" == "-I" ]]; then echo "$dest"; exit; fi 720 | if [[ "$1" == "-h" ]]; then show_help; exit; fi 721 | if [[ "$1" =~ \-.* ]]; then echo "unknown option: $1"; show_help; exit; fi 722 | if torrent_active; then echo "torrent has not finished downloading"; exit 10; fi 723 | if [[ -z "$tdir" ]]; then if [[ -d "$1" ]]; then tdir="$1"; else show_help; exit; fi; fi 724 | count=0 725 | until [[ $count == 5 ]]; do 726 | if copy_file; then break; fi 727 | sleep 5 728 | ((count++)) 729 | done 730 | if [[ -f "$dest" ]]; then 731 | if ! check_md5 "$dest"; then 732 | echo "download corrupted, md5 does not match" 733 | fi 734 | else 735 | echo "download failed" 736 | fi 737 | cronjob_remove 738 | exit 739 | 740 | # torrent client output under this line 741 | :<<'END_OF_LOG' 742 | EOT 743 | 744 | (${ttool} add-selective "$torrent_abspath" "$md5";echo END_OF_LOG) >> "$torrent_log" 2>&1 & 745 | 746 | echo -e "torrent job started, job script:\n$torrent_log" 747 | 748 | # launch cron job? 749 | if [[ -n "$torrent_download_directory" && "$torrent_cron_job" -gt 0 ]]; then 750 | add_cron_job "bash $torrent_log" 751 | echo "torrent cron job started" 752 | fi 753 | fi 754 | else 755 | echo "try direct download (-u n)" 756 | fi 757 | } 758 | 759 | dl_torrent () { 760 | db="$1" 761 | md5="$2" 762 | torrent_filename="$(get_torrent_filename "${db}" "${md5}")" 763 | torrent_abspath="$(get_torrent_filename "${db}" "${md5}" 1)" 764 | 765 | if [[ ! -f "$torrent_abspath" ]]; then 766 | url="${torrenturl[$db]}/${torrent_filename}" 767 | if url_available "$url"; then 768 | get_file "'${torrent_directory:+$torrent_directory/}${torrent_filename}'" "${url}" 769 | else 770 | echo "Torrent $torrent_filename not available" 771 | false 772 | fi 773 | fi 774 | } 775 | 776 | # DATABASE 777 | 778 | # currently only the main libgen db can be updated through the api 779 | update_database () { 780 | db="${programs[books]}" 781 | last_update=$(($(date +%s)-$(date -d "$(get_time_last_modified "$db")" +%s))) 782 | if [[ $last_update -gt $((max_age*60)) ]]; then 783 | if [[ $no_update -eq 0 ]]; then 784 | update_libgen=$(find_tool "update_libgen") 785 | "$update_libgen" 786 | else 787 | echo "The database was last updated $((last_update/60)) minutes ago, consider updating" 788 | fi 789 | fi 790 | } 791 | 792 | get_current_fields () { 793 | db="${programs[$program]}" 794 | declare -a db_tables="${tables[$db]}" 795 | 796 | for table in "${db_tables[@]}"; do 797 | dbx "$db" "describe $table;"|awk '{print tolower($1)}' 798 | done 799 | } 800 | 801 | get_time_last_modified () { 802 | dbx "$db" 'select MAX(TimeLastModified) FROM updated;' 803 | } 804 | 805 | add_clause () { 806 | option="$1" 807 | shift 808 | pattern="$*" 809 | 810 | db="${programs[$program]}" 811 | 812 | if [[ ${pattern:0:1} == '-' ]]; then 813 | # option as argument, rewind OPTIND 814 | ((OPTIND-=1)) 815 | unset pattern 816 | fi 817 | 818 | if [[ $current_fields =~ ${schema[${option}]} ]]; then 819 | if [[ -n $pattern ]]; then 820 | # escape ' " \ % _ 821 | pattern=${pattern//\\/\\\\} 822 | pattern=${pattern//\'/\\\'} 823 | pattern=${pattern//\"/\\\"} 824 | pattern=${pattern//%/\\%} 825 | pattern=${pattern//_/\\_} 826 | [[ ! $opt_pattern =~ $pattern ]] && opt_pattern+=" $pattern" 827 | if [ -z "$exact_match" ]; then 828 | clauses+=" and ${schema[${option}]} like '%${pattern}%'" 829 | elif [ "$exact_match" -eq 1 ]; then 830 | clauses+=" and ${schema[${option}]} like '${pattern}%'" 831 | elif [ "$exact_match" -eq 2 ]; then 832 | clauses+=" and ${schema[${option}]} like '%${pattern}'" 833 | else 834 | clauses+=" and ${schema[${option}]}='${pattern}'" 835 | fi 836 | fi 837 | 838 | [[ ! $opt_fields =~ ${schema[${option}]} ]] && opt_fields+="${schema[${option}]}," 839 | else 840 | echo "warning: option -$option ignored (database $db does not contain column ${schema[${option}]})" 841 | fi 842 | } 843 | 844 | # the 'pager' gets special treatment as it likes its lines unbroken... 845 | run_query () { 846 | db="$1" 847 | shift 848 | filter="$1" 849 | shift 850 | sql="$*" 851 | 852 | declare -a line 853 | query_result=() 854 | 855 | while IFS=$'\t' read -ra line; do 856 | if [[ $ui_tool == "pager" ]]; then 857 | IFS='' query_result+=("$(printf '%s\t' "${line[@]}")") 858 | else 859 | query_result+=("${line[@]}") 860 | fi 861 | done < <(dbx "$db" "$sql"|(eval "${filters[$filter]}")) 862 | } 863 | 864 | get_attr () { 865 | role="$1" 866 | db="$2" 867 | md5="$3" 868 | 869 | # special case for filename with md5 870 | if [[ "$role" == 'filename' && $filename_add_md5 -gt 0 ]]; then 871 | role='filename_md5' 872 | fi 873 | 874 | sql=$(prepare_sql "${db}" "${role}") 875 | 876 | run_query "${db}" "${role}" "${sql}" 877 | 878 | if [[ ${#query_result[@]} -gt 0 ]]; then 879 | echo "${query_result[0]}" 880 | fi 881 | } 882 | 883 | get_torrent () { 884 | db="$1" 885 | md5="$2" 886 | 887 | id=$(get_attr 'id' "${db}" "${md5}") 888 | 889 | [[ -n "$id" ]] && echo "$((id-id%1000))" 890 | } 891 | 892 | # this creates leading directories when called with 2 parameters (value of $2 does not matter) 893 | get_filename () { 894 | md5="$1" 895 | create="$2" 896 | db="${programs[$program]}" 897 | 898 | filename=$(get_attr "filename" "$db" "$md5") 899 | 900 | # trim overly long filenames to 255 characters 901 | [[ ${#filename} -gt 255 ]] && filename="${filename:0:126}...${filename: -126}" 902 | 903 | if [[ -n $filename ]]; then 904 | if [[ -n $use_deep_path ]]; then 905 | dirname="${dirprefix[$db]}/$(get_attr 'dirname' "$db" "$md5")" 906 | if [[ -n $dirname && -n $create ]]; then 907 | mkdir -p "${target_directory}/${dirname}" 908 | filename="${dirname}/${filename}" 909 | fi 910 | fi 911 | echo "$filename" 912 | fi 913 | } 914 | 915 | get_torrentpath () { 916 | md5="$1" 917 | db="${programs[$program]}" 918 | 919 | torrent=$(get_torrent "$db" "$md5") 920 | [[ -n "$torrent" ]] && echo "$torrent/$md5" 921 | } 922 | 923 | get_attributes () { 924 | md5="$1" 925 | db="${programs[$program]}" 926 | role="attributes" 927 | 928 | get_attr "$role" "$db" "$md5" 929 | } 930 | 931 | # PREVIEWS 932 | 933 | preview () { 934 | db=$1 935 | shift 936 | for md5 in "$@"; do 937 | preview_${ui_tool} "$db" "$md5" 938 | done 939 | } 940 | 941 | # don't mess with the 'ugly formatting', the embedded newlines are part of the preview dialog 942 | preview_dialog () { 943 | db="$1" 944 | md5="$2" 945 | if [[ -n $db && -n $md5 ]]; then 946 | sql=$(prepare_sql "$db" "preview" "$ui_tool") 947 | run_query "$db" "preview_dialog" "${sql}" 948 | if [[ ${#query_result[@]} -gt 0 ]]; then 949 | filename=$(get_attr 'filename' "$db" "$md5") 950 | exec 3>&1 951 | dialog_result=$(dialog --backtitle "${program} - preview" --colors --cr-wrap --no-collapse --extra-button --ok-label "Download" --extra-label "Skip" --no-cancel --yesno \ 952 | "\Z1Author\Zn: ${query_result[0]} 953 | \Z1Title\Zn: ${query_result[1]} 954 | \Z1Volume\Zn: ${query_result[2]} \Z1Series\Zn: ${query_result[3]} \Z1Edition\Zn: ${query_result[4]} 955 | \Z1Year\Zn: ${query_result[5]} \Z1Publisher\Zn: ${query_result[6]} 956 | \Z1Language\Zn: ${query_result[7]} \Z1Size\Zn: ${query_result[8]} \Z1Type\Zn: ${query_result[9]} 957 | \Z1OLID\Zn: ${query_result[10]} \Z1ISBN\Zn: ${query_result[11]} \Z1MD5\Zn: ${md5^^} 958 | 959 | \Z1Filename\Zn: ${filename} 960 | 961 | ${query_result[12]}" \ 962 | 0 0 2>&1 1>&3) 963 | dialog_exit=$? 964 | exec 3>&- 965 | if [[ $dialog_exit -eq 0 ]]; then 966 | download "${db}" "${md5}" 967 | fi 968 | fi 969 | fi 970 | } 971 | 972 | preview_whiptail () { 973 | db=$1 974 | md5=$2 975 | if [[ -n $db && -n $md5 ]]; then 976 | sql=$(prepare_sql "$db" "preview" "$ui_tool") 977 | run_query "$db" "preview_whiptail" "${sql}" 978 | if [[ ${#query_result[@]} -gt 0 ]]; then 979 | filename=$(get_attr 'filename' "$db" "$md5") 980 | exec 3>&1 981 | whiptail_result=$(whiptail --backtitle "${program} - preview" --yes-button "Download" --no-button "Skip" --nocancel --yesno \ 982 | "Author: ${query_result[0]} 983 | Title: ${query_result[1]} 984 | Volume: ${query_result[2]} Series: ${query_result[3]} Edition: ${query_result[4]} 985 | Year: ${query_result[5]} Publisher: ${query_result[6]} 986 | Language: ${query_result[7]} Size: ${query_result[8]} Type: ${query_result[9]} 987 | OLID: ${query_result[10]} ISBN: ${query_result[11]} MD5: ${md5^^} 988 | 989 | Filename: ${filename} 990 | 991 | ${query_result[12]}" \ 992 | 0 0 2>&1 1>&3) 993 | whiptail_exit=$? 994 | exec 3>&- 995 | if [[ $whiptail_exit -eq 0 ]]; then 996 | download "${db}" "${md5}" 997 | fi 998 | fi 999 | fi 1000 | } 1001 | 1002 | 1003 | preview_zenity () { 1004 | db="$1" 1005 | md5="$2" 1006 | if [[ -n $db && -n $md5 ]]; then 1007 | sql=$(prepare_sql "$db" "preview" "$ui_tool") 1008 | run_query "$db" "preview_zenity" "${sql}" 1009 | if [[ ${#query_result[@]} -gt 0 ]]; then 1010 | filename=$(get_attr 'filename' "$db" "$md5") 1011 | info="
Author:${query_result[0]}
Title:${query_result[1]}
Volume:${query_result[2]}Series:${query_result[3]}Edition:${query_result[4]}
Year:${query_result[5]}Publisher:${query_result[6]}
Language:${query_result[7]}Size:${query_result[8]}Type:${query_result[9]}
OLID:${query_result[10]}ISBN:${query_result[11]}MD5:${md5^^}
${filename}

$(strip_html "${query_result[12]}")
" 1012 | if zenity_result=$(echo "$info"|zenity --width $x_size --height $y_size --text-info --html --ok-label "Download" --cancel-label "Skip" --filename=/dev/stdin 2>/dev/null); then 1013 | download "${db}" "${md5}" 1014 | fi 1015 | fi 1016 | fi 1017 | } 1018 | 1019 | preview_yad () { 1020 | db="$1" 1021 | md5="$2" 1022 | for md5 in "$@"; do 1023 | sql=$(prepare_sql "$db" "preview" "$ui_tool") 1024 | 1025 | run_query "$db" "preview_yad" "${sql}" 1026 | if [[ ${#query_result[@]} -gt 0 ]]; then 1027 | filename=$(get_attr 'filename' "$db" "$md5") 1028 | info="
Author:${query_result[0]}
Title:${query_result[1]}
Volume:${query_result[2]}Series:${query_result[3]}Edition:${query_result[4]}
Year:${query_result[5]}Publisher:${query_result[6]}
Language:${query_result[7]}Size:${query_result[8]}Type:${query_result[9]}
OLID:${query_result[10]}ISBN:${query_result[11]}MD5:${md5^^}
${filename}

$(strip_html "${query_result[12]}")
" 1029 | if yad_result=$(echo "$info"|yad --width $x_size --height $y_size --html --button='gtk-cancel:1' --button='Download!filesave!Download this publication:0' --filename=/dev/stdin 2>/dev/null); then 1030 | download "${db}" "${md5}" 1031 | fi 1032 | fi 1033 | done 1034 | } 1035 | 1036 | # LIST VIEWS 1037 | 1038 | list_pager () { 1039 | show_preview="$1" # ignored, no preview possible using pager 1040 | 1041 | pager=$(find_tool "$pager_tools") 1042 | 1043 | [[ $pager == "less" ]] && pager_options="-S" 1044 | 1045 | 1046 | if [[ ${#query_result[@]} -gt 0 ]]; then 1047 | (for index in "${!query_result[@]}"; do 1048 | echo "${query_result[$index]}" 1049 | done)|column -t -n -x -s $'\t'|$pager $pager_options 1050 | fi 1051 | } 1052 | 1053 | list_dialog () { 1054 | show_preview="$1" 1055 | if [[ ${#query_result[@]} -gt 0 ]]; then 1056 | exec 3>&1 1057 | dialog_result=$(dialog --separate-output --no-tags --backtitle "$program - search" --title "$window_title" --checklist "${list_heading}" 0 0 0 -- "${query_result[@]}" 2>&1 1>&3) 1058 | dialog_exit=$? 1059 | exec 3>&- 1060 | clear 1061 | if [[ -n $dialog_result ]]; then 1062 | if [[ $show_preview -gt 0 ]]; then 1063 | # shellcheck disable=SC2086 1064 | preview "$db" $dialog_result 1065 | else 1066 | # shellcheck disable=SC2086 1067 | download "$db" $dialog_result 1068 | fi 1069 | fi 1070 | fi 1071 | } 1072 | 1073 | # current whiptail (as of the date of writing, 20160326) has a bug which makes it ignore --notags 1074 | # https://bugzilla.redhat.com/show_bug.cgi?id=1215239 1075 | list_whiptail () { 1076 | show_preview="$1" 1077 | if [[ ${#query_result[@]} -gt 0 ]]; then 1078 | exec 3>&1 1079 | whiptail_result=$(whiptail --separate-output --notags --backtitle "$program - search" --title "$window_title" --checklist "${list_heading}" 0 0 0 -- "${query_result[@]}" 2>&1 1>&3) 1080 | whiptail_exit=$? 1081 | exec 3>&- 1082 | clear 1083 | if [[ -n $whiptail_result ]]; then 1084 | if [[ $show_preview -gt 0 ]]; then 1085 | # shellcheck disable=SC2086 1086 | preview "$db" $whiptail_result 1087 | else 1088 | # shellcheck disable=SC2086 1089 | download "$db" $whiptail_result 1090 | fi 1091 | fi 1092 | fi 1093 | } 1094 | 1095 | list_yad () { 1096 | show_preview="$1" 1097 | db="${programs[$program]}" 1098 | if [[ ${#query_result[@]} -gt 0 ]]; then 1099 | # shellcheck disable=SC2086 1100 | yad_result=$(yad --width $x_size --height $y_size --separator=" " --title "$program :: ${window_title}" --text "${list_heading}" --list --checklist --dclick-action='bash -c "libgen_preview '"$db"' %s" &' ${yad_columns[$db]} -- "${query_result[@]}" 2>/dev/null) 1101 | if [[ -n $yad_result ]]; then 1102 | if [[ $show_preview -gt 0 ]]; then 1103 | # shellcheck disable=SC2086 1104 | preview "$db" $yad_result 1105 | else 1106 | # shellcheck disable=SC2086 1107 | download "$db" $yad_result 1108 | fi 1109 | fi 1110 | fi 1111 | 1112 | } 1113 | 1114 | # zenity does not support the '--' end of options convention leading to problems when the query result contains dashes, 1115 | # hence these are replaced by underscores in the query_result. 1116 | list_zenity () { 1117 | show_preview="$1" 1118 | db="${programs[$program]}" 1119 | if [[ ${#query_result[@]} -gt 0 ]]; then 1120 | # shellcheck disable=SC2086 1121 | zenity_result=$(zenity --width $x_size --height $y_size --separator=" " --title "$program :: ${window_title}" --text "${list_heading}" --list --checklist ${zenity_columns[$db]} "${query_result[@]}" 2>/dev/null) 1122 | if [[ -n $zenity_result ]];then 1123 | if [[ $show_preview -gt 0 ]]; then 1124 | # shellcheck disable=SC2086 1125 | preview "$db" $zenity_result 1126 | else 1127 | # shellcheck disable=SC2086 1128 | download "$db" $zenity_result 1129 | fi 1130 | fi 1131 | fi 1132 | 1133 | } 1134 | 1135 | # SQL 1136 | 1137 | get_fields () { 1138 | # shellcheck disable=SC2086 1139 | db="${programs[$program]}" 1140 | [[ -z "$show_fields" ]] && show_fields="${pager_columns[$db]}" 1141 | IFS=',' read -ra fields <<< "$show_fields" 1142 | result="" 1143 | 1144 | for field in "${fields[@]}"; do 1145 | [[ ! "${current_fields[*],,}" =~ ${field,,} ]] && exit_with_error "no such field: $field" 1146 | table="m" 1147 | for category in "${!attribute_columns[@]}"; do 1148 | if [[ "${field,,}" =~ ${attribute_columns[$category],,} ]]; then 1149 | table="${category:0:1}" 1150 | break 1151 | fi 1152 | done 1153 | result+="${result:+,}greatest(${table}.${field}, '-')" 1154 | done 1155 | 1156 | echo -n "$result" 1157 | } 1158 | 1159 | 1160 | prepare_sql () { 1161 | db="$1" 1162 | role="$2" 1163 | ui_tool="$3" 1164 | 1165 | # SQL to: 1166 | # build filenames... 1167 | 1168 | declare -A sql_filename=( 1169 | [libgen]="select concat_ws('.',concat_ws('-', trim(Series), trim(Author), trim(Title), trim(Year), trim(Publisher), trim(language)), trim(extension)) from updated where md5='${md5}' limit 1;" 1170 | [libgen_fiction]="select concat_ws('.',concat_ws('-', trim(Series), trim(Author), trim(Title), trim(Year), trim(Publisher), trim(language)), trim(extension)) from fiction where md5='${md5}' limit 1;" 1171 | ) 1172 | 1173 | declare -A sql_filename_md5=( 1174 | [libgen]="select concat_ws('.',concat_ws('-', trim(Series), trim(Author), trim(Title), trim(Year), trim(Publisher), trim(language), trim(md5)), trim(extension)) from updated where md5='${md5}' limit 1;" 1175 | [libgen_fiction]="select concat_ws('.',concat_ws('-', trim(Series), trim(Author), trim(Title), trim(Year), trim(Publisher), trim(language), trim(md5)), trim(extension)) from fiction where md5='${md5}' limit 1;" 1176 | ) 1177 | 1178 | # build directory names... 1179 | declare -A sql_dirname=( 1180 | [libgen]="select concat_ws('/', trim(language), regexp_replace(trim(topic_descr),'"'[\\\\]+'"','/'), trim(Author), trim(Series)) as dirname from updated as u left join topics as t on u.topic=t.topic_id and t.lang='${language}' where md5='${md5}' limit 1;" 1181 | [libgen_fiction]="select concat_ws('/', trim(language), trim(Author), trim(Series)) as dirname from fiction where md5='${md5}' limit 1;" 1182 | ) 1183 | 1184 | # get id 1185 | declare -A sql_id=( 1186 | [libgen]="select id from updated where md5='${md5}' limit 1;" 1187 | [libgen_fiction]="select id from fiction where md5='${md5}' limit 1;" 1188 | ) 1189 | 1190 | # get extension 1191 | declare -A sql_extension=( 1192 | [libgen]="select extension from updated where md5='${md5}' limit 1;" 1193 | [libgen_fiction]="select extension from fiction where md5='${md5}' limit 1;" 1194 | ) 1195 | 1196 | # get attributes 1197 | declare -A sql_attributes=( 1198 | [libgen]="select $(get_fields) from updated as m left join description as d on m.md5=d.md5 left join topics as t on m.topic=t.topic_id and t.lang='${language}' left join hashes as h on m.md5=h.md5 where m.md5='${md5}' limit 1;" 1199 | [libgen_fiction]="select $(get_fields) from fiction as m left join fiction_description as d on m.md5=d.md5 left join fiction_hashes as h on m.md5=h.md5 where m.md5='${md5}' limit 1;" 1200 | ) 1201 | 1202 | # get hashes 1203 | declare -A sql_sha1s=( 1204 | [libgen]="select sha1 from hashes where md5='$md5' limit 1;" 1205 | [libgen_fiction]="select sha1 from fiction_hashes where md5='$md5' limit 1;" 1206 | ) 1207 | 1208 | # get ipfs content id hash 1209 | declare -A sql_ipfs_cid=( 1210 | [libgen]="select ipfs_cid from hashes where md5='${md5}' limit 1;" 1211 | [libgen_fiction]="select ipfs_cid from fiction_hashes where md5='${md5}' limit 1;" 1212 | ) 1213 | 1214 | # preview publication... 1215 | 1216 | declare -A sql_preview_dialog=( 1217 | [libgen]="select greatest(Author, '-'), greatest(Title, '-'), greatest(VolumeInfo, '-'), greatest(Series, '-'), greatest(Edition, '-'), greatest(Year, '-'), greatest(Publisher, '-'), greatest(language, '-'), greatest(filesize, '-'), greatest(extension, '-'), greatest(OpenLibraryID, '-'), greatest(IdentifierWODash, '-'), greatest(ifnull(descr,'-'), '-'), Coverurl from updated left join description on updated.md5=description.md5 where updated.md5='${md5}' limit 1;" 1218 | [libgen_fiction]="select greatest(Author, '-'), greatest(Title, '-'), greatest(Issue, '-'), greatest(Series, '-'), greatest(Edition, '-'), greatest(Year, '-'), greatest(Publisher, '-'), greatest(Language, '-'), greatest(Filesize, '-'), greatest(Extension, '-'), greatest(Identifier,'-'), greatest(Commentary,'-'), greatest(ifnull(descr,'-'), '-'), Coverurl from fiction as f left join fiction_description as d on f.md5=d.md5 where f.md5='${md5}' limit 1;" 1219 | ) 1220 | 1221 | 1222 | declare -n sql_preview_whiptail="sql_preview_dialog" 1223 | 1224 | declare -A sql_preview_zenity=( 1225 | [libgen]="select greatest(Author, '-'), greatest(Title, '-'), greatest(VolumeInfo, '-'), greatest(Series, '-'), greatest(Edition, '-'), greatest(Year, '-'), greatest(Publisher, '-'), greatest(language, '-'), greatest(filesize, '-'), greatest(extension, '-'), greatest(OpenLibraryID, '-'), greatest(IdentifierWODash, '-'), greatest(ifnull(descr,'-'), '-'), Coverurl from updated left join description on updated.md5=description.md5 where updated.md5='${md5}' limit 1;" 1226 | [libgen_fiction]="select greatest(Author, '-'), greatest(Title, '-'), greatest(Issue, '-'), greatest(Series, '-'), greatest(Edition, '-'), greatest(Year, '-'), greatest(Publisher, '-'), greatest(language, '-'), greatest(filesize, '-'), greatest(extension, '-'), greatest(Identifier, '-'), greatest(Commentary,'-'), greatest(ifnull(descr,'-'), '-'), Coverurl from fiction as f left join fiction_description as d on f.md5=d.md5 where f.md5='${md5}' limit 1;" 1227 | ) 1228 | 1229 | declare -A sql_preview_yad=( 1230 | [libgen]="select greatest(Author, '-'), greatest(Title, '-'), greatest(VolumeInfo, '-'), greatest(Series, '-'), greatest(Edition, '-'), greatest(Year, '-'), greatest(Publisher, '-'), greatest(language, '-'), greatest(filesize, '-'), greatest(extension, '-'), greatest(OpenLibraryID, '-'), greatest(IdentifierWODash, '-'), greatest(ifnull(descr,'-'), '-'), Coverurl from updated left join description on updated.md5=description.md5 where updated.md5='${md5}' limit 1;" 1231 | [libgen_fiction]="select greatest(Author, '-'), greatest(Title, '-'), greatest(Issue, '-'), greatest(Series, '-'), greatest(Edition, '-'), greatest(Year, '-'), greatest(Publisher, '-'), greatest(language, '-'), greatest(filesize, '-'), greatest(extension, '-'), greatest(Identifier, '-'), greatest(Commentary,'-'), greatest(ifnull(descr,'-'), '-'), Coverurl from fiction as f left join fiction_description as d on f.md5=d.md5 where f.md5='${md5}' limit 1;" 1232 | ) 1233 | 1234 | # search... 1235 | 1236 | declare -A sql_search_pager=( 1237 | [libgen]="select $(get_fields) from updated as m left join description as d on m.md5=d.md5 left join topics as t on m.topic=t.topic_id and t.lang='${language}' where TRUE ${query} ${clauses} ${sql_limit};" 1238 | [libgen_fiction]="select $(get_fields) from fiction as m left join fiction_description as d on m.md5=d.md5 where TRUE ${query} ${clauses} ${sql_limit};" 1239 | ) 1240 | 1241 | declare -A sql_search_dialog=( 1242 | [libgen]="select u.MD5, concat_ws('|', rpad(greatest(u.Title,'-'),70,' '), rpad(greatest(u.Author,'-'), 30,' '), rpad(greatest(u.Year,'-'), 5,' '), rpad(greatest(u.Edition,'-'), 20, ' '), rpad(greatest(u.Publisher,'-'), 30,' '), rpad(greatest(u.Language,'-'), 10, ' '), rpad(greatest(ifnull(t.Topic_descr, '-'),'-'),30,' '), rpad(greatest(u.Filesize,'-'),10,' '), rpad(greatest(u.Extension,'-'),6,' ')), 'off' from updated as u left join topics as t on u.topic=t.topic_id and t.lang='${language}' where TRUE ${query} ${clauses} ${sql_limit};" 1243 | [libgen_fiction]="select f.MD5, concat_ws('|', rpad(greatest(f.Title,'-'),70,' '), rpad(greatest(f.Author,'-'), 30,' '), rpad(greatest(f.Year,'-'), 5,' '), rpad(greatest(f.Edition,'-'), 20, ' '), rpad(greatest(f.Publisher,'-'), 30,' '), rpad(greatest(f.Language,'-'), 10, ' '), rpad(greatest(f.Series,'-'),30,' '), rpad(greatest(f.Filesize,'-'),10,' '), rpad(greatest(f.Extension,'-'),6,' ')), 'off' from fiction as f where TRUE ${query} ${clauses} ${sql_limit};" 1244 | ) 1245 | 1246 | declare -n sql_search_whiptail="sql_search_dialog" 1247 | 1248 | declare -A sql_search_yad=( 1249 | [libgen]="select 'FALSE', u.MD5, left(greatest(u.Title,'-'),70), left(greatest(u.Author, '-'),50),greatest(u.Year, '-'),left(greatest(u.Edition, '-'),20),left(greatest(u.Publisher, '-'),30),greatest(u.language, '-'), left(greatest(ifnull(t.Topic_descr,'-'),'-'),30), greatest(u.Filesize,'-'), greatest(u.Extension, '-'), concat('Title: ',Title, ' Author: ', Author, ' Path: ', Locator) from updated as u left join topics as t on u.topic=t.topic_id and t.lang='${language}' where TRUE ${query} ${clauses} ${sql_limit};" 1250 | [libgen_fiction]="select 'FALSE', f.MD5, left(greatest(f.Title,'-'),70), left(greatest(f.Author, '-'),50),greatest(f.Year, '-'),left(greatest(f.Edition, '-'),20),left(greatest(f.Publisher, '-'),30),greatest(f.language, '-'), left(greatest(f.Series,'-'),30), greatest(f.Filesize,'-'), greatest(f.Extension, '-'), concat('Title: ',Title, ' Author: ', Author, ' Path: ', Locator) from fiction as f where TRUE ${query} ${clauses} ${sql_limit};" 1251 | ) 1252 | 1253 | declare -A sql_search_zenity=( 1254 | [libgen]="select 'FALSE', u.MD5, left(greatest(u.Title,'-'),70), left(greatest(u.Author, '-'),50),greatest(u.Year, '-'),left(greatest(u.Edition, '-'),20),left(greatest(u.Publisher, '-'),30),greatest(u.language, '-'), left(greatest(ifnull(t.Topic_descr,'-'),'-'),30), greatest(u.Filesize,'-'), greatest(u.Extension, '-') from updated as u left join topics as t on u.topic=t.topic_id and t.lang='${language}' where TRUE ${query} ${clauses} ${sql_limit};" 1255 | [libgen_fiction]="select 'FALSE', f.MD5, left(greatest(f.Title,'-'),70), left(greatest(f.Author, '-'),50),greatest(f.Year, '-'),left(greatest(f.Edition, '-'),20),left(greatest(f.Publisher, '-'),30),greatest(f.language, '-'), left(greatest(f.Series,'-'),30), greatest(f.Filesize,'-'), greatest(f.Extension, '-') from fiction as f where TRUE ${query} ${clauses} ${sql_limit};" 1256 | ) 1257 | 1258 | declare -A sql_search_all_pager=( 1259 | [libgen]="(select u.Title, u.Author,u.Year,u.Edition,u.Publisher,u.language,ifnull(t.Topic_descr, '-'), u.Filesize, u.Extension, u.Locator, md5 from updated as u left join topics as t on u.topic=t.topic_id and t.lang='${language}' where TRUE ${query} ${clauses} ${sql_limit}) UNION (select u.Title, u.Author,u.Year,u.Edition,u.Publisher,u.language,t.Topic_descr,u.Filesize, u.Extension, u.Locator, u.md5 from updated_edited as u left join topics as t on u.topic=t.topic_id and t.lang='${language}' where TRUE ${query} ${clauses} ${sql_limit});" 1260 | ) 1261 | 1262 | if [[ -n $ui_tool ]]; then 1263 | sql_source="sql_${role}_${ui_tool}" 1264 | else 1265 | sql_source="sql_${role}" 1266 | fi 1267 | 1268 | echo "${sql_source[$db]}" 1269 | } 1270 | 1271 | # UTILITY FUNCTIONS 1272 | 1273 | get_db_for_md5 () { 1274 | md5="$1" 1275 | 1276 | for dbs in "${!tables[@]}"; do 1277 | fmd5=$(get_attr "md5" "$db" "$md5") 1278 | if [[ "$fmd5" == "$md5" ]]; then 1279 | db=$dbs 1280 | fi 1281 | done 1282 | 1283 | if [[ -n "$db" ]]; then 1284 | echo -n "$db" 1285 | fi 1286 | } 1287 | 1288 | get_torrent_filename () { 1289 | db="$1" 1290 | md5="$2" 1291 | absolute="$3" 1292 | 1293 | echo -n "${absolute:+$target_directory/${torrent_directory:+$torrent_directory/}}${torrentprefix[$db]}_$(get_torrent "${db}" "${md5}").torrent" 1294 | } 1295 | 1296 | create_symlinks () { 1297 | basedir="$(dirname "$0")" 1298 | sourcefile="$(readlink -e "$0")" 1299 | for name in "${!programs[@]}"; do 1300 | if [[ ! -e "$basedir/$name" ]]; then 1301 | ln -s "$sourcefile" "$basedir/$name" 1302 | fi 1303 | done 1304 | 1305 | exit 1306 | } 1307 | 1308 | check_settings () { 1309 | # does target directory exist? 1310 | [[ ! -d "$target_directory" ]] && exit_with_error "target_directory $target_directory does not exist"; 1311 | # when defined, does torrent download directory exist? 1312 | [[ -n "$torrent_download_directory" && ! -d "$torrent_download_directory" ]] && exit_with_error "torrent_download_directory $torrent_download_directory does not exist"; 1313 | # when defined, does torrent helper script exist? 1314 | if [[ -n "$use_torrent" ]]; then 1315 | if [[ -z "$torrent_tools" ]]; then 1316 | exit_with_error "-u needs torrent helper script, see \$torrent_tools" 1317 | elif ! find_tool "$torrent_tools" >/dev/null; then 1318 | exit_with_error "-u: torrent helper script ($torrent_tools) not found" 1319 | fi 1320 | fi 1321 | 1322 | if [[ "$use_torrent" && "$use_ipfs" ]]; then 1323 | exit_with_error "can not use_torrent and use_ipfs at the same time, check $config" 1324 | fi 1325 | } 1326 | 1327 | cleanup () { 1328 | if [[ $ui_tool == "whiptail" ]]; then 1329 | reset 1330 | fi 1331 | } 1332 | 1333 | # HELP 1334 | 1335 | help () { 1336 | echo "$(basename "$(readlink -f "$0")")" "version $version" 1337 | cat <<- 'EOF' 1338 | 1339 | Use: books OPTIONS [like] [] 1340 | 1341 | [B]ooks - which is only one of the names this program goes by - is a 1342 | front-end for accessing a locally accessible libgen / libgen_fiction database 1343 | instance, offering versatile search and download directly from the command 1344 | line. The included update_libgen tool is used to keep the database up to date - 1345 | if the database is older than a user-defined value it is updated before the 1346 | query is executed. This generally only takes a few seconds, but it might take 1347 | longer on a slow connection or after a long update interval. Updating can be 1348 | temporarily disabled by using the ‘-x’ command line option. To refresh the 1349 | database(s) from a dump file use the included refresh_libgen program. 1350 | 1351 | When books, nbook and/or xbook are regularly used the database should be kept 1352 | up to date automatically. In that case it is only necessary to use 1353 | refresh_libgen to refresh the database when you get a warning from 1354 | update_libgen about unknown columns in the API response. 1355 | 1356 | If the programs have not been used for a while it can take a long time - and a 1357 | lot of data transfer - to update the database through the API (which is what 1358 | update_libgen does). Especially when using the compact database it can be 1359 | quicker to use refresh_libgen to just pull the latest dump instead of waiting 1360 | for update_libgen to do its job. 1361 | 1362 | The fiction database can not be updated through the API (yet), so for 1363 | that databases refresh_libgen is currently the canonical way to get the latest 1364 | version. 1365 | 1366 | SEARCH BY FIELD: 1367 | 1368 | This is the default search mode. If no field options are given this searches 1369 | the Title field for the PATTERN. Search uses partial matching by default, use 1370 | -X for matching words starting with PATTERN, -XX to match words which end with 1371 | PATTERN and -XXX for exact matching. 1372 | 1373 | FULLTEXT SEARCH (-f): 1374 | 1375 | Performs a pattern match search over all fields indicated by the options. If no 1376 | field options are given, perform a pattern match search over the Author and 1377 | Title fields. 1378 | 1379 | Depending on which name this program is executed under it behaves differently: 1380 | 1381 | books: query database and show results, direct download with md5 1382 | books-all: query database and show results (exhaustive search over all tables, slow) 1383 | 1384 | nbook: select publications for download from list (terminal-based) 1385 | xbook: select publications for download from list (GUI) 1386 | 1387 | fiction: query database and show results (using 'fiction' database), direct download with md5 1388 | 1389 | nfiction: select publications for download from list (terminal-based, use 'fiction' database) 1390 | xfiction: select publications for download from list (GUI, use 'fiction' database) 1391 | 1392 | OPTIONS 1393 | 1394 | EOF 1395 | 1396 | for key in "${!schema[@]}"; do 1397 | echo " -${key} search on ${schema[$key]^^}" 1398 | done 1399 | 1400 | cat <<- 'EOF' 1401 | 1402 | -f fulltext search 1403 | searches for the given words in the fields indicated by the other options. 1404 | when no other options are given this will perform a pattern match search 1405 | for the given words over the Author and Title fields. 1406 | 1407 | -X search for fields starting with PATTERN 1408 | -XX search for fields ending with PATTERN 1409 | -XXX search for fields exacly matching PATTERN 1410 | 1411 | -w preview publication info before downloading (cover preview only in GUI tools) 1412 | select one or more publication to preview and press enter/click OK. 1413 | 1414 | double-clicking a result row also shows a preview irrespective of this option, 1415 | but this only works when using the yad gui tool 1416 | 1417 | -= DIR set download location to DIR 1418 | 1419 | -$ use extended path when downloading: 1420 | nonfiction/[topic/]author[/series]/title 1421 | fiction/language/author[/series]/title 1422 | 1423 | -u BOOL use bittorrent (-u 1 or -u y) or direct download (-u 0 or -u n) 1424 | this parameter overrides the default download method 1425 | bittorrent download depends on an external helper script 1426 | to interface with a bittorrent client 1427 | 1428 | -I BOOL use ipfs (-I 1 or -I y) or direct download (-I 0 or -I n) 1429 | this parameter overrides the default download method 1430 | ipfs download depends on a functioning ipfs gateway. 1431 | default gateway is hosted by Cloudfront, see https://ipfs.io/ 1432 | for instructions on how to run a local gateway 1433 | 1434 | -U MD5 print torrent path (torrent#/md5) for given MD5 1435 | 1436 | -j MD5 print filename for given MD5 1437 | 1438 | -J MD5 download file for given MD5 1439 | can be combined with -u to download with bittorrent 1440 | 1441 | -M MD5 fast path search on md5, only works in _books_ and _fiction_ 1442 | can be combined with -F FIELDS to select fields to be shown 1443 | output goes directly to the terminal (no pager) 1444 | 1445 | -F FIELDS select which fields to show in pager output 1446 | 1447 | -# LIMIT limit search to LIMIT hits (default: 1000) 1448 | 1449 | -x skip database update 1450 | (currently only the 'libgen' database can be updated) 1451 | 1452 | -@ TORPORT use torsocks to connect to the libgen server(s). You'll need to install 1453 | torsocks before using this option; try this in case your ISP 1454 | (or a transit provider somewhere en-route) blocks access to libgen 1455 | 1456 | -k install symlinks for all program invocations 1457 | 1458 | -h show this help message 1459 | 1460 | EXAMPLES 1461 | 1462 | Do a pattern match search on the Title field for 'ilias' and show the results in the terminal 1463 | 1464 | $ books like ilias 1465 | 1466 | 1467 | Do an exact search on the Title field for 'The Odyssey' and show the results in the terminal 1468 | 1469 | $ books 'the odyssey' 1470 | 1471 | 1472 | Do an exact search on the Title field for 'The Odyssey' and the Author field for 'Homer', showing 1473 | the result in the terminal 1474 | 1475 | $ books -X -t 'The Odyssey' -a 'Homer' 1476 | 1477 | 1478 | Do the same search as above, showing the results in a list on the terminal with checkboxes to select 1479 | one or more publications for download 1480 | 1481 | $ nbook -X -t 'The Odyssey' -a 'Homer' 1482 | 1483 | 1484 | A case-insensitive pattern search using an X11-based interface; use bittorrent (-u) when downloading files 1485 | 1486 | $ xbook -u y -t 'the odyssey' -a 'homer' 1487 | 1488 | 1489 | Do a fulltext search over the Title, Author, Series, Periodical and Publisher fields, showing the 1490 | results in a terminal-based checklist for download after preview (-w) 1491 | 1492 | $ nbook -w -f -t -a -s -r -p 'odyssey' 1493 | 1494 | 1495 | Walk over a directory of publications, compute md5 and use this to generate file names: 1496 | 1497 | $ find /path/to/publications -type f|while read f; do books -j $(md5sum "$f"|awk '{print $1}');done 1498 | 1499 | 1500 | As above, but print torrent number and path in torrent file 1501 | 1502 | $ find /path/to/publications -type f|while read f; do books -U $(md5sum "$f"|awk '{print $1}');done 1503 | 1504 | 1505 | Find publications by author 'thucydides' and show their md5,title and year in the terminal 1506 | 1507 | $ books -a thucydides -F md5,title,year 1508 | 1509 | 1510 | Get data on a single publication using fast path MD5 search, show author, title and extension 1511 | 1512 | $ books -M 51b4ee7bc7eeb6ed7f164830d5d904ae -F author,title,extension 1513 | 1514 | 1515 | Download a publication using its MD5 (-J MD5), using IPFS (-I y) to download 1516 | 1517 | $ books -I y -J 51b4ee7bc7eeb6ed7f164830d5d904ae 1518 | 1519 | EOF 1520 | } 1521 | 1522 | main "$@" 1523 | --------------------------------------------------------------------------------