diff --git a/src/main.py b/src/main.py index 83a9dbd..4c13c2c 100644 --- a/src/main.py +++ b/src/main.py @@ -1,8 +1,28 @@ -from nicegui import ui +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: @@ -23,25 +43,254 @@ def add_nhentai_id(): 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 a button to mark as success - ui.button('Mark Complete', - on_click=lambda: update_status(status, id_value, True), - color='green').classes('ml-2').props('size=sm') + # 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') + 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') + 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') -ui.label('nhentai ID Tracker').classes('text-2xl font-bold mb-4') +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'): +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') @@ -53,4 +302,9 @@ history_container = ui.column().classes('w-full border rounded-lg p-2') 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()