Compare commits

...

7 Commits

7 changed files with 573 additions and 314 deletions

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "nhentai"]
path = nhentai
url = https://git.nussnougate.net/NussNougate/nhentai.git

View File

@ -5,3 +5,16 @@ This is a Python script / wrapper for the nhentai tool to download doujinshi fro
It uses the nhentai Python tool to download doujinshi and converts them to CBZ files. It uses the nhentai Python tool to download doujinshi and converts them to CBZ files.
There is a simple web UI to add the doujinshi IDs to the queue. There is a simple web UI to add the doujinshi IDs to the queue.
## Todo
- [x] Adding GUI
- [x] Adding nhentail cli wrapper
- [ ] Input normalizer
- [ ] Automatically start downloading after adding
- [ ] Add file name normalizer (for kavita)
- [ ] Add file mover (organizer)
- [ ] Add config file
- [x] Add Database integration (sqlite from nhentail cli tool)
- [ ] Add Dockerfile
- [ ] Better error handeling (Show in UI)

Submodule nhentai deleted from aa84b57a43

56
requirements.txt Normal file
View File

@ -0,0 +1,56 @@
aiofiles==24.1.0
aiohappyeyeballs==2.6.1
aiohttp==3.11.13
aiosignal==1.3.2
annotated-types==0.7.0
anyio==4.8.0
attrs==25.3.0
beautifulsoup4==4.13.3
bidict==0.23.1
certifi==2025.1.31
chardet==5.2.0
charset-normalizer==3.4.1
click==8.1.8
docutils==0.21.2
fastapi==0.115.11
frozenlist==1.5.0
h11==0.14.0
httpcore==1.0.7
httptools==0.6.4
httpx==0.28.1
idna==3.10
ifaddr==0.2.0
iso8601==1.1.0
itsdangerous==2.2.0
Jinja2==3.1.6
markdown2==2.5.3
MarkupSafe==3.0.2
multidict==6.1.0
nhentai==0.5.25
nicegui==2.12.1
orjson==3.10.15
propcache==0.3.0
pscript==0.7.7
pydantic==2.10.6
pydantic_core==2.27.2
Pygments==2.19.1
python-dotenv==1.0.1
python-engineio==4.11.2
python-multipart==0.0.20
python-socketio==5.12.1
PyYAML==6.0.2
requests==2.32.3
simple-websocket==1.1.0
sniffio==1.3.1
soupsieve==2.6
starlette==0.46.1
tabulate==0.9.0
typing_extensions==4.12.2
urllib3==1.26.20
uvicorn==0.34.0
uvloop==0.21.0
vbuild==0.8.2
watchfiles==1.0.4
websockets==15.0.1
wsproto==1.2.0
yarl==1.18.3

245
src/app.py Normal file
View File

