mirror of
https://github.com/RicterZ/nhentai.git
synced 2025-07-02 00:19:29 +02:00
Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
1b7f19ee18 | |||
132f4c83da | |||
6789b2b363 | |||
a6ac725ca7 | |||
b32962bca4 | |||
8a7be0e33d | |||
0a47527461 | |||
023c8969eb | |||
29c3abbe5c | |||
057fae8a83 | |||
248d31edf0 | |||
4bfe0de078 | |||
780a6c82b2 | |||
8791e7af55 | |||
b434c4d58d | |||
ba59dcf4db |
@ -22,7 +22,7 @@ From Github:
|
|||||||
|
|
||||||
git clone https://github.com/RicterZ/nhentai
|
git clone https://github.com/RicterZ/nhentai
|
||||||
cd nhentai
|
cd nhentai
|
||||||
python setup.py install
|
pip install --no-cache-dir .
|
||||||
|
|
||||||
Build Docker container:
|
Build Docker container:
|
||||||
|
|
||||||
@ -136,6 +136,8 @@ Format output doujinshi folder name:
|
|||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
nhentai --id 261100 --format '[%i]%s'
|
nhentai --id 261100 --format '[%i]%s'
|
||||||
|
# for Windows
|
||||||
|
nhentai --id 261100 --format "[%%i]%%s"
|
||||||
|
|
||||||
Supported doujinshi folder formatter:
|
Supported doujinshi folder formatter:
|
||||||
|
|
||||||
@ -148,6 +150,7 @@ Supported doujinshi folder formatter:
|
|||||||
- %p: Doujinshi pretty name
|
- %p: Doujinshi pretty name
|
||||||
- %ag: Doujinshi authors name or groups name
|
- %ag: Doujinshi authors name or groups name
|
||||||
|
|
||||||
|
Note: for Windows operation system, please use double "%", such as "%%i".
|
||||||
|
|
||||||
Other options:
|
Other options:
|
||||||
|
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
__version__ = '0.5.20'
|
__version__ = '0.5.25'
|
||||||
__author__ = 'RicterZ'
|
__author__ = 'RicterZ'
|
||||||
__email__ = 'ricterzheng@gmail.com'
|
__email__ = 'ricterzheng@gmail.com'
|
||||||
|
@ -131,6 +131,8 @@ def cmd_parser():
|
|||||||
help='generate a metadata file in doujinshi format')
|
help='generate a metadata file in doujinshi format')
|
||||||
parser.add_option('--regenerate', dest='regenerate', action='store_true', default=False,
|
parser.add_option('--regenerate', dest='regenerate', action='store_true', default=False,
|
||||||
help='regenerate the cbz or pdf file if exists')
|
help='regenerate the cbz or pdf file if exists')
|
||||||
|
parser.add_option('--no-metadata', dest='no_metadata', action='store_true', default=False,
|
||||||
|
help='don\'t generate metadata json file in doujinshi output path')
|
||||||
|
|
||||||
# nhentai options
|
# nhentai options
|
||||||
parser.add_option('--cookie', type='str', dest='cookie', action='store',
|
parser.add_option('--cookie', type='str', dest='cookie', action='store',
|
||||||
@ -169,22 +171,24 @@ def cmd_parser():
|
|||||||
|
|
||||||
# --- set config ---
|
# --- set config ---
|
||||||
if args.cookie is not None:
|
if args.cookie is not None:
|
||||||
constant.CONFIG['cookie'] = args.cookie
|
constant.CONFIG['cookie'] = args.cookie.strip()
|
||||||
write_config()
|
write_config()
|
||||||
logger.info('Cookie saved.')
|
logger.info('Cookie saved.')
|
||||||
sys.exit(0)
|
|
||||||
elif args.useragent is not None:
|
if args.useragent is not None:
|
||||||
constant.CONFIG['useragent'] = args.useragent
|
constant.CONFIG['useragent'] = args.useragent.strip()
|
||||||
write_config()
|
write_config()
|
||||||
logger.info('User-Agent saved.')
|
logger.info('User-Agent saved.')
|
||||||
sys.exit(0)
|
|
||||||
elif args.language is not None:
|
if args.language is not None:
|
||||||
constant.CONFIG['language'] = args.language
|
constant.CONFIG['language'] = args.language
|
||||||
write_config()
|
write_config()
|
||||||
logger.info(f'Default language now set to "{args.language}"')
|
logger.info(f'Default language now set to "{args.language}"')
|
||||||
sys.exit(0)
|
|
||||||
# TODO: search without language
|
# TODO: search without language
|
||||||
|
|
||||||
|
if any([args.cookie, args.useragent, args.language]):
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
if args.proxy is not None:
|
if args.proxy is not None:
|
||||||
proxy_url = urlparse(args.proxy)
|
proxy_url = urlparse(args.proxy)
|
||||||
if not args.proxy == '' and proxy_url.scheme not in ('http', 'https', 'socks5', 'socks5h',
|
if not args.proxy == '' and proxy_url.scheme not in ('http', 'https', 'socks5', 'socks5h',
|
||||||
|
@ -4,8 +4,6 @@ import shutil
|
|||||||
import sys
|
import sys
|
||||||
import signal
|
import signal
|
||||||
import platform
|
import platform
|
||||||
import urllib
|
|
||||||
|
|
||||||
import urllib3.exceptions
|
import urllib3.exceptions
|
||||||
|
|
||||||
from nhentai import constant
|
from nhentai import constant
|
||||||
@ -51,6 +49,9 @@ def main():
|
|||||||
|
|
||||||
page_list = paging(options.page)
|
page_list = paging(options.page)
|
||||||
|
|
||||||
|
if options.retry:
|
||||||
|
constant.RETRY_TIMES = int(options.retry)
|
||||||
|
|
||||||
if options.favorites:
|
if options.favorites:
|
||||||
if not options.is_download:
|
if not options.is_download:
|
||||||
logger.warning('You do not specify --download option')
|
logger.warning('You do not specify --download option')
|
||||||
@ -86,7 +87,7 @@ def main():
|
|||||||
if not options.is_show:
|
if not options.is_show:
|
||||||
downloader = Downloader(path=options.output_dir, threads=options.threads,
|
downloader = Downloader(path=options.output_dir, threads=options.threads,
|
||||||
timeout=options.timeout, delay=options.delay,
|
timeout=options.timeout, delay=options.delay,
|
||||||
retry=options.retry, exit_on_fail=options.exit_on_fail,
|
exit_on_fail=options.exit_on_fail,
|
||||||
no_filename_padding=options.no_filename_padding)
|
no_filename_padding=options.no_filename_padding)
|
||||||
|
|
||||||
for doujinshi_id in doujinshi_ids:
|
for doujinshi_id in doujinshi_ids:
|
||||||
@ -115,6 +116,9 @@ def main():
|
|||||||
if not options.is_nohtml:
|
if not options.is_nohtml:
|
||||||
generate_html(options.output_dir, doujinshi, template=constant.CONFIG['template'])
|
generate_html(options.output_dir, doujinshi, template=constant.CONFIG['template'])
|
||||||
|
|
||||||
|
if not options.no_metadata:
|
||||||
|
generate_doc('json', options.output_dir, doujinshi, options.regenerate)
|
||||||
|
|
||||||
if options.is_cbz:
|
if options.is_cbz:
|
||||||
generate_doc('cbz', options.output_dir, doujinshi, options.regenerate)
|
generate_doc('cbz', options.output_dir, doujinshi, options.regenerate)
|
||||||
|
|
||||||
|
@ -37,6 +37,8 @@ FAV_URL = f'{BASE_URL}/favorites/'
|
|||||||
|
|
||||||
PATH_SEPARATOR = os.path.sep
|
PATH_SEPARATOR = os.path.sep
|
||||||
|
|
||||||
|
RETRY_TIMES = 3
|
||||||
|
|
||||||
|
|
||||||
IMAGE_URL = f'{urlparse(BASE_URL).scheme}://i1.{urlparse(BASE_URL).hostname}/galleries'
|
IMAGE_URL = f'{urlparse(BASE_URL).scheme}://i1.{urlparse(BASE_URL).hostname}/galleries'
|
||||||
IMAGE_URL_MIRRORS = [
|
IMAGE_URL_MIRRORS = [
|
||||||
|
@ -34,13 +34,12 @@ def download_callback(result):
|
|||||||
|
|
||||||
|
|
||||||
class Downloader(Singleton):
|
class Downloader(Singleton):
|
||||||
def __init__(self, path='', threads=5, timeout=30, delay=0, retry=3, exit_on_fail=False,
|
def __init__(self, path='', threads=5, timeout=30, delay=0, exit_on_fail=False,
|
||||||
no_filename_padding=False):
|
no_filename_padding=False):
|
||||||
self.threads = threads
|
self.threads = threads
|
||||||
self.path = str(path)
|
self.path = str(path)
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
self.delay = delay
|
self.delay = delay
|
||||||
self.retry = retry
|
|
||||||
self.exit_on_fail = exit_on_fail
|
self.exit_on_fail = exit_on_fail
|
||||||
self.folder = None
|
self.folder = None
|
||||||
self.semaphore = None
|
self.semaphore = None
|
||||||
@ -101,7 +100,7 @@ class Downloader(Singleton):
|
|||||||
return -1, url
|
return -1, url
|
||||||
|
|
||||||
except (httpx.HTTPStatusError, httpx.TimeoutException, httpx.ConnectError) as e:
|
except (httpx.HTTPStatusError, httpx.TimeoutException, httpx.ConnectError) as e:
|
||||||
if retried < self.retry:
|
if retried < constant.RETRY_TIMES:
|
||||||
logger.warning(f'Download {filename} failed, retrying({retried + 1}) times...')
|
logger.warning(f'Download {filename} failed, retrying({retried + 1}) times...')
|
||||||
return await self.download(
|
return await self.download(
|
||||||
url=url,
|
url=url,
|
||||||
@ -111,7 +110,7 @@ class Downloader(Singleton):
|
|||||||
proxy=proxy,
|
proxy=proxy,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logger.warning(f'Download {filename} failed with {self.retry} times retried, skipped')
|
logger.warning(f'Download {filename} failed with {constant.RETRY_TIMES} times retried, skipped')
|
||||||
return -2, url
|
return -2, url
|
||||||
|
|
||||||
except NHentaiImageNotExistException as e:
|
except NHentaiImageNotExistException as e:
|
||||||
|
@ -92,13 +92,27 @@ def favorites_parser(page=None):
|
|||||||
page_range_list = range(1, pages + 1)
|
page_range_list = range(1, pages + 1)
|
||||||
|
|
||||||
for page in page_range_list:
|
for page in page_range_list:
|
||||||
try:
|
logger.info(f'Getting doujinshi ids of page {page}')
|
||||||
logger.info(f'Getting doujinshi ids of page {page}')
|
|
||||||
resp = request('get', f'{constant.FAV_URL}?page={page}').content
|
|
||||||
|
|
||||||
result.extend(_get_title_and_id(resp))
|
i = 0
|
||||||
except Exception as e:
|
while i <= constant.RETRY_TIMES + 1:
|
||||||
logger.error(f'Error: {e}, continue')
|
i += 1
|
||||||
|
if i > 3:
|
||||||
|
logger.error(f'Failed to get favorites at page {page} after 3 times retried, skipped')
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = request('get', f'{constant.FAV_URL}?page={page}').content
|
||||||
|
temp_result = _get_title_and_id(resp)
|
||||||
|
if not temp_result:
|
||||||
|
logger.warning(f'Failed to get favorites at page {page}, retrying ({i} times) ...')
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
result.extend(temp_result)
|
||||||
|
break
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f'Error: {e}, retrying ({i} times) ...')
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@ -141,17 +155,19 @@ def doujinshi_parser(id_, counter=0):
|
|||||||
title = doujinshi_info.find('h1').text
|
title = doujinshi_info.find('h1').text
|
||||||
pretty_name = doujinshi_info.find('h1').find('span', attrs={'class': 'pretty'}).text
|
pretty_name = doujinshi_info.find('h1').find('span', attrs={'class': 'pretty'}).text
|
||||||
subtitle = doujinshi_info.find('h2')
|
subtitle = doujinshi_info.find('h2')
|
||||||
favorite_counts = doujinshi_info.find('span', class_='nobold').find('span', class_='count')
|
favorite_counts = doujinshi_info.find('span', class_='nobold').text.strip('(').strip(')')
|
||||||
|
|
||||||
doujinshi['name'] = title
|
doujinshi['name'] = title
|
||||||
doujinshi['pretty_name'] = pretty_name
|
doujinshi['pretty_name'] = pretty_name
|
||||||
doujinshi['subtitle'] = subtitle.text if subtitle else ''
|
doujinshi['subtitle'] = subtitle.text if subtitle else ''
|
||||||
doujinshi['favorite_counts'] = int(favorite_counts.text.strip()) if favorite_counts else 0
|
doujinshi['favorite_counts'] = int(favorite_counts) if favorite_counts and favorite_counts.isdigit() else 0
|
||||||
|
|
||||||
doujinshi_cover = html.find('div', attrs={'id': 'cover'})
|
doujinshi_cover = html.find('div', attrs={'id': 'cover'})
|
||||||
# img_id = re.search('/galleries/([0-9]+)/cover.(jpg|png|gif|webp)$',
|
# img_id = re.search('/galleries/([0-9]+)/cover.(jpg|png|gif|webp)$',
|
||||||
# doujinshi_cover.a.img.attrs['data-src'])
|
# doujinshi_cover.a.img.attrs['data-src'])
|
||||||
img_id = re.search(r'/galleries/(\d+)/cover\.\w+$', doujinshi_cover.a.img.attrs['data-src'])
|
|
||||||
|
# fix cover.webp.webp
|
||||||
|
img_id = re.search(r'/galleries/(\d+)/cover(.webp)?\.\w+$', doujinshi_cover.a.img.attrs['data-src'])
|
||||||
|
|
||||||
ext = []
|
ext = []
|
||||||
for i in html.find_all('div', attrs={'class': 'thumb-container'}):
|
for i in html.find_all('div', attrs={'class': 'thumb-container'}):
|
||||||
@ -261,7 +277,7 @@ def search_parser(keyword, sorting, page, is_page_all=False):
|
|||||||
i = 0
|
i = 0
|
||||||
|
|
||||||
logger.info(f'Searching doujinshis using keywords "{keyword}" on page {p}{total}')
|
logger.info(f'Searching doujinshis using keywords "{keyword}" on page {p}{total}')
|
||||||
while i < 3:
|
while i < constant.RETRY_TIMES:
|
||||||
try:
|
try:
|
||||||
url = request('get', url=constant.SEARCH_URL, params={'query': keyword,
|
url = request('get', url=constant.SEARCH_URL, params={'query': keyword,
|
||||||
'page': p, 'sort': sorting}).url
|
'page': p, 'sort': sorting}).url
|
||||||
|
@ -2,12 +2,11 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from nhentai.constant import PATH_SEPARATOR
|
from nhentai.constant import PATH_SEPARATOR, LANGUAGE_ISO
|
||||||
from xml.sax.saxutils import escape
|
from xml.sax.saxutils import escape
|
||||||
from nhentai.constant import LANGUAGE_ISO
|
|
||||||
|
|
||||||
|
|
||||||
def serialize_json(doujinshi, output_dir):
|
def serialize_json(doujinshi, output_dir: str):
|
||||||
metadata = {'title': doujinshi.name,
|
metadata = {'title': doujinshi.name,
|
||||||
'subtitle': doujinshi.info.subtitle}
|
'subtitle': doujinshi.info.subtitle}
|
||||||
if doujinshi.info.favorite_counts:
|
if doujinshi.info.favorite_counts:
|
||||||
|
@ -16,7 +16,7 @@ from requests.structures import CaseInsensitiveDict
|
|||||||
from nhentai import constant
|
from nhentai import constant
|
||||||
from nhentai.constant import PATH_SEPARATOR
|
from nhentai.constant import PATH_SEPARATOR
|
||||||
from nhentai.logger import logger
|
from nhentai.logger import logger
|
||||||
from nhentai.serializer import serialize_json, serialize_comic_xml, set_js_database
|
from nhentai.serializer import serialize_comic_xml, serialize_json, set_js_database
|
||||||
|
|
||||||
MAX_FIELD_LENGTH = 100
|
MAX_FIELD_LENGTH = 100
|
||||||
EXTENSIONS = ('.png', '.jpg', '.jpeg', '.gif', '.webp')
|
EXTENSIONS = ('.png', '.jpg', '.jpeg', '.gif', '.webp')
|
||||||
@ -142,7 +142,7 @@ def generate_html(output_dir='.', doujinshi_obj=None, template='default'):
|
|||||||
js = readfile(f'viewer/{template}/scripts.js')
|
js = readfile(f'viewer/{template}/scripts.js')
|
||||||
|
|
||||||
if doujinshi_obj is not None:
|
if doujinshi_obj is not None:
|
||||||
serialize_json(doujinshi_obj, doujinshi_dir)
|
# serialize_json(doujinshi_obj, doujinshi_dir)
|
||||||
name = doujinshi_obj.name
|
name = doujinshi_obj.name
|
||||||
else:
|
else:
|
||||||
name = {'title': 'nHentai HTML Viewer'}
|
name = {'title': 'nHentai HTML Viewer'}
|
||||||
@ -274,6 +274,9 @@ def generate_doc(file_type='', output_dir='.', doujinshi_obj=None, regenerate=Fa
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
logger.error("Please install img2pdf package by using pip.")
|
logger.error("Please install img2pdf package by using pip.")
|
||||||
|
|
||||||
|
elif file_type == 'json':
|
||||||
|
serialize_json(doujinshi_obj, doujinshi_dir)
|
||||||
|
|
||||||
|
|
||||||
def format_filename(s, length=MAX_FIELD_LENGTH, _truncate_only=False):
|
def format_filename(s, length=MAX_FIELD_LENGTH, _truncate_only=False):
|
||||||
"""
|
"""
|
||||||
|
@ -49,8 +49,8 @@ document.onkeypress = event => {
|
|||||||
switch (event.key.toLowerCase()) {
|
switch (event.key.toLowerCase()) {
|
||||||
// Previous Image
|
// Previous Image
|
||||||
case 'w':
|
case 'w':
|
||||||
scrollBy(0, -40);
|
scrollBy(0, -40);
|
||||||
break;
|
break;
|
||||||
case 'a':
|
case 'a':
|
||||||
changePage(currentPage - 1);
|
changePage(currentPage - 1);
|
||||||
break;
|
break;
|
||||||
@ -61,7 +61,7 @@ document.onkeypress = event => {
|
|||||||
// Next Image
|
// Next Image
|
||||||
case ' ':
|
case ' ':
|
||||||
case 's':
|
case 's':
|
||||||
scrollBy(0, 40);
|
scrollBy(0, 40);
|
||||||
break;
|
break;
|
||||||
case 'd':
|
case 'd':
|
||||||
changePage(currentPage + 1);
|
changePage(currentPage + 1);
|
||||||
@ -75,11 +75,13 @@ document.onkeydown = event =>{
|
|||||||
changePage(currentPage - 1);
|
changePage(currentPage - 1);
|
||||||
break;
|
break;
|
||||||
case 38: //up
|
case 38: //up
|
||||||
|
changePage(currentPage - 1);
|
||||||
break;
|
break;
|
||||||
case 39: //right
|
case 39: //right
|
||||||
changePage(currentPage + 1);
|
changePage(currentPage + 1);
|
||||||
break;
|
break;
|
||||||
case 40: //down
|
case 40: //down
|
||||||
|
changePage(currentPage + 1);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "nhentai"
|
name = "nhentai"
|
||||||
version = "0.5.20"
|
version = "0.5.25"
|
||||||
description = "nhentai doujinshi downloader"
|
description = "nhentai doujinshi downloader"
|
||||||
authors = ["Ricter Z <ricterzheng@gmail.com>"]
|
authors = ["Ricter Z <ricterzheng@gmail.com>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
Reference in New Issue
Block a user