975 lines
46 KiB
Python
Raw Normal View History

2025-05-05 19:35:24 +05:30
import sys
import os
import time
import requests
import re
2025-05-06 22:08:27 +05:30
import threading
import queue
2025-05-06 22:49:19 +05:30
import hashlib
2025-05-06 22:08:27 +05:30
from concurrent.futures import ThreadPoolExecutor, Future, CancelledError
from PyQt5.QtGui import QIcon
2025-05-05 19:35:24 +05:30
from PyQt5.QtWidgets import (
QApplication, QWidget, QLabel, QLineEdit, QTextEdit, QPushButton,
QVBoxLayout, QHBoxLayout, QFileDialog, QMessageBox, QListWidget,
2025-05-07 07:20:40 +05:30
QRadioButton, QButtonGroup, QCheckBox
2025-05-05 19:35:24 +05:30
)
2025-05-06 22:08:27 +05:30
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QMutex, QMutexLocker, QObject
2025-05-05 19:35:24 +05:30
from urllib.parse import urlparse
2025-05-07 07:20:40 +05:30
2025-05-06 22:08:27 +05:30
try:
from PIL import Image
except ImportError:
2025-05-07 07:20:40 +05:30
Image = None # Will be handled in downloader_utils
2025-05-06 22:08:27 +05:30
from io import BytesIO
2025-05-07 07:20:40 +05:30
# Import from the new utils/backend file
from downloader_utils import (
KNOWN_NAMES,
clean_folder_name,
extract_post_info,
download_from_api,
PostProcessorSignals,
PostProcessorWorker,
DownloadThread as BackendDownloadThread # Rename to avoid conflict if any
)
2025-05-05 19:35:24 +05:30
2025-05-06 22:08:27 +05:30
class DownloaderApp(QWidget):
character_prompt_response_signal = pyqtSignal(bool)
log_signal = pyqtSignal(str)
add_character_prompt_signal = pyqtSignal(str)
2025-05-06 22:49:19 +05:30
file_download_status_signal = pyqtSignal(bool)
overall_progress_signal = pyqtSignal(int, int)
finished_signal = pyqtSignal(int, int, bool)
2025-05-05 19:35:24 +05:30
2025-05-06 22:08:27 +05:30
def __init__(self):
super().__init__()
self.config_file = "Known.txt"
self.download_thread = None
self.thread_pool = None
self.cancellation_event = threading.Event()
self.active_futures = []
self.total_posts_to_process = 0
self.processed_posts_count = 0
self.download_counter = 0
self.skip_counter = 0
2025-05-06 22:49:19 +05:30
self.worker_signals = PostProcessorSignals()
2025-05-06 22:08:27 +05:30
self.prompt_mutex = QMutex()
self._add_character_response = None
2025-05-06 22:49:19 +05:30
self.downloaded_files = set()
self.downloaded_files_lock = threading.Lock()
self.downloaded_file_hashes = set()
self.downloaded_file_hashes_lock = threading.Lock()
2025-05-07 07:20:40 +05:30
self.load_known_names_from_util() # Changed to reflect it's from utils
2025-05-06 22:49:19 +05:30
self.setWindowTitle("Kemono Downloader v2.3 (Content Dedupe & Skip)")
self.setGeometry(150, 150, 1050, 820)
2025-05-06 22:08:27 +05:30
self.setStyleSheet(self.get_dark_theme())
2025-05-06 22:49:19 +05:30
self.init_ui()
2025-05-06 22:08:27 +05:30
self._connect_signals()
self.log_signal.emit(" Local API server functionality has been removed.")
2025-05-05 19:35:24 +05:30
2025-05-06 22:08:27 +05:30
def _connect_signals(self):
self.worker_signals.progress_signal.connect(self.log)
self.worker_signals.file_download_status_signal.connect(self.update_skip_button_state)
self.log_signal.connect(self.log)
self.add_character_prompt_signal.connect(self.prompt_add_character)
self.character_prompt_response_signal.connect(self.receive_add_character_result)
self.overall_progress_signal.connect(self.update_progress_display)
self.finished_signal.connect(self.download_finished)
2025-05-06 22:49:19 +05:30
self.character_search_input.textChanged.connect(self.filter_character_list)
2025-05-07 07:20:40 +05:30
def load_known_names_from_util(self):
# KNOWN_NAMES is now managed in downloader_utils, but GUI needs to populate its list
# and this method also handles initial log messages.
2025-05-06 22:08:27 +05:30
loaded_names = []
2025-05-05 19:35:24 +05:30
if os.path.exists(self.config_file):
try:
with open(self.config_file, 'r', encoding='utf-8') as f:
2025-05-06 22:08:27 +05:30
raw_names = [line.strip() for line in f]
loaded_names = sorted(list(set(filter(None, raw_names))))
log_msg = f" Loaded {len(loaded_names)} known names from {self.config_file}"
2025-05-05 19:35:24 +05:30
except Exception as e:
2025-05-06 22:08:27 +05:30
log_msg = f"❌ Error loading config '{self.config_file}': {e}"
QMessageBox.warning(self, "Config Load Error", f"Could not load list from {self.config_file}:\n{e}")
2025-05-06 22:49:19 +05:30
loaded_names = []
2025-05-05 19:35:24 +05:30
else:
2025-05-06 22:08:27 +05:30
log_msg = f" Config file '{self.config_file}' not found. Starting empty."
loaded_names = []
2025-05-07 07:20:40 +05:30
# Update the global KNOWN_NAMES in downloader_utils
# This requires downloader_utils.KNOWN_NAMES to be mutable (it's a list)
# Or pass the list back if it were a function in utils returning the list.
# For simplicity with global-like config, directly modify.
# Ensure downloader_utils.py defines KNOWN_NAMES = [] at the top.
import downloader_utils
downloader_utils.KNOWN_NAMES[:] = loaded_names # Modify in place
2025-05-06 22:08:27 +05:30
if hasattr(self, 'log_output'):
self.log_signal.emit(log_msg)
else:
print(log_msg)
2025-05-07 07:20:40 +05:30
# Populate the GUI list if it exists
if hasattr(self, 'character_list'):
self.character_list.clear()
self.character_list.addItems(downloader_utils.KNOWN_NAMES)
2025-05-06 22:08:27 +05:30
2025-05-05 19:35:24 +05:30
def save_known_names(self):
2025-05-07 07:20:40 +05:30
# KNOWN_NAMES is from downloader_utils
import downloader_utils
2025-05-05 19:35:24 +05:30
try:
2025-05-07 07:20:40 +05:30
unique_sorted_names = sorted(list(set(filter(None, downloader_utils.KNOWN_NAMES))))
2025-05-05 19:35:24 +05:30
with open(self.config_file, 'w', encoding='utf-8') as f:
2025-05-06 22:08:27 +05:30
for name in unique_sorted_names:
2025-05-05 19:35:24 +05:30
f.write(name + '\n')
2025-05-07 07:20:40 +05:30
downloader_utils.KNOWN_NAMES[:] = unique_sorted_names # Update in place
2025-05-06 22:08:27 +05:30
if hasattr(self, 'log_signal'):
self.log_signal.emit(f"💾 Saved {len(unique_sorted_names)} known names to {self.config_file}")
else:
print(f"Saved {len(unique_sorted_names)} names to {self.config_file}")
2025-05-05 19:35:24 +05:30
except Exception as e:
2025-05-06 22:08:27 +05:30
log_msg = f"❌ Error saving config '{self.config_file}': {e}"
if hasattr(self, 'log_signal'):
self.log_signal.emit(log_msg)
else:
print(log_msg)
QMessageBox.warning(self, "Config Save Error", f"Could not save list to {self.config_file}:\n{e}")
2025-05-07 07:20:40 +05:30
2025-05-05 19:35:24 +05:30
def closeEvent(self, event):
2025-05-06 22:49:19 +05:30
self.save_known_names()
2025-05-06 22:08:27 +05:30
should_exit = True
is_downloading = (self.download_thread and self.download_thread.isRunning()) or (self.thread_pool is not None)
if is_downloading:
2025-05-05 19:35:24 +05:30
reply = QMessageBox.question(self, "Confirm Exit",
2025-05-06 22:08:27 +05:30
"Download in progress. Are you sure you want to exit and cancel?",
2025-05-05 19:35:24 +05:30
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.Yes:
2025-05-06 22:08:27 +05:30
self.log_signal.emit("⚠️ Cancelling active download due to application exit...")
2025-05-06 22:49:19 +05:30
self.cancel_download()
2025-05-05 19:35:24 +05:30
else:
2025-05-06 22:08:27 +05:30
should_exit = False
self.log_signal.emit(" Application exit cancelled.")
2025-05-06 22:49:19 +05:30
event.ignore()
2025-05-06 22:08:27 +05:30
return
if should_exit:
2025-05-06 22:49:19 +05:30
self.log_signal.emit(" Application closing.")
2025-05-06 22:08:27 +05:30
self.log_signal.emit("👋 Exiting application.")
2025-05-06 22:49:19 +05:30
event.accept()
2025-05-07 07:20:40 +05:30
2025-05-05 19:35:24 +05:30
def init_ui(self):
main_layout = QHBoxLayout()
left_layout = QVBoxLayout()
2025-05-06 22:08:27 +05:30
right_layout = QVBoxLayout()
left_layout.addWidget(QLabel("🔗 Kemono Creator/Post URL:"))
2025-05-05 19:35:24 +05:30
self.link_input = QLineEdit()
2025-05-06 22:08:27 +05:30
self.link_input.setPlaceholderText("e.g., https://kemono.su/patreon/user/12345 or .../post/98765")
self.link_input.textChanged.connect(self.update_custom_folder_visibility)
2025-05-05 19:35:24 +05:30
left_layout.addWidget(self.link_input)
2025-05-06 22:08:27 +05:30
left_layout.addWidget(QLabel("📁 Download Location:"))
2025-05-05 19:35:24 +05:30
self.dir_input = QLineEdit()
2025-05-06 22:08:27 +05:30
self.dir_input.setPlaceholderText("Select folder where downloads will be saved")
self.dir_button = QPushButton("Browse...")
2025-05-05 19:35:24 +05:30
self.dir_button.clicked.connect(self.browse_directory)
dir_layout = QHBoxLayout()
2025-05-06 22:49:19 +05:30
dir_layout.addWidget(self.dir_input, 1)
2025-05-05 19:35:24 +05:30
dir_layout.addWidget(self.dir_button)
left_layout.addLayout(dir_layout)
2025-05-06 22:08:27 +05:30
self.custom_folder_widget = QWidget()
custom_folder_layout = QVBoxLayout(self.custom_folder_widget)
2025-05-06 22:49:19 +05:30
custom_folder_layout.setContentsMargins(0, 5, 0, 0)
2025-05-06 22:08:27 +05:30
self.custom_folder_label = QLabel("🗄️ Custom Folder Name (Single Post Only):")
self.custom_folder_input = QLineEdit()
self.custom_folder_input.setPlaceholderText("Optional: Save this post to specific folder")
custom_folder_layout.addWidget(self.custom_folder_label)
custom_folder_layout.addWidget(self.custom_folder_input)
2025-05-06 22:49:19 +05:30
self.custom_folder_widget.setVisible(False)
2025-05-06 22:08:27 +05:30
left_layout.addWidget(self.custom_folder_widget)
self.character_filter_widget = QWidget()
character_filter_layout = QVBoxLayout(self.character_filter_widget)
2025-05-06 22:49:19 +05:30
character_filter_layout.setContentsMargins(0, 5, 0, 0)
2025-05-06 22:08:27 +05:30
self.character_label = QLabel("🎯 Filter by Show/Character Name:")
2025-05-05 19:35:24 +05:30
self.character_input = QLineEdit()
2025-05-06 22:08:27 +05:30
self.character_input.setPlaceholderText("Only download posts matching this known name in title")
character_filter_layout.addWidget(self.character_label)
character_filter_layout.addWidget(self.character_input)
2025-05-06 22:49:19 +05:30
self.character_filter_widget.setVisible(True)
2025-05-06 22:08:27 +05:30
left_layout.addWidget(self.character_filter_widget)
left_layout.addWidget(QLabel("🚫 Skip Posts/Files with Words (comma-separated):"))
self.skip_words_input = QLineEdit()
self.skip_words_input.setPlaceholderText("e.g., WM, WIP, sketch, preview")
left_layout.addWidget(self.skip_words_input)
options_layout_1 = QHBoxLayout()
options_layout_1.addWidget(QLabel("Filter Files:"))
2025-05-05 19:35:24 +05:30
self.radio_group = QButtonGroup(self)
2025-05-06 22:08:27 +05:30
self.radio_all = QRadioButton("All")
self.radio_images = QRadioButton("Images/GIFs")
self.radio_videos = QRadioButton("Videos")
2025-05-05 19:35:24 +05:30
self.radio_all.setChecked(True)
self.radio_group.addButton(self.radio_all)
self.radio_group.addButton(self.radio_images)
self.radio_group.addButton(self.radio_videos)
2025-05-06 22:08:27 +05:30
options_layout_1.addWidget(self.radio_all)
options_layout_1.addWidget(self.radio_images)
options_layout_1.addWidget(self.radio_videos)
options_layout_1.addStretch(1)
left_layout.addLayout(options_layout_1)
options_layout_2 = QHBoxLayout()
self.use_subfolders_checkbox = QCheckBox("Separate Folders by Name/Title")
self.use_subfolders_checkbox.setChecked(True)
self.use_subfolders_checkbox.toggled.connect(self.update_ui_for_subfolders)
options_layout_2.addWidget(self.use_subfolders_checkbox)
2025-05-06 22:49:19 +05:30
self.download_thumbnails_checkbox = QCheckBox("Download Thumbnails Only")
2025-05-06 22:08:27 +05:30
self.download_thumbnails_checkbox.setChecked(False)
2025-05-06 22:49:19 +05:30
self.download_thumbnails_checkbox.setToolTip("Thumbnail download functionality is currently limited without the API.")
2025-05-06 22:08:27 +05:30
options_layout_2.addWidget(self.download_thumbnails_checkbox)
options_layout_2.addStretch(1)
left_layout.addLayout(options_layout_2)
options_layout_3 = QHBoxLayout()
self.skip_zip_checkbox = QCheckBox("Skip .zip")
2025-05-05 19:35:24 +05:30
self.skip_zip_checkbox.setChecked(True)
2025-05-06 22:08:27 +05:30
options_layout_3.addWidget(self.skip_zip_checkbox)
self.skip_rar_checkbox = QCheckBox("Skip .rar")
2025-05-05 19:35:24 +05:30
self.skip_rar_checkbox.setChecked(True)
2025-05-06 22:08:27 +05:30
options_layout_3.addWidget(self.skip_rar_checkbox)
self.compress_images_checkbox = QCheckBox("Compress Large Images (to WebP)")
self.compress_images_checkbox.setChecked(False)
self.compress_images_checkbox.setToolTip("Compress images > 1.5MB to WebP format (requires Pillow).")
options_layout_3.addWidget(self.compress_images_checkbox)
options_layout_3.addStretch(1)
left_layout.addLayout(options_layout_3)
options_layout_4 = QHBoxLayout()
2025-05-06 22:49:19 +05:30
self.use_multithreading_checkbox = QCheckBox(f"Use Multithreading ({4} Threads)")
self.use_multithreading_checkbox.setChecked(True)
2025-05-06 22:08:27 +05:30
self.use_multithreading_checkbox.setToolTip("Speeds up downloads for full creator pages.\nSingle post URLs always use one thread.")
options_layout_4.addWidget(self.use_multithreading_checkbox)
options_layout_4.addStretch(1)
left_layout.addLayout(options_layout_4)
2025-05-05 19:35:24 +05:30
btn_layout = QHBoxLayout()
self.download_btn = QPushButton("⬇️ Start Download")
2025-05-06 22:49:19 +05:30
self.download_btn.setStyleSheet("padding: 8px 15px; font-weight: bold;")
2025-05-05 19:35:24 +05:30
self.download_btn.clicked.connect(self.start_download)
2025-05-06 22:08:27 +05:30
self.cancel_btn = QPushButton("❌ Cancel")
2025-05-05 19:35:24 +05:30
self.cancel_btn.setEnabled(False)
2025-05-06 22:08:27 +05:30
self.cancel_btn.clicked.connect(self.cancel_download)
2025-05-05 19:35:24 +05:30
self.skip_file_btn = QPushButton("⏭️ Skip Current File")
self.skip_file_btn.setEnabled(False)
2025-05-06 22:08:27 +05:30
self.skip_file_btn.setToolTip("Only available in single-thread mode during file download.")
self.skip_file_btn.clicked.connect(self.skip_current_file)
2025-05-05 19:35:24 +05:30
btn_layout.addWidget(self.download_btn)
btn_layout.addWidget(self.cancel_btn)
btn_layout.addWidget(self.skip_file_btn)
left_layout.addLayout(btn_layout)
2025-05-06 22:49:19 +05:30
left_layout.addSpacing(10)
2025-05-06 22:08:27 +05:30
known_chars_label_layout = QHBoxLayout()
self.known_chars_label = QLabel("🎭 Known Shows/Characters (for Folder Names):")
2025-05-06 22:49:19 +05:30
self.character_search_input = QLineEdit()
self.character_search_input.setPlaceholderText("Search characters...")
known_chars_label_layout.addWidget(self.known_chars_label, 1)
known_chars_label_layout.addWidget(self.character_search_input)
2025-05-06 22:08:27 +05:30
2025-05-06 22:49:19 +05:30
left_layout.addLayout(known_chars_label_layout)
2025-05-06 22:08:27 +05:30
2025-05-05 19:35:24 +05:30
self.character_list = QListWidget()
2025-05-07 07:20:40 +05:30
# KNOWN_NAMES will be populated by load_known_names_from_util
2025-05-06 22:08:27 +05:30
self.character_list.setSelectionMode(QListWidget.ExtendedSelection)
2025-05-06 22:49:19 +05:30
left_layout.addWidget(self.character_list, 1)
2025-05-06 22:08:27 +05:30
char_manage_layout = QHBoxLayout()
2025-05-05 19:35:24 +05:30
self.new_char_input = QLineEdit()
2025-05-06 22:08:27 +05:30
self.new_char_input.setPlaceholderText("Add new show/character name")
2025-05-05 19:35:24 +05:30
self.add_char_button = QPushButton(" Add")
self.delete_char_button = QPushButton("🗑️ Delete Selected")
self.add_char_button.clicked.connect(self.add_new_character)
self.new_char_input.returnPressed.connect(self.add_char_button.click)
self.delete_char_button.clicked.connect(self.delete_selected_character)
2025-05-06 22:49:19 +05:30
char_manage_layout.addWidget(self.new_char_input, 2)
2025-05-06 22:08:27 +05:30
char_manage_layout.addWidget(self.add_char_button, 1)
char_manage_layout.addWidget(self.delete_char_button, 1)
left_layout.addLayout(char_manage_layout)
right_layout.addWidget(QLabel("📜 Progress Log:"))
self.log_output = QTextEdit()
self.log_output.setReadOnly(True)
2025-05-06 22:49:19 +05:30
self.log_output.setMinimumWidth(450)
self.log_output.setLineWrapMode(QTextEdit.WidgetWidth)
right_layout.addWidget(self.log_output, 1)
2025-05-06 22:08:27 +05:30
self.progress_label = QLabel("Progress: Idle")
self.progress_label.setStyleSheet("padding-top: 5px; font-style: italic;")
right_layout.addWidget(self.progress_label)
2025-05-06 22:49:19 +05:30
main_layout.addLayout(left_layout, 5)
main_layout.addLayout(right_layout, 4)
2025-05-05 19:35:24 +05:30
self.setLayout(main_layout)
2025-05-06 22:08:27 +05:30
self.update_ui_for_subfolders(self.use_subfolders_checkbox.isChecked())
self.update_custom_folder_visibility()
2025-05-05 19:35:24 +05:30
def get_dark_theme(self):
return """
QWidget {
2025-05-06 22:49:19 +05:30
background-color: #2E2E2E;
color: #E0E0E0;
2025-05-05 19:35:24 +05:30
font-family: Segoe UI, Arial, sans-serif;
font-size: 10pt;
}
QLineEdit, QTextEdit, QListWidget {
2025-05-06 22:08:27 +05:30
background-color: #3C3F41;
2025-05-06 22:49:19 +05:30
border: 1px solid #5A5A5A;
2025-05-05 19:35:24 +05:30
padding: 5px;
2025-05-06 22:49:19 +05:30
color: #F0F0F0;
border-radius: 4px;
2025-05-06 22:08:27 +05:30
}
QTextEdit {
2025-05-06 22:49:19 +05:30
font-family: Consolas, Courier New, monospace;
2025-05-06 22:08:27 +05:30
font-size: 9.5pt;
2025-05-05 19:35:24 +05:30
}
QPushButton {
background-color: #555;
2025-05-06 22:08:27 +05:30
color: #F0F0F0;
border: 1px solid #6A6A6A;
2025-05-05 19:35:24 +05:30
padding: 6px 12px;
2025-05-06 22:08:27 +05:30
border-radius: 4px;
2025-05-06 22:49:19 +05:30
min-height: 22px;
2025-05-05 19:35:24 +05:30
}
QPushButton:hover {
2025-05-06 22:49:19 +05:30
background-color: #656565;
2025-05-06 22:08:27 +05:30
border: 1px solid #7A7A7A;
2025-05-05 19:35:24 +05:30
}
QPushButton:pressed {
2025-05-06 22:49:19 +05:30
background-color: #4A4A4A;
2025-05-05 19:35:24 +05:30
}
QPushButton:disabled {
2025-05-06 22:49:19 +05:30
background-color: #404040;
2025-05-05 19:35:24 +05:30
color: #888;
border-color: #555;
}
QLabel {
font-weight: bold;
padding-top: 4px;
2025-05-06 22:08:27 +05:30
padding-bottom: 2px;
2025-05-06 22:49:19 +05:30
color: #C0C0C0;
2025-05-05 19:35:24 +05:30
}
2025-05-06 22:08:27 +05:30
QRadioButton, QCheckBox {
2025-05-05 19:35:24 +05:30
spacing: 5px;
2025-05-06 22:08:27 +05:30
color: #E0E0E0;
padding-top: 4px;
padding-bottom: 4px;
2025-05-05 19:35:24 +05:30
}
2025-05-06 22:08:27 +05:30
QRadioButton::indicator, QCheckBox::indicator {
2025-05-06 22:49:19 +05:30
width: 14px;
2025-05-06 22:08:27 +05:30
height: 14px;
2025-05-05 19:35:24 +05:30
}
QListWidget {
2025-05-06 22:49:19 +05:30
alternate-background-color: #353535;
2025-05-06 22:08:27 +05:30
border: 1px solid #5A5A5A;
2025-05-05 19:35:24 +05:30
}
QListWidget::item:selected {
2025-05-06 22:49:19 +05:30
background-color: #007ACC;
2025-05-06 22:08:27 +05:30
color: #FFFFFF;
2025-05-05 19:35:24 +05:30
}
2025-05-06 22:08:27 +05:30
QToolTip {
background-color: #4A4A4A;
color: #F0F0F0;
border: 1px solid #6A6A6A;
padding: 4px;
border-radius: 3px;
2025-05-05 19:35:24 +05:30
}
"""
def browse_directory(self):
2025-05-06 22:08:27 +05:30
current_dir = self.dir_input.text() if os.path.isdir(self.dir_input.text()) else ""
folder = QFileDialog.getExistingDirectory(self, "Select Download Folder", current_dir)
2025-05-05 19:35:24 +05:30
if folder:
self.dir_input.setText(folder)
def log(self, message):
2025-05-06 22:08:27 +05:30
try:
2025-05-06 22:49:19 +05:30
safe_message = str(message).replace('\x00', '[NULL]')
2025-05-06 22:08:27 +05:30
self.log_output.append(safe_message)
scrollbar = self.log_output.verticalScrollBar()
2025-05-06 22:49:19 +05:30
if scrollbar.value() >= scrollbar.maximum() - 30:
2025-05-06 22:08:27 +05:30
scrollbar.setValue(scrollbar.maximum())
except Exception as e:
print(f"GUI Log Error: {e}")
print(f"Original Message: {message}")
2025-05-05 19:35:24 +05:30
def get_filter_mode(self):
if self.radio_images.isChecked():
return 'image'
elif self.radio_videos.isChecked():
return 'video'
return 'all'
def add_new_character(self):
2025-05-07 07:20:40 +05:30
import downloader_utils # Ensure we are using the list from utils
2025-05-06 22:08:27 +05:30
name_to_add = self.new_char_input.text().strip()
if not name_to_add:
QMessageBox.warning(self, "Input Error", "Name cannot be empty.")
return
name_lower = name_to_add.lower()
2025-05-07 07:20:40 +05:30
is_duplicate = any(existing.lower() == name_lower for existing in downloader_utils.KNOWN_NAMES)
2025-05-06 22:08:27 +05:30
if not is_duplicate:
2025-05-07 07:20:40 +05:30
downloader_utils.KNOWN_NAMES.append(name_to_add)
downloader_utils.KNOWN_NAMES.sort(key=str.lower)
2025-05-06 22:08:27 +05:30
self.character_list.clear()
2025-05-07 07:20:40 +05:30
self.character_list.addItems(downloader_utils.KNOWN_NAMES)
2025-05-06 22:49:19 +05:30
self.filter_character_list(self.character_search_input.text())
2025-05-06 22:08:27 +05:30
self.log_signal.emit(f"✅ Added '{name_to_add}' to known names list.")
self.new_char_input.clear()
2025-05-06 22:49:19 +05:30
self.save_known_names()
2025-05-05 19:35:24 +05:30
else:
2025-05-06 22:08:27 +05:30
QMessageBox.warning(self, "Duplicate Name", f"The name '{name_to_add}' (or similar) already exists in the list.")
2025-05-05 19:35:24 +05:30
def delete_selected_character(self):
2025-05-07 07:20:40 +05:30
import downloader_utils
2025-05-05 19:35:24 +05:30
selected_items = self.character_list.selectedItems()
if not selected_items:
2025-05-06 22:08:27 +05:30
QMessageBox.warning(self, "Selection Error", "Please select one or more names to delete.")
2025-05-05 19:35:24 +05:30
return
2025-05-06 22:08:27 +05:30
names_to_remove = {item.text() for item in selected_items}
2025-05-05 19:35:24 +05:30
confirm = QMessageBox.question(self, "Confirm Deletion",
2025-05-06 22:08:27 +05:30
f"Are you sure you want to delete {len(names_to_remove)} selected name(s)?",
2025-05-05 19:35:24 +05:30
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if confirm == QMessageBox.Yes:
2025-05-07 07:20:40 +05:30
original_count = len(downloader_utils.KNOWN_NAMES)
downloader_utils.KNOWN_NAMES = [n for n in downloader_utils.KNOWN_NAMES if n not in names_to_remove]
removed_count = original_count - len(downloader_utils.KNOWN_NAMES)
2025-05-06 22:08:27 +05:30
2025-05-05 19:35:24 +05:30
if removed_count > 0:
2025-05-06 22:08:27 +05:30
self.log_signal.emit(f"🗑️ Removed {removed_count} name(s) from the list.")
2025-05-05 19:35:24 +05:30
self.character_list.clear()
2025-05-07 07:20:40 +05:30
downloader_utils.KNOWN_NAMES.sort(key=str.lower)
self.character_list.addItems(downloader_utils.KNOWN_NAMES)
2025-05-06 22:49:19 +05:30
self.filter_character_list(self.character_search_input.text())
self.save_known_names()
2025-05-06 22:08:27 +05:30
else:
self.log_signal.emit(" No names were removed (selection might have changed?).")
def update_custom_folder_visibility(self, url_text=None):
if url_text is None:
url_text = self.link_input.text()
2025-05-07 07:20:40 +05:30
_, _, post_id = extract_post_info(url_text.strip()) # from downloader_utils
2025-05-06 22:08:27 +05:30
should_show = bool(post_id) and self.use_subfolders_checkbox.isChecked()
self.custom_folder_widget.setVisible(should_show)
if not should_show:
2025-05-06 22:49:19 +05:30
self.custom_folder_input.clear()
2025-05-06 22:08:27 +05:30
def update_ui_for_subfolders(self, checked):
self.character_filter_widget.setVisible(checked)
self.update_custom_folder_visibility()
if not checked:
self.character_input.clear()
2025-05-07 07:20:40 +05:30
2025-05-06 22:08:27 +05:30
def filter_character_list(self, search_text):
search_text = search_text.lower()
for i in range(self.character_list.count()):
item = self.character_list.item(i)
if search_text in item.text().lower():
item.setHidden(False)
2025-05-05 19:35:24 +05:30
else:
2025-05-06 22:08:27 +05:30
item.setHidden(True)
def update_progress_display(self, total_posts, processed_posts):
if total_posts > 0:
try:
percent = (processed_posts / total_posts) * 100
self.progress_label.setText(f"Progress: {processed_posts} / {total_posts} posts ({percent:.1f}%)")
except ZeroDivisionError:
2025-05-06 22:49:19 +05:30
self.progress_label.setText(f"Progress: {processed_posts} / {total_posts} posts")
elif processed_posts > 0:
2025-05-06 22:08:27 +05:30
self.progress_label.setText(f"Progress: Processing post {processed_posts}...")
else:
self.progress_label.setText("Progress: Starting...")
2025-05-05 19:35:24 +05:30
def start_download(self):
2025-05-07 07:20:40 +05:30
import downloader_utils # For KNOWN_NAMES
2025-05-06 22:08:27 +05:30
is_running = (self.download_thread and self.download_thread.isRunning()) or (self.thread_pool is not None)
if is_running:
self.log_signal.emit("⚠️ Download already in progress.")
QMessageBox.warning(self, "Busy", "A download is already running.")
2025-05-05 19:35:24 +05:30
return
api_url = self.link_input.text().strip()
output_dir = self.dir_input.text().strip()
filter_mode = self.get_filter_mode()
skip_zip = self.skip_zip_checkbox.isChecked()
skip_rar = self.skip_rar_checkbox.isChecked()
use_subfolders = self.use_subfolders_checkbox.isChecked()
2025-05-06 22:08:27 +05:30
compress_images = self.compress_images_checkbox.isChecked()
download_thumbnails = self.download_thumbnails_checkbox.isChecked()
use_multithreading = self.use_multithreading_checkbox.isChecked()
2025-05-06 22:49:19 +05:30
num_threads = 4
2025-05-06 22:08:27 +05:30
raw_skip_words = self.skip_words_input.text().strip()
skip_words_list = []
if raw_skip_words:
skip_words_list = [word.strip() for word in raw_skip_words.split(',') if word.strip()]
2025-05-07 07:20:40 +05:30
service, user_id, post_id_from_url = extract_post_info(api_url) # from downloader_utils
2025-05-06 22:08:27 +05:30
2025-05-05 19:35:24 +05:30
if not api_url:
2025-05-06 22:08:27 +05:30
QMessageBox.critical(self, "Input Error", "Please enter a Kemono/Coomer URL.")
2025-05-05 19:35:24 +05:30
return
2025-05-06 22:08:27 +05:30
if not service or not user_id:
QMessageBox.critical(self, "Input Error", "Invalid or unsupported URL format.\nPlease provide a valid creator page or post URL.")
self.log_signal.emit(f"❌ Invalid URL detected: {api_url}")
return
2025-05-05 19:35:24 +05:30
if not output_dir:
2025-05-06 22:08:27 +05:30
QMessageBox.critical(self, "Input Error", "Please select a download directory.")
return
if not os.path.isdir(output_dir):
reply = QMessageBox.question(self, "Directory Not Found",
f"The directory '{output_dir}' does not exist.\n\nCreate it?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes)
if reply == QMessageBox.Yes:
try:
os.makedirs(output_dir)
self.log_signal.emit(f" Created download directory: {output_dir}")
except Exception as e:
QMessageBox.critical(self, "Directory Error", f"Could not create directory:\n{e}")
self.log_signal.emit(f"❌ Failed to create directory: {output_dir} - {e}")
return
2025-05-06 22:49:19 +05:30
else:
2025-05-06 22:08:27 +05:30
return
2025-05-07 07:20:40 +05:30
if compress_images and Image is None: # Image imported in this file
2025-05-06 22:08:27 +05:30
QMessageBox.warning(self, "Dependency Missing", "Image compression requires the Pillow library, but it's not installed.\nPlease run: pip install Pillow\n\nCompression will be disabled for this session.")
self.log_signal.emit("❌ Cannot compress images: Pillow library not found.")
2025-05-06 22:49:19 +05:30
compress_images = False
2025-05-06 22:08:27 +05:30
filter_character = None
if use_subfolders and self.character_filter_widget.isVisible():
filter_character = self.character_input.text().strip() or None
custom_folder_name = None
if use_subfolders and post_id_from_url and self.custom_folder_widget.isVisible():
raw_custom_name = self.custom_folder_input.text().strip()
if raw_custom_name:
2025-05-07 07:20:40 +05:30
cleaned_custom = clean_folder_name(raw_custom_name) # from downloader_utils
2025-05-06 22:08:27 +05:30
if cleaned_custom:
custom_folder_name = cleaned_custom
else:
QMessageBox.warning(self, "Input Warning", f"Custom folder name '{raw_custom_name}' is invalid and will be ignored.")
self.log_signal.emit(f"⚠️ Invalid custom folder name ignored: {raw_custom_name}")
2025-05-06 22:49:19 +05:30
if use_subfolders and filter_character and not post_id_from_url:
2025-05-07 07:20:40 +05:30
clean_char_filter = clean_folder_name(filter_character.lower()) # from downloader_utils
known_names_lower = {name.lower() for name in downloader_utils.KNOWN_NAMES}
2025-05-06 22:08:27 +05:30
if not clean_char_filter:
self.log_signal.emit(f"❌ Filter name '{filter_character}' is invalid. Aborting.")
QMessageBox.critical(self, "Filter Error", "The provided filter name is invalid (contains only spaces or special characters).")
return
elif filter_character.lower() not in known_names_lower:
reply = QMessageBox.question(self, "Add Filter Name?",
f"The filter name '{filter_character}' is not in your known names list.\n\nAdd it now and continue?",
QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel, QMessageBox.Yes)
if reply == QMessageBox.Yes:
2025-05-06 22:49:19 +05:30
self.new_char_input.setText(filter_character)
self.add_new_character()
2025-05-07 07:20:40 +05:30
if filter_character.lower() not in {name.lower() for name in downloader_utils.KNOWN_NAMES}:
2025-05-06 22:08:27 +05:30
self.log_signal.emit(f"⚠️ Failed to add '{filter_character}' automatically. Please add manually if needed.")
else:
self.log_signal.emit(f"✅ Added filter '{filter_character}' to list.")
elif reply == QMessageBox.No:
self.log_signal.emit(f" Proceeding without adding '{filter_character}'. Posts matching it might not be saved to a specific folder unless name is derived.")
2025-05-06 22:49:19 +05:30
else:
2025-05-06 22:08:27 +05:30
self.log_signal.emit("❌ Download cancelled by user during filter check.")
2025-05-06 22:49:19 +05:30
return
2025-05-05 19:35:24 +05:30
self.log_output.clear()
2025-05-06 22:49:19 +05:30
self.cancellation_event.clear()
2025-05-06 22:08:27 +05:30
self.active_futures = []
self.total_posts_to_process = 0
self.processed_posts_count = 0
self.download_counter = 0
self.skip_counter = 0
with self.downloaded_files_lock:
2025-05-06 22:49:19 +05:30
self.downloaded_files.clear()
2025-05-06 22:08:27 +05:30
with self.downloaded_file_hashes_lock:
2025-05-06 22:49:19 +05:30
self.downloaded_file_hashes.clear()
2025-05-06 22:08:27 +05:30
self.progress_label.setText("Progress: Initializing...")
self.log_signal.emit("="*40)
self.log_signal.emit(f"🚀 Starting Download Task @ {time.strftime('%Y-%m-%d %H:%M:%S')}")
self.log_signal.emit(f" URL: {api_url}")
self.log_signal.emit(f" Save Location: {output_dir}")
mode = "Single Post" if post_id_from_url else "Creator Feed"
self.log_signal.emit(f" Mode: {mode}")
self.log_signal.emit(f" Subfolders: {'Enabled' if use_subfolders else 'Disabled'}")
if use_subfolders:
if custom_folder_name:
self.log_signal.emit(f" Custom Folder (Post): '{custom_folder_name}'")
elif filter_character:
self.log_signal.emit(f" Character Filter: '{filter_character}'")
else:
self.log_signal.emit(f" Folder Naming: Automatic (Known Names > Title Extraction)")
self.log_signal.emit(f" File Type Filter: {filter_mode}")
self.log_signal.emit(f" Skip: {'.zip' if skip_zip else ''}{', ' if skip_zip and skip_rar else ''}{'.rar' if skip_rar else ''}{'None' if not (skip_zip or skip_rar) else ''}")
if skip_words_list:
self.log_signal.emit(f" Skip Words (Title/Filename): {', '.join(skip_words_list)}")
else:
self.log_signal.emit(f" Skip Words (Title/Filename): None")
self.log_signal.emit(f" Compress Images: {'Enabled' if compress_images else 'Disabled'}")
self.log_signal.emit(f" Thumbnails Only: {'Enabled' if download_thumbnails else 'Disabled'}")
should_use_multithreading = use_multithreading and not post_id_from_url
self.log_signal.emit(f" Threading: {'Multi-threaded' if should_use_multithreading else 'Single-threaded'}")
self.log_signal.emit("="*40)
self.set_ui_enabled(False)
2025-05-05 19:35:24 +05:30
self.cancel_btn.setEnabled(True)
2025-05-06 22:08:27 +05:30
try:
common_args = {
'api_url': api_url,
'output_dir': output_dir,
2025-05-07 07:20:40 +05:30
'known_names_copy': list(downloader_utils.KNOWN_NAMES), # From downloader_utils
2025-05-06 22:08:27 +05:30
'filter_character': filter_character,
'filter_mode': filter_mode,
'skip_zip': skip_zip,
'skip_rar': skip_rar,
'use_subfolders': use_subfolders,
'compress_images': compress_images,
'download_thumbnails': download_thumbnails,
'service': service,
'user_id': user_id,
'downloaded_files': self.downloaded_files,
'downloaded_files_lock': self.downloaded_files_lock,
2025-05-06 22:49:19 +05:30
'downloaded_file_hashes': self.downloaded_file_hashes,
'downloaded_file_hashes_lock': self.downloaded_file_hashes_lock,
'skip_words_list': skip_words_list,
2025-05-06 22:08:27 +05:30
}
if should_use_multithreading:
self.log_signal.emit(" Initializing multi-threaded download...")
multi_args = common_args.copy()
multi_args['num_threads'] = num_threads
self.start_multi_threaded_download(**multi_args)
else:
self.log_signal.emit(" Initializing single-threaded download...")
single_args = common_args.copy()
single_args['custom_folder_name'] = custom_folder_name
single_args['single_post_id'] = post_id_from_url
self.start_single_threaded_download(**single_args)
except Exception as e:
self.log_signal.emit(f"❌ CRITICAL ERROR preparing download task: {e}")
import traceback
self.log_signal.emit(traceback.format_exc())
QMessageBox.critical(self, "Start Error", f"Failed to start download task:\n{e}")
2025-05-06 22:49:19 +05:30
self.download_finished(0, 0, False)
2025-05-06 22:08:27 +05:30
def start_single_threaded_download(self, **kwargs):
try:
2025-05-07 07:20:40 +05:30
self.download_thread = BackendDownloadThread( # Use renamed import
2025-05-06 22:49:19 +05:30
cancellation_event = self.cancellation_event,
2025-05-06 22:08:27 +05:30
**kwargs
)
if self.download_thread._init_failed:
QMessageBox.critical(self, "Thread Error", "Failed to initialize the download thread.\nCheck the log for details.")
2025-05-06 22:49:19 +05:30
self.download_finished(0, 0, False)
2025-05-06 22:08:27 +05:30
return
2025-05-06 22:49:19 +05:30
self.download_thread.progress_signal.connect(self.log_signal)
self.download_thread.add_character_prompt_signal.connect(self.add_character_prompt_signal)
self.download_thread.file_download_status_signal.connect(self.file_download_status_signal)
self.download_thread.finished_signal.connect(self.finished_signal)
2025-05-06 22:08:27 +05:30
self.character_prompt_response_signal.connect(self.download_thread.receive_add_character_result)
self.download_thread.start()
self.log_signal.emit("✅ Single download thread started.")
except Exception as e:
self.log_signal.emit(f"❌ CRITICAL ERROR starting single-thread task: {e}")
import traceback
self.log_signal.emit(traceback.format_exc())
QMessageBox.critical(self, "Thread Start Error", f"Failed to start download thread:\n{e}")
2025-05-06 22:49:19 +05:30
self.download_finished(0, 0, False)
2025-05-06 22:08:27 +05:30
def start_multi_threaded_download(self, **kwargs):
2025-05-07 07:20:40 +05:30
import downloader_utils # For KNOWN_NAMES
2025-05-06 22:08:27 +05:30
num_threads = kwargs['num_threads']
self.thread_pool = ThreadPoolExecutor(max_workers=num_threads, thread_name_prefix='Downloader_')
self.active_futures = []
self.processed_posts_count = 0
2025-05-06 22:49:19 +05:30
self.total_posts_to_process = 0
2025-05-06 22:08:27 +05:30
self.download_counter = 0
self.skip_counter = 0
worker_args_template = kwargs.copy()
del worker_args_template['num_threads']
fetcher_thread = threading.Thread(
target=self._fetch_and_queue_posts,
args=(kwargs['api_url'], worker_args_template),
daemon=True,
name="PostFetcher"
)
fetcher_thread.start()
self.log_signal.emit(f"✅ Post fetcher thread started. {num_threads} worker threads initializing...")
def _fetch_and_queue_posts(self, api_url_input, worker_args_template):
2025-05-07 07:20:40 +05:30
import downloader_utils # For download_from_api
2025-05-06 22:08:27 +05:30
all_posts = []
fetch_error = False
try:
self.log_signal.emit(" Starting post fetch...")
def fetcher_logger(msg):
self.log_signal.emit(f"[Fetcher] {msg}")
2025-05-07 07:20:40 +05:30
post_generator = downloader_utils.download_from_api(api_url_input, logger=fetcher_logger)
2025-05-06 22:08:27 +05:30
for posts_batch in post_generator:
if self.cancellation_event.is_set():
self.log_signal.emit("⚠️ Post fetching cancelled by user.")
2025-05-06 22:49:19 +05:30
fetch_error = True
2025-05-06 22:08:27 +05:30
break
if isinstance(posts_batch, list):
all_posts.extend(posts_batch)
self.total_posts_to_process = len(all_posts)
2025-05-06 22:49:19 +05:30
if self.total_posts_to_process % 250 == 0:
2025-05-06 22:08:27 +05:30
self.log_signal.emit(f" Fetched {self.total_posts_to_process} posts...")
else:
self.log_signal.emit(f"❌ API returned non-list batch: {type(posts_batch)}. Stopping fetch.")
fetch_error = True
break
if not fetch_error:
self.log_signal.emit(f"✅ Finished fetching. Total posts found: {self.total_posts_to_process}")
except Exception as e:
self.log_signal.emit(f"❌ Unexpected Error during post fetching: {e}")
import traceback
self.log_signal.emit(traceback.format_exc(limit=3))
fetch_error = True
if self.cancellation_event.is_set() or fetch_error:
self.finished_signal.emit(self.download_counter, self.skip_counter, self.cancellation_event.is_set())
if self.thread_pool:
self.thread_pool.shutdown(wait=False, cancel_futures=True)
self.thread_pool = None
2025-05-06 22:49:19 +05:30
return
2025-05-06 22:08:27 +05:30
if self.total_posts_to_process == 0:
self.log_signal.emit("😕 No posts found or fetched successfully.")
2025-05-06 22:49:19 +05:30
self.finished_signal.emit(0, 0, False)
2025-05-06 22:08:27 +05:30
return
self.log_signal.emit(f" Submitting {self.total_posts_to_process} post tasks to worker pool...")
2025-05-06 22:49:19 +05:30
self.processed_posts_count = 0
self.overall_progress_signal.emit(self.total_posts_to_process, 0)
2025-05-06 22:08:27 +05:30
common_worker_args = {
2025-05-06 22:49:19 +05:30
'download_root': worker_args_template['output_dir'],
2025-05-07 07:20:40 +05:30
'known_names': worker_args_template['known_names_copy'], # Already a copy from KNOWN_NAMES
2025-05-06 22:08:27 +05:30
'filter_character': worker_args_template['filter_character'],
2025-05-06 22:49:19 +05:30
'unwanted_keywords': {'spicy', 'hd', 'nsfw', '4k', 'preview'},
2025-05-06 22:08:27 +05:30
'filter_mode': worker_args_template['filter_mode'],
'skip_zip': worker_args_template['skip_zip'],
'skip_rar': worker_args_template['skip_rar'],
'use_subfolders': worker_args_template['use_subfolders'],
'target_post_id_from_initial_url': worker_args_template.get('single_post_id'),
'custom_folder_name': worker_args_template.get('custom_folder_name'),
'compress_images': worker_args_template['compress_images'],
'download_thumbnails': worker_args_template['download_thumbnails'],
'service': worker_args_template['service'],
'user_id': worker_args_template['user_id'],
2025-05-06 22:49:19 +05:30
'api_url_input': worker_args_template['api_url'],
2025-05-06 22:08:27 +05:30
'cancellation_event': self.cancellation_event,
2025-05-06 22:49:19 +05:30
'signals': self.worker_signals,
2025-05-06 22:08:27 +05:30
'downloaded_files': self.downloaded_files,
'downloaded_files_lock': self.downloaded_files_lock,
2025-05-06 22:49:19 +05:30
'downloaded_file_hashes': self.downloaded_file_hashes,
'downloaded_file_hashes_lock': self.downloaded_file_hashes_lock,
'skip_words_list': worker_args_template['skip_words_list'],
2025-05-06 22:08:27 +05:30
}
for post_data in all_posts:
if self.cancellation_event.is_set():
self.log_signal.emit("⚠️ Cancellation detected during task submission.")
2025-05-06 22:49:19 +05:30
break
2025-05-06 22:08:27 +05:30
if not isinstance(post_data, dict):
self.log_signal.emit(f"⚠️ Skipping invalid post data item (type: {type(post_data)}).")
2025-05-06 22:49:19 +05:30
self.processed_posts_count += 1
self.total_posts_to_process -=1
2025-05-06 22:08:27 +05:30
continue
2025-05-07 07:20:40 +05:30
worker = PostProcessorWorker(post_data=post_data, **common_worker_args) # PostProcessorWorker from downloader_utils
2025-05-06 22:08:27 +05:30
try:
2025-05-06 22:49:19 +05:30
if self.thread_pool:
2025-05-06 22:08:27 +05:30
future = self.thread_pool.submit(worker.process)
future.add_done_callback(self._handle_future_result)
self.active_futures.append(future)
2025-05-06 22:49:19 +05:30
else:
2025-05-06 22:08:27 +05:30
self.log_signal.emit("⚠️ Thread pool shutdown before submitting all tasks.")
break
2025-05-06 22:49:19 +05:30
except RuntimeError as e:
2025-05-06 22:08:27 +05:30
self.log_signal.emit(f"⚠️ Error submitting task (pool might be shutting down): {e}")
break
except Exception as e:
self.log_signal.emit(f"❌ Unexpected error submitting task: {e}")
break
submitted_count = len(self.active_futures)
self.log_signal.emit(f" {submitted_count} / {self.total_posts_to_process} tasks submitted.")
def _handle_future_result(self, future: Future):
self.processed_posts_count += 1
2025-05-06 22:49:19 +05:30
downloaded_res, skipped_res = 0, 0
2025-05-06 22:08:27 +05:30
try:
if future.cancelled():
pass
elif future.exception():
exc = future.exception()
self.log_signal.emit(f"❌ Error in worker thread: {exc}")
pass
else:
2025-05-06 22:49:19 +05:30
downloaded, skipped = future.result()
2025-05-06 22:08:27 +05:30
downloaded_res = downloaded
skipped_res = skipped
2025-05-05 19:35:24 +05:30
2025-05-06 22:49:19 +05:30
with threading.Lock():
2025-05-06 22:08:27 +05:30
self.download_counter += downloaded_res
self.skip_counter += skipped_res
self.overall_progress_signal.emit(self.total_posts_to_process, self.processed_posts_count)
except Exception as e:
self.log_signal.emit(f"❌ Error in result callback handling: {e}")
if self.processed_posts_count >= self.total_posts_to_process and self.total_posts_to_process > 0:
2025-05-06 22:49:19 +05:30
2025-05-06 22:08:27 +05:30
if self.processed_posts_count >= self.total_posts_to_process:
self.log_signal.emit("🏁 All submitted tasks have completed or failed.")
cancelled = self.cancellation_event.is_set()
self.finished_signal.emit(self.download_counter, self.skip_counter, cancelled)
2025-05-07 07:20:40 +05:30
2025-05-06 22:08:27 +05:30
def set_ui_enabled(self, enabled):
self.download_btn.setEnabled(enabled)
self.link_input.setEnabled(enabled)
self.dir_input.setEnabled(enabled)
self.dir_button.setEnabled(enabled)
self.radio_all.setEnabled(enabled)
self.radio_images.setEnabled(enabled)
self.radio_videos.setEnabled(enabled)
self.skip_zip_checkbox.setEnabled(enabled)
self.skip_rar_checkbox.setEnabled(enabled)
self.use_subfolders_checkbox.setEnabled(enabled)
self.compress_images_checkbox.setEnabled(enabled)
self.download_thumbnails_checkbox.setEnabled(enabled)
self.use_multithreading_checkbox.setEnabled(enabled)
2025-05-06 22:49:19 +05:30
self.skip_words_input.setEnabled(enabled)
self.character_search_input.setEnabled(enabled)
2025-05-06 22:08:27 +05:30
self.new_char_input.setEnabled(enabled)
self.add_char_button.setEnabled(enabled)
self.delete_char_button.setEnabled(enabled)
subfolders_on = self.use_subfolders_checkbox.isChecked()
self.custom_folder_widget.setEnabled(enabled and subfolders_on)
self.character_filter_widget.setEnabled(enabled and subfolders_on)
if enabled:
self.update_ui_for_subfolders(subfolders_on)
2025-05-06 22:49:19 +05:30
self.update_custom_folder_visibility()
2025-05-06 22:08:27 +05:30
self.cancel_btn.setEnabled(not enabled)
if enabled:
self.skip_file_btn.setEnabled(False)
2025-05-07 07:20:40 +05:30
2025-05-05 19:35:24 +05:30
def cancel_download(self):
2025-05-06 22:49:19 +05:30
if not self.cancel_btn.isEnabled(): return
2025-05-06 22:08:27 +05:30
self.log_signal.emit("⚠️ Requesting cancellation...")
2025-05-06 22:49:19 +05:30
self.cancellation_event.set()
2025-05-06 22:08:27 +05:30
self.cancel_btn.setEnabled(False)
self.progress_label.setText("Progress: Cancelling...")
if self.thread_pool and self.active_futures:
cancelled_count = 0
for future in self.active_futures:
2025-05-06 22:49:19 +05:30
if future.cancel():
2025-05-06 22:08:27 +05:30
cancelled_count += 1
if cancelled_count > 0:
self.log_signal.emit(f" Attempted to cancel {cancelled_count} pending/running tasks.")
2025-05-05 19:35:24 +05:30
def skip_current_file(self):
if self.download_thread and self.download_thread.isRunning():
2025-05-06 22:49:19 +05:30
self.download_thread.skip_file()
2025-05-06 22:08:27 +05:30
elif self.thread_pool:
self.log_signal.emit(" Skipping individual files is not supported in multi-threaded mode.")
QMessageBox.information(self, "Action Not Supported", "Skipping individual files is only available in single-threaded mode.")
else:
self.log_signal.emit(" Skip requested, but no download is active.")
2025-05-05 19:35:24 +05:30
2025-05-06 22:08:27 +05:30
def update_skip_button_state(self, is_downloading_active):
can_skip = (not self.download_btn.isEnabled()) and \
(self.download_thread and self.download_thread.isRunning()) and \
is_downloading_active
if self.thread_pool is not None:
can_skip = False
2025-05-05 19:35:24 +05:30
2025-05-06 22:08:27 +05:30
self.skip_file_btn.setEnabled(can_skip)
def download_finished(self, total_downloaded, total_skipped, cancelled):
self.log_signal.emit("="*40)
status = "Cancelled" if cancelled else "Finished"
self.log_signal.emit(f"🏁 Download {status}!")
self.log_signal.emit(f" Summary: Downloaded={total_downloaded}, Skipped={total_skipped}")
self.progress_label.setText(f"{status}: {total_downloaded} downloaded, {total_skipped} skipped.")
self.log_signal.emit("="*40)
if self.download_thread:
try:
self.character_prompt_response_signal.disconnect(self.download_thread.receive_add_character_result)
2025-05-06 22:49:19 +05:30
except TypeError: pass
2025-05-06 22:08:27 +05:30
self.download_thread = None
if self.thread_pool:
self.log_signal.emit(" Shutting down worker thread pool...")
self.thread_pool.shutdown(wait=False, cancel_futures=True)
self.thread_pool = None
2025-05-06 22:49:19 +05:30
self.active_futures = []
2025-05-06 22:08:27 +05:30
self.cancellation_event.clear()
self.set_ui_enabled(True)
2025-05-05 19:35:24 +05:30
self.cancel_btn.setEnabled(False)
self.skip_file_btn.setEnabled(False)
2025-05-06 22:08:27 +05:30
2025-05-07 07:20:40 +05:30
def prompt_add_character(self, character_name):
import downloader_utils # For KNOWN_NAMES
reply = QMessageBox.question(self, "Add Filter Name?",
2025-05-06 22:08:27 +05:30
f"The filter name '{character_name}' is not in your known list.\n\nAdd it now and continue download?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes)
2025-05-07 07:20:40 +05:30
result = (reply == QMessageBox.Yes)
2025-05-06 22:08:27 +05:30
2025-05-07 07:20:40 +05:30
if result:
2025-05-06 22:49:19 +05:30
self.new_char_input.setText(character_name)
2025-05-07 07:20:40 +05:30
if character_name.lower() not in {n.lower() for n in downloader_utils.KNOWN_NAMES}:
2025-05-06 22:49:19 +05:30
self.add_new_character()
2025-05-07 07:20:40 +05:30
if character_name.lower() not in {n.lower() for n in downloader_utils.KNOWN_NAMES}:
2025-05-06 22:08:27 +05:30
self.log_signal.emit(f"⚠️ Failed to add '{character_name}' via prompt. Check for errors.")
2025-05-06 22:49:19 +05:30
result = False
2025-05-06 22:08:27 +05:30
else:
self.log_signal.emit(f" Filter name '{character_name}' was already present or added.")
2025-05-07 07:20:40 +05:30
self.character_prompt_response_signal.emit(result)
2025-05-06 22:08:27 +05:30
def receive_add_character_result(self, result):
with QMutexLocker(self.prompt_mutex):
self._add_character_response = result
2025-05-07 07:20:40 +05:30
self.log_signal.emit(f" Received prompt response: {'Yes' if result else 'No'}")
2025-05-06 22:08:27 +05:30
2025-05-05 19:35:24 +05:30
if __name__ == '__main__':
2025-05-06 22:08:27 +05:30
qt_app = QApplication(sys.argv)
2025-05-07 07:20:40 +05:30
icon_path = os.path.join(os.path.dirname(__file__), 'Kemono.ico')
if os.path.exists(icon_path):
qt_app.setWindowIcon(QIcon(icon_path))
2025-05-06 22:08:27 +05:30
2025-05-06 22:49:19 +05:30
downloader = DownloaderApp()
downloader.show()
2025-05-06 22:08:27 +05:30
exit_code = qt_app.exec_()
print(f"Application finished with exit code: {exit_code}")
2025-05-06 22:49:19 +05:30
sys.exit(exit_code)