@ -0,0 +1,245 @@
from nicegui import ui, app
from datetime import datetime
import time
import nhentai_manager # Import the nhentai_manager module with core functionality
# Set up dark mode
ui.dark_mode().enable()
# Global reference to UI components
history_container = None
id_input = None
app.storage.status_changed = False
def add_nhentai_id():
"""Handle adding a new nhentai ID for download"""
id_value = id_input.value
if not id_value:
ui.notify('Please enter an ID', color='warning')
return
# Clear the input field after submission
id_input.value = ''
# Add to database
nhentai_manager.db_add_download(id_value)
# Show notification
ui.notify(f'Adding nhentai ID: {id_value}')
# Refresh the download history display
refresh_download_history()
def update_status_icon(status_icon, status, message=None):
"""Update the status icon based on download status"""
if status == "pending":
status_icon.name = 'circle'
status_icon.classes('text-yellow-500', remove='text-green-500 text-red-500 text-blue-500')
elif status == "downloading":
status_icon.name = 'downloading'
status_icon.classes('text-blue-500', remove='text-yellow-500 text-green-500 text-red-500')
elif status == "success":
status_icon.name = 'check_circle'
status_icon.classes('text-green-500', remove='text-yellow-500 text-red-500 text-blue-500')
ui.notify(f'Download completed successfully', color='positive')
elif status == "error":
status_icon.name = 'error'
status_icon.classes('text-red-500', remove='text-yellow-500 text-green-500 text-blue-500')
if message:
ui.notify(f'Download failed: {message}', color='negative')
else:
ui.notify(f'Download failed', color='negative')
def safely_refresh_history():
if app.storage.status_changed:
refresh_download_history()
app.storage.status_changed = False
def on_status_change(nhentai_id, status, message=None):
"""Callback for status changes during download"""
# We need to refresh the UI to reflect the changes
app.storage.status_changed = True
def start_download(nhentai_id, status_icon):
"""Start downloading a doujinshi"""
# Update the UI status
update_status_icon(status_icon, "downloading")
ui.notify(f'Starting download for ID: {nhentai_id}', color='info')
# Start the download in a background thread
nhentai_manager.start_download_thread(nhentai_id, on_status_change)
# Set up periodic UI refresh to check for status updates
refresh_status_periodically(nhentai_id, status_icon)
def refresh_status_periodically(nhentai_id, status_icon):
"""Periodically check the download status and update the UI"""
# Get current status from database
status_info = nhentai_manager.db_get_download_status(nhentai_id)
if status_info:
status = status_info['status']
message = status_info['message']
# Update the UI based on the status
update_status_icon(status_icon, status, message)
# If still downloading, check again in a second
if status == "downloading":
ui.timer(1.0, lambda: refresh_status_periodically(nhentai_id, status_icon), once=True)
else:
# If no status found, check again in a second
ui.timer(1.0, lambda: refresh_status_periodically(nhentai_id, status_icon), once=True)
def refresh_download_history():
"""Refresh the download history display from the database"""
# Clear current history
history_container.clear()
# Get all downloads from database
downloads = nhentai_manager.db_get_all_downloads()
if not downloads:
with history_container:
ui.label('No download history yet').classes('text-gray-500 italic')
return
# Populate with data from database
for download in downloads:
nhentai_id = download['nhentai_id']
status = download['status']
added_time = datetime.fromisoformat(download['added_time']).strftime("%Y-%m-%d %H:%M:%S")
with history_container:
item_row = ui.row().classes('w-full items-center')
with item_row:
# Set icon based on status
if status == 'pending':
status_icon = ui.icon('circle').classes('text-yellow-500')
elif status == 'downloading':
status_icon = ui.icon('downloading').classes('text-blue-500')
elif status == 'success':
status_icon = ui.icon('check_circle').classes('text-green-500')
elif status == 'error':
status_icon = ui.icon('error').classes('text-red-500')
else:
status_icon = ui.icon('help').classes('text-gray-500')
ui.label(f'ID: {nhentai_id}').classes('text-lg ml-2')
ui.label(f'Added: {added_time}').classes('text-sm text-gray-500 ml-auto')
# Only show download button for pending or failed downloads
if status in ['pending', 'error']:
download_btn = ui.button('Download',
on_click=lambda id=nhentai_id, icon=status_icon: start_download(id, icon),
color='primary').classes('ml-2').props('size=sm')
def show_settings():
"""Display settings dialog"""
current_settings = nhentai_manager.get_settings()
with ui.dialog() as dialog, ui.card().classes('w-96'):
ui.label('Download Settings').classes('text-xl font-bold')
# Basic Settings
ui.label('Basic Settings').classes('text-lg font-semibold mt-4')
download_dir_input = ui.input('Download Directory', value=current_settings['download_dir']).classes('w-full')
output_format_input = ui.input('Output Format', value=current_settings['output_format'],
placeholder='e.g. [%i]%s').classes('w-full')
ui.label('Supported: %i (ID), %s (subtitle), %t (title), %a (authors), %g (groups), %p (pretty name)').classes('text-xs text-gray-500')
# Advanced Settings
ui.label('Advanced Settings').classes('text-lg font-semibold mt-4')
thread_count_input = ui.number('Thread Count', value=current_settings['thread_count'], min=1, max=20)
timeout_input = ui.number('Timeout (seconds)', value=current_settings['timeout'], min=5, max=120)
retry_count_input = ui.number('Retry Count', value=current_settings['retry_count'], min=0, max=10)
# Output Options
ui.label('Output Options').classes('text-lg font-semibold mt-4')
html_viewer_toggle = ui.switch('Generate HTML Viewer', value=current_settings['html_viewer'])
cbz_toggle = ui.switch('Generate CBZ File', value=current_settings['generate_cbz'])
pdf_toggle = ui.switch('Generate PDF File', value=current_settings['generate_pdf'])
# Authentication (for bypassing Cloudflare)
ui.label('Authentication (to bypass Cloudflare)').classes('text-lg font-semibold mt-4')
cookie_input = ui.input('Cookie', value=current_settings['cookie'],
placeholder='csrftoken=TOKEN; sessionid=ID; cf_clearance=CLOUDFLARE').classes('w-full')
useragent_input = ui.input('User Agent', value=current_settings['user_agent'],
placeholder='Mozilla/5.0 ...').classes('w-full')
with ui.row():
ui.button('Cancel', on_click=dialog.close).props('outline')
ui.button('Save', on_click=lambda: save_settings(
download_dir_input.value,
output_format_input.value,
cookie_input.value,
useragent_input.value,
html_viewer_toggle.value,
cbz_toggle.value,
pdf_toggle.value,
thread_count_input.value,
timeout_input.value,
retry_count_input.value,
dialog
))
dialog.open()
def save_settings(download_dir, output_format, cookie, useragent,
html_viewer, generate_cbz, generate_pdf, thread_count, timeout, retry_count, dialog):
"""Save settings to the nhentai_manager module"""
# Update settings
new_settings = {
"download_dir": download_dir,
"output_format": output_format,
"cookie": cookie,
"user_agent": useragent,
"html_viewer": html_viewer,
"generate_cbz": generate_cbz,
"generate_pdf": generate_pdf,
"thread_count": thread_count,
"timeout": timeout,
"retry_count": retry_count
}
nhentai_manager.update_settings(new_settings)
ui.notify('Settings saved', color='positive')
dialog.close()
def setup_ui():
"""Set up the main UI components"""
global history_container, id_input
with ui.header().classes('bg-primary'):
ui.label('nhentai Downloader').classes('text-xl font-bold')
ui.space()
ui.button('Settings', on_click=show_settings).props('icon=settings flat')
# Input and button row
with ui.row().classes('w-full items-center mt-4'):
id_input = ui.input(label='ID:').classes('mr-2')
ui.button('Add ID!', on_click=add_nhentai_id).props('icon=add')
# History section
ui.label('History').classes('text-xl font-bold mt-4 mb-2')
history_container = ui.column().classes('w-full border rounded-lg p-2')
# Add some instructions initially
with history_container:
ui.label('Enter an ID and click "Add ID!" to see the history').classes('text-gray-500 italic')
# Add footer with help info
with ui.footer().classes('bg-gray-100 dark:bg-gray-800 text-sm'):
ui.label('Using nhentai CLI tool. Need help? Check the GitHub page:')
ui.link('https://github.com/RicterZ/nhentai', 'https://github.com/RicterZ/nhentai').classes('ml-1')
# Add a periodic timer to safely refresh the UI
ui.timer(1.0, safely_refresh_history)
# Initialize UI and load existing downloads on startup
@app.on_startup
def on_startup():
refresh_download_history()
if __name__ in {"__main__", "__mp_main__"}:
setup_ui()
ui.run()

