From e1820ccf780f6ae62b606ed21f1816ba7d456476 Mon Sep 17 00:00:00 2001 From: Elias Fleckenstein Date: Mon, 9 May 2022 20:38:41 +0200 Subject: [PATCH] Initial commit --- .gitignore | 7 + README.md | 31 +++++ du.js | 22 ++++ fzf-previews | 320 ++++++++++++++++++++++++++++++++++++++++++++++ fzf.js | 24 ++++ info.js | 30 +++++ package-lock.json | 32 +++++ package.json | 21 +++ select.js | 16 +++ stats-doujins.js | 14 ++ stats-tags.js | 6 + symlinks.js | 10 ++ wholesome.js | 24 ++++ 13 files changed, 557 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 du.js create mode 100755 fzf-previews create mode 100644 fzf.js create mode 100644 info.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 select.js create mode 100644 stats-doujins.js create mode 100644 stats-tags.js create mode 100644 symlinks.js create mode 100644 wholesome.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..932f882 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +* +!*.js +!*.json +!fzf-previews +!LICENSE +!README.md +!.gitignore diff --git a/README.md b/README.md new file mode 100644 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 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 index 0000000..59cb388 --- /dev/null +++ b/fzf-previews @@ -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 . +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 + < <( 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 \ + 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 + { + 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 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 index 0000000..8b7989e --- /dev/null +++ b/package-lock.json @@ -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 index 0000000..2eed851 --- /dev/null +++ b/package.json @@ -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 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 index 0000000..6d7b0e5 --- /dev/null +++ b/stats-doujins.js @@ -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 index 0000000..4278e27 --- /dev/null +++ b/stats-tags.js @@ -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 index 0000000..e7ef204 --- /dev/null +++ b/symlinks.js @@ -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 index 0000000..551fb82 --- /dev/null +++ b/wholesome.js @@ -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" }) + }) + -- 2.44.0