mirror of
https://github.com/RicterZ/nhentai.git
synced 2025-07-01 16:09:28 +02:00
Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
405d879db6 | |||
41342a6da0 |
@ -5,7 +5,7 @@ COPY requirements.txt ./
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
RUN pip install .
|
||||
RUN python setup.py install
|
||||
|
||||
WORKDIR /output
|
||||
ENTRYPOINT ["nhentai"]
|
||||
|
@ -59,7 +59,7 @@ On Gentoo Linux:
|
||||
|
||||
.. code-block::
|
||||
|
||||
layman -fa glibOne
|
||||
layman -fa glicOne
|
||||
sudo emerge net-misc/nhentai
|
||||
|
||||
On NixOS:
|
||||
|
@ -1,3 +1,3 @@
|
||||
__version__ = '0.5.17.2'
|
||||
__version__ = '0.5.16'
|
||||
__author__ = 'RicterZ'
|
||||
__email__ = 'ricterzheng@gmail.com'
|
||||
|
@ -37,7 +37,7 @@ def write_config():
|
||||
f.write(json.dumps(constant.CONFIG))
|
||||
|
||||
|
||||
def callback(option, _opt_str, _value, parser):
|
||||
def callback(option, opt_str, value, parser):
|
||||
if option == '--id':
|
||||
pass
|
||||
value = []
|
||||
|
@ -57,7 +57,7 @@ class Doujinshi(object):
|
||||
|
||||
self.table = [
|
||||
['Parodies', self.info.parodies],
|
||||
['Title', self.name],
|
||||
['Doujinshi', self.name],
|
||||
['Subtitle', self.info.subtitle],
|
||||
['Date', self.info.date],
|
||||
['Characters', self.info.characters],
|
||||
|
@ -13,7 +13,6 @@ from nhentai.utils import Singleton, async_request
|
||||
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
|
||||
class NHentaiImageNotExistException(Exception):
|
||||
pass
|
||||
|
||||
@ -33,14 +32,13 @@ def download_callback(result):
|
||||
logger.log(16, f'{data} downloaded successfully')
|
||||
|
||||
|
||||
|
||||
class Downloader(Singleton):
|
||||
def __init__(self, path='', threads=5, timeout=30, delay=0):
|
||||
self.threads = threads
|
||||
self.path = str(path)
|
||||
self.timeout = timeout
|
||||
self.delay = delay
|
||||
self.folder = None
|
||||
self.semaphore = None
|
||||
|
||||
async def fiber(self, tasks):
|
||||
self.semaphore = asyncio.Semaphore(self.threads)
|
||||
@ -51,19 +49,18 @@ class Downloader(Singleton):
|
||||
except Exception as e:
|
||||
logger.error(f'An error occurred: {e}')
|
||||
|
||||
|
||||
async def _semaphore_download(self, *args, **kwargs):
|
||||
async with self.semaphore:
|
||||
return await self.download(*args, **kwargs)
|
||||
|
||||
async def download(self, url, folder='', filename='', retried=0, proxy=None, length=0):
|
||||
async def download(self, url, folder='', filename='', retried=0, proxy=None):
|
||||
logger.info(f'Starting to download {url} ...')
|
||||
|
||||
if self.delay:
|
||||
await asyncio.sleep(self.delay)
|
||||
|
||||
filename = filename if filename else os.path.basename(urlparse(url).path)
|
||||
base_filename, extension = os.path.splitext(filename)
|
||||
filename = base_filename.zfill(length) + extension
|
||||
|
||||
save_file_path = os.path.join(self.folder, filename)
|
||||
|
||||
@ -132,6 +129,7 @@ class Downloader(Singleton):
|
||||
f.write(chunk)
|
||||
return True
|
||||
|
||||
|
||||
def start_download(self, queue, folder='') -> bool:
|
||||
if not isinstance(folder, (str,)):
|
||||
folder = str(folder)
|
||||
@ -151,9 +149,9 @@ class Downloader(Singleton):
|
||||
# Assuming we want to continue with rest of process.
|
||||
return True
|
||||
|
||||
digit_length = len(str(len(queue)))
|
||||
|
||||
coroutines = [
|
||||
self._semaphore_download(url, filename=os.path.basename(urlparse(url).path), length=digit_length)
|
||||
self._semaphore_download(url, filename=os.path.basename(urlparse(url).path))
|
||||
for url in queue
|
||||
]
|
||||
|
||||
|
@ -135,20 +135,24 @@ def doujinshi_parser(id_, counter=0):
|
||||
logger.warning(f'Error: {e}, ignored')
|
||||
return None
|
||||
|
||||
# print(response)
|
||||
html = BeautifulSoup(response, 'html.parser')
|
||||
doujinshi_info = html.find('div', attrs={'id': 'info'})
|
||||
|
||||
title = doujinshi_info.find('h1').text
|
||||
pretty_name = doujinshi_info.find('h1').find('span', attrs={'class': 'pretty'}).text
|
||||
subtitle = doujinshi_info.find('h2')
|
||||
favorite_counts = str(doujinshi_info.find('span', class_='nobold').find('span', class_='count'))
|
||||
if favorite_counts is None:
|
||||
favorite_counts = '0'
|
||||
favorite_counts = doujinshi_info.find('span', class_='nobold').find('span', class_='count')\
|
||||
|
||||
if favorite_counts:
|
||||
favorite_counts = favorite_counts.text.strip()
|
||||
else:
|
||||
favorite_counts = 0
|
||||
|
||||
doujinshi['name'] = title
|
||||
doujinshi['pretty_name'] = pretty_name
|
||||
doujinshi['subtitle'] = subtitle.text if subtitle else ''
|
||||
doujinshi['favorite_counts'] = favorite_counts.strip()
|
||||
doujinshi['favorite_counts'] = favorite_counts
|
||||
|
||||
doujinshi_cover = html.find('div', attrs={'id': 'cover'})
|
||||
img_id = re.search('/galleries/([0-9]+)/cover.(jpg|png|gif|webp)$',
|
||||
@ -160,8 +164,8 @@ def doujinshi_parser(id_, counter=0):
|
||||
ext.append(ext_name)
|
||||
|
||||
if not img_id:
|
||||
logger.critical(f'Tried yo get image id failed of id: {id_}')
|
||||
return None
|
||||
logger.critical('Tried yo get image id failed')
|
||||
sys.exit(1)
|
||||
|
||||
doujinshi['img_id'] = img_id.group(1)
|
||||
doujinshi['ext'] = ext
|
||||
@ -188,6 +192,53 @@ def doujinshi_parser(id_, counter=0):
|
||||
return doujinshi
|
||||
|
||||
|
||||
def legacy_doujinshi_parser(id_):
|
||||
if not isinstance(id_, (int,)) and (isinstance(id_, (str,)) and not id_.isdigit()):
|
||||
raise Exception(f'Doujinshi id({id_}) is not valid')
|
||||
|
||||
id_ = int(id_)
|
||||
logger.info(f'Fetching information of doujinshi id {id_}')
|
||||
doujinshi = dict()
|
||||
doujinshi['id'] = id_
|
||||
url = f'{constant.DETAIL_URL}/{id_}'
|
||||
i = 0
|
||||
while 5 > i:
|
||||
try:
|
||||
response = request('get', url).json()
|
||||
except Exception as e:
|
||||
i += 1
|
||||
if not i < 5:
|
||||
logger.critical(str(e))
|
||||
sys.exit(1)
|
||||
continue
|
||||
break
|
||||
|
||||
doujinshi['name'] = response['title']['english']
|
||||
doujinshi['subtitle'] = response['title']['japanese']
|
||||
doujinshi['img_id'] = response['media_id']
|
||||
doujinshi['ext'] = ''.join([i['t'] for i in response['images']['pages']])
|
||||
doujinshi['pages'] = len(response['images']['pages'])
|
||||
|
||||
# gain information of the doujinshi
|
||||
needed_fields = ['character', 'artist', 'language', 'tag', 'parody', 'group', 'category']
|
||||
for tag in response['tags']:
|
||||
tag_type = tag['type']
|
||||
if tag_type in needed_fields:
|
||||
if tag_type == 'tag':
|
||||
if tag_type not in doujinshi:
|
||||
doujinshi[tag_type] = {}
|
||||
|
||||
tag['name'] = tag['name'].replace(' ', '-')
|
||||
tag['name'] = tag['name'].lower()
|
||||
doujinshi[tag_type][tag['name']] = tag['id']
|
||||
elif tag_type not in doujinshi:
|
||||
doujinshi[tag_type] = tag['name']
|
||||
else:
|
||||
doujinshi[tag_type] += ', ' + tag['name']
|
||||
|
||||
return doujinshi
|
||||
|
||||
|
||||
def print_doujinshi(doujinshi_list):
|
||||
if not doujinshi_list:
|
||||
return
|
||||
|
@ -5,13 +5,13 @@ import re
|
||||
import os
|
||||
import zipfile
|
||||
import shutil
|
||||
import copy
|
||||
|
||||
import httpx
|
||||
import requests
|
||||
import sqlite3
|
||||
import urllib.parse
|
||||
from typing import Tuple
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from nhentai import constant
|
||||
from nhentai.logger import logger
|
||||
@ -277,7 +277,7 @@ def format_filename(s, length=MAX_FIELD_LENGTH, _truncate_only=False):
|
||||
It used to be a whitelist approach allowed only alphabet and a part of symbols.
|
||||
but most doujinshi's names include Japanese 2-byte characters and these was rejected.
|
||||
so it is using blacklist approach now.
|
||||
if filename include forbidden characters (\'/:,;*?"<>|) ,it replaces space character(" ").
|
||||
if filename include forbidden characters (\'/:,;*?"<>|) ,it replace space character(' ').
|
||||
"""
|
||||
# maybe you can use `--format` to select a suitable filename
|
||||
|
||||
@ -300,7 +300,7 @@ def format_filename(s, length=MAX_FIELD_LENGTH, _truncate_only=False):
|
||||
return filename
|
||||
|
||||
|
||||
def signal_handler(_signal, _frame):
|
||||
def signal_handler(signal, frame):
|
||||
logger.error('Ctrl-C signal received. Stopping...')
|
||||
sys.exit(1)
|
||||
|
||||
@ -335,14 +335,14 @@ def generate_metadata_file(output_dir, doujinshi_obj):
|
||||
'TRANSLATOR', 'PUBLISHER', 'DESCRIPTION', 'STATUS', 'CHAPTERS', 'PAGES',
|
||||
'TAGS', 'TYPE', 'LANGUAGE', 'RELEASED', 'READING DIRECTION', 'CHARACTERS',
|
||||
'SERIES', 'PARODY', 'URL']
|
||||
special_fields = ['PARODY', 'TITLE', 'ORIGINAL TITLE', 'DATE', 'CHARACTERS', 'AUTHOR', 'GROUPS',
|
||||
'LANGUAGE', 'TAGS', 'URL', 'PAGES']
|
||||
|
||||
temp_dict = CaseInsensitiveDict(dict(doujinshi_obj.table))
|
||||
for i in fields:
|
||||
v = temp_dict.get(i)
|
||||
v = temp_dict.get(f'{i}s') if v is None else v
|
||||
v = doujinshi_obj.info.get(i.lower(), None) if v is None else v
|
||||
v = doujinshi_obj.info.get(f'{i.lower()}s', "Unknown") if v is None else v
|
||||
f.write(f'{i}: {v}\n')
|
||||
for i in range(len(fields)):
|
||||
f.write(f'{fields[i]}: ')
|
||||
if fields[i] in special_fields:
|
||||
f.write(str(doujinshi_obj.table[special_fields.index(fields[i])][1]))
|
||||
f.write('\n')
|
||||
|
||||
f.close()
|
||||
logger.log(16, f'Metadata Info has been written to "{info_txt_path}"')
|
||||
|
@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "nhentai"
|
||||
version = "0.5.17.2"
|
||||
version = "0.5.15"
|
||||
description = "nhentai doujinshi downloader"
|
||||
authors = ["Ricter Z <ricterzheng@gmail.com>"]
|
||||
license = "MIT"
|
||||
@ -20,6 +20,3 @@ httpx = "0.27.2"
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
nhentai = 'nhentai.command:main'
|
29
qodana.yaml
29
qodana.yaml
@ -1,29 +0,0 @@
|
||||
#-------------------------------------------------------------------------------#
|
||||
# Qodana analysis is configured by qodana.yaml file #
|
||||
# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
|
||||
#-------------------------------------------------------------------------------#
|
||||
version: "1.0"
|
||||
|
||||
#Specify inspection profile for code analysis
|
||||
profile:
|
||||
name: qodana.starter
|
||||
|
||||
#Enable inspections
|
||||
#include:
|
||||
# - name: <SomeEnabledInspectionId>
|
||||
|
||||
#Disable inspections
|
||||
#exclude:
|
||||
# - name: <SomeDisabledInspectionId>
|
||||
# paths:
|
||||
# - <path/where/not/run/inspection>
|
||||
|
||||
#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
|
||||
#bootstrap: sh ./prepare-qodana.sh
|
||||
|
||||
#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
|
||||
#plugins:
|
||||
# - id: <plugin.id> #(plugin id can be found at https://plugins.jetbrains.com)
|
||||
|
||||
#Specify Qodana linter for analysis (Applied in CI/CD pipeline)
|
||||
linter: jetbrains/qodana-python:2024.3
|
38
setup.py
Normal file
38
setup.py
Normal file
@ -0,0 +1,38 @@
|
||||
# coding: utf-8
|
||||
import codecs
|
||||
from setuptools import setup, find_packages
|
||||
from nhentai import __version__, __author__, __email__
|
||||
|
||||
|
||||
with open('requirements.txt') as f:
|
||||
requirements = [l for l in f.read().splitlines() if l]
|
||||
|
||||
|
||||
def long_description():
|
||||
with codecs.open('README.rst', 'rb') as readme:
|
||||
return readme.read().decode('utf-8')
|
||||
|
||||
|
||||
setup(
|
||||
name='nhentai',
|
||||
version=__version__,
|
||||
packages=find_packages(),
|
||||
|
||||
author=__author__,
|
||||
author_email=__email__,
|
||||
keywords=['nhentai', 'doujinshi', 'downloader'],
|
||||
description='nhentai.net doujinshis downloader',
|
||||
long_description=long_description(),
|
||||
url='https://github.com/RicterZ/nhentai',
|
||||
download_url='https://github.com/RicterZ/nhentai/tarball/master',
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
|
||||
install_requires=requirements,
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'nhentai = nhentai.command:main',
|
||||
]
|
||||
},
|
||||
license='MIT',
|
||||
)
|
Reference in New Issue
Block a user