]> git.lizzy.rs Git - local-nhentai.git/commitdiff
Initial commit
authorElias Fleckenstein <eliasfleckenstein@web.de>
Mon, 9 May 2022 18:38:41 +0000 (20:38 +0200)
committerElias Fleckenstein <eliasfleckenstein@web.de>
Mon, 9 May 2022 18:38:41 +0000 (20:38 +0200)
13 files changed:
.gitignore [new file with mode: 0644]
README.md [new file with mode: 0644]
du.js [new file with mode: 0644]
fzf-previews [new file with mode: 0755]
fzf.js [new file with mode: 0644]
info.js [new file with mode: 0644]
package-lock.json [new file with mode: 0644]
package.json [new file with mode: 0644]
select.js [new file with mode: 0644]
stats-doujins.js [new file with mode: 0644]
stats-tags.js [new file with mode: 0644]
symlinks.js [new file with mode: 0644]
wholesome.js [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..932f882
--- /dev/null
@@ -0,0 +1,7 @@
+*
+!*.js
+!*.json
+!fzf-previews
+!LICENSE
+!README.md
+!.gitignore
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..cf90880
--- /dev/null
+++ b/README.md
@@ -0,0 +1,31 @@
+# Local nHentai
+This is a collection of Node.js scripts that can be used to manage a local nhentai library.
+
+# Install instructions
+Dependencies:
+
+- `fzf`: [repo](https://github.com/junegunn/fzf), or `sudo apt install fzf`
+- `ueberzug`: [repo](https://github.com/seebye/ueberzug), or `pip3 install ueberzug`
+- `nhentai`: [repo](https://github.com/RicterZ/nhentai)
+- `npm`
+- `firefox`
+
+Remember to install NPM deps: `npm install`
+
+# Usage
+
+`nhentai --id ${id}`: Download a doujin
+`node symlinks.js`: Updates symlinks. Must be run whenever new doujins have been added, otherwise they will not be seen by the other scripts.
+`node select.js`: Open fzf/überzug menu to select a doujin. Shows a list of tags first. When a tag is selected, shows all doujins with that tag and lets the user select one, displaying the thumbnails of the doujins at the side. The "*" tag can be used to search/select from all doujins.
+`node stats-doujins.js`: Displays number of downloaded doujins, total size and average size per doujin
+`node stats-tags.js`: Displays tags sorted by how many doujins are available for each tag. May produce long output, you might want to pipe it into `head`, `grep` or `less`.
+`node wholesome.js`: Scrapes [wholesome hentais](https://wholesomelist.com/list). This takes a long time since it's about 2700 hentais in total (may consume 50GB of disk space), but you can abort it any time (and resume it later).
+
+Note: you might want to create a subdirectory and put the doujins into there, they will all be put into the current working directory. (Run the scripts from a different directory to prevent spamming this directory)
+Of course, you can also run these scripts in a directory where you already downloaded doujins using the `nhentai` tool, but make sure to run the symlinks script to "register" them all in the system.
+
+# Notes
+
+`./fzf-previews` taken from [fzf-ueberzogen](https://github.com/seebye/fzf-ueberzogen) with slight modifications.
+
+Happy scraping and jerking!
diff --git a/du.js b/du.js
new file mode 100644 (file)
index 0000000..947fe81
--- /dev/null
+++ b/du.js
@@ -0,0 +1,22 @@
+const child = require("child_process")
+
+module.exports = dir => {
+       let res, rej
+       const prom = new Promise((rs, rj) => [res, rej] = [rs, rj])
+
+       const proc = child.spawn("du", ["-b", "-L", dir])
+
+       let data = ""
+       proc.stdout.on("data", chunk => {
+               data += chunk
+       })
+
+       proc.on("close", code => {
+               if (code == 0)
+                       res(parseInt(data))
+               else
+                       rej(code)
+       })
+
+       return prom
+}
diff --git a/fzf-previews b/fzf-previews
new file mode 100755 (executable)
index 0000000..59cb388
--- /dev/null
@@ -0,0 +1,320 @@
+#!/usr/bin/env bash
+# fzf-ueberzogen.sh is a wrapper script which allows to use ueberzug with fzf.
+# Copyright (C) 2019  Nico Baeurer
+
+# 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.
+
+# 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.
+
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+readonly BASH_BINARY="$(which bash)"
+readonly REDRAW_COMMAND="toggle-preview+toggle-preview"
+readonly REDRAW_KEY="µ"
+declare -r -x DEFAULT_PREVIEW_POSITION="right"
+declare -r -x UEBERZUG_FIFO="$(mktemp --dry-run --suffix "fzf-$$-ueberzug")"
+declare -r -x PREVIEW_ID="preview"
+
+
+function STORE_TERMINAL_SIZE_IN {
+    # Usage: STORE_TERMINAL_SIZE_IN 
+    #           lines_variable columns_variable
+    [[ ! -v "$1" || ! -v "$2" ]] && return 1
+    < <(</dev/tty stty size) \
+        read "$1" "$2"
+}
+
+
+function STORE_FZF_HEIGHT_IN {
+    # Usage: STORE_FZF_HEIGHT_IN
+    #           fzf_height_lines_variable
+    #           terminal_lines fzf_height fzf_min_height
+    [[ $# -ne 4 || ! -v "$1" ]] && return 1
+    local -n _fzf_height_lines="$1"
+    local terminal_lines="$2"
+    local fzf_height_text="$3"
+    local fzf_min_height="$4"
+
+    _fzf_height_lines="${fzf_height_text}"
+
+    if [[ "${fzf_height_text}" == *"%" ]]; then
+        ((_fzf_height_lines=(terminal_lines * ${fzf_height_text%\%}) / 100))
+    else
+        ((_fzf_height_lines=_fzf_height_lines > terminal_lines ? terminal_lines :_fzf_height_lines))
+    fi
+
+    ((_fzf_height_lines=fzf_min_height > _fzf_height_lines
+                        ? fzf_min_height : _fzf_height_lines))
+}
+
+
+function STORE_FZF_OFFSET_IN {
+    # Usage: STORE_FZF_OFFSET_IN
+    #           fzf_offset_y_variable
+    #           terminal_lines fzf_height fzf_start_offset_y
+    [[ $# -ne 4 || ! -v "$1" ]] && return 1
+    local -n _fzf_offset_y="$1"
+    local terminal_lines="$2"
+    local fzf_height="$3"
+    local fzf_start_offset_y="$4"
+
+    # Two cases:
+    # 1. There isn't enough space, so fzf will print blank lines.
+    #    -> OFFSET_Y = terminal height - required lines
+    # 2. There is enough space -> OFFSET_Y = START_OFFSET_Y
+    ((_fzf_offset_y=terminal_lines - fzf_height))
+    ((_fzf_offset_y=_fzf_offset_y < fzf_start_offset_y
+                    ? _fzf_offset_y : fzf_start_offset_y))
+}
+
+
+function STORE_PREVIEW_POSITION_IN {
+    # Usage: STORE_PREVIEW_POSITION_IN
+    #           preview_y_variable preview_x_variable
+    #           preview_position fzf_offset_y fzf_height
+    #           terminal_width preview_height preview_width
+    [[ $# -ne 8 || ! -v "$1" || ! -v "$2" ]] && return 1
+    local -n _preview_y="$1"
+    local -n _preview_x="$2"
+    local preview_position="$3"
+    local fzf_offset_y="$4"
+    local fzf_height="$5"
+    local terminal_width="$6"
+    local preview_height="$7"
+    local preview_width="$8"
+
+    case "${preview_position}" in
+        left|up|top)
+            _preview_x=2
+            _preview_y=$((1 + fzf_offset_y))
+            ;;
+        right)
+            _preview_x=$((terminal_width - preview_width - 2))
+            _preview_y=$((1 + fzf_offset_y))
+            ;;
+        down|bottom)
+            _preview_x=2
+            _preview_y=$((fzf_offset_y + fzf_height - preview_height - 1))
+            ;;
+    esac
+}
+
+
+function DRAW_PREVIEW {
+       local preview_path=""
+
+       if test -f "${@}/001.jpg"; then
+               preview_path="${@}/001.jpg"
+       elif test -f "${@}/001.png"; then
+               preview_path="${@}/001.png"
+       else
+               return
+       fi
+
+    # Usage: DRAW_PREVIEW path
+    local -A add_preview_command=( \
+        [identifier]="${PREVIEW_ID}" \
+        [scaler]=contain \
+        [path]="${preview_path}")
+    ADD_PLACEMENT add_preview_command
+}
+
+
+function CLEAR_PREVIEW {
+    # Usage: CLEAR_PREVIEW
+    REMOVE_PLACEMENT "${PREVIEW_ID}"
+}
+
+
+function IDENTITY_RECT {
+    # Usage: IDENTITY_RECT
+    #           placement_rect_variable
+    [[ $# -ne 1 ]] && return 1
+}
+
+
+function ADD_PLACEMENT {
+    # Usage: ADD_PLACEMENT
+    #           add_command_variable [adjust_rect_function]
+    # references can't be checked.. -v doesn't seem to support associative arrays..
+    local terminal_lines= terminal_columns=
+    local fzf_height= fzf_offset_y=
+    local preview_y= preview_x=
+    local preview_height="${LINES}" preview_width="${COLUMNS}"
+    STORE_TERMINAL_SIZE_IN \
+        terminal_lines terminal_columns
+    STORE_FZF_HEIGHT_IN \
+        fzf_height \
+        "$terminal_lines" "${FZF_HEIGHT}" "${FZF_MIN_HEIGHT}"
+    STORE_FZF_OFFSET_IN \
+        fzf_offset_y \
+        "$terminal_lines" "${fzf_height}" "${FZF_START_OFFSET_Y}"
+    STORE_PREVIEW_POSITION_IN \
+        preview_y preview_x \
+        "${PREVIEW_POSITION:-${DEFAULT_PREVIEW_POSITION}}" \
+        "${fzf_offset_y}" "${fzf_height}" "${terminal_columns}" \
+        "${preview_height}" "${preview_width}"
+
+    local _add_command_nameref="$1"
+    local -n _add_command="${_add_command_nameref}"
+    local adjust_rect_callback="${2:-IDENTITY_RECT}"
+    local -A adjusted_placement_rect=( \
+        [y]="${preview_y}" [x]="${preview_x}" \
+        [height]="${preview_height}" [width]="${preview_width}")
+    "${adjust_rect_callback}" adjusted_placement_rect
+    _add_command[action]=add
+    _add_command[x]="${adjusted_placement_rect[x]}"
+    _add_command[y]="${adjusted_placement_rect[y]}"
+    _add_command[width]="${adjusted_placement_rect[width]}"
+    _add_command[height]="${adjusted_placement_rect[height]}"
+
+    >"${UEBERZUG_FIFO}" \
+        declare -p "${_add_command_nameref}"
+}
+
+
+function REMOVE_PLACEMENT {
+    # Usage: REMOVE_PLACEMENT placement-id
+    [[ $# -ne 1 ]] && return 1
+    >"${UEBERZUG_FIFO}" \
+        declare -A -p _remove_command=( \
+        [action]=remove [identifier]="${1}")
+}
+
+
+function is_option_key [[ "${@}" =~ ^(\-.*|\+.*) ]]
+function is_key_value [[ "${@}" == *=* ]]
+
+
+function store_options_map_in {
+    # Usage: store_options_map_in
+    #           options_map_variable options_variable
+    # references can't be checked.. -v doesn't seem to support associative arrays..
+    [[ $# -ne 2 || ! -v "$2" ]] && return 1
+    local -n _options_map="${1}"
+    local -n _options="${2}"
+
+    for ((i=0; i < ${#_options[@]}; i++)); do
+        local key="${_options[$i]}" next_key="${_options[$((i + 1))]:---}"
+        local value=true
+        is_option_key "${key}" || \
+            continue
+        if is_key_value "${key}"; then
+            <<<"${key}" \
+                IFS='=' read key value
+        elif ! is_option_key "${next_key}"; then
+            value="${next_key}"
+        fi
+        _options_map["${key}"]="${value}"
+    done
+}
+
+
+function process_options {
+    # Usage: process_options command-line-arguments
+    local -a "default_options=(${FZF_DEFAULT_OPTS})"
+    local -a script_options=("${@}")
+    local -A mapped_options
+    store_options_map_in mapped_options default_options
+    store_options_map_in mapped_options script_options 
+
+    local cursor_y= cursor_x=
+    store_cursor_position_in cursor_y cursor_x
+    # If fzf is used as completion tool we will get the position of the prompt.
+    # If it's normally used we get the position the output will be displayed at.
+    # If it's normally used we need to subtract one to get the position of the prompt.
+    ((cursor_y=cursor_x != 1 ? cursor_y : cursor_y - 1))
+    declare -g -r -x FZF_START_OFFSET_Y="${cursor_y}"
+    declare -g -r -x PREVIEW_POSITION="${mapped_options[--preview-window]%%:[^:]*}"
+    declare -g -r -x FZF_HEIGHT="${mapped_options[--height]:-100%}"
+    declare -g -r -x FZF_MIN_HEIGHT="${mapped_options[--min-height]:-10}"
+}
+
+
+function store_cursor_position_in {
+    # Usage: store_cursor_pos_in
+    #           y_variable x_variable
+    # based on https://github.com/dylanaraps/pure-bash-bible#get-the-current-cursor-position
+    [[ ! -v "$1" || ! -v "$2" ]] && return 1
+    </dev/tty &>/dev/tty \
+        IFS='[;' \
+        read -p $'\e[6n' -d R -rs _ "${1}" "${2}" _
+}
+
+
+function start_ueberzug {
+    # Usage: start_ueberzug
+    mkfifo "${UEBERZUG_FIFO}"
+    <"${UEBERZUG_FIFO}" \
+        ueberzug layer --parser bash --silent &
+    # prevent EOF
+    3>"${UEBERZUG_FIFO}" \
+        exec
+}
+
+
+function finalise {
+    # Usage: finalise
+    3>&- \
+        exec
+    &>/dev/null \
+        rm "${UEBERZUG_FIFO}"
+    &>/dev/null \
+        kill $(jobs -p)
+}
+
+
+function print_on_winch {
+    # Usage: print_on_winch text
+    # print "$@" to stdin on receiving SIGWINCH
+    # use exec as we will only kill direct childs on exiting,
+    # also the additional bash process isn't needed
+    </dev/tty \
+        exec perl -e '
+            require "sys/ioctl.ph";
+            while (1) {
+                local $SIG{WINCH} = sub {
+                    ioctl(STDIN, &TIOCSTI, $_) for split "", join " ", @ARGV;
+                };
+                sleep;
+            }' \
+            "${@}" &
+}
+
+
+function export_functions {
+    # Usage: export_functions
+    # Exports all functions with a name
+    # which only consists of underscores,
+    # figures, upper case charactars
+    local -a function_names="( $(compgen -A function) )"
+
+    for function_name in "${function_names[@]}"; do
+        [[ "${function_name}" =~ ^[A-Z0-9_]+$ ]] && {
+            export -f "${function_name}"
+        }
+    done
+}
+
+
+if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
+    trap finalise EXIT
+    process_options "${@}"
+    # print the redraw key twice as there's a run condition we can't circumvent
+    # (we can't know the time fzf finished redrawing it's layout)
+    print_on_winch "${REDRAW_KEY}${REDRAW_KEY}"
+    start_ueberzug
+
+    export_functions
+    SHELL="${BASH_BINARY}" \
+        fzf --preview "DRAW_PREVIEW {}" \
+            --preview-window "${DEFAULT_PREVIEW_POSITION}" \
+            --bind "${REDRAW_KEY}:${REDRAW_COMMAND}" \
+            "${@}"
+fi
diff --git a/fzf.js b/fzf.js
new file mode 100644 (file)
index 0000000..85f544c
--- /dev/null
+++ b/fzf.js
@@ -0,0 +1,24 @@
+const child = require("child_process")
+
+module.exports = (options, binary = "fzf") => {
+       let res, rej
+       const prom = new Promise((rs, rj) => [res, rej] = [rs, rj])
+
+       const proc = child.spawn(binary)
+       options.forEach(opt => proc.stdin.write(opt + "\n"))
+
+       let data = ""
+       proc.stdout.on("data", chunk => {
+               data += chunk
+       })
+       proc.stderr.pipe(process.stderr)
+
+       proc.on("close", code => {
+               if (code == 0)
+                       res(data.trim())
+               else
+                       rej(code)
+       })
+
+       return prom
+}
diff --git a/info.js b/info.js
new file mode 100644 (file)
index 0000000..95813b6
--- /dev/null
+++ b/info.js
@@ -0,0 +1,30 @@
+const fs = require("fs").promises
+
+module.exports.doujins = shortNames => fs.readdir(".", {encoding: "utf8", withFileTypes: true})
+       .then(doujins => doujins
+               .map(dirent => fs.readFile(`${dirent.name}/metadata.json`)
+                       .then(data => JSON.parse(data.toString()))
+                       .then(data => [dirent.name, data])
+                       .catch(_ => [])))
+       .then(promises => Promise.all(promises))
+       .then(doujins => doujins
+               .filter(([title, data]) => title && data)
+               .filter(([title, data]) => (title == data.title) == !shortNames))
+       .then(doujins => Object.fromEntries(doujins))
+
+module.exports.tags = _ => {
+       const tags = {"*": []}
+
+       return module.exports.doujins()
+               .then(doujins => Object.values(doujins))
+               .then(doujins => doujins
+                       .forEach(doujin => {
+                               tags["*"].push(doujin.title)    
+
+                               doujin.tag && doujin.tag.forEach(tag => {
+                                       tags[tag] = tags[tag] || []
+                                       tags[tag].push(doujin.title)
+                               })
+                       }))
+               .then(_ => tags)
+}
diff --git a/package-lock.json b/package-lock.json
new file mode 100644 (file)
index 0000000..8b7989e
--- /dev/null
@@ -0,0 +1,32 @@
+{
+  "name": "nhentai-mgr",
+  "version": "1.0.0",
+  "lockfileVersion": 2,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "nhentai-mgr",
+      "version": "1.0.0",
+      "license": "GPL-3.0-or-later",
+      "dependencies": {
+        "node-fetch": "^2.0.0"
+      },
+      "devDependencies": {}
+    },
+    "node_modules/node-fetch": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.0.0.tgz",
+      "integrity": "sha1-mCu6Q+zU8pIqKcwYamu7C7c/y6Y=",
+      "engines": {
+        "node": "4.x || >=6.0.0"
+      }
+    }
+  },
+  "dependencies": {
+    "node-fetch": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.0.0.tgz",
+      "integrity": "sha1-mCu6Q+zU8pIqKcwYamu7C7c/y6Y="
+    }
+  }
+}
diff --git a/package.json b/package.json
new file mode 100644 (file)
index 0000000..2eed851
--- /dev/null
@@ -0,0 +1,21 @@
+{
+  "dependencies": {
+    "node-fetch": "^2.0.0"
+  },
+  "name": "nhentai-mgr",
+  "version": "1.0.0",
+  "main": "select.js",
+  "devDependencies": {},
+  "scripts": {},
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/EliasFleckenstein03/nhentai-mgr.git"
+  },
+  "author": "",
+  "license": "GPL-3.0-or-later",
+  "bugs": {
+    "url": "https://github.com/EliasFleckenstein03/nhentai-mgr/issues"
+  },
+  "homepage": "https://github.com/EliasFleckenstein03/nhentai-mgr#readme",
+  "description": ""
+}
diff --git a/select.js b/select.js
new file mode 100644 (file)
index 0000000..6938749
--- /dev/null
+++ b/select.js
@@ -0,0 +1,16 @@
+const child = require("child_process")
+const fzf = require("./fzf")
+const info = require("./info")
+
+;(async _ => {
+       const doujins = await info.doujins()
+       const tags = await info.tags()
+
+       let doujin, tag
+       while (!doujin) {
+               try { tag    = await fzf(Object.keys(tags).sort()) } catch { return }
+               try { doujin = await fzf(Object.values(tags[tag]).sort(), __dirname + "/fzf-previews") } catch {}
+       }
+
+       child.spawn("firefox", [`file://${process.cwd()}/${doujin}/index.html`])
+})()
diff --git a/stats-doujins.js b/stats-doujins.js
new file mode 100644 (file)
index 0000000..6d7b0e5
--- /dev/null
@@ -0,0 +1,14 @@
+const info = require("./info")
+const du = require("./du")
+
+const fmt = (num, size) => 
+`Number of doujins:       ${num}
+Total size:              ${(size       / 1e9).toFixed(2)}GB
+Average size per doujin: ${(size / num / 1e6).toFixed(2)}MB`
+
+info.doujins()
+       .then(doujins => Object.keys(doujins))
+       .then(doujins => Promise.all(doujins.map(du))
+               .then(sizes => sizes.reduce((a, b) => a + b, 0))
+               .then(total => console.log(fmt(doujins.length, total)))
+       )
diff --git a/stats-tags.js b/stats-tags.js
new file mode 100644 (file)
index 0000000..4278e27
--- /dev/null
@@ -0,0 +1,6 @@
+require("./info").tags()
+       .then(tags => Object.entries(tags)
+               .sort((a, b) => b[1].length - a[1].length)
+               .forEach(elem => console.log(elem[1].length, elem[0]))
+       )
+       .then(_ => {})
diff --git a/symlinks.js b/symlinks.js
new file mode 100644 (file)
index 0000000..e7ef204
--- /dev/null
@@ -0,0 +1,10 @@
+const fs = require("fs").promises
+const info = require("./info")
+
+info.doujins(true)
+       .then(doujins => Object.entries(doujins))
+       .then(doujins => doujins
+               .map(([dirname, data]) =>
+                       fs.symlink(dirname, data.title, "dir")
+                               .then(_ => console.log(data.title))
+                               .catch(_ => {})))
diff --git a/wholesome.js b/wholesome.js
new file mode 100644 (file)
index 0000000..551fb82
--- /dev/null
@@ -0,0 +1,24 @@
+const fetch = require("node-fetch")
+const child = require("child_process")
+
+const link = "https://nhentai.net/g/"
+const ids = []
+
+fetch("https://wholesomelist.com/list")
+       .then(data => data.text())
+       .then(data => {
+               while (true) {
+                       const pos = data.search(link)
+
+                       if (pos == -1)
+                               break;
+
+                       data = data.slice(pos + link.length)
+                       const id = parseInt(data)
+                       if (id)
+                               ids.push(id)
+               }
+
+               child.spawn("nhentai", ["--id", ids.join(",")], { stdio: "inherit" })
+       })
+