Adding sqlite and splitting into two files
This commit is contained in:
parent
25ae87a13c
commit
c8886d69ed
@ -15,5 +15,6 @@ There is a simple web UI to add the doujinshi IDs to the queue.
|
|||||||
- [ ] Add file name normalizer (for kavita)
|
- [ ] Add file name normalizer (for kavita)
|
||||||
- [ ] Add file mover (organizer)
|
- [ ] Add file mover (organizer)
|
||||||
- [ ] Add config file
|
- [ ] Add config file
|
||||||
- [ ] Add Database integration (sqlite from nhentail cli tool)
|
- [x] Add Database integration (sqlite from nhentail cli tool)
|
||||||
- [ ] Add Dockerfile
|
- [ ] Add Dockerfile
|
||||||
|
- [ ] Better error handeling (Show in UI)
|
||||||
|
307
src/main.py
307
src/main.py
@ -1,307 +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")
|
|
||||||
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()
|
|
Loading…
x
Reference in New Issue
Block a user