From 830af873b9e30ac9f0d8fe19d0c77f3b92465766 Mon Sep 17 00:00:00 2001 From: sevi-kun Date: Tue, 18 Mar 2025 00:07:18 +0100 Subject: [PATCH] Adding src/ folder? no idea what happened --- src/app.py | 245 ++++++++++++++++++++++++++++++++++++++ src/nhentai_manager.py | 259 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 504 insertions(+) create mode 100644 src/app.py create mode 100644 src/nhentai_manager.py diff --git a/src/app.py b/src/app.py new file mode 100644 index 0000000..ba4c3c5 --- /dev/null +++ b/src/app.py @@ -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() diff --git a/src/nhentai_manager.py b/src/nhentai_manager.py new file mode 100644 index 0000000..83fdd48 --- /dev/null +++ b/src/nhentai_manager.py @@ -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()