View File

@ -1,310 +0,0 @@
from nicegui import ui, app
import time
import subprocess
import os
import threading
from collections import defaultdict
ui.dark_mode().enable()
# Configuration
DOWNLOAD_DIR = os.path.expanduser("~/Downloads/doujinshi")
DOCKER_IMAGE = "ricterz/nhentai" # Official Docker image from DockerHub
OUTPUT_FORMAT = "[%i]%s" # Default format for folder naming
COOKIE = "" # For bypassing Cloudflare captcha
USER_AGENT = "" # For bypassing Cloudflare captcha
HTML_VIEWER = True # Generate HTML viewer
GENERATE_CBZ = False # Generate Comic Book Archive
GENERATE_PDF = False # Generate PDF file
THREAD_COUNT = 5 # Thread count for downloading
TIMEOUT = 30 # Timeout in seconds
RETRY_COUNT = 3 # Retry times when download fails
# Global dictionary to track download statuses
download_statuses = {}
def add_nhentai_id():
id_value = id_input.value
if not id_value:
ui.notify('Please enter an ID', color='warning')
return
# Clear the input field after submission
id_input.value = ''
# Show notification
ui.notify(f'Adding nhentai ID: {id_value}')
# Create a new history item
with history_container:
item_row = ui.row().classes('w-full items-center')
with item_row:
status = ui.icon('circle').classes('text-yellow-500') # Initial "processing" status
ui.label(f'ID: {id_value}').classes('text-lg ml-2')
timestamp = ui.label(f'Added: {time.strftime("%H:%M:%S")}').classes('text-sm text-gray-500 ml-auto')
# Add buttons for actions
download_btn = ui.button('Download',
on_click=lambda: start_download(id_value, status),
color='primary').classes('ml-2').props('size=sm')
complete_btn = ui.button('Mark Complete',
on_click=lambda: update_status(status, id_value, True),
color='green').classes('ml-2').props('size=sm')
def update_status(status_icon, id_value, success=True):
if success:
status_icon.name = 'check_circle'
status_icon.classes('text-green-500', remove='text-yellow-500 text-red-500 text-blue-500')
ui.notify(f'ID {id_value} marked as complete', color='positive')
else:
status_icon.name = 'error'
status_icon.classes('text-red-500', remove='text-yellow-500 text-green-500 text-blue-500')
ui.notify(f'ID {id_value} marked as failed', color='negative')
def download_doujinshi(id_value, status_icon):
# This function runs in a background thread
try:
# Create download directory if it doesn't exist
os.makedirs(DOWNLOAD_DIR, exist_ok=True)
# Build Docker command with all options
cmd = [
"docker", "run", "--rm",
"-v", f"{DOWNLOAD_DIR}:/output",
"-v", f"{os.path.expanduser('~/.nhentai')}:/root/.nhentai",
DOCKER_IMAGE
]
# Add the ID
cmd.extend(["--id", str(id_value)])
# Add output directory
cmd.extend(["--output", "/output"])
# Add format option
cmd.extend(["--format", OUTPUT_FORMAT])
# Add thread count
if THREAD_COUNT > 0:
cmd.extend(["--threads", str(THREAD_COUNT)])
# Add timeout
if TIMEOUT > 0:
cmd.extend(["--timeout", str(TIMEOUT)])
# Add retry count
if RETRY_COUNT > 0:
cmd.extend(["--retry", str(RETRY_COUNT)])
# Add exit-on-fail option to prevent incomplete downloads
cmd.append("--exit-on-fail")
# Add HTML viewer option
if HTML_VIEWER:
cmd.append("--html")
else:
cmd.append("--no-html")
# Generate CBZ if selected
if GENERATE_CBZ:
cmd.append("--cbz")
# Generate PDF if selected
if GENERATE_PDF:
cmd.append("--pdf")
# Add cookie if provided
if COOKIE:
cmd.extend(["--cookie", COOKIE])
# Add user agent if provided
if USER_AGENT:
cmd.extend(["--useragent", USER_AGENT])
# Save download history
cmd.append("--save-download-history")
# For debugging: print the command (excluding sensitive info)
debug_cmd = list(cmd)
if COOKIE:
cookie_index = debug_cmd.index("--cookie")
if len(debug_cmd) > cookie_index + 1:
debug_cmd[cookie_index + 1] = "***COOKIE***"
if USER_AGENT:
ua_index = debug_cmd.index("--useragent")
if len(debug_cmd) > ua_index + 1:
debug_cmd[ua_index + 1] = "***USER-AGENT***"
print(f"Executing: {' '.join(debug_cmd)}")
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
stdout, stderr = process.communicate()
# Use the global dict to track status
global download_statuses
if process.returncode == 0:
# Success
download_statuses[id_value] = {
'status': 'success',
'message': 'Download completed'
}
print(f"Download successful for ID {id_value}")
print(f"STDOUT: {stdout}")
else:
# Error
download_statuses[id_value] = {
'status': 'error',
'message': f'Download failed: {stderr}'
}
print(f"Download failed for ID {id_value}")
print(f"STDERR: {stderr}")
except Exception as e:
# Error handling
download_statuses[id_value] = {
'status': 'error',
'message': f'Error: {str(e)}'
}
print(f"Exception during download for ID {id_value}: {str(e)}")
def start_download(id_value, status_icon):
# Update status to "downloading"
status_icon.name = 'downloading'
status_icon.classes('text-blue-500', remove='text-yellow-500 text-green-500 text-red-500')
ui.notify(f'Starting download for ID: {id_value}', color='info')
# Initialize download status
global download_statuses
download_statuses[id_value] = {'status': 'downloading'}
# Start download in a background thread
threading.Thread(target=download_doujinshi, args=(id_value, status_icon), daemon=True).start()
# Set up a periodic task to check for download completion
check_download_status(id_value, status_icon)
def check_download_status(id_value, status_icon):
"""Check if the download has completed and update the UI accordingly"""
global download_statuses
if id_value in download_statuses:
result = download_statuses[id_value]
if result.get('status') == 'success':
ui.notify(f'Download completed for ID: {id_value}', color='positive')
update_status(status_icon, id_value, True)
# Remove from tracking to avoid duplicate notifications
del download_statuses[id_value]
elif result.get('status') == 'error':
ui.notify(f'Download failed for ID {id_value}: {result.get("message", "Unknown error")}', color='negative')
update_status(status_icon, id_value, False)
# Remove from tracking to avoid duplicate notifications
del download_statuses[id_value]
else:
# Still processing, check again in a second
ui.timer(1.0, lambda: check_download_status(id_value, status_icon), once=True)
else:
# No result yet, check again in a second
ui.timer(1.0, lambda: check_download_status(id_value, status_icon), once=True)
# Add advanced settings dialog
def show_settings():
with ui.dialog() as dialog, ui.card().classes('w-96'):
ui.label('Download Settings').classes('text-xl font-bold')
global DOWNLOAD_DIR, DOCKER_IMAGE, OUTPUT_FORMAT, COOKIE, USER_AGENT, HTML_VIEWER, GENERATE_CBZ, GENERATE_PDF, THREAD_COUNT, TIMEOUT, RETRY_COUNT
# Basic Settings
ui.label('Basic Settings').classes('text-lg font-semibold mt-4')
download_dir_input = ui.input('Download Directory', value=DOWNLOAD_DIR).classes('w-full')
docker_image_input = ui.input('Docker Image', value=DOCKER_IMAGE).classes('w-full')
output_format_input = ui.input('Output Format', value=OUTPUT_FORMAT,
placeholder='e.g. [%i]%s').classes('w-full')
ui.label('Supported: %i (ID), %s (subtitle), %t (title), %a (authors), %g (groups), %p (pretty name)').classes('text-xs text-gray-500')
# Advanced Settings
ui.label('Advanced Settings').classes('text-lg font-semibold mt-4')
thread_count_input = ui.number('Thread Count', value=THREAD_COUNT, min=1, max=20)
timeout_input = ui.number('Timeout (seconds)', value=TIMEOUT, min=5, max=120)
retry_count_input = ui.number('Retry Count', value=RETRY_COUNT, min=0, max=10)
# Output Options
ui.label('Output Options').classes('text-lg font-semibold mt-4')
html_viewer_toggle = ui.switch('Generate HTML Viewer', value=HTML_VIEWER)
cbz_toggle = ui.switch('Generate CBZ File', value=GENERATE_CBZ)
pdf_toggle = ui.switch('Generate PDF File', value=GENERATE_PDF)
# Authentication (for bypassing Cloudflare)
ui.label('Authentication (to bypass Cloudflare)').classes('text-lg font-semibold mt-4')
cookie_input = ui.input('Cookie', value=COOKIE,
placeholder='csrftoken=TOKEN; sessionid=ID; cf_clearance=CLOUDFLARE').classes('w-full')
useragent_input = ui.input('User Agent', value=USER_AGENT,
placeholder='Mozilla/5.0 ...').classes('w-full')
with ui.row():
ui.button('Cancel', on_click=dialog.close).props('outline')
ui.button('Save', on_click=lambda: save_settings(
download_dir_input.value,
docker_image_input.value,
output_format_input.value,
cookie_input.value,
useragent_input.value,
html_viewer_toggle.value,
cbz_toggle.value,
pdf_toggle.value,
thread_count_input.value,
timeout_input.value,
retry_count_input.value,
dialog
))
dialog.open()
def save_settings(download_dir, docker_image, output_format, cookie, useragent,
html_viewer, generate_cbz, generate_pdf, thread_count, timeout, retry_count, dialog):
global DOWNLOAD_DIR, DOCKER_IMAGE, OUTPUT_FORMAT, COOKIE, USER_AGENT, HTML_VIEWER, GENERATE_CBZ, GENERATE_PDF, THREAD_COUNT, TIMEOUT, RETRY_COUNT
DOWNLOAD_DIR = download_dir
DOCKER_IMAGE = docker_image
OUTPUT_FORMAT = output_format
COOKIE = cookie
USER_AGENT = useragent
HTML_VIEWER = html_viewer
GENERATE_CBZ = generate_cbz
GENERATE_PDF = generate_pdf
THREAD_COUNT = thread_count
TIMEOUT = timeout
RETRY_COUNT = retry_count
ui.notify('Settings saved', color='positive')
dialog.close()
# UI Layout
with ui.header().classes('bg-primary'):
ui.label('nhentai Downloader').classes('text-xl font-bold')
ui.space()
ui.button('Settings', on_click=show_settings).props('icon=settings flat')
# Input and button row
with ui.row().classes('w-full items-center mt-4'):
id_input = ui.input(label='ID:').classes('mr-2')
ui.button('Add ID!', on_click=add_nhentai_id).props('icon=add')
# History section
ui.label('History').classes('text-xl font-bold mt-4 mb-2')
history_container = ui.column().classes('w-full border rounded-lg p-2')
# Add some instructions initially
with history_container:
ui.label('Enter an ID and click "Add ID!" to see the history').classes('text-gray-500 italic')
# Add footer with help info
with ui.footer().classes('bg-gray-100 dark:bg-gray-800 text-sm'):
ui.label('Using ricterz/nhentai Docker image. Need help? Check the GitHub page:')
ui.link('https://github.com/RicterZ/nhentai', 'https://github.com/RicterZ/nhentai').classes('ml-1')
ui.run()

