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") OUTPUT_FORMAT = "[%i]%s" # Default format for folder naming COOKIE = "" # For bypassing Cloudflare captcha USER_AGENT = "" # 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 = 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') 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') 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 global download_statuses 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): download_statuses[id_value] = { 'status': 'error', 'message': 'nhentai CLI tool is not installed' } 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(id_value)]) # Add output directory cmd.extend(["--output", DOWNLOAD_DIR]) # 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 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, 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') 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, 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): global DOWNLOAD_DIR, OUTPUT_FORMAT, COOKIE, USER_AGENT, HTML_VIEWER, GENERATE_CBZ, GENERATE_PDF, THREAD_COUNT, TIMEOUT, RETRY_COUNT 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 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 nhentai CLI tool. Need help? Check the GitHub page:') ui.link('https://github.com/RicterZ/nhentai', 'https://github.com/RicterZ/nhentai').classes('ml-1') ui.run()