Getting some functionality
This commit is contained in:
		
							
								
								
									
										268
									
								
								src/main.py
									
									
									
									
									
								
							
							
						
						
									
										268
									
								
								src/main.py
									
									
									
									
									
								
							@@ -1,8 +1,28 @@
 | 
				
			|||||||
from nicegui import ui
 | 
					from nicegui import ui, app
 | 
				
			||||||
import time
 | 
					import time
 | 
				
			||||||
 | 
					import subprocess
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					import threading
 | 
				
			||||||
 | 
					from collections import defaultdict
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ui.dark_mode().enable()
 | 
					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():
 | 
					def add_nhentai_id():
 | 
				
			||||||
    id_value = id_input.value
 | 
					    id_value = id_input.value
 | 
				
			||||||
    if not id_value:
 | 
					    if not id_value:
 | 
				
			||||||
@@ -23,25 +43,254 @@ def add_nhentai_id():
 | 
				
			|||||||
            ui.label(f'ID: {id_value}').classes('text-lg ml-2')
 | 
					            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')
 | 
					            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
 | 
					            # Add buttons for actions
 | 
				
			||||||
            ui.button('Mark Complete',
 | 
					            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),
 | 
					                                    on_click=lambda: update_status(status, id_value, True),
 | 
				
			||||||
                                    color='green').classes('ml-2').props('size=sm')
 | 
					                                    color='green').classes('ml-2').props('size=sm')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def update_status(status_icon, id_value, success=True):
 | 
					def update_status(status_icon, id_value, success=True):
 | 
				
			||||||
    if success:
 | 
					    if success:
 | 
				
			||||||
        status_icon.name = 'check_circle'
 | 
					        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')
 | 
					        ui.notify(f'ID {id_value} marked as complete', color='positive')
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        status_icon.name = 'error'
 | 
					        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.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
 | 
					# 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')
 | 
					    id_input = ui.input(label='ID:').classes('mr-2')
 | 
				
			||||||
    ui.button('Add ID!', on_click=add_nhentai_id).props('icon=add')
 | 
					    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:
 | 
					with history_container:
 | 
				
			||||||
    ui.label('Enter an ID and click "Add ID!" to see the history').classes('text-gray-500 italic')
 | 
					    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()
 | 
					ui.run()
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user