259
src/nhentai_manager.py Normal file
View File

@ -0,0 +1,259 @@
from statistics import stdev
import sqlite3
import os
import subprocess
import threading
from datetime import datetime
# Configuration
DOWNLOAD_DIR = os.path.expanduser("out")
OUTPUT_FORMAT = "[%i]%s" # Default format for folder naming
COOKIE = "session-affinity=1742247176.302.46.898572|2968378f2272707dac237fc5e1f12aaf; csrftoken=UPArZ6krsCd44PZK5zut7f8tQfb7HqVH; sessionid=qszf9bsjr5kxlm9t9avhx8ivogoa4q44" # For bypassing Cloudflare captcha
USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0" # For bypassing Cloudflare captcha
HTML_VIEWER = False # Generate HTML viewer
GENERATE_CBZ = True # Generate Comic Book Archive
GENERATE_PDF = False # Generate PDF file
THREAD_COUNT = 1 # Thread count for downloading
TIMEOUT = 30 # Timeout in seconds
RETRY_COUNT = 3 # Retry times when download fails
# Database setup
def init_database():
conn = sqlite3.connect('nhentai_downloads.db')
cursor = conn.cursor()
# Create downloads table if it doesn't exist
cursor.execute('''
CREATE TABLE IF NOT EXISTS downloads (
id INTEGER PRIMARY KEY,
nhentai_id TEXT NOT NULL,
status TEXT NOT NULL,
message TEXT,
added_time TIMESTAMP NOT NULL,
updated_time TIMESTAMP NOT NULL
)
''')
conn.commit()
conn.close()
def db_add_download(nhentai_id):
conn = sqlite3.connect('nhentai_downloads.db')
cursor = conn.cursor()
now = datetime.now().isoformat()
cursor.execute(
"INSERT INTO downloads (nhentai_id, status, added_time, updated_time) VALUES (?, ?, ?, ?)",
(nhentai_id, "pending", now, now)
)
conn.commit()
conn.close()
def db_update_download_status(nhentai_id, status, message=None):
conn = sqlite3.connect('nhentai_downloads.db')
cursor = conn.cursor()
now = datetime.now().isoformat()
cursor.execute(
"UPDATE downloads SET status = ?, message = ?, updated_time = ? WHERE nhentai_id = ?",
(status, message, now, nhentai_id)
)
conn.commit()
conn.close()
def db_get_all_downloads():
conn = sqlite3.connect('nhentai_downloads.db')
conn.row_factory = sqlite3.Row # This enables column access by name
cursor = conn.cursor()
cursor.execute("SELECT * FROM downloads ORDER BY added_time DESC")
downloads = [dict(row) for row in cursor.fetchall()]
conn.close()
return downloads
def db_get_download_status(nhentai_id):
conn = sqlite3.connect('nhentai_downloads.db')
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("SELECT status, message FROM downloads WHERE nhentai_id = ?", (nhentai_id,))
result = cursor.fetchone()
conn.close()
if result:
return {'status': result['status'], 'message': result['message']}
return None
def download_doujinshi(nhentai_id, on_status_change=None):
"""
Download a doujinshi using nhentai CLI.
Args:
nhentai_id: The ID to download
on_status_change: Optional callback for status updates
"""
# Update status in the database
db_update_download_status(nhentai_id, "downloading")
if on_status_change:
on_status_change(nhentai_id, "downloading")
try:
# Try to check if nhentai CLI is installed
try:
subprocess.run(["nhentai", "--help"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
except (subprocess.SubprocessError, FileNotFoundError):
error_msg = "nhentai CLI tool is not installed"
db_update_download_status(nhentai_id, "error", error_msg)
if on_status_change:
on_status_change(nhentai_id, "error", error_msg)
return
# Create download directory if it doesn't exist
os.makedirs(DOWNLOAD_DIR, exist_ok=True)
# Build command with all options
cmd = ["nhentai"]
# Add the ID
cmd.extend(["--id", str(nhentai_id)])
# Add explicit download flag to make sure it downloads
cmd.append("--download")
# Add output directory
cmd.extend(["--output", DOWNLOAD_DIR])
# Cleanup
cmd.extend(["--move-to-folder"])
# Add format option
cmd.extend(["--format", f"'{OUTPUT_FORMAT}'"])
# Add thread count
if THREAD_COUNT > 0:
cmd.extend(["--threads", str(THREAD_COUNT)])
# Add timeout
if TIMEOUT > 0:
cmd.extend(["--timeout", str(TIMEOUT)])
# Add retry count
if RETRY_COUNT > 0:
cmd.extend(["--retry", str(RETRY_COUNT)])
# Add exit-on-fail option to prevent incomplete downloads
cmd.append("--exit-on-fail")
# Add HTML viewer option
if HTML_VIEWER:
cmd.append("--html")
else:
cmd.append("--no-html")
# Generate CBZ if selected
if GENERATE_CBZ:
cmd.append("--cbz")
# Generate PDF if selected
if GENERATE_PDF:
cmd.append("--pdf")
# Add cookie if provided
if COOKIE:
subprocess.run(["nhentai", f"--cookie=\"{COOKIE}\""],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
print("Set nhentai cookie")
# Add user agent if provided
if USER_AGENT:
subprocess.run(["nhentai", f"--useragent=\"{USER_AGENT}\""],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
print("Set nhentai user agent")
# For debugging: print the command (excluding sensitive info)
debug_cmd = list(cmd)
print(f"Executing: {' '.join(debug_cmd)}")
print(f"Working directory: {os.getcwd()}")
print(f"Download directory: {DOWNLOAD_DIR}")
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
stdout, stderr = process.communicate()
print(f"Full STDOUT: {stdout}")
if (stderr):
print(f"Full STDERR: {stderr}")
if process.returncode == 0:
# Success
db_update_download_status(nhentai_id, "success", "Download completed")
if on_status_change:
on_status_change(nhentai_id, "success", "Download completed")
print(f"Download successful for ID {nhentai_id}")
else:
# Error
error_msg = f'Download failed: {stderr}'
db_update_download_status(nhentai_id, "error", error_msg)
if on_status_change:
on_status_change(nhentai_id, "error", error_msg)
print(f"Download failed for ID {nhentai_id}")
print(f"STDERR: {stderr}")
except Exception as e:
# Error handling
error_msg = f'Error: {str(e)}'
db_update_download_status(nhentai_id, "error", error_msg)
if on_status_change:
on_status_change(nhentai_id, "error", error_msg)
print(f"Exception during download for ID {nhentai_id}: {str(e)}")
def start_download_thread(nhentai_id, on_status_change=None):
"""Starts a download in a background thread"""
threading.Thread(
target=download_doujinshi,
args=(nhentai_id, on_status_change),
daemon=True
).start()
def get_settings():
"""Return current settings as a dict"""
return {
"download_dir": DOWNLOAD_DIR,
"output_format": OUTPUT_FORMAT,
"cookie": COOKIE,
"user_agent": USER_AGENT,
"html_viewer": HTML_VIEWER,
"generate_cbz": GENERATE_CBZ,
"generate_pdf": GENERATE_PDF,
"thread_count": THREAD_COUNT,
"timeout": TIMEOUT,
"retry_count": RETRY_COUNT
}
def update_settings(settings):
"""Update global settings from a dict"""
global DOWNLOAD_DIR, OUTPUT_FORMAT, COOKIE, USER_AGENT, HTML_VIEWER
global GENERATE_CBZ, GENERATE_PDF, THREAD_COUNT, TIMEOUT, RETRY_COUNT
DOWNLOAD_DIR = settings.get("download_dir", DOWNLOAD_DIR)
OUTPUT_FORMAT = settings.get("output_format", OUTPUT_FORMAT)
COOKIE = settings.get("cookie", COOKIE)
USER_AGENT = settings.get("user_agent", USER_AGENT)
HTML_VIEWER = settings.get("html_viewer", HTML_VIEWER)
GENERATE_CBZ = settings.get("generate_cbz", GENERATE_CBZ)
GENERATE_PDF = settings.get("generate_pdf", GENERATE_PDF)
THREAD_COUNT = settings.get("thread_count", THREAD_COUNT)
TIMEOUT = settings.get("timeout", TIMEOUT)
RETRY_COUNT = settings.get("retry_count", RETRY_COUNT)
# Initialize database when module is imported
init_database()