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
2025-05-08 19:49:50 +05:30
import queue # Standard library queue, not directly used for the new link queue
2025-05-06 22:49:19 +05:30
import hashlib
2025-05-08 19:49:50 +05:30
import http . client
import traceback
import random # <-- Import random for generating delays
from collections import deque # <-- Import deque for the link queue
2025-05-06 22:08:27 +05:30
2025-05-08 19:49:50 +05:30
from concurrent . futures import ThreadPoolExecutor , CancelledError , Future
from PyQt5 . QtGui import (
QIcon ,
QIntValidator
)
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-09 19:03:01 +05:30
QRadioButton , QButtonGroup , QCheckBox , QSplitter , QSizePolicy , QDialog
2025-05-05 19:35:24 +05:30
)
2025-05-08 19:49:50 +05:30
# Ensure QTimer is imported
2025-05-09 19:03:01 +05:30
from PyQt5 . QtCore import Qt , QThread , pyqtSignal , QMutex , QMutexLocker , QObject , QTimer
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-08 19:49:50 +05:30
Image = None
2025-05-06 22:08:27 +05:30
from io import BytesIO
2025-05-08 19:49:50 +05:30
# --- Import from downloader_utils ---
try :
print ( " Attempting to import from downloader_utils... " )
# Assuming downloader_utils_link_text is the correct version
2025-05-09 19:03:01 +05:30
from downloader_utils import (
2025-05-08 19:49:50 +05:30
KNOWN_NAMES ,
2025-05-09 19:03:01 +05:30
clean_folder_name ,
2025-05-08 19:49:50 +05:30
extract_post_info ,
download_from_api ,
PostProcessorSignals ,
PostProcessorWorker ,
DownloadThread as BackendDownloadThread
)
print ( " Successfully imported names from downloader_utils. " )
except ImportError as e :
print ( f " --- IMPORT ERROR --- " )
print ( f " Failed to import from ' downloader_utils.py ' : { e } " )
# ... (rest of error handling as in your original file) ...
KNOWN_NAMES = [ ]
PostProcessorSignals = QObject
PostProcessorWorker = object
BackendDownloadThread = QThread
def clean_folder_name ( n ) : return str ( n ) # Fallback
def extract_post_info ( u ) : return None , None , None
def download_from_api ( * a , * * k ) : yield [ ]
except Exception as e :
print ( f " --- UNEXPECTED IMPORT ERROR --- " )
print ( f " An unexpected error occurred during import: { e } " )
traceback . print_exc ( )
print ( f " ----------------------------- " , file = sys . stderr )
sys . exit ( 1 )
# --- End Import ---
2025-05-05 19:35:24 +05:30
2025-05-09 19:03:01 +05:30
# --- Import Tour Dialog ---
try :
from tour import TourDialog
print ( " Successfully imported TourDialog from tour.py. " )
except ImportError as e :
print ( f " --- TOUR IMPORT ERROR --- " )
print ( f " Failed to import TourDialog from ' tour.py ' : { e } " )
print ( " Tour functionality will be unavailable. " )
TourDialog = None # Fallback if tour.py is missing
except Exception as e :
print ( f " --- UNEXPECTED TOUR IMPORT ERROR --- " )
print ( f " An unexpected error occurred during tour import: { e } " )
traceback . print_exc ( )
TourDialog = None
# --- End Tour Import ---
2025-05-08 22:13:12 +05:30
# --- Constants for Thread Limits ---
MAX_THREADS = 200 # Absolute maximum allowed by the input validator
RECOMMENDED_MAX_THREADS = 50 # Threshold for showing the informational warning
# --- END ---
2025-05-05 19:35:24 +05:30
2025-05-09 19:03:01 +05:30
# --- ADDED: Prefix for HTML messages in main log ---
HTML_PREFIX = " <!HTML!> " # Used to identify HTML lines for insertHtml
# --- END ADDED ---
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
overall_progress_signal = pyqtSignal ( int , int )
finished_signal = pyqtSignal ( int , int , bool )
2025-05-08 19:49:50 +05:30
# Signal now carries link_text (ensure this matches downloader_utils)
external_link_signal = pyqtSignal ( str , str , str , str ) # post_title, link_text, link_url, platform
file_progress_signal = pyqtSignal ( str , int , int )
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-08 19:49:50 +05:30
self . worker_signals = PostProcessorSignals ( ) # Instance of signals for multi-thread workers
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-08 19:49:50 +05:30
# self.external_links = [] # This list seems unused now
self . show_external_links = False
# --- For sequential delayed link display ---
self . external_link_queue = deque ( )
self . _is_processing_external_link_queue = False
2025-05-09 19:03:01 +05:30
self . _current_link_post_title = None # Track title for grouping
self . extracted_links_cache = [ ] # Store all links when in "Only Links" mode
2025-05-08 19:49:50 +05:30
# --- END ---
2025-05-09 19:03:01 +05:30
2025-05-08 22:13:12 +05:30
# --- For Log Verbosity ---
self . basic_log_mode = False # Start with full log (basic_log_mode is False)
2025-05-08 19:49:50 +05:30
self . log_verbosity_button = None
2025-05-08 22:13:12 +05:30
# --- END ---
2025-05-08 19:49:50 +05:30
self . main_log_output = None
self . external_log_output = None
self . log_splitter = None # This is the VERTICAL splitter for logs
self . main_splitter = None # This will be the main HORIZONTAL splitter
self . reset_button = None
2025-05-09 19:03:01 +05:30
self . progress_log_label = None # To change title
# --- For Link Search ---
self . link_search_input = None
self . link_search_button = None
# --- END ---
# --- For Export Links ---
self . export_links_button = None
# --- END ---
2025-05-08 19:49:50 +05:30
self . manga_mode_checkbox = None
2025-05-09 19:03:01 +05:30
self . radio_only_links = None # Define radio button attribute
2025-05-08 19:49:50 +05:30
self . load_known_names_from_util ( )
2025-05-08 22:13:12 +05:30
self . setWindowTitle ( " Kemono Downloader v2.9 (Manga Mode - No Skip Button) " )
2025-05-08 19:49:50 +05:30
self . setGeometry ( 150 , 150 , 1050 , 820 ) # Initial size
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-08 19:49:50 +05:30
self . log_signal . emit ( " ℹ ️ ' Skip Current File ' button has been removed. " )
self . character_input . setToolTip ( " Enter one or more character names, separated by commas (e.g., yor, makima) " )
2025-05-05 19:35:24 +05:30
2025-05-06 22:08:27 +05:30
def _connect_signals ( self ) :
2025-05-08 19:49:50 +05:30
# Signals from the worker_signals object (used by PostProcessorWorker in multi-threaded mode)
if hasattr ( self . worker_signals , ' progress_signal ' ) :
self . worker_signals . progress_signal . connect ( self . handle_main_log )
if hasattr ( self . worker_signals , ' file_progress_signal ' ) :
self . worker_signals . file_progress_signal . connect ( self . update_file_progress_display )
# Connect the external_link_signal from worker_signals to the queue handler
if hasattr ( self . worker_signals , ' external_link_signal ' ) :
2025-05-09 19:03:01 +05:30
self . worker_signals . external_link_signal . connect ( self . handle_external_link_signal )
2025-05-08 19:49:50 +05:30
# App's own signals (some of which might be emitted by DownloadThread which then connects to these handlers)
self . log_signal . connect ( self . handle_main_log )
2025-05-06 22:08:27 +05:30
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-08 19:49:50 +05:30
# Connect the app's external_link_signal also to the queue handler
2025-05-09 19:03:01 +05:30
self . external_link_signal . connect ( self . handle_external_link_signal )
2025-05-08 19:49:50 +05:30
self . file_progress_signal . connect ( self . update_file_progress_display )
2025-05-06 22:49:19 +05:30
self . character_search_input . textChanged . connect ( self . filter_character_list )
2025-05-08 19:49:50 +05:30
self . external_links_checkbox . toggled . connect ( self . update_external_links_setting )
self . thread_count_input . textChanged . connect ( self . update_multithreading_label )
self . use_subfolder_per_post_checkbox . toggled . connect ( self . update_ui_for_subfolders )
2025-05-08 22:13:12 +05:30
# --- MODIFIED: Connect multithreading checkbox toggle ---
self . use_multithreading_checkbox . toggled . connect ( self . _handle_multithreading_toggle )
# --- END MODIFIED ---
2025-05-09 19:03:01 +05:30
# --- MODIFIED: Connect radio group toggle ---
if self . radio_group :
self . radio_group . buttonToggled . connect ( self . _handle_filter_mode_change ) # Use buttonToggled for group signal
# --- END MODIFIED ---
2025-05-08 19:49:50 +05:30
if self . reset_button :
self . reset_button . clicked . connect ( self . reset_application_state )
2025-05-09 19:03:01 +05:30
2025-05-08 19:49:50 +05:30
# Connect log verbosity button if it exists
if self . log_verbosity_button :
self . log_verbosity_button . clicked . connect ( self . toggle_log_verbosity )
2025-05-09 19:03:01 +05:30
# --- ADDED: Connect link search elements ---
if self . link_search_button :
self . link_search_button . clicked . connect ( self . _filter_links_log )
if self . link_search_input :
self . link_search_input . returnPressed . connect ( self . _filter_links_log )
self . link_search_input . textChanged . connect ( self . _filter_links_log ) # Real-time filtering
# --- END ADDED ---
# --- ADDED: Connect export links button ---
if self . export_links_button :
self . export_links_button . clicked . connect ( self . _export_links_to_file )
# --- END ADDED ---
2025-05-08 19:49:50 +05:30
if self . manga_mode_checkbox :
self . manga_mode_checkbox . toggled . connect ( self . update_ui_for_manga_mode )
self . link_input . textChanged . connect ( lambda : self . update_ui_for_manga_mode ( self . manga_mode_checkbox . isChecked ( ) if self . manga_mode_checkbox else False ) )
# --- load_known_names_from_util, save_known_names, closeEvent ---
# These methods remain unchanged from your original file
2025-05-07 07:20:40 +05:30
def load_known_names_from_util ( self ) :
2025-05-08 19:49:50 +05:30
global KNOWN_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 ]
2025-05-08 19:49:50 +05:30
# Filter out empty strings before setting KNOWN_NAMES
KNOWN_NAMES [ : ] = sorted ( list ( set ( filter ( None , raw_names ) ) ) )
log_msg = f " ℹ ️ Loaded { len ( KNOWN_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-08 19:49:50 +05:30
KNOWN_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. "
2025-05-08 19:49:50 +05:30
KNOWN_NAMES [ : ] = [ ]
self . log_signal . emit ( log_msg )
if hasattr ( self , ' character_list ' ) : # Ensure character_list widget exists
2025-05-07 07:20:40 +05:30
self . character_list . clear ( )
2025-05-08 19:49:50 +05:30
self . character_list . addItems ( 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-08 19:49:50 +05:30
global KNOWN_NAMES
2025-05-05 19:35:24 +05:30
try :
2025-05-08 19:49:50 +05:30
# Ensure KNOWN_NAMES contains unique, non-empty, sorted strings
unique_sorted_names = sorted ( list ( set ( filter ( None , KNOWN_NAMES ) ) ) )
KNOWN_NAMES [ : ] = unique_sorted_names # Update global list in place
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-08 19:49:50 +05:30
self . log_signal . emit ( f " 💾 Saved { len ( unique_sorted_names ) } known 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 } "
2025-05-08 19:49:50 +05:30
self . log_signal . emit ( log_msg )
2025-05-06 22:08:27 +05:30
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
2025-05-08 19:49:50 +05:30
is_downloading = ( self . download_thread and self . download_thread . isRunning ( ) ) or \
( self . thread_pool is not None and any ( not f . done ( ) for f in self . active_futures if f is not None ) )
2025-05-06 22:08:27 +05:30
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-08 19:49:50 +05:30
self . cancel_download ( ) # Signal cancellation
# --- MODIFICATION START: Wait for threads to finish ---
self . log_signal . emit ( " Waiting briefly for threads to acknowledge cancellation... " )
# Wait for the single thread if it exists
if self . download_thread and self . download_thread . isRunning ( ) :
self . download_thread . wait ( 3000 ) # Wait up to 3 seconds
if self . download_thread . isRunning ( ) :
self . log_signal . emit ( " ⚠️ Single download thread did not terminate gracefully. " )
# Wait for the thread pool if it exists
if self . thread_pool :
# Shutdown was already initiated by cancel_download, just wait here
# Use wait=True here for cleaner exit
self . thread_pool . shutdown ( wait = True , cancel_futures = True )
self . log_signal . emit ( " Thread pool shutdown complete. " )
self . thread_pool = None # Clear reference
# --- MODIFICATION END ---
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-08 19:49:50 +05:30
# Ensure thread pool is None if already shut down above
if self . thread_pool :
self . log_signal . emit ( " Final thread pool check: Shutting down... " )
self . cancellation_event . set ( )
self . thread_pool . shutdown ( wait = True , cancel_futures = True )
self . thread_pool = None
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 ) :
2025-05-08 19:49:50 +05:30
# --- MODIFIED: Use QSplitter for main layout ---
self . main_splitter = QSplitter ( Qt . Horizontal )
# Create container widgets for left and right panels
left_panel_widget = QWidget ( )
right_panel_widget = QWidget ( )
# Setup layouts for the panels
left_layout = QVBoxLayout ( left_panel_widget ) # Apply layout to widget
right_layout = QVBoxLayout ( right_panel_widget ) # Apply layout to widget
left_layout . setContentsMargins ( 10 , 10 , 10 , 10 ) # Add some margins
right_layout . setContentsMargins ( 10 , 10 , 10 , 10 )
# --- Populate Left Panel (Controls) ---
# (All the QLineEdit, QCheckBox, QPushButton, etc. setup code goes here, adding to left_layout)
# URL and Page Range Input
url_page_layout = QHBoxLayout ( )
url_page_layout . setContentsMargins ( 0 , 0 , 0 , 0 )
url_page_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-08 19:49:50 +05:30
# self.link_input.setFixedWidth(int(self.width() * 0.45)) # Remove fixed width for splitter
url_page_layout . addWidget ( self . link_input , 1 ) # Give it stretch factor
self . page_range_label = QLabel ( " Page Range: " )
self . page_range_label . setStyleSheet ( " font-weight: bold; padding-left: 10px; " )
self . start_page_input = QLineEdit ( )
self . start_page_input . setPlaceholderText ( " Start " )
self . start_page_input . setFixedWidth ( 50 )
self . start_page_input . setValidator ( QIntValidator ( 1 , 99999 ) ) # Min 1
self . to_label = QLabel ( " to " )
self . end_page_input = QLineEdit ( )
self . end_page_input . setPlaceholderText ( " End " )
self . end_page_input . setFixedWidth ( 50 )
self . end_page_input . setValidator ( QIntValidator ( 1 , 99999 ) ) # Min 1
url_page_layout . addWidget ( self . page_range_label )
url_page_layout . addWidget ( self . start_page_input )
url_page_layout . addWidget ( self . to_label )
url_page_layout . addWidget ( self . end_page_input )
# url_page_layout.addStretch(1) # No need for stretch with splitter
left_layout . addLayout ( url_page_layout )
# Download Directory 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-08 19:49:50 +05:30
dir_layout . addWidget ( self . dir_input , 1 ) # Input takes more space
2025-05-05 19:35:24 +05:30
dir_layout . addWidget ( self . dir_button )
left_layout . addLayout ( dir_layout )
2025-05-08 19:49:50 +05:30
# Custom Folder Name (for single post)
self . custom_folder_widget = QWidget ( ) # Use a widget to hide/show group
2025-05-06 22:08:27 +05:30
custom_folder_layout = QVBoxLayout ( self . custom_folder_widget )
2025-05-08 19:49:50 +05:30
custom_folder_layout . setContentsMargins ( 0 , 5 , 0 , 0 ) # No top margin if hidden
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-08 19:49:50 +05:30
self . custom_folder_widget . setVisible ( False ) # Initially hidden
2025-05-06 22:08:27 +05:30
left_layout . addWidget ( self . custom_folder_widget )
2025-05-09 19:03:01 +05:30
2025-05-08 19:49:50 +05:30
# Character Filter Input
2025-05-06 22:08:27 +05:30
self . character_filter_widget = QWidget ( )
character_filter_layout = QVBoxLayout ( self . character_filter_widget )
2025-05-08 19:49:50 +05:30
character_filter_layout . setContentsMargins ( 0 , 5 , 0 , 0 )
self . character_label = QLabel ( " 🎯 Filter by Character(s) (comma-separated): " )
2025-05-05 19:35:24 +05:30
self . character_input = QLineEdit ( )
2025-05-09 19:03:01 +05:30
self . character_input . setPlaceholderText ( " e.g., yor, Tifa, Reyna " )
2025-05-06 22:08:27 +05:30
character_filter_layout . addWidget ( self . character_label )
character_filter_layout . addWidget ( self . character_input )
2025-05-08 19:49:50 +05:30
self . character_filter_widget . setVisible ( True ) # Visible by default
2025-05-06 22:08:27 +05:30
left_layout . addWidget ( self . character_filter_widget )
2025-05-08 19:49:50 +05:30
# Skip Words Input
2025-05-06 22:08:27 +05:30
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 )
2025-05-08 19:49:50 +05:30
2025-05-09 19:03:01 +05:30
# --- MODIFIED: File Type Filter Radio Buttons ---
2025-05-08 19:49:50 +05:30
file_filter_layout = QVBoxLayout ( ) # Group label and radio buttons
file_filter_layout . setContentsMargins ( 0 , 0 , 0 , 0 ) # Compact
file_filter_layout . addWidget ( QLabel ( " Filter Files: " ) )
radio_button_layout = QHBoxLayout ( )
radio_button_layout . setSpacing ( 10 )
self . radio_group = QButtonGroup ( self ) # Ensures one selection
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-09 19:03:01 +05:30
self . radio_only_links = QRadioButton ( " 🔗 Only Links " ) # New button
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-09 19:03:01 +05:30
self . radio_group . addButton ( self . radio_only_links ) # Add to group
2025-05-08 19:49:50 +05:30
radio_button_layout . addWidget ( self . radio_all )
radio_button_layout . addWidget ( self . radio_images )
radio_button_layout . addWidget ( self . radio_videos )
2025-05-09 19:03:01 +05:30
radio_button_layout . addWidget ( self . radio_only_links ) # Add to layout
2025-05-08 19:49:50 +05:30
radio_button_layout . addStretch ( 1 ) # Pushes buttons to left
file_filter_layout . addLayout ( radio_button_layout )
left_layout . addLayout ( file_filter_layout )
2025-05-09 19:03:01 +05:30
# --- END MODIFIED ---
2025-05-08 19:49:50 +05:30
# Checkboxes Group
checkboxes_group_layout = QVBoxLayout ( )
checkboxes_group_layout . setSpacing ( 10 ) # Spacing between rows of checkboxes
row1_layout = QHBoxLayout ( ) # First row of checkboxes
row1_layout . setSpacing ( 10 )
2025-05-06 22:08:27 +05:30
self . skip_zip_checkbox = QCheckBox ( " Skip .zip " )
2025-05-05 19:35:24 +05:30
self . skip_zip_checkbox . setChecked ( True )
2025-05-08 19:49:50 +05:30
row1_layout . addWidget ( self . skip_zip_checkbox )
2025-05-06 22:08:27 +05:30
self . skip_rar_checkbox = QCheckBox ( " Skip .rar " )
2025-05-05 19:35:24 +05:30
self . skip_rar_checkbox . setChecked ( True )
2025-05-08 19:49:50 +05:30
row1_layout . addWidget ( self . skip_rar_checkbox )
self . download_thumbnails_checkbox = QCheckBox ( " Download Thumbnails Only " )
self . download_thumbnails_checkbox . setChecked ( False )
self . download_thumbnails_checkbox . setToolTip ( " Thumbnail download functionality is currently limited without the API. " )
row1_layout . addWidget ( self . download_thumbnails_checkbox )
2025-05-06 22:08:27 +05:30
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). " )
2025-05-08 19:49:50 +05:30
row1_layout . addWidget ( self . compress_images_checkbox )
row1_layout . addStretch ( 1 ) # Pushes checkboxes to left
checkboxes_group_layout . addLayout ( row1_layout )
2025-05-09 19:03:01 +05:30
2025-05-08 19:49:50 +05:30
# Advanced Settings Section
advanced_settings_label = QLabel ( " ⚙️ Advanced Settings: " )
checkboxes_group_layout . addWidget ( advanced_settings_label )
advanced_row1_layout = QHBoxLayout ( ) # Subfolders options
advanced_row1_layout . setSpacing ( 10 )
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 )
advanced_row1_layout . addWidget ( self . use_subfolders_checkbox )
self . use_subfolder_per_post_checkbox = QCheckBox ( " Subfolder per Post " )
self . use_subfolder_per_post_checkbox . setChecked ( False )
self . use_subfolder_per_post_checkbox . setToolTip ( " Creates a subfolder for each post inside the character/title folder. " )
self . use_subfolder_per_post_checkbox . toggled . connect ( self . update_ui_for_subfolders ) # Also update UI
advanced_row1_layout . addWidget ( self . use_subfolder_per_post_checkbox )
advanced_row1_layout . addStretch ( 1 )
checkboxes_group_layout . addLayout ( advanced_row1_layout )
advanced_row2_layout = QHBoxLayout ( ) # Multithreading, External Links, Manga Mode
advanced_row2_layout . setSpacing ( 10 )
multithreading_layout = QHBoxLayout ( ) # Group multithreading checkbox and input
multithreading_layout . setContentsMargins ( 0 , 0 , 0 , 0 )
self . use_multithreading_checkbox = QCheckBox ( " Use Multithreading " )
2025-05-06 22:49:19 +05:30
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. \n Single post URLs always use one thread. " )
2025-05-08 19:49:50 +05:30
multithreading_layout . addWidget ( self . use_multithreading_checkbox )
self . thread_count_label = QLabel ( " Threads: " )
multithreading_layout . addWidget ( self . thread_count_label )
self . thread_count_input = QLineEdit ( )
self . thread_count_input . setFixedWidth ( 40 )
self . thread_count_input . setText ( " 4 " )
2025-05-08 22:13:12 +05:30
# --- MODIFIED: Updated tooltip to remove recommendation ---
self . thread_count_input . setToolTip ( f " Number of threads (max: { MAX_THREADS } ). " )
# --- END MODIFIED ---
self . thread_count_input . setValidator ( QIntValidator ( 1 , MAX_THREADS ) ) # Use constant
2025-05-08 19:49:50 +05:30
multithreading_layout . addWidget ( self . thread_count_input )
advanced_row2_layout . addLayout ( multithreading_layout )
self . external_links_checkbox = QCheckBox ( " Show External Links in Log " )
self . external_links_checkbox . setChecked ( False )
advanced_row2_layout . addWidget ( self . external_links_checkbox )
2025-05-09 19:03:01 +05:30
self . manga_mode_checkbox = QCheckBox ( " Manga/Comic Mode " )
2025-05-08 19:49:50 +05:30
self . manga_mode_checkbox . setToolTip ( " Process newest posts first, rename files based on post title (for creator feeds only). " )
self . manga_mode_checkbox . setChecked ( False )
advanced_row2_layout . addWidget ( self . manga_mode_checkbox )
advanced_row2_layout . addStretch ( 1 )
checkboxes_group_layout . addLayout ( advanced_row2_layout )
2025-05-09 19:03:01 +05:30
2025-05-08 19:49:50 +05:30
left_layout . addLayout ( checkboxes_group_layout )
# Download and Cancel Buttons
2025-05-05 19:35:24 +05:30
btn_layout = QHBoxLayout ( )
2025-05-08 19:49:50 +05:30
btn_layout . setSpacing ( 10 )
2025-05-05 19:35:24 +05:30
self . download_btn = QPushButton ( " ⬇️ Start Download " )
2025-05-08 19:49:50 +05:30
self . download_btn . setStyleSheet ( " padding: 8px 15px; font-weight: bold; " ) # Make it prominent
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-08 19:49:50 +05:30
self . cancel_btn . setEnabled ( False ) # Initially disabled
2025-05-06 22:08:27 +05:30
self . cancel_btn . clicked . connect ( self . cancel_download )
2025-05-05 19:35:24 +05:30
btn_layout . addWidget ( self . download_btn )
btn_layout . addWidget ( self . cancel_btn )
left_layout . addLayout ( btn_layout )
2025-05-08 19:49:50 +05:30
left_layout . addSpacing ( 10 ) # Some space before known characters list
# Known Characters/Shows List Management
2025-05-06 22:08:27 +05:30
known_chars_label_layout = QHBoxLayout ( )
2025-05-08 19:49:50 +05:30
known_chars_label_layout . setSpacing ( 10 )
2025-05-06 22:08:27 +05:30
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... " )
2025-05-08 19:49:50 +05:30
known_chars_label_layout . addWidget ( self . known_chars_label , 1 ) # Label takes more space
2025-05-06 22:49:19 +05:30
known_chars_label_layout . addWidget ( self . character_search_input )
left_layout . addLayout ( known_chars_label_layout )
2025-05-09 19:03:01 +05:30
2025-05-05 19:35:24 +05:30
self . character_list = QListWidget ( )
2025-05-08 19:49:50 +05:30
self . character_list . setSelectionMode ( QListWidget . ExtendedSelection ) # Allow multi-select for delete
left_layout . addWidget ( self . character_list , 1 ) # Takes remaining vertical space
char_manage_layout = QHBoxLayout ( ) # Add/Delete character buttons
char_manage_layout . setSpacing ( 10 )
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 )
2025-05-08 19:49:50 +05:30
self . new_char_input . returnPressed . connect ( self . add_char_button . click ) # Add on Enter
2025-05-05 19:35:24 +05:30
self . delete_char_button . clicked . connect ( self . delete_selected_character )
2025-05-08 19:49:50 +05:30
char_manage_layout . addWidget ( self . new_char_input , 2 ) # Input field wider
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 )
2025-05-08 19:49:50 +05:30
left_layout . addStretch ( 0 ) # Prevent vertical stretching of controls
# --- Populate Right Panel (Logs) ---
log_title_layout = QHBoxLayout ( )
2025-05-09 19:03:01 +05:30
self . progress_log_label = QLabel ( " 📜 Progress Log: " ) # Store label reference
log_title_layout . addWidget ( self . progress_log_label )
2025-05-08 19:49:50 +05:30
log_title_layout . addStretch ( 1 )
2025-05-09 19:03:01 +05:30
# --- ADDED: Link Search Bar ---
self . link_search_input = QLineEdit ( )
self . link_search_input . setPlaceholderText ( " Search Links... " )
self . link_search_input . setVisible ( False ) # Initially hidden
self . link_search_input . setFixedWidth ( 150 ) # Adjust width
log_title_layout . addWidget ( self . link_search_input )
self . link_search_button = QPushButton ( " 🔍 " )
self . link_search_button . setToolTip ( " Filter displayed links " )
self . link_search_button . setVisible ( False ) # Initially hidden
self . link_search_button . setFixedWidth ( 30 )
self . link_search_button . setStyleSheet ( " padding: 4px 4px; " )
log_title_layout . addWidget ( self . link_search_button )
# --- END ADDED ---
2025-05-08 19:49:50 +05:30
# --- ADDED: Log Verbosity Button ---
2025-05-08 22:13:12 +05:30
self . log_verbosity_button = QPushButton ( " Show Basic Log " ) # Default text
2025-05-08 19:49:50 +05:30
self . log_verbosity_button . setToolTip ( " Toggle between full and basic log details. " )
self . log_verbosity_button . setFixedWidth ( 110 ) # Adjust width as needed
self . log_verbosity_button . setStyleSheet ( " padding: 4px 8px; " )
log_title_layout . addWidget ( self . log_verbosity_button )
# --- END ADDED ---
2025-05-09 19:03:01 +05:30
2025-05-08 19:49:50 +05:30
self . reset_button = QPushButton ( " 🔄 Reset " )
self . reset_button . setToolTip ( " Reset all inputs and logs to default state (only when idle). " )
self . reset_button . setFixedWidth ( 80 )
self . reset_button . setStyleSheet ( " padding: 4px 8px; " ) # Smaller padding
log_title_layout . addWidget ( self . reset_button )
right_layout . addLayout ( log_title_layout )
self . log_splitter = QSplitter ( Qt . Vertical ) # Keep the vertical splitter for logs
self . main_log_output = QTextEdit ( )
self . main_log_output . setReadOnly ( True )
# self.main_log_output.setMinimumWidth(450) # Remove minimum width
self . main_log_output . setLineWrapMode ( QTextEdit . NoWrap ) # Disable line wrapping
self . main_log_output . setStyleSheet ( """
2025-05-09 19:03:01 +05:30
QTextEdit {
background - color : #3C3F41; border: 1px solid #5A5A5A; padding: 5px;
2025-05-08 19:49:50 +05:30
color : #F0F0F0; border-radius: 4px; font-family: Consolas, Courier New, monospace; font-size: 9.5pt;
} """ )
self . external_log_output = QTextEdit ( )
self . external_log_output . setReadOnly ( True )
# self.external_log_output.setMinimumWidth(450) # Remove minimum width
self . external_log_output . setLineWrapMode ( QTextEdit . NoWrap ) # Disable line wrapping
self . external_log_output . setStyleSheet ( """
2025-05-09 19:03:01 +05:30
QTextEdit {
background - color : #3C3F41; border: 1px solid #5A5A5A; padding: 5px;
2025-05-08 19:49:50 +05:30
color : #F0F0F0; border-radius: 4px; font-family: Consolas, Courier New, monospace; font-size: 9.5pt;
} """ )
self . external_log_output . hide ( ) # Initially hidden
self . log_splitter . addWidget ( self . main_log_output )
self . log_splitter . addWidget ( self . external_log_output )
self . log_splitter . setSizes ( [ self . height ( ) , 0 ] ) # Main log takes all space initially
right_layout . addWidget ( self . log_splitter , 1 ) # Log splitter takes available vertical space
2025-05-09 19:03:01 +05:30
# --- ADDED: Export Links Button ---
export_button_layout = QHBoxLayout ( )
export_button_layout . addStretch ( 1 ) # Push button to the right
self . export_links_button = QPushButton ( " Export Links " )
self . export_links_button . setToolTip ( " Export all extracted links to a .txt file. " )
self . export_links_button . setFixedWidth ( 100 )
self . export_links_button . setStyleSheet ( " padding: 4px 8px; margin-top: 5px; " )
self . export_links_button . setEnabled ( False ) # Initially disabled
self . export_links_button . setVisible ( False ) # Initially hidden
export_button_layout . addWidget ( self . export_links_button )
right_layout . addLayout ( export_button_layout ) # Add to bottom of right panel
# --- END ADDED ---
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-08 19:49:50 +05:30
self . file_progress_label = QLabel ( " " ) # For individual file progress
self . file_progress_label . setWordWrap ( True ) # Enable word wrapping for the status label
self . file_progress_label . setStyleSheet ( " padding-top: 2px; font-style: italic; color: #A0A0A0; " )
right_layout . addWidget ( self . file_progress_label )
# --- Add panels to the main horizontal splitter ---
self . main_splitter . addWidget ( left_panel_widget )
self . main_splitter . addWidget ( right_panel_widget )
# --- Set initial sizes for the splitter ---
2025-05-08 22:13:12 +05:30
# Calculate initial sizes (e.g., left 30%, right 70%)
2025-05-08 19:49:50 +05:30
initial_width = self . width ( ) # Use the initial window width
2025-05-09 19:03:01 +05:30
left_width = int ( initial_width * 0.30 )
2025-05-08 19:49:50 +05:30
right_width = initial_width - left_width
self . main_splitter . setSizes ( [ left_width , right_width ] )
2025-05-09 19:03:01 +05:30
2025-05-08 19:49:50 +05:30
# --- Set the main splitter as the central layout ---
# Need a top-level layout to hold the splitter
top_level_layout = QHBoxLayout ( self ) # Apply layout directly to the main widget (self)
top_level_layout . setContentsMargins ( 0 , 0 , 0 , 0 ) # No margins for the main layout
top_level_layout . addWidget ( self . main_splitter )
# self.setLayout(top_level_layout) # Already set above
# --- End Layout Modification ---
# Initial UI state updates
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-08 19:49:50 +05:30
self . update_external_links_setting ( self . external_links_checkbox . isChecked ( ) )
self . update_multithreading_label ( self . thread_count_input . text ( ) )
self . update_page_range_enabled_state ( )
if self . manga_mode_checkbox : # Ensure it exists before accessing
self . update_ui_for_manga_mode ( self . manga_mode_checkbox . isChecked ( ) )
self . link_input . textChanged . connect ( self . update_page_range_enabled_state ) # Connect after init
self . load_known_names_from_util ( ) # Load names into the list widget
2025-05-08 22:13:12 +05:30
self . _handle_multithreading_toggle ( self . use_multithreading_checkbox . isChecked ( ) ) # Set initial state
2025-05-09 19:03:01 +05:30
self . _handle_filter_mode_change ( self . radio_group . checkedButton ( ) , True ) # Set initial filter mode UI state
2025-05-06 22:08:27 +05:30
2025-05-05 19:35:24 +05:30
def get_dark_theme ( self ) :
return """
2025-05-08 19:49:50 +05:30
QWidget { background - color : #2E2E2E; color: #E0E0E0; font-family: Segoe UI, Arial, sans-serif; font-size: 10pt; }
QLineEdit , QListWidget { background - color : #3C3F41; border: 1px solid #5A5A5A; padding: 5px; color: #F0F0F0; border-radius: 4px; }
QTextEdit { background - color : #3C3F41; border: 1px solid #5A5A5A; padding: 5px; color: #F0F0F0; border-radius: 4px; }
QPushButton { background - color : #555; color: #F0F0F0; border: 1px solid #6A6A6A; padding: 6px 12px; border-radius: 4px; min-height: 22px; }
QPushButton : hover { background - color : #656565; border: 1px solid #7A7A7A; }
QPushButton : pressed { background - color : #4A4A4A; }
QPushButton : disabled { background - color : #404040; color: #888; border-color: #555; }
QLabel { font - weight : bold ; padding - top : 4 px ; padding - bottom : 2 px ; color : #C0C0C0; }
QRadioButton , QCheckBox { spacing : 5 px ; color : #E0E0E0; padding-top: 4px; padding-bottom: 4px; }
QRadioButton : : indicator , QCheckBox : : indicator { width : 14 px ; height : 14 px ; }
QListWidget { alternate - background - color : #353535; border: 1px solid #5A5A5A; }
QListWidget : : item : selected { background - color : #007ACC; color: #FFFFFF; }
QToolTip { background - color : #4A4A4A; color: #F0F0F0; border: 1px solid #6A6A6A; padding: 4px; border-radius: 3px; }
2025-05-09 19:03:01 +05:30
QSplitter : : handle { background - color : #5A5A5A; width: 5px; /* Make handle slightly wider */ }
2025-05-08 19:49:50 +05:30
QSplitter : : handle : horizontal { width : 5 px ; }
QSplitter : : handle : vertical { height : 5 px ; }
""" # Added styling for splitter handle
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 )
2025-05-08 19:49:50 +05:30
def handle_main_log ( self , message ) :
2025-05-09 19:03:01 +05:30
# --- MODIFIED: Check for HTML_PREFIX ---
is_html_message = message . startswith ( HTML_PREFIX )
if is_html_message :
# If it's HTML, strip the prefix and use insertHtml
display_message = message [ len ( HTML_PREFIX ) : ]
use_html = True
elif self . basic_log_mode : # Apply basic filtering only if NOT HTML
2025-05-08 19:49:50 +05:30
# Define keywords/prefixes for messages to ALWAYS show in basic mode
basic_keywords = [
2025-05-08 22:13:12 +05:30
' 🚀 starting download ' , ' 🏁 download finished ' , ' 🏁 download cancelled ' ,
' ❌ ' , ' ⚠️ ' , ' ✅ all posts processed ' , ' ✅ reached end of posts ' ,
' summary: ' , ' progress: ' , ' [fetcher] ' , # Show fetcher logs for context
' critical error ' , ' import error ' , ' error ' , ' fail ' , ' timeout ' ,
' unsupported url ' , ' invalid url ' , ' no posts found ' , ' could not create directory ' ,
' missing dependency ' , ' high thread count ' , ' manga mode filter warning ' ,
' duplicate name ' , ' potential name conflict ' , ' invalid filter name ' ,
' no valid character filters '
2025-05-08 19:49:50 +05:30
]
2025-05-08 22:13:12 +05:30
message_lower = message . lower ( )
if not any ( keyword in message_lower for keyword in basic_keywords ) :
if not message . strip ( ) . startswith ( " ✅ Saved: " ) and \
not message . strip ( ) . startswith ( " ✅ Added " ) and \
not message . strip ( ) . startswith ( " ✅ Application reset complete " ) :
return # Skip appending less important messages in basic mode
2025-05-09 19:03:01 +05:30
display_message = message # Use original message if it passes basic filter
use_html = False
else : # Full log mode and not HTML
display_message = message
use_html = False
# --- END MODIFIED ---
2025-05-06 22:08:27 +05:30
try :
2025-05-08 19:49:50 +05:30
# Ensure message is a string and replace null characters that can crash QTextEdit
2025-05-09 19:03:01 +05:30
safe_message = str ( display_message ) . replace ( ' \x00 ' , ' [NULL] ' )
if use_html :
self . main_log_output . insertHtml ( safe_message ) # Use insertHtml for formatted titles
else :
self . main_log_output . append ( safe_message ) # Use append for plain text
2025-05-08 19:49:50 +05:30
# Auto-scroll if near the bottom
scrollbar = self . main_log_output . verticalScrollBar ( )
if scrollbar . value ( ) > = scrollbar . maximum ( ) - 30 : # Threshold for auto-scroll
2025-05-06 22:08:27 +05:30
scrollbar . setValue ( scrollbar . maximum ( ) )
except Exception as e :
2025-05-08 19:49:50 +05:30
# Fallback logging if GUI logging fails
print ( f " GUI Main Log Error: { e } \n Original Message: { message } " )
# --- ADDED: Helper to check download state ---
def _is_download_active ( self ) :
""" Checks if a download thread or pool is currently active. """
single_thread_active = self . download_thread and self . download_thread . isRunning ( )
# Check if pool exists AND has any futures that are not done
pool_active = self . thread_pool is not None and any ( not f . done ( ) for f in self . active_futures if f is not None )
return single_thread_active or pool_active
# --- END ADDED ---
# --- ADDED: New system for handling external links with sequential CONDIITONAL delay ---
# MODIFIED: Slot now takes link_text as the second argument
def handle_external_link_signal ( self , post_title , link_text , link_url , platform ) :
""" Receives link signals, adds them to a queue, and triggers processing. """
2025-05-09 19:03:01 +05:30
link_data = ( post_title , link_text , link_url , platform )
self . external_link_queue . append ( link_data )
# --- ADDED: Cache link if in "Only Links" mode ---
if self . radio_only_links and self . radio_only_links . isChecked ( ) :
self . extracted_links_cache . append ( link_data )
# --- END ADDED ---
2025-05-08 19:49:50 +05:30
self . _try_process_next_external_link ( )
def _try_process_next_external_link ( self ) :
""" Processes the next link from the queue if not already processing. """
2025-05-09 19:03:01 +05:30
if self . _is_processing_external_link_queue or not self . external_link_queue :
return # Don't process if busy or queue empty
# Determine if we should display based on mode and checkbox state
is_only_links_mode = self . radio_only_links and self . radio_only_links . isChecked ( )
should_display_in_external = self . show_external_links and not is_only_links_mode
# Only proceed if displaying in *either* log is currently possible/enabled
if not ( is_only_links_mode or should_display_in_external ) :
# If neither log is active/visible for this link, still need to allow queue processing
self . _is_processing_external_link_queue = False
if self . external_link_queue :
QTimer . singleShot ( 0 , self . _try_process_next_external_link )
return
2025-05-08 19:49:50 +05:30
self . _is_processing_external_link_queue = True
2025-05-09 19:03:01 +05:30
link_data = self . external_link_queue . popleft ( )
# --- MODIFIED: Schedule the display AND the next step based on mode ---
if is_only_links_mode :
# Schedule with fixed 0.4s delay for "Only Links" mode
delay_ms = 80 # 0.08 seconds
QTimer . singleShot ( delay_ms , lambda data = link_data : self . _display_and_schedule_next ( data ) )
elif self . _is_download_active ( ) :
# Schedule with random delay for other modes during download
delay_ms = random . randint ( 4000 , 8000 )
QTimer . singleShot ( delay_ms , lambda data = link_data : self . _display_and_schedule_next ( data ) )
2025-05-08 19:49:50 +05:30
else :
2025-05-09 19:03:01 +05:30
# No download active in other modes, process immediately
QTimer . singleShot ( 0 , lambda data = link_data : self . _display_and_schedule_next ( data ) )
2025-05-08 19:49:50 +05:30
# --- END MODIFIED ---
2025-05-09 19:03:01 +05:30
# --- NEW Method ---
def _display_and_schedule_next ( self , link_data ) :
""" Displays the link in the correct log and schedules the check for the next link. """
post_title , link_text , link_url , platform = link_data # Unpack all data
is_only_links_mode = self . radio_only_links and self . radio_only_links . isChecked ( )
# Format the link text part
max_link_text_len = 35
display_text = link_text [ : max_link_text_len ] . strip ( ) + " ... " if len ( link_text ) > max_link_text_len else link_text
formatted_link_info = f " { display_text } - { link_url } - { platform } "
separator = " - " * 45
if is_only_links_mode :
# Check if the post title has changed
if post_title != self . _current_link_post_title :
# Emit separator and new title (formatted as HTML)
self . log_signal . emit ( HTML_PREFIX + " <br> " + separator + " <br> " )
# Use HTML for bold blue title
title_html = f ' <b style= " color: #87CEEB; " > { post_title } </b><br> '
self . log_signal . emit ( HTML_PREFIX + title_html )
self . _current_link_post_title = post_title # Update current title
# Emit the link info as plain text (handle_main_log will append it)
self . log_signal . emit ( formatted_link_info )
elif self . show_external_links :
# Append directly to external log (plain text)
self . _append_to_external_log ( formatted_link_info , separator )
# Allow the next link to be processed
2025-05-08 19:49:50 +05:30
self . _is_processing_external_link_queue = False
2025-05-09 19:03:01 +05:30
self . _try_process_next_external_link ( ) # Check queue again
# --- END NEW Method ---
2025-05-08 19:49:50 +05:30
2025-05-09 19:03:01 +05:30
# --- RENAMED and MODIFIED: Appends ONLY to external log ---
def _append_to_external_log ( self , formatted_link_text , separator ) :
2025-05-08 19:49:50 +05:30
""" Appends a single formatted link to the external_log_output widget. """
2025-05-09 19:03:01 +05:30
# Visibility check is done before calling this now
if not ( self . external_log_output and self . external_log_output . isVisible ( ) ) :
2025-05-08 19:49:50 +05:30
return
try :
2025-05-09 19:03:01 +05:30
self . external_log_output . append ( separator )
2025-05-08 19:49:50 +05:30
self . external_log_output . append ( formatted_link_text )
self . external_log_output . append ( " " ) # Add a blank line for spacing
# Auto-scroll
scrollbar = self . external_log_output . verticalScrollBar ( )
if scrollbar . value ( ) > = scrollbar . maximum ( ) - 50 : # Adjust threshold if needed
scrollbar . setValue ( scrollbar . maximum ( ) )
except Exception as e :
2025-05-09 19:03:01 +05:30
# Log errors related to external log to the main log
2025-05-08 19:49:50 +05:30
self . log_signal . emit ( f " GUI External Log Append Error: { e } \n Original Message: { formatted_link_text } " )
print ( f " GUI External Log Error (Append): { e } \n Original Message: { formatted_link_text } " )
2025-05-09 19:03:01 +05:30
# --- END MODIFIED ---
2025-05-06 22:08:27 +05:30
2025-05-05 19:35:24 +05:30
2025-05-08 19:49:50 +05:30
def update_file_progress_display ( self , filename , downloaded_bytes , total_bytes ) :
if not filename and total_bytes == 0 and downloaded_bytes == 0 : # Clear signal
self . file_progress_label . setText ( " " )
return
# MODIFIED: Truncate filename more aggressively (e.g., max 25 chars)
2025-05-09 19:03:01 +05:30
max_filename_len = 25
display_filename = filename [ : max_filename_len - 3 ] . strip ( ) + " ... " if len ( filename ) > max_filename_len else filename
2025-05-08 19:49:50 +05:30
if total_bytes > 0 :
downloaded_mb = downloaded_bytes / ( 1024 * 1024 )
total_mb = total_bytes / ( 1024 * 1024 )
progress_text = f " Downloading ' { display_filename } ' ( { downloaded_mb : .1f } MB / { total_mb : .1f } MB) "
else : # If total size is unknown
downloaded_mb = downloaded_bytes / ( 1024 * 1024 )
progress_text = f " Downloading ' { display_filename } ' ( { downloaded_mb : .1f } MB) "
2025-05-09 19:03:01 +05:30
2025-05-08 19:49:50 +05:30
# Check if the resulting text might still be too long (heuristic)
# This is a basic check, might need refinement based on typical log width
if len ( progress_text ) > 75 : # Example threshold, adjust as needed
# If still too long, truncate the display_filename even more
display_filename = filename [ : 15 ] . strip ( ) + " ... " if len ( filename ) > 18 else display_filename
if total_bytes > 0 :
progress_text = f " DL ' { display_filename } ' ( { downloaded_mb : .1f } / { total_mb : .1f } MB) "
else :
progress_text = f " DL ' { display_filename } ' ( { downloaded_mb : .1f } MB) "
self . file_progress_label . setText ( progress_text )
def update_external_links_setting ( self , checked ) :
2025-05-09 19:03:01 +05:30
# This function is now primarily controlled by _handle_filter_mode_change
# when the "Only Links" mode is NOT selected.
is_only_links_mode = self . radio_only_links and self . radio_only_links . isChecked ( )
if is_only_links_mode :
# In "Only Links" mode, the external log is always hidden.
if self . external_log_output : self . external_log_output . hide ( )
if self . log_splitter : self . log_splitter . setSizes ( [ self . height ( ) , 0 ] )
return
# Proceed only if NOT in "Only Links" mode
2025-05-08 19:49:50 +05:30
self . show_external_links = checked
if checked :
2025-05-09 19:03:01 +05:30
if self . external_log_output : self . external_log_output . show ( )
2025-05-08 19:49:50 +05:30
# Adjust splitter, give both logs some space
2025-05-09 19:03:01 +05:30
if self . log_splitter : self . log_splitter . setSizes ( [ self . height ( ) / / 2 , self . height ( ) / / 2 ] )
if self . main_log_output : self . main_log_output . setMinimumHeight ( 50 ) # Ensure it doesn't disappear
if self . external_log_output : self . external_log_output . setMinimumHeight ( 50 )
2025-05-08 19:49:50 +05:30
self . log_signal . emit ( " \n " + " = " * 40 + " \n 🔗 External Links Log Enabled \n " + " = " * 40 )
2025-05-09 19:03:01 +05:30
if self . external_log_output :
self . external_log_output . clear ( ) # Clear previous content
self . external_log_output . append ( " 🔗 External Links Found: " ) # Header
# Try processing queue if log becomes visible
2025-05-08 19:49:50 +05:30
self . _try_process_next_external_link ( )
else :
2025-05-09 19:03:01 +05:30
if self . external_log_output : self . external_log_output . hide ( )
# Adjust splitter
if self . log_splitter : self . log_splitter . setSizes ( [ self . height ( ) , 0 ] ) # Main log takes all space
if self . main_log_output : self . main_log_output . setMinimumHeight ( 0 ) # Reset min height
if self . external_log_output : self . external_log_output . setMinimumHeight ( 0 )
if self . external_log_output : self . external_log_output . clear ( ) # Clear content when hidden
2025-05-08 19:49:50 +05:30
self . log_signal . emit ( " \n " + " = " * 40 + " \n 🔗 External Links Log Disabled \n " + " = " * 40 )
2025-05-09 19:03:01 +05:30
# --- ADDED: Handler for filter mode radio buttons ---
def _handle_filter_mode_change ( self , button , checked ) :
# button can be None during initial setup sometimes
if not button or not checked :
return
filter_mode_text = button . text ( )
is_only_links = ( filter_mode_text == " 🔗 Only Links " )
# --- MODIFIED: Enable/disable widgets based on mode ---
file_options_enabled = not is_only_links
widgets_to_disable_in_links_mode = [
self . dir_input , self . dir_button , # Download Location
self . skip_zip_checkbox , self . skip_rar_checkbox ,
self . download_thumbnails_checkbox , self . compress_images_checkbox ,
self . use_subfolders_checkbox , self . use_subfolder_per_post_checkbox ,
self . character_filter_widget , # Includes label and input
self . skip_words_input ,
self . custom_folder_widget # Includes label and input
]
# --- END MODIFIED ---
for widget in widgets_to_disable_in_links_mode :
if widget : widget . setEnabled ( file_options_enabled )
# --- ADDED: Show/hide link search bar and export button ---
if self . link_search_input : self . link_search_input . setVisible ( is_only_links )
if self . link_search_button : self . link_search_button . setVisible ( is_only_links )
if self . export_links_button :
self . export_links_button . setVisible ( is_only_links )
self . export_links_button . setEnabled ( is_only_links and bool ( self . extracted_links_cache ) ) # Enable if cache has items
if not is_only_links and self . link_search_input : self . link_search_input . clear ( ) # Clear search when hiding
# --- END ADDED ---
# Specific handling for "Only Links" mode vs others
if is_only_links :
self . progress_log_label . setText ( " 📜 Extracted Links Log: " ) # Change title
# Ensure external log is hidden and main log takes full vertical space
if self . external_log_output : self . external_log_output . hide ( )
if self . log_splitter : self . log_splitter . setSizes ( [ self . height ( ) , 0 ] )
if self . main_log_output : self . main_log_output . setMinimumHeight ( 0 )
if self . external_log_output : self . external_log_output . setMinimumHeight ( 0 )
# Clear logs for the new mode
if self . main_log_output : self . main_log_output . clear ( )
if self . external_log_output : self . external_log_output . clear ( )
# External links checkbox is irrelevant in this mode, keep it enabled but ignored
if self . external_links_checkbox : self . external_links_checkbox . setEnabled ( True )
self . log_signal . emit ( " = " * 20 + " Mode changed to: Only Links " + " = " * 20 )
# Start processing links immediately for the main log display
self . _filter_links_log ( ) # Display initially filtered (all) links
self . _try_process_next_external_link ( ) # Start paced display
else : # Other modes (All, Images, Videos)
self . progress_log_label . setText ( " 📜 Progress Log: " ) # Restore title
if self . external_links_checkbox :
self . external_links_checkbox . setEnabled ( True ) # Ensure checkbox is enabled
# Restore log visibility based on checkbox state
self . update_external_links_setting ( self . external_links_checkbox . isChecked ( ) )
# Re-enable potentially disabled subfolder options if needed
self . update_ui_for_subfolders ( self . use_subfolders_checkbox . isChecked ( ) )
self . log_signal . emit ( f " = " * 20 + f " Mode changed to: { filter_mode_text } " + " = " * 20 )
# --- END ADDED ---
# --- ADDED: Method to filter links in "Only Links" mode ---
def _filter_links_log ( self ) :
""" Filters and displays links from the cache in the main log. """
if not ( self . radio_only_links and self . radio_only_links . isChecked ( ) ) :
return # Only filter when in "Only Links" mode
search_term = self . link_search_input . text ( ) . lower ( ) . strip ( )
self . main_log_output . clear ( ) # Clear current display
current_title_for_display = None # Track title for grouping in this filtered view
separator = " - " * 45
for post_title , link_text , link_url , platform in self . extracted_links_cache :
# Check if the search term matches any part of the link info
matches_search = (
not search_term or
search_term in link_text . lower ( ) or
search_term in link_url . lower ( ) or
search_term in platform . lower ( )
)
if matches_search :
# Check if the post title has changed
if post_title != current_title_for_display :
# Append separator and new title (formatted as HTML)
self . main_log_output . insertHtml ( " <br> " + separator + " <br> " )
title_html = f ' <b style= " color: #87CEEB; " > { post_title } </b><br> '
self . main_log_output . insertHtml ( title_html )
current_title_for_display = post_title # Update current title
# Format and append the link info as plain text
max_link_text_len = 35
display_text = link_text [ : max_link_text_len ] . strip ( ) + " ... " if len ( link_text ) > max_link_text_len else link_text
formatted_link_info = f " { display_text } - { link_url } - { platform } "
self . main_log_output . append ( formatted_link_info )
# Add a final blank line if any links were displayed
if self . main_log_output . toPlainText ( ) . strip ( ) :
self . main_log_output . append ( " " )
# Scroll to top after filtering
self . main_log_output . verticalScrollBar ( ) . setValue ( 0 )
# --- END ADDED ---
# --- ADDED: Method to export links ---
def _export_links_to_file ( self ) :
if not ( self . radio_only_links and self . radio_only_links . isChecked ( ) ) :
QMessageBox . information ( self , " Export Links " , " Link export is only available in ' Only Links ' mode. " )
return
if not self . extracted_links_cache :
QMessageBox . information ( self , " Export Links " , " No links have been extracted yet. " )
return
default_filename = " extracted_links.txt "
filepath , _ = QFileDialog . getSaveFileName ( self , " Save Links " , default_filename , " Text Files (*.txt);;All Files (*) " )
if filepath :
try :
with open ( filepath , ' w ' , encoding = ' utf-8 ' ) as f :
current_title_for_export = None
separator = " - " * 60 + " \n " # For file output
for post_title , link_text , link_url , platform in self . extracted_links_cache :
if post_title != current_title_for_export :
if current_title_for_export is not None : # Add separator before new title, except for the first one
f . write ( " \n " + separator + " \n " )
f . write ( f " Post Title: { post_title } \n \n " )
current_title_for_export = post_title
f . write ( f " { link_text } - { link_url } - { platform } \n " )
self . log_signal . emit ( f " ✅ Links successfully exported to: { filepath } " )
QMessageBox . information ( self , " Export Successful " , f " Links exported to: \n { filepath } " )
except Exception as e :
self . log_signal . emit ( f " ❌ Error exporting links: { e } " )
QMessageBox . critical ( self , " Export Error " , f " Could not export links: { e } " )
# --- END ADDED ---
2025-05-08 19:49:50 +05:30
2025-05-05 19:35:24 +05:30
def get_filter_mode ( self ) :
2025-05-09 19:03:01 +05:30
# This method returns the simplified filter mode string for the backend
if self . radio_only_links and self . radio_only_links . isChecked ( ) :
# When "Only Links" is checked, the backend doesn't filter by file type,
# but it does need a 'filter_mode'. 'all' is a safe default.
# The actual link extraction is controlled by the 'extract_links_only' flag.
return ' all '
elif self . radio_images . isChecked ( ) : return ' image '
elif self . radio_videos . isChecked ( ) : return ' video '
return ' all ' # Default for "All" radio or if somehow no radio is checked.
2025-05-05 19:35:24 +05:30
def add_new_character ( self ) :
2025-05-08 19:49:50 +05:30
global KNOWN_NAMES , clean_folder_name # Ensure clean_folder_name is accessible
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. " )
2025-05-08 19:49:50 +05:30
return False # Indicate failure
2025-05-06 22:08:27 +05:30
name_lower = name_to_add . lower ( )
2025-05-08 19:49:50 +05:30
# 1. Exact Duplicate Check (case-insensitive)
is_exact_duplicate = any ( existing . lower ( ) == name_lower for existing in KNOWN_NAMES )
if is_exact_duplicate :
QMessageBox . warning ( self , " Duplicate Name " , f " The name ' { name_to_add } ' (case-insensitive) already exists. " )
return False
# 2. Similarity Check (substring, case-insensitive)
similar_names_details = [ ] # Store tuples of (new_name, existing_name)
for existing_name in KNOWN_NAMES :
existing_name_lower = existing_name . lower ( )
# Avoid self-comparison if somehow name_lower was already in a different case
if name_lower != existing_name_lower :
if name_lower in existing_name_lower or existing_name_lower in name_lower :
similar_names_details . append ( ( name_to_add , existing_name ) )
if similar_names_details :
first_similar_new , first_similar_existing = similar_names_details [ 0 ]
2025-05-09 19:03:01 +05:30
2025-05-08 19:49:50 +05:30
# Determine shorter and longer for the example message
shorter_name_for_msg , longer_name_for_msg = sorted (
[ first_similar_new , first_similar_existing ] , key = len
)
msg_box = QMessageBox ( self )
msg_box . setIcon ( QMessageBox . Warning )
msg_box . setWindowTitle ( " Potential Name Conflict " )
msg_box . setText (
f " The name ' { first_similar_new } ' is very similar to an existing name: ' { first_similar_existing } ' . \n \n "
f " For example, if a post title primarily matches the shorter name ( ' { shorter_name_for_msg } ' ), "
f " files might be saved under a folder for ' { clean_folder_name ( shorter_name_for_msg ) } ' , "
f " even if the longer name ( ' { longer_name_for_msg } ' ) was also relevant or intended for a more specific folder. \n "
" This could lead to files being grouped into less specific or overly broad folders than desired. \n \n "
" Do you want to change the name you are adding, or proceed anyway? "
)
change_button = msg_box . addButton ( " Change Name " , QMessageBox . RejectRole )
proceed_button = msg_box . addButton ( " Proceed Anyway " , QMessageBox . AcceptRole )
msg_box . setDefaultButton ( proceed_button ) # Default to proceed
msg_box . setEscapeButton ( change_button ) # Escape cancels/rejects
msg_box . exec_ ( )
if msg_box . clickedButton ( ) == change_button :
self . log_signal . emit ( f " ℹ ️ User chose to change the name ' { first_similar_new } ' due to similarity with ' { first_similar_existing } ' . " )
return False # Don't add, user will change input and click "Add" again
# If proceed_button is clicked (or dialog is closed and proceed is default)
self . log_signal . emit ( f " ⚠️ User chose to proceed with adding ' { first_similar_new } ' despite similarity with ' { first_similar_existing } ' . " )
# Fall through to add the name
# If no exact duplicate, and (no similar names OR user chose to proceed with similar name)
KNOWN_NAMES . append ( name_to_add )
KNOWN_NAMES . sort ( key = str . lower ) # Keep the list sorted (case-insensitive for sorting)
self . character_list . clear ( )
self . character_list . addItems ( KNOWN_NAMES )
self . filter_character_list ( self . character_search_input . text ( ) ) # Re-apply filter
self . log_signal . emit ( f " ✅ Added ' { name_to_add } ' to known names list. " )
self . new_char_input . clear ( )
self . save_known_names ( ) # Save to file
return True # Indicate success
2025-05-06 22:08:27 +05:30
2025-05-05 19:35:24 +05:30
def delete_selected_character ( self ) :
2025-05-08 19:49:50 +05:30
global KNOWN_NAMES
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-08 19:49:50 +05:30
f " Are you sure you want to delete { len ( names_to_remove ) } name(s)? " ,
2025-05-05 19:35:24 +05:30
QMessageBox . Yes | QMessageBox . No , QMessageBox . No )
if confirm == QMessageBox . Yes :
2025-05-08 19:49:50 +05:30
original_count = len ( KNOWN_NAMES )
# Filter out names to remove
KNOWN_NAMES = [ n for n in KNOWN_NAMES if n not in names_to_remove ]
removed_count = original_count - len ( KNOWN_NAMES )
2025-05-06 22:08:27 +05:30
2025-05-05 19:35:24 +05:30
if removed_count > 0 :
2025-05-08 19:49:50 +05:30
self . log_signal . emit ( f " 🗑️ Removed { removed_count } name(s). " )
self . character_list . clear ( ) # Update UI
self . character_list . addItems ( KNOWN_NAMES )
self . filter_character_list ( self . character_search_input . text ( ) ) # Re-apply filter
self . save_known_names ( ) # Save changes
2025-05-06 22:08:27 +05:30
else :
2025-05-08 19:49:50 +05:30
self . log_signal . emit ( " ℹ ️ No names were removed (they might not have been in the list or already deleted)." )
2025-05-06 22:08:27 +05:30
def update_custom_folder_visibility ( self , url_text = None ) :
2025-05-08 19:49:50 +05:30
if url_text is None : url_text = self . link_input . text ( ) # Get current text if not passed
_ , _ , post_id = extract_post_info ( url_text . strip ( ) )
# Show if it's a post URL AND subfolders are generally enabled
2025-05-06 22:08:27 +05:30
should_show = bool ( post_id ) and self . use_subfolders_checkbox . isChecked ( )
2025-05-09 19:03:01 +05:30
# --- MODIFIED: Also hide if in "Only Links" mode ---
is_only_links = self . radio_only_links and self . radio_only_links . isChecked ( )
self . custom_folder_widget . setVisible ( should_show and not is_only_links )
# --- END MODIFIED ---
if not self . custom_folder_widget . isVisible ( ) : self . custom_folder_input . clear ( ) # Clear if hidden
2025-05-06 22:08:27 +05:30
def update_ui_for_subfolders ( self , checked ) :
2025-05-08 19:49:50 +05:30
# Character filter input visibility depends on subfolder usage
2025-05-09 19:03:01 +05:30
is_only_links = self . radio_only_links and self . radio_only_links . isChecked ( )
self . character_filter_widget . setVisible ( checked and not is_only_links ) # Hide if only links
2025-05-08 19:49:50 +05:30
if not checked : self . character_input . clear ( ) # Clear filter if hiding
self . update_custom_folder_visibility ( ) # Custom folder also depends on this
# "Subfolder per Post" is only enabled if "Separate Folders" is also checked
2025-05-09 19:03:01 +05:30
self . use_subfolder_per_post_checkbox . setEnabled ( checked and not is_only_links ) # Disable if only links
if not checked or is_only_links : self . use_subfolder_per_post_checkbox . setChecked ( False ) # Uncheck if parent is disabled or only links
2025-05-08 19:49:50 +05:30
def update_page_range_enabled_state ( self ) :
url_text = self . link_input . text ( ) . strip ( )
service , user_id , post_id = extract_post_info ( url_text )
# Page range is for creator feeds (no post_id)
is_creator_feed = service is not None and user_id is not None and post_id is None
2025-05-09 19:03:01 +05:30
2025-05-08 19:49:50 +05:30
manga_mode_active = self . manga_mode_checkbox . isChecked ( ) if self . manga_mode_checkbox else False
# Enable page range if it's a creator feed AND manga mode is NOT active
enable_page_range = is_creator_feed and not manga_mode_active
for widget in [ self . page_range_label , self . start_page_input , self . to_label , self . end_page_input ] :
if widget : widget . setEnabled ( enable_page_range )
if not enable_page_range : # Clear inputs if disabled
self . start_page_input . clear ( )
self . end_page_input . clear ( )
def update_ui_for_manga_mode ( self , checked ) :
url_text = self . link_input . text ( ) . strip ( )
_ , _ , post_id = extract_post_info ( url_text )
is_creator_feed = not post_id if url_text else False # Manga mode only for creator feeds
if self . manga_mode_checkbox : # Ensure checkbox exists
self . manga_mode_checkbox . setEnabled ( is_creator_feed ) # Only enable for creator feeds
if not is_creator_feed and self . manga_mode_checkbox . isChecked ( ) :
self . manga_mode_checkbox . setChecked ( False ) # Uncheck if URL changes to non-creator feed
# If manga mode is active (checked and enabled), disable page range
if is_creator_feed and self . manga_mode_checkbox and self . manga_mode_checkbox . isChecked ( ) :
self . page_range_label . setEnabled ( False )
self . start_page_input . setEnabled ( False ) ; self . start_page_input . clear ( )
self . to_label . setEnabled ( False )
self . end_page_input . setEnabled ( False ) ; self . end_page_input . clear ( )
else : # Otherwise, let update_page_range_enabled_state handle it
self . update_page_range_enabled_state ( )
2025-05-07 07:20:40 +05:30
2025-05-06 22:08:27 +05:30
def filter_character_list ( self , search_text ) :
2025-05-08 19:49:50 +05:30
search_text_lower = search_text . lower ( )
2025-05-06 22:08:27 +05:30
for i in range ( self . character_list . count ( ) ) :
item = self . character_list . item ( i )
2025-05-08 19:49:50 +05:30
item . setHidden ( search_text_lower not in item . text ( ) . lower ( ) )
def update_multithreading_label ( self , text ) :
2025-05-08 22:13:12 +05:30
# This method only updates the checkbox label text
# The actual enabling/disabling is handled by _handle_multithreading_toggle
if self . use_multithreading_checkbox . isChecked ( ) :
try :
num_threads = int ( text )
if num_threads > 0 :
self . use_multithreading_checkbox . setText ( f " Use Multithreading ( { num_threads } Threads) " )
2025-05-09 19:03:01 +05:30
else :
2025-05-08 22:13:12 +05:30
self . use_multithreading_checkbox . setText ( " Use Multithreading (Invalid: >0) " )
2025-05-09 19:03:01 +05:30
except ValueError :
2025-05-08 22:13:12 +05:30
self . use_multithreading_checkbox . setText ( " Use Multithreading (Invalid Input) " )
else :
self . use_multithreading_checkbox . setText ( " Use Multithreading (1 Thread) " ) # Show 1 thread when disabled
# --- ADDED: Handler for multithreading checkbox toggle ---
def _handle_multithreading_toggle ( self , checked ) :
""" Handles enabling/disabling the thread count input. """
if not checked :
# Unchecked: Set to 1 and disable
self . thread_count_input . setText ( " 1 " )
self . thread_count_input . setEnabled ( False )
self . thread_count_label . setEnabled ( False )
self . use_multithreading_checkbox . setText ( " Use Multithreading (1 Thread) " )
else :
# Checked: Enable and update label based on current value
self . thread_count_input . setEnabled ( True )
self . thread_count_label . setEnabled ( True )
self . update_multithreading_label ( self . thread_count_input . text ( ) )
# --- END ADDED ---
2025-05-06 22:08:27 +05:30
def update_progress_display ( self , total_posts , processed_posts ) :
if total_posts > 0 :
2025-05-08 19:49:50 +05:30
progress_percent = ( processed_posts / total_posts ) * 100
self . progress_label . setText ( f " Progress: { processed_posts } / { total_posts } posts ( { progress_percent : .1f } %) " )
elif processed_posts > 0 : # If total_posts is unknown (e.g., single post)
2025-05-06 22:08:27 +05:30
self . progress_label . setText ( f " Progress: Processing post { processed_posts } ... " )
2025-05-08 19:49:50 +05:30
else : # Initial state or no posts
2025-05-06 22:08:27 +05:30
self . progress_label . setText ( " Progress: Starting... " )
2025-05-08 19:49:50 +05:30
if total_posts > 0 or processed_posts > 0 : self . file_progress_label . setText ( " " ) # Clear file progress
2025-05-05 19:35:24 +05:30
def start_download ( self ) :
2025-05-08 19:49:50 +05:30
global KNOWN_NAMES , BackendDownloadThread , PostProcessorWorker , extract_post_info , clean_folder_name
if ( self . download_thread and self . download_thread . isRunning ( ) ) or self . thread_pool :
2025-05-06 22:08:27 +05:30
QMessageBox . warning ( self , " Busy " , " A download is already running. " )
2025-05-05 19:35:24 +05:30
return
2025-05-08 19:49:50 +05:30
2025-05-05 19:35:24 +05:30
api_url = self . link_input . text ( ) . strip ( )
output_dir = self . dir_input . text ( ) . strip ( )
skip_zip = self . skip_zip_checkbox . isChecked ( )
skip_rar = self . skip_rar_checkbox . isChecked ( )
use_subfolders = self . use_subfolders_checkbox . isChecked ( )
2025-05-08 19:49:50 +05:30
use_post_subfolders = self . use_subfolder_per_post_checkbox . isChecked ( ) and use_subfolders
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 ( )
raw_skip_words = self . skip_words_input . text ( ) . strip ( )
2025-05-08 19:49:50 +05:30
skip_words_list = [ word . strip ( ) . lower ( ) for word in raw_skip_words . split ( ' , ' ) if word . strip ( ) ]
manga_mode_is_checked = self . manga_mode_checkbox . isChecked ( ) if self . manga_mode_checkbox else False
2025-05-09 19:03:01 +05:30
extract_links_only = ( self . radio_only_links and self . radio_only_links . isChecked ( ) )
# --- MODIFICATION FOR FILTER MODE ---
# Get the simplified filter mode for the backend (e.g., 'image', 'video', 'all')
backend_filter_mode = self . get_filter_mode ( )
# Get the user-facing text of the selected radio button for logging purposes
user_selected_filter_text = self . radio_group . checkedButton ( ) . text ( ) if self . radio_group . checkedButton ( ) else " All "
# --- END MODIFICATION FOR FILTER MODE ---
if not api_url :
QMessageBox . critical ( self , " Input Error " , " URL is required. " ) ; return
if not extract_links_only and not output_dir :
QMessageBox . critical ( self , " Input Error " , " Download Directory is required when not in ' Only Links ' mode. " ) ; return
2025-05-08 19:49:50 +05:30
service , user_id , post_id_from_url = extract_post_info ( api_url )
2025-05-09 19:03:01 +05:30
if not service or not user_id :
2025-05-08 19:49:50 +05:30
QMessageBox . critical ( self , " Input Error " , " Invalid or unsupported URL format. " ) ; return
2025-05-06 22:08:27 +05:30
2025-05-09 19:03:01 +05:30
if not extract_links_only and not os . path . isdir ( output_dir ) :
2025-05-08 19:49:50 +05:30
reply = QMessageBox . question ( self , " Create Directory? " ,
f " The directory ' { output_dir } ' does not exist. \n Create it now? " ,
QMessageBox . Yes | QMessageBox . No , QMessageBox . Yes )
if reply == QMessageBox . Yes :
try :
2025-05-09 19:03:01 +05:30
os . makedirs ( output_dir , exist_ok = True )
2025-05-08 19:49:50 +05:30
self . log_signal . emit ( f " ℹ ️ Created directory: { output_dir } " )
except Exception as e :
QMessageBox . critical ( self , " Directory Error " , f " Could not create directory: { e } " ) ; return
else :
self . log_signal . emit ( " ❌ Download cancelled: Output directory does not exist and was not created. " )
return
2025-05-09 19:03:01 +05:30
if compress_images and Image is None :
2025-05-08 19:49:50 +05:30
QMessageBox . warning ( self , " Missing Dependency " , " Pillow library (for image compression) not found. Compression will be disabled. " )
2025-05-09 19:03:01 +05:30
compress_images = False
self . compress_images_checkbox . setChecked ( False )
2025-05-08 19:49:50 +05:30
2025-05-09 19:03:01 +05:30
manga_mode = manga_mode_is_checked and not post_id_from_url
2025-05-08 19:49:50 +05:30
num_threads_str = self . thread_count_input . text ( ) . strip ( )
2025-05-09 19:03:01 +05:30
num_threads = 1
if use_multithreading :
2025-05-08 22:13:12 +05:30
try :
num_threads_requested = int ( num_threads_str )
if num_threads_requested > MAX_THREADS :
warning_message = (
f " You have requested { num_threads_requested } threads, which is above the maximum limit of { MAX_THREADS } . \n \n "
f " High thread counts can lead to instability or rate-limiting. \n \n "
f " The thread count will be automatically capped at { MAX_THREADS } for this download. "
)
QMessageBox . warning ( self , " High Thread Count Warning " , warning_message )
self . log_signal . emit ( f " ⚠️ High thread count requested ( { num_threads_requested } ). Capping at { MAX_THREADS } . " )
2025-05-09 19:03:01 +05:30
num_threads = MAX_THREADS
self . thread_count_input . setText ( str ( num_threads ) )
2025-05-08 22:13:12 +05:30
elif num_threads_requested > RECOMMENDED_MAX_THREADS :
QMessageBox . information ( self , " High Thread Count Note " ,
f " Using { num_threads_requested } threads (above { RECOMMENDED_MAX_THREADS } ) may increase resource usage and risk rate-limiting from the site. \n \n Proceeding with caution. " )
self . log_signal . emit ( f " ℹ ️ Using high thread count: { num_threads_requested } . " )
2025-05-09 19:03:01 +05:30
num_threads = num_threads_requested
elif num_threads_requested < 1 :
2025-05-08 22:13:12 +05:30
self . log_signal . emit ( f " ⚠️ Invalid thread count ( { num_threads_requested } ). Using 1 thread. " )
num_threads = 1
self . thread_count_input . setText ( str ( num_threads ) )
else :
2025-05-09 19:03:01 +05:30
num_threads = num_threads_requested
2025-05-08 22:13:12 +05:30
except ValueError :
QMessageBox . critical ( self , " Thread Count Error " , " Invalid number of threads. Please enter a numeric value. " ) ; return
else :
2025-05-09 19:03:01 +05:30
num_threads = 1
2025-05-08 22:13:12 +05:30
2025-05-08 19:49:50 +05:30
start_page_str , end_page_str = self . start_page_input . text ( ) . strip ( ) , self . end_page_input . text ( ) . strip ( )
start_page , end_page = None , None
is_creator_feed = bool ( not post_id_from_url )
2025-05-09 19:03:01 +05:30
if is_creator_feed and not manga_mode :
2025-05-08 19:49:50 +05:30
try :
if start_page_str : start_page = int ( start_page_str )
if end_page_str : end_page = int ( end_page_str )
if start_page is not None and start_page < = 0 : raise ValueError ( " Start page must be positive. " )
if end_page is not None and end_page < = 0 : raise ValueError ( " End page must be positive. " )
if start_page and end_page and start_page > end_page :
raise ValueError ( " Start page cannot be greater than end page. " )
except ValueError as e :
QMessageBox . critical ( self , " Page Range Error " , f " Invalid page range: { e } " ) ; return
2025-05-09 19:03:01 +05:30
elif manga_mode :
start_page , end_page = None , None
2025-05-08 19:49:50 +05:30
self . external_link_queue . clear ( )
2025-05-09 19:03:01 +05:30
self . extracted_links_cache = [ ]
2025-05-08 19:49:50 +05:30
self . _is_processing_external_link_queue = False
2025-05-09 19:03:01 +05:30
self . _current_link_post_title = None
2025-05-08 19:49:50 +05:30
raw_character_filters_text = self . character_input . text ( ) . strip ( )
parsed_character_list = None
if raw_character_filters_text :
temp_list = [ name . strip ( ) for name in raw_character_filters_text . split ( ' , ' ) if name . strip ( ) ]
if temp_list : parsed_character_list = temp_list
2025-05-09 19:03:01 +05:30
filter_character_list_to_pass = None
if use_subfolders and parsed_character_list and not post_id_from_url and not extract_links_only :
2025-05-08 19:49:50 +05:30
self . log_signal . emit ( f " ℹ ️ Validating character filters for subfolder naming: { ' , ' . join ( parsed_character_list ) } " )
valid_filters_for_backend = [ ]
user_cancelled_validation = False
for char_name in parsed_character_list :
2025-05-09 19:03:01 +05:30
cleaned_name_test = clean_folder_name ( char_name )
2025-05-08 19:49:50 +05:30
if not cleaned_name_test :
QMessageBox . warning ( self , " Invalid Filter Name " , f " Filter name ' { char_name } ' is invalid for a folder and will be skipped. " )
self . log_signal . emit ( f " ⚠️ Skipping invalid filter for folder: ' { char_name } ' " )
continue
if char_name . lower ( ) not in { kn . lower ( ) for kn in KNOWN_NAMES } :
reply = QMessageBox . question ( self , " Add Filter Name to Known List? " ,
f " The character filter ' { char_name } ' is not in your known names list (used for folder suggestions). \n Add it now? " ,
QMessageBox . Yes | QMessageBox . No | QMessageBox . Cancel , QMessageBox . Yes )
if reply == QMessageBox . Yes :
2025-05-09 19:03:01 +05:30
self . new_char_input . setText ( char_name )
if self . add_new_character ( ) :
2025-05-08 19:49:50 +05:30
self . log_signal . emit ( f " ✅ Added ' { char_name } ' to known names via filter prompt. " )
valid_filters_for_backend . append ( char_name )
2025-05-09 19:03:01 +05:30
else :
2025-05-08 19:49:50 +05:30
self . log_signal . emit ( f " ⚠️ Failed to add ' { char_name } ' via filter prompt (or user opted out). It will still be used for filtering this session if valid. " )
if cleaned_name_test : valid_filters_for_backend . append ( char_name )
elif reply == QMessageBox . Cancel :
self . log_signal . emit ( f " ❌ Download cancelled by user during filter validation for ' { char_name } ' . " )
user_cancelled_validation = True ; break
2025-05-09 19:03:01 +05:30
else :
2025-05-08 19:49:50 +05:30
self . log_signal . emit ( f " ℹ ️ Proceeding with filter ' { char_name } ' for matching without adding to known list. " )
if cleaned_name_test : valid_filters_for_backend . append ( char_name )
2025-05-09 19:03:01 +05:30
else :
2025-05-08 19:49:50 +05:30
if cleaned_name_test : valid_filters_for_backend . append ( char_name )
2025-05-09 19:03:01 +05:30
if user_cancelled_validation : return
2025-05-08 19:49:50 +05:30
if valid_filters_for_backend :
filter_character_list_to_pass = valid_filters_for_backend
self . log_signal . emit ( f " Using validated character filters for subfolders: { ' , ' . join ( filter_character_list_to_pass ) } " )
else :
self . log_signal . emit ( " ⚠️ No valid character filters remaining after validation for subfolder naming. " )
2025-05-09 19:03:01 +05:30
elif parsed_character_list :
2025-05-08 19:49:50 +05:30
filter_character_list_to_pass = parsed_character_list
self . log_signal . emit ( f " ℹ ️ Character filters provided: { ' , ' . join ( filter_character_list_to_pass ) } (Subfolder creation rules may differ). " )
2025-05-09 19:03:01 +05:30
if manga_mode and not filter_character_list_to_pass and not extract_links_only :
2025-05-08 19:49:50 +05:30
msg_box = QMessageBox ( self )
msg_box . setIcon ( QMessageBox . Warning )
msg_box . setWindowTitle ( " Manga Mode Filter Warning " )
msg_box . setText (
" Manga Mode is enabled, but the ' Filter by Character(s) ' field is empty. \n \n "
" For best results (correct file naming and grouping), please enter the exact Manga/Series title "
" (as used by the creator on the site) into the filter field. \n \n "
" Do you want to proceed without a filter (file names might be generic) or cancel? "
)
2025-05-09 19:03:01 +05:30
proceed_button = msg_box . addButton ( " Proceed Anyway " , QMessageBox . AcceptRole )
cancel_button = msg_box . addButton ( " Cancel Download " , QMessageBox . RejectRole )
2025-05-08 19:49:50 +05:30
msg_box . exec_ ( )
if msg_box . clickedButton ( ) == cancel_button :
self . log_signal . emit ( " ❌ Download cancelled by user due to Manga Mode filter warning. " )
2025-05-09 19:03:01 +05:30
return
2025-05-08 19:49:50 +05:30
else :
self . log_signal . emit ( " ⚠️ Proceeding with Manga Mode without a specific title filter. " )
custom_folder_name_cleaned = None
2025-05-09 19:03:01 +05:30
if use_subfolders and post_id_from_url and self . custom_folder_widget . isVisible ( ) and not extract_links_only :
2025-05-06 22:08:27 +05:30
raw_custom_name = self . custom_folder_input . text ( ) . strip ( )
if raw_custom_name :
2025-05-08 19:49:50 +05:30
cleaned_custom = clean_folder_name ( raw_custom_name )
if cleaned_custom : custom_folder_name_cleaned = cleaned_custom
else : self . log_signal . emit ( f " ⚠️ Invalid custom folder name ignored: ' { raw_custom_name } ' " )
self . main_log_output . clear ( )
2025-05-09 19:03:01 +05:30
if extract_links_only :
self . main_log_output . append ( " 🔗 Extracting Links... " )
if self . external_log_output : self . external_log_output . clear ( )
elif self . show_external_links :
self . external_log_output . clear ( )
self . external_log_output . append ( " 🔗 External Links Found: " )
2025-05-08 19:49:50 +05:30
self . file_progress_label . setText ( " " )
2025-05-09 19:03:01 +05:30
self . cancellation_event . clear ( )
2025-05-06 22:08:27 +05:30
self . active_futures = [ ]
2025-05-08 19:49:50 +05:30
self . total_posts_to_process = self . processed_posts_count = self . download_counter = self . skip_counter = 0
2025-05-06 22:08:27 +05:30
self . progress_label . setText ( " Progress: Initializing... " )
2025-05-08 19:49:50 +05:30
log_messages = [
2025-05-09 19:03:01 +05:30
" = " * 40 , f " 🚀 Starting { ' Link Extraction ' if extract_links_only else ' Download ' } @ { time . strftime ( ' % Y- % m- %d % H: % M: % S ' ) } " ,
f " URL: { api_url } " ,
2025-05-08 19:49:50 +05:30
]
2025-05-09 19:03:01 +05:30
if not extract_links_only :
log_messages . append ( f " Save Location: { output_dir } " )
log_messages . append ( f " Mode: { ' Single Post ' if post_id_from_url else ' Creator Feed ' } " )
2025-05-08 19:49:50 +05:30
if is_creator_feed :
if manga_mode :
log_messages . append ( " Page Range: All (Manga Mode - Oldest Posts Processed First) " )
else :
pr_log = " All "
2025-05-09 19:03:01 +05:30
if start_page or end_page :
2025-05-08 19:49:50 +05:30
pr_log = f " { f ' From { start_page } ' if start_page else ' ' } { ' to ' if start_page and end_page else ' ' } { f ' { end_page } ' if end_page else ( f ' Up to { end_page } ' if end_page else ( f ' From { start_page } ' if start_page else ' Specific Range ' ) ) } " . strip ( )
log_messages . append ( f " Page Range: { pr_log if pr_log else ' All ' } " )
2025-05-09 19:03:01 +05:30
if not extract_links_only :
log_messages . append ( f " Subfolders: { ' Enabled ' if use_subfolders else ' Disabled ' } " )
if use_subfolders :
if custom_folder_name_cleaned : log_messages . append ( f " Custom Folder (Post): ' { custom_folder_name_cleaned } ' " )
elif filter_character_list_to_pass and not post_id_from_url : log_messages . append ( f " Character Filters for Folders: { ' , ' . join ( filter_character_list_to_pass ) } " )
else : log_messages . append ( f " Folder Naming: Automatic (based on title/known names) " )
log_messages . append ( f " Subfolder per Post: { ' Enabled ' if use_post_subfolders else ' Disabled ' } " )
log_messages . extend ( [
# --- MODIFIED LOGGING FOR FILTER MODE ---
f " File Type Filter: { user_selected_filter_text } (Backend processing as: { backend_filter_mode } ) " ,
# --- END MODIFIED LOGGING ---
f " Skip Archives: { ' .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 ' ' } " ,
f " Skip Words (posts/files): { ' , ' . join ( skip_words_list ) if skip_words_list else ' None ' } " ,
f " Compress Images: { ' Enabled ' if compress_images else ' Disabled ' } " ,
f " Thumbnails Only: { ' Enabled ' if download_thumbnails else ' Disabled ' } " ,
] )
else :
log_messages . append ( f " Mode: Extracting Links Only " ) # This handles the "Only Links" case
2025-05-08 19:49:50 +05:30
2025-05-09 19:03:01 +05:30
log_messages . append ( f " Show External Links: { ' Enabled ' if self . show_external_links else ' Disabled ' } " )
2025-05-08 19:49:50 +05:30
if manga_mode : log_messages . append ( f " Manga Mode (File Renaming by Post Title): Enabled " )
2025-05-09 19:03:01 +05:30
should_use_multithreading = use_multithreading and not post_id_from_url
2025-05-08 19:49:50 +05:30
log_messages . append ( f " Threading: { ' Multi-threaded (posts) ' if should_use_multithreading else ' Single-threaded (posts) ' } " )
2025-05-09 19:03:01 +05:30
if should_use_multithreading : log_messages . append ( f " Number of Post Worker Threads: { num_threads } " )
2025-05-08 19:49:50 +05:30
log_messages . append ( " = " * 40 )
for msg in log_messages : self . log_signal . emit ( msg )
2025-05-09 19:03:01 +05:30
self . set_ui_enabled ( False )
2025-05-08 19:49:50 +05:30
unwanted_keywords_for_folders = { ' spicy ' , ' hd ' , ' nsfw ' , ' 4k ' , ' preview ' , ' teaser ' , ' clip ' }
args_template = {
' api_url_input ' : api_url ,
2025-05-09 19:03:01 +05:30
' download_root ' : output_dir ,
' output_dir ' : output_dir ,
' known_names ' : list ( KNOWN_NAMES ) ,
' known_names_copy ' : list ( KNOWN_NAMES ) ,
2025-05-08 19:49:50 +05:30
' filter_character_list ' : filter_character_list_to_pass ,
2025-05-09 19:03:01 +05:30
# --- MODIFIED: Pass the correct backend_filter_mode ---
' filter_mode ' : backend_filter_mode ,
# --- END MODIFICATION ---
' skip_zip ' : skip_zip , ' skip_rar ' : skip_rar ,
2025-05-08 19:49:50 +05:30
' use_subfolders ' : use_subfolders , ' use_post_subfolders ' : use_post_subfolders ,
' compress_images ' : compress_images , ' download_thumbnails ' : download_thumbnails ,
' service ' : service , ' user_id ' : user_id ,
2025-05-09 19:03:01 +05:30
' downloaded_files ' : self . downloaded_files ,
' downloaded_files_lock ' : self . downloaded_files_lock ,
' downloaded_file_hashes ' : self . downloaded_file_hashes ,
' downloaded_file_hashes_lock ' : self . downloaded_file_hashes_lock ,
2025-05-08 19:49:50 +05:30
' skip_words_list ' : skip_words_list ,
' show_external_links ' : self . show_external_links ,
2025-05-09 19:03:01 +05:30
' extract_links_only ' : extract_links_only ,
' start_page ' : start_page ,
' end_page ' : end_page ,
2025-05-08 19:49:50 +05:30
' target_post_id_from_initial_url ' : post_id_from_url ,
' custom_folder_name ' : custom_folder_name_cleaned ,
' manga_mode_active ' : manga_mode ,
' unwanted_keywords ' : unwanted_keywords_for_folders ,
2025-05-09 19:03:01 +05:30
' cancellation_event ' : self . cancellation_event ,
' signals ' : self . worker_signals ,
2025-05-08 19:49:50 +05:30
}
2025-05-06 22:08:27 +05:30
2025-05-08 19:49:50 +05:30
try :
2025-05-06 22:08:27 +05:30
if should_use_multithreading :
2025-05-09 19:03:01 +05:30
self . log_signal . emit ( f " Initializing multi-threaded { ' link extraction ' if extract_links_only else ' download ' } with { num_threads } post workers... " )
self . start_multi_threaded_download ( num_post_workers = num_threads , * * args_template )
else :
self . log_signal . emit ( f " Initializing single-threaded { ' link extraction ' if extract_links_only else ' download ' } ... " )
2025-05-08 19:49:50 +05:30
dt_expected_keys = [
' api_url_input ' , ' output_dir ' , ' known_names_copy ' , ' cancellation_event ' ,
' filter_character_list ' , ' filter_mode ' , ' skip_zip ' , ' skip_rar ' ,
' use_subfolders ' , ' use_post_subfolders ' , ' custom_folder_name ' ,
' compress_images ' , ' download_thumbnails ' , ' service ' , ' user_id ' ,
' downloaded_files ' , ' downloaded_file_hashes ' , ' downloaded_files_lock ' ,
' downloaded_file_hashes_lock ' , ' skip_words_list ' , ' show_external_links ' ,
2025-05-09 19:03:01 +05:30
' extract_links_only ' ,
2025-05-08 19:49:50 +05:30
' num_file_threads_for_worker ' , ' skip_current_file_flag ' ,
' start_page ' , ' end_page ' , ' target_post_id_from_initial_url ' ,
' manga_mode_active ' , ' unwanted_keywords '
]
2025-05-09 19:03:01 +05:30
args_template [ ' num_file_threads_for_worker ' ] = 1
args_template [ ' skip_current_file_flag ' ] = None
2025-05-08 19:49:50 +05:30
single_thread_args = { }
for key in dt_expected_keys :
if key in args_template :
single_thread_args [ key ] = args_template [ key ]
self . start_single_threaded_download ( * * single_thread_args )
2025-05-06 22:08:27 +05:30
except Exception as e :
2025-05-09 19:03:01 +05:30
self . log_signal . emit ( f " ❌ CRITICAL ERROR preparing { ' link extraction ' if extract_links_only else ' download ' } : { e } \n { traceback . format_exc ( ) } " )
QMessageBox . critical ( self , " Start Error " , f " Failed to start process: \n { e } " )
self . download_finished ( 0 , 0 , False )
2025-05-06 22:08:27 +05:30
def start_single_threaded_download ( self , * * kwargs ) :
2025-05-08 19:49:50 +05:30
global BackendDownloadThread
2025-05-06 22:08:27 +05:30
try :
2025-05-09 19:03:01 +05:30
self . download_thread = BackendDownloadThread ( * * kwargs )
2025-05-08 19:49:50 +05:30
if hasattr ( self . download_thread , ' progress_signal ' ) :
self . download_thread . progress_signal . connect ( self . handle_main_log )
2025-05-09 19:03:01 +05:30
if hasattr ( self . download_thread , ' add_character_prompt_signal ' ) :
2025-05-08 19:49:50 +05:30
self . download_thread . add_character_prompt_signal . connect ( self . add_character_prompt_signal )
if hasattr ( self . download_thread , ' finished_signal ' ) :
2025-05-09 19:03:01 +05:30
self . download_thread . finished_signal . connect ( self . finished_signal )
if hasattr ( self . download_thread , ' receive_add_character_result ' ) :
2025-05-08 19:49:50 +05:30
self . character_prompt_response_signal . connect ( self . download_thread . receive_add_character_result )
2025-05-09 19:03:01 +05:30
if hasattr ( self . download_thread , ' external_link_signal ' ) :
self . download_thread . external_link_signal . connect ( self . handle_external_link_signal )
2025-05-08 19:49:50 +05:30
if hasattr ( self . download_thread , ' file_progress_signal ' ) :
self . download_thread . file_progress_signal . connect ( self . update_file_progress_display )
2025-05-06 22:08:27 +05:30
self . download_thread . start ( )
2025-05-08 19:49:50 +05:30
self . log_signal . emit ( " ✅ Single download thread (for posts) started. " )
2025-05-06 22:08:27 +05:30
except Exception as e :
2025-05-08 19:49:50 +05:30
self . log_signal . emit ( f " ❌ CRITICAL ERROR starting single-thread: { e } \n { traceback . format_exc ( ) } " )
QMessageBox . critical ( self , " Thread Start Error " , f " Failed to start download process: { e } " )
2025-05-09 19:03:01 +05:30
self . download_finished ( 0 , 0 , False )
2025-05-06 22:08:27 +05:30
2025-05-08 19:49:50 +05:30
def start_multi_threaded_download ( self , num_post_workers , * * kwargs ) :
2025-05-09 19:03:01 +05:30
global PostProcessorWorker
2025-05-08 19:49:50 +05:30
self . thread_pool = ThreadPoolExecutor ( max_workers = num_post_workers , thread_name_prefix = ' PostWorker_ ' )
2025-05-09 19:03:01 +05:30
self . active_futures = [ ]
2025-05-06 22:08:27 +05:30
self . processed_posts_count = 0
2025-05-09 19:03:01 +05:30
self . total_posts_to_process = 0
2025-05-06 22:08:27 +05:30
self . download_counter = 0
self . skip_counter = 0
2025-05-08 19:49:50 +05:30
2025-05-06 22:08:27 +05:30
fetcher_thread = threading . Thread (
2025-05-08 19:49:50 +05:30
target = self . _fetch_and_queue_posts ,
2025-05-09 19:03:01 +05:30
args = ( kwargs [ ' api_url_input ' ] , kwargs , num_post_workers ) ,
daemon = True , name = " PostFetcher "
2025-05-06 22:08:27 +05:30
)
fetcher_thread . start ( )
2025-05-08 19:49:50 +05:30
self . log_signal . emit ( f " ✅ Post fetcher thread started. { num_post_workers } post worker threads initializing... " )
2025-05-06 22:08:27 +05:30
2025-05-08 19:49:50 +05:30
def _fetch_and_queue_posts ( self , api_url_input_for_fetcher , worker_args_template , num_post_workers ) :
2025-05-09 19:03:01 +05:30
global PostProcessorWorker , download_from_api
2025-05-08 19:49:50 +05:30
all_posts_data = [ ]
fetch_error_occurred = False
2025-05-06 22:08:27 +05:30
2025-05-08 19:49:50 +05:30
manga_mode_active_for_fetch = worker_args_template . get ( ' manga_mode_active ' , False )
2025-05-09 19:03:01 +05:30
signals_for_worker = worker_args_template . get ( ' signals ' )
if not signals_for_worker :
2025-05-08 19:49:50 +05:30
self . log_signal . emit ( " ❌ CRITICAL ERROR: Signals object missing for worker in _fetch_and_queue_posts. " )
2025-05-09 19:03:01 +05:30
self . finished_signal . emit ( 0 , 0 , True )
2025-05-08 19:49:50 +05:30
return
2025-05-06 22:08:27 +05:30
2025-05-08 19:49:50 +05:30
try :
self . log_signal . emit ( " Fetching post data from API... " )
post_generator = download_from_api (
api_url_input_for_fetcher ,
2025-05-09 19:03:01 +05:30
logger = lambda msg : self . log_signal . emit ( f " [Fetcher] { msg } " ) ,
start_page = worker_args_template . get ( ' start_page ' ) ,
2025-05-08 19:49:50 +05:30
end_page = worker_args_template . get ( ' end_page ' ) ,
manga_mode = manga_mode_active_for_fetch ,
2025-05-09 19:03:01 +05:30
cancellation_event = self . cancellation_event
2025-05-08 19:49:50 +05:30
)
2025-05-06 22:08:27 +05:30
for posts_batch in post_generator :
2025-05-09 19:03:01 +05:30
if self . cancellation_event . is_set ( ) :
2025-05-08 19:49:50 +05:30
fetch_error_occurred = True ; self . log_signal . emit ( " Post fetching cancelled by user. " ) ; break
2025-05-06 22:08:27 +05:30
if isinstance ( posts_batch , list ) :
2025-05-08 19:49:50 +05:30
all_posts_data . extend ( posts_batch )
2025-05-09 19:03:01 +05:30
self . total_posts_to_process = len ( all_posts_data )
2025-05-08 19:49:50 +05:30
if self . total_posts_to_process > 0 and self . total_posts_to_process % 100 == 0 :
self . log_signal . emit ( f " Fetched { self . total_posts_to_process } posts so far... " )
2025-05-09 19:03:01 +05:30
else :
2025-05-08 19:49:50 +05:30
fetch_error_occurred = True
self . log_signal . emit ( f " ❌ API fetcher returned non-list type: { type ( posts_batch ) } " ) ; break
if not fetch_error_occurred and not self . cancellation_event . is_set ( ) :
self . log_signal . emit ( f " ✅ Post fetching complete. Total posts to process: { self . total_posts_to_process } " )
2025-05-09 19:03:01 +05:30
except TypeError as te :
2025-05-08 19:49:50 +05:30
self . log_signal . emit ( f " ❌ TypeError calling download_from_api: { te } " )
self . log_signal . emit ( " Check if ' downloader_utils.py ' has the correct ' download_from_api ' signature (including ' manga_mode ' and ' cancellation_event ' ). " )
self . log_signal . emit ( traceback . format_exc ( limit = 2 ) )
fetch_error_occurred = True
2025-05-09 19:03:01 +05:30
except RuntimeError as re :
2025-05-08 19:49:50 +05:30
self . log_signal . emit ( f " ℹ ️ Post fetching runtime error (likely cancellation): { re } " )
2025-05-09 19:03:01 +05:30
fetch_error_occurred = True
2025-05-06 22:08:27 +05:30
except Exception as e :
2025-05-08 19:49:50 +05:30
self . log_signal . emit ( f " ❌ Error during post fetching: { e } \n { traceback . format_exc ( limit = 2 ) } " )
fetch_error_occurred = True
if self . cancellation_event . is_set ( ) or fetch_error_occurred :
2025-05-06 22:08:27 +05:30
self . finished_signal . emit ( self . download_counter , self . skip_counter , self . cancellation_event . is_set ( ) )
2025-05-09 19:03:01 +05:30
if self . thread_pool :
2025-05-08 19:49:50 +05:30
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 :
2025-05-08 19:49:50 +05:30
self . log_signal . emit ( " 😕 No posts found or fetched to process. " )
self . finished_signal . emit ( 0 , 0 , False ) ; return
self . log_signal . emit ( f " Submitting { self . total_posts_to_process } post processing tasks to thread pool... " )
2025-05-09 19:03:01 +05:30
self . processed_posts_count = 0
self . overall_progress_signal . emit ( self . total_posts_to_process , 0 )
2025-05-08 19:49:50 +05:30
2025-05-09 19:03:01 +05:30
num_file_dl_threads = 4
2025-05-08 19:49:50 +05:30
ppw_expected_keys = [
' post_data ' , ' download_root ' , ' known_names ' , ' filter_character_list ' ,
' unwanted_keywords ' , ' filter_mode ' , ' skip_zip ' , ' skip_rar ' ,
' use_subfolders ' , ' use_post_subfolders ' , ' target_post_id_from_initial_url ' ,
' custom_folder_name ' , ' compress_images ' , ' download_thumbnails ' , ' service ' ,
' user_id ' , ' api_url_input ' , ' cancellation_event ' , ' signals ' ,
' downloaded_files ' , ' downloaded_file_hashes ' , ' downloaded_files_lock ' ,
' downloaded_file_hashes_lock ' , ' skip_words_list ' , ' show_external_links ' ,
' extract_links_only ' , ' num_file_threads ' , ' skip_current_file_flag ' ,
' manga_mode_active '
]
ppw_optional_keys_with_defaults = {
' skip_words_list ' , ' show_external_links ' , ' extract_links_only ' ,
' num_file_threads ' , ' skip_current_file_flag ' , ' manga_mode_active '
2025-05-06 22:08:27 +05:30
}
2025-05-08 19:49:50 +05:30
for post_data_item in all_posts_data :
2025-05-09 19:03:01 +05:30
if self . cancellation_event . is_set ( ) : break
if not isinstance ( post_data_item , dict ) :
2025-05-08 19:49:50 +05:30
self . log_signal . emit ( f " ⚠️ Skipping invalid post data item (not a dict): { type ( post_data_item ) } " )
2025-05-09 19:03:01 +05:30
self . processed_posts_count + = 1
2025-05-06 22:08:27 +05:30
continue
2025-05-08 19:49:50 +05:30
worker_init_args = { }
missing_keys = [ ]
for key in ppw_expected_keys :
if key == ' post_data ' : worker_init_args [ key ] = post_data_item
elif key == ' num_file_threads ' : worker_init_args [ key ] = num_file_dl_threads
2025-05-09 19:03:01 +05:30
elif key == ' signals ' : worker_init_args [ key ] = signals_for_worker
2025-05-08 19:49:50 +05:30
elif key in worker_args_template : worker_init_args [ key ] = worker_args_template [ key ]
2025-05-09 19:03:01 +05:30
elif key in ppw_optional_keys_with_defaults : pass
else : missing_keys . append ( key )
2025-05-08 19:49:50 +05:30
if missing_keys :
self . log_signal . emit ( f " ❌ CRITICAL ERROR: Missing expected keys for PostProcessorWorker: { ' , ' . join ( missing_keys ) } " )
2025-05-09 19:03:01 +05:30
self . cancellation_event . set ( )
2025-05-06 22:08:27 +05:30
break
2025-05-08 19:49:50 +05:30
try :
worker_instance = PostProcessorWorker ( * * worker_init_args )
2025-05-09 19:03:01 +05:30
if self . thread_pool :
2025-05-08 19:49:50 +05:30
future = self . thread_pool . submit ( worker_instance . process )
2025-05-09 19:03:01 +05:30
future . add_done_callback ( self . _handle_future_result )
2025-05-08 19:49:50 +05:30
self . active_futures . append ( future )
2025-05-09 19:03:01 +05:30
else :
2025-05-08 19:49:50 +05:30
self . log_signal . emit ( " ⚠️ Thread pool not available. Cannot submit more tasks. " )
break
2025-05-09 19:03:01 +05:30
except TypeError as te :
2025-05-08 19:49:50 +05:30
self . log_signal . emit ( f " ❌ TypeError creating PostProcessorWorker: { te } " )
passed_keys_str = " , " . join ( sorted ( worker_init_args . keys ( ) ) )
self . log_signal . emit ( f " Passed Args: [ { passed_keys_str } ] " )
self . log_signal . emit ( traceback . format_exc ( limit = 5 ) )
2025-05-09 19:03:01 +05:30
self . cancellation_event . set ( ) ; break
except RuntimeError :
2025-05-08 19:49:50 +05:30
self . log_signal . emit ( " ⚠️ Runtime error submitting task (pool likely shutting down). " ) ; break
2025-05-09 19:03:01 +05:30
except Exception as e :
2025-05-08 19:49:50 +05:30
self . log_signal . emit ( f " ❌ Error submitting post { post_data_item . get ( ' id ' , ' N/A ' ) } to worker: { e } " ) ; break
if not self . cancellation_event . is_set ( ) :
self . log_signal . emit ( f " { len ( self . active_futures ) } post processing tasks submitted to pool. " )
2025-05-09 19:03:01 +05:30
else :
2025-05-08 19:49:50 +05:30
self . finished_signal . emit ( self . download_counter , self . skip_counter , True )
if self . thread_pool :
self . thread_pool . shutdown ( wait = False , cancel_futures = True ) ; self . thread_pool = None
2025-05-06 22:08:27 +05:30
def _handle_future_result ( self , future : Future ) :
self . processed_posts_count + = 1
2025-05-08 19:49:50 +05:30
downloaded_files_from_future = 0
skipped_files_from_future = 0
2025-05-06 22:08:27 +05:30
try :
if future . cancelled ( ) :
2025-05-08 19:49:50 +05:30
self . log_signal . emit ( " A post processing task was cancelled. " )
2025-05-06 22:08:27 +05:30
elif future . exception ( ) :
2025-05-08 19:49:50 +05:30
worker_exception = future . exception ( )
self . log_signal . emit ( f " ❌ Post processing worker error: { worker_exception } " )
else : # Success
downloaded_files_from_future , skipped_files_from_future = future . result ( )
2025-05-09 19:03:01 +05:30
with self . downloaded_files_lock :
2025-05-08 19:49:50 +05:30
self . download_counter + = downloaded_files_from_future
self . skip_counter + = skipped_files_from_future
2025-05-05 19:35:24 +05:30
2025-05-06 22:08:27 +05:30
self . overall_progress_signal . emit ( self . total_posts_to_process , self . processed_posts_count )
2025-05-09 19:03:01 +05:30
except Exception as e :
2025-05-08 19:49:50 +05:30
self . log_signal . emit ( f " ❌ Error in _handle_future_result callback: { e } \n { traceback . format_exc ( limit = 2 ) } " )
if self . total_posts_to_process > 0 and self . processed_posts_count > = self . total_posts_to_process :
all_done = all ( f . done ( ) for f in self . active_futures )
if all_done :
2025-05-09 19:03:01 +05:30
QApplication . processEvents ( )
2025-05-08 19:49:50 +05:30
self . log_signal . emit ( " 🏁 All submitted post tasks have completed or failed. " )
self . finished_signal . emit ( self . download_counter , self . skip_counter , self . cancellation_event . is_set ( ) )
2025-05-06 22:49:19 +05:30
2025-05-07 07:20:40 +05:30
2025-05-06 22:08:27 +05:30
def set_ui_enabled ( self , enabled ) :
2025-05-08 19:49:50 +05:30
widgets_to_toggle = [
2025-05-09 19:03:01 +05:30
self . download_btn , self . link_input ,
self . radio_all , self . radio_images , self . radio_videos , self . radio_only_links ,
2025-05-08 19:49:50 +05:30
self . skip_zip_checkbox , self . skip_rar_checkbox ,
self . use_subfolders_checkbox , self . compress_images_checkbox ,
self . download_thumbnails_checkbox , self . use_multithreading_checkbox ,
self . skip_words_input , self . character_search_input , self . new_char_input ,
2025-05-09 19:03:01 +05:30
self . add_char_button , self . delete_char_button ,
2025-05-08 19:49:50 +05:30
self . start_page_input , self . end_page_input , self . page_range_label , self . to_label ,
self . character_input , self . custom_folder_input , self . custom_folder_label ,
2025-05-09 19:03:01 +05:30
self . reset_button ,
2025-05-08 19:49:50 +05:30
self . manga_mode_checkbox
]
for widget in widgets_to_toggle :
2025-05-09 19:03:01 +05:30
if widget :
2025-05-08 19:49:50 +05:30
widget . setEnabled ( enabled )
2025-05-09 19:03:01 +05:30
2025-05-08 19:49:50 +05:30
if self . external_links_checkbox :
2025-05-09 19:03:01 +05:30
is_only_links = self . radio_only_links and self . radio_only_links . isChecked ( )
self . external_links_checkbox . setEnabled ( not is_only_links )
2025-05-08 19:49:50 +05:30
if self . log_verbosity_button :
self . log_verbosity_button . setEnabled ( True )
2025-05-09 19:03:01 +05:30
2025-05-08 19:49:50 +05:30
multithreading_currently_on = self . use_multithreading_checkbox . isChecked ( )
self . thread_count_input . setEnabled ( enabled and multithreading_currently_on )
self . thread_count_label . setEnabled ( enabled and multithreading_currently_on )
2025-05-08 22:13:12 +05:30
2025-05-09 19:03:01 +05:30
2025-05-08 22:13:12 +05:30
subfolders_currently_on = self . use_subfolders_checkbox . isChecked ( )
self . use_subfolder_per_post_checkbox . setEnabled ( enabled and subfolders_currently_on )
2025-05-08 19:49:50 +05:30
2025-05-09 19:03:01 +05:30
self . cancel_btn . setEnabled ( not enabled )
2025-05-08 19:49:50 +05:30
2025-05-09 19:03:01 +05:30
if enabled :
self . _handle_filter_mode_change ( self . radio_group . checkedButton ( ) , True )
self . _handle_multithreading_toggle ( multithreading_currently_on )
2025-05-08 19:49:50 +05:30
2025-05-07 07:20:40 +05:30
2025-05-05 19:35:24 +05:30
def cancel_download ( self ) :
2025-05-09 19:03:01 +05:30
if not self . cancel_btn . isEnabled ( ) and not self . cancellation_event . is_set ( ) :
2025-05-08 19:49:50 +05:30
self . log_signal . emit ( " ℹ ️ No active download to cancel or already cancelling." )
return
2025-05-06 22:08:27 +05:30
2025-05-08 19:49:50 +05:30
self . log_signal . emit ( " ⚠️ Requesting cancellation of download process... " )
2025-05-09 19:03:01 +05:30
self . cancellation_event . set ( )
2025-05-08 19:49:50 +05:30
if self . download_thread and self . download_thread . isRunning ( ) :
2025-05-09 19:03:01 +05:30
self . download_thread . requestInterruption ( )
2025-05-08 19:49:50 +05:30
self . log_signal . emit ( " Signaled single download thread to interrupt. " )
if self . thread_pool :
self . log_signal . emit ( " Initiating immediate shutdown and cancellation of worker pool tasks... " )
self . thread_pool . shutdown ( wait = False , cancel_futures = True )
2025-05-09 19:03:01 +05:30
2025-05-08 19:49:50 +05:30
self . external_link_queue . clear ( )
2025-05-09 19:03:01 +05:30
self . _is_processing_external_link_queue = False
self . _current_link_post_title = None
2025-05-08 19:49:50 +05:30
2025-05-09 19:03:01 +05:30
self . cancel_btn . setEnabled ( False )
2025-05-06 22:08:27 +05:30
self . progress_label . setText ( " Progress: Cancelling... " )
2025-05-08 19:49:50 +05:30
self . file_progress_label . setText ( " " )
def download_finished ( self , total_downloaded , total_skipped , cancelled_by_user ) :
status_message = " Cancelled by user " if cancelled_by_user else " Finished "
self . log_signal . emit ( " = " * 40 + f " \n 🏁 Download { status_message } ! \n Summary: Downloaded Files= { total_downloaded } , Skipped Files= { total_skipped } \n " + " = " * 40 )
self . progress_label . setText ( f " { status_message } : { total_downloaded } downloaded, { total_skipped } skipped. " )
2025-05-09 19:03:01 +05:30
self . file_progress_label . setText ( " " )
2025-05-08 19:49:50 +05:30
if not cancelled_by_user :
self . _try_process_next_external_link ( )
2025-05-06 22:08:27 +05:30
if self . download_thread :
2025-05-09 19:03:01 +05:30
try :
2025-05-08 19:49:50 +05:30
if hasattr ( self . download_thread , ' progress_signal ' ) : self . download_thread . progress_signal . disconnect ( self . handle_main_log )
if hasattr ( self . download_thread , ' add_character_prompt_signal ' ) : self . download_thread . add_character_prompt_signal . disconnect ( self . add_character_prompt_signal )
if hasattr ( self . download_thread , ' finished_signal ' ) : self . download_thread . finished_signal . disconnect ( self . finished_signal )
if hasattr ( self . download_thread , ' receive_add_character_result ' ) : self . character_prompt_response_signal . disconnect ( self . download_thread . receive_add_character_result )
if hasattr ( self . download_thread , ' external_link_signal ' ) : self . download_thread . external_link_signal . disconnect ( self . handle_external_link_signal )
if hasattr ( self . download_thread , ' file_progress_signal ' ) : self . download_thread . file_progress_signal . disconnect ( self . update_file_progress_display )
2025-05-09 19:03:01 +05:30
except ( TypeError , RuntimeError ) as e :
2025-05-08 19:49:50 +05:30
self . log_signal . emit ( f " ℹ ️ Note during single-thread signal disconnection: { e } " )
2025-05-09 19:03:01 +05:30
self . download_thread = None
2025-05-08 19:49:50 +05:30
2025-05-06 22:08:27 +05:30
if self . thread_pool :
2025-05-08 19:49:50 +05:30
self . log_signal . emit ( " Ensuring worker thread pool is shut down... " )
2025-05-09 19:03:01 +05:30
self . thread_pool . shutdown ( wait = True , cancel_futures = True )
2025-05-06 22:08:27 +05:30
self . thread_pool = None
2025-05-09 19:03:01 +05:30
self . active_futures = [ ]
2025-05-08 19:49:50 +05:30
2025-05-09 19:03:01 +05:30
self . set_ui_enabled ( True )
self . cancel_btn . setEnabled ( False )
2025-05-08 19:49:50 +05:30
def toggle_log_verbosity ( self ) :
self . basic_log_mode = not self . basic_log_mode
if self . basic_log_mode :
self . log_verbosity_button . setText ( " Show Full Log " )
self . log_signal . emit ( " = " * 20 + " Basic Log Mode Enabled " + " = " * 20 )
else :
self . log_verbosity_button . setText ( " Show Basic Log " )
self . log_signal . emit ( " = " * 20 + " Full Log Mode Enabled " + " = " * 20 )
def reset_application_state ( self ) :
is_running = ( self . download_thread and self . download_thread . isRunning ( ) ) or \
( self . thread_pool is not None and any ( not f . done ( ) for f in self . active_futures if f is not None ) )
if is_running :
QMessageBox . warning ( self , " Reset Error " , " Cannot reset while a download is in progress. Please cancel the download first. " )
return
self . log_signal . emit ( " 🔄 Resetting application state to defaults... " )
2025-05-09 19:03:01 +05:30
self . _reset_ui_to_defaults ( )
2025-05-08 19:49:50 +05:30
self . main_log_output . clear ( )
self . external_log_output . clear ( )
2025-05-09 19:03:01 +05:30
if self . show_external_links :
self . external_log_output . append ( " 🔗 External Links Found: " )
2025-05-08 19:49:50 +05:30
self . external_link_queue . clear ( )
2025-05-09 19:03:01 +05:30
self . extracted_links_cache = [ ]
2025-05-08 19:49:50 +05:30
self . _is_processing_external_link_queue = False
2025-05-09 19:03:01 +05:30
self . _current_link_post_title = None
2025-05-08 19:49:50 +05:30
self . progress_label . setText ( " Progress: Idle " )
self . file_progress_label . setText ( " " )
with self . downloaded_files_lock :
count = len ( self . downloaded_files )
self . downloaded_files . clear ( )
if count > 0 : self . log_signal . emit ( f " Cleared { count } downloaded filename(s) from session memory. " )
with self . downloaded_file_hashes_lock :
count = len ( self . downloaded_file_hashes )
self . downloaded_file_hashes . clear ( )
if count > 0 : self . log_signal . emit ( f " Cleared { count } downloaded file hash(es) from session memory. " )
self . total_posts_to_process = 0
self . processed_posts_count = 0
self . download_counter = 0
self . skip_counter = 0
2025-05-09 19:03:01 +05:30
self . cancellation_event . clear ( )
2025-05-08 19:49:50 +05:30
self . basic_log_mode = False
if self . log_verbosity_button :
self . log_verbosity_button . setText ( " Show Basic Log " )
self . log_signal . emit ( " ✅ Application reset complete. " )
def _reset_ui_to_defaults ( self ) :
self . link_input . clear ( )
self . dir_input . clear ( )
self . custom_folder_input . clear ( )
self . character_input . clear ( )
self . skip_words_input . clear ( )
self . start_page_input . clear ( )
self . end_page_input . clear ( )
self . new_char_input . clear ( )
self . character_search_input . clear ( )
self . thread_count_input . setText ( " 4 " )
self . radio_all . setChecked ( True )
self . skip_zip_checkbox . setChecked ( True )
self . skip_rar_checkbox . setChecked ( True )
self . download_thumbnails_checkbox . setChecked ( False )
self . compress_images_checkbox . setChecked ( False )
2025-05-09 19:03:01 +05:30
self . use_subfolders_checkbox . setChecked ( True )
2025-05-08 19:49:50 +05:30
self . use_subfolder_per_post_checkbox . setChecked ( False )
2025-05-09 19:03:01 +05:30
self . use_multithreading_checkbox . setChecked ( True )
self . external_links_checkbox . setChecked ( False )
2025-05-08 19:49:50 +05:30
if self . manga_mode_checkbox :
2025-05-09 19:03:01 +05:30
self . manga_mode_checkbox . setChecked ( False )
2025-05-08 19:49:50 +05:30
2025-05-09 19:03:01 +05:30
self . _handle_filter_mode_change ( self . radio_all , True )
2025-05-08 22:13:12 +05:30
self . _handle_multithreading_toggle ( self . use_multithreading_checkbox . isChecked ( ) )
2025-05-08 19:49:50 +05:30
2025-05-09 19:03:01 +05:30
self . filter_character_list ( " " )
2025-05-08 19:49:50 +05:30
self . download_btn . setEnabled ( True )
2025-05-05 19:35:24 +05:30
self . cancel_btn . setEnabled ( False )
2025-05-08 19:49:50 +05:30
if self . reset_button : self . reset_button . setEnabled ( True )
if self . log_verbosity_button : self . log_verbosity_button . setText ( " Show Basic Log " )
2025-05-06 22:08:27 +05:30
2025-05-07 07:20:40 +05:30
def prompt_add_character ( self , character_name ) :
2025-05-09 19:03:01 +05:30
global KNOWN_NAMES
2025-05-08 19:49:50 +05:30
reply = QMessageBox . question ( self , " Add Filter Name to Known List? " ,
f " The name ' { character_name } ' was encountered or used as a filter. \n It ' s not in your known names list (used for folder suggestions). \n Add it now? " ,
2025-05-06 22:08:27 +05:30
QMessageBox . Yes | QMessageBox . No , QMessageBox . Yes )
2025-05-07 07:20:40 +05:30
result = ( reply == QMessageBox . Yes )
if result :
2025-05-09 19:03:01 +05:30
self . new_char_input . setText ( character_name )
if self . add_new_character ( ) :
2025-05-08 19:49:50 +05:30
self . log_signal . emit ( f " ✅ Added ' { character_name } ' to known names via background prompt. " )
2025-05-06 22:08:27 +05:30
else :
2025-05-09 19:03:01 +05:30
result = False
2025-05-08 19:49:50 +05:30
self . log_signal . emit ( f " ℹ ️ Adding ' { character_name } ' via background prompt was declined or failed (e.g., similarity warning, duplicate). " )
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 ) :
2025-05-09 19:03:01 +05:30
with QMutexLocker ( self . prompt_mutex ) :
2025-05-06 22:08:27 +05:30
self . _add_character_response = result
2025-05-08 19:49:50 +05:30
self . log_signal . emit ( f " Main thread received character prompt response: { ' Action resulted in addition/confirmation ' if result else ' Action resulted in no addition/declined ' } " )
2025-05-06 22:08:27 +05:30
2025-05-05 19:35:24 +05:30
if __name__ == ' __main__ ' :
2025-05-09 19:03:01 +05:30
import traceback
2025-05-08 19:49:50 +05:30
try :
qt_app = QApplication ( sys . argv )
2025-05-09 19:03:01 +05:30
if getattr ( sys , ' frozen ' , False ) :
2025-05-08 19:49:50 +05:30
base_dir = sys . _MEIPASS
else :
base_dir = os . path . dirname ( os . path . abspath ( __file__ ) )
2025-05-09 19:03:01 +05:30
2025-05-08 19:49:50 +05:30
icon_path = os . path . join ( base_dir , ' Kemono.ico ' )
if os . path . exists ( icon_path ) :
qt_app . setWindowIcon ( QIcon ( icon_path ) )
else :
print ( f " Warning: Application icon ' Kemono.ico ' not found at { icon_path } " )
downloader_app_instance = DownloaderApp ( )
downloader_app_instance . show ( )
2025-05-09 19:03:01 +05:30
# --- ADDED: Show Tour Dialog if needed ---
if TourDialog : # Check if TourDialog was imported successfully
tour_result = TourDialog . run_tour_if_needed ( downloader_app_instance )
if tour_result == QDialog . Accepted :
print ( " Tour completed by user. " )
elif tour_result == QDialog . Rejected :
# This means tour was skipped OR already shown.
# You can use TourDialog.settings.value(TourDialog.TOUR_SHOWN_KEY)
# to differentiate if needed, but run_tour_if_needed handles the "show once" logic.
print ( " Tour skipped or was already shown. " )
# --- END ADDED ---
2025-05-08 19:49:50 +05:30
exit_code = qt_app . exec_ ( )
2025-05-09 19:03:01 +05:30
print ( f " Application finished with exit code: { exit_code } " )
2025-05-08 19:49:50 +05:30
sys . exit ( exit_code )
except SystemExit :
pass # Allow clean exit
except Exception as e :
print ( " --- CRITICAL APPLICATION ERROR --- " )
print ( f " An unhandled exception occurred: { e } " )
2025-05-09 19:03:01 +05:30
traceback . print_exc ( )
2025-05-08 19:49:50 +05:30
print ( " --- END CRITICAL ERROR --- " )
2025-05-09 19:03:01 +05:30
sys . exit ( 1 )