mirror of
https://github.com/Yuvi9587/Kemono-Downloader.git
synced 2025-12-17 15:36:51 +00:00
Commit
This commit is contained in:
parent
b78d543f16
commit
c8b77fb0d7
29
main.py
29
main.py
@ -9,24 +9,26 @@ from PyQt5.QtWidgets import QApplication, QDialog
|
|||||||
from PyQt5.QtCore import QCoreApplication
|
from PyQt5.QtCore import QCoreApplication
|
||||||
|
|
||||||
# --- Local Application Imports ---
|
# --- Local Application Imports ---
|
||||||
# These imports reflect the new, organized project structure.
|
|
||||||
from src.ui.main_window import DownloaderApp
|
from src.ui.main_window import DownloaderApp
|
||||||
from src.ui.dialogs.TourDialog import TourDialog
|
from src.ui.dialogs.TourDialog import TourDialog
|
||||||
from src.config.constants import CONFIG_ORGANIZATION_NAME, CONFIG_APP_NAME_MAIN
|
from src.config.constants import CONFIG_ORGANIZATION_NAME, CONFIG_APP_NAME_MAIN
|
||||||
|
|
||||||
|
# --- Define APP_BASE_DIR globally and make available early ---
|
||||||
|
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
|
||||||
|
APP_BASE_DIR = sys._MEIPASS
|
||||||
|
else:
|
||||||
|
APP_BASE_DIR = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
|
# Optional: Set a global variable or pass it into modules if needed
|
||||||
|
# Or re-export it via constants.py for cleaner imports
|
||||||
|
|
||||||
def handle_uncaught_exception(exc_type, exc_value, exc_traceback):
|
def handle_uncaught_exception(exc_type, exc_value, exc_traceback):
|
||||||
"""
|
"""
|
||||||
Handles uncaught exceptions by logging them to a file for easier debugging,
|
Handles uncaught exceptions by logging them to a file for easier debugging,
|
||||||
especially for bundled applications.
|
especially for bundled applications.
|
||||||
"""
|
"""
|
||||||
# Determine the base directory for logging
|
# Use APP_BASE_DIR to determine logging location
|
||||||
if getattr(sys, 'frozen', False):
|
log_dir = os.path.join(APP_BASE_DIR, "logs")
|
||||||
base_dir_for_log = os.path.dirname(sys.executable)
|
|
||||||
else:
|
|
||||||
base_dir_for_log = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
|
|
||||||
log_dir = os.path.join(base_dir_for_log, "logs")
|
|
||||||
log_file_path = os.path.join(log_dir, "uncaught_exceptions.log")
|
log_file_path = os.path.join(log_dir, "uncaught_exceptions.log")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -57,41 +59,35 @@ def main():
|
|||||||
|
|
||||||
qt_app = QApplication(sys.argv)
|
qt_app = QApplication(sys.argv)
|
||||||
|
|
||||||
# Create the main application window from its new module
|
# Create the main application window
|
||||||
downloader_app_instance = DownloaderApp()
|
downloader_app_instance = DownloaderApp()
|
||||||
|
|
||||||
# --- Window Sizing and Positioning ---
|
# --- Window Sizing and Positioning ---
|
||||||
# Logic moved from the old main.py to set an appropriate initial size
|
|
||||||
primary_screen = QApplication.primaryScreen()
|
primary_screen = QApplication.primaryScreen()
|
||||||
if not primary_screen:
|
if not primary_screen:
|
||||||
# Fallback for systems with no primary screen detected
|
|
||||||
downloader_app_instance.resize(1024, 768)
|
downloader_app_instance.resize(1024, 768)
|
||||||
else:
|
else:
|
||||||
available_geo = primary_screen.availableGeometry()
|
available_geo = primary_screen.availableGeometry()
|
||||||
screen_width = available_geo.width()
|
screen_width = available_geo.width()
|
||||||
screen_height = available_geo.height()
|
screen_height = available_geo.height()
|
||||||
|
|
||||||
# Define minimums and desired ratios
|
|
||||||
min_app_width, min_app_height = 960, 680
|
min_app_width, min_app_height = 960, 680
|
||||||
desired_width_ratio, desired_height_ratio = 0.80, 0.85
|
desired_width_ratio, desired_height_ratio = 0.80, 0.85
|
||||||
|
|
||||||
app_width = max(min_app_width, int(screen_width * desired_width_ratio))
|
app_width = max(min_app_width, int(screen_width * desired_width_ratio))
|
||||||
app_height = max(min_app_height, int(screen_height * desired_height_ratio))
|
app_height = max(min_app_height, int(screen_height * desired_height_ratio))
|
||||||
|
|
||||||
# Ensure the window is not larger than the screen
|
|
||||||
app_width = min(app_width, screen_width)
|
app_width = min(app_width, screen_width)
|
||||||
app_height = min(app_height, screen_height)
|
app_height = min(app_height, screen_height)
|
||||||
|
|
||||||
downloader_app_instance.resize(app_width, app_height)
|
downloader_app_instance.resize(app_width, app_height)
|
||||||
|
|
||||||
# Show the main window and center it
|
# Show and center the main window
|
||||||
downloader_app_instance.show()
|
downloader_app_instance.show()
|
||||||
if hasattr(downloader_app_instance, '_center_on_screen'):
|
if hasattr(downloader_app_instance, '_center_on_screen'):
|
||||||
downloader_app_instance._center_on_screen()
|
downloader_app_instance._center_on_screen()
|
||||||
|
|
||||||
# --- First-Run Welcome Tour ---
|
# --- First-Run Welcome Tour ---
|
||||||
# Check if the tour should be shown and run it.
|
|
||||||
# This static method call keeps the logic clean and contained.
|
|
||||||
if TourDialog.should_show_tour():
|
if TourDialog.should_show_tour():
|
||||||
tour_dialog = TourDialog(parent_app=downloader_app_instance)
|
tour_dialog = TourDialog(parent_app=downloader_app_instance)
|
||||||
tour_dialog.exec_()
|
tour_dialog.exec_()
|
||||||
@ -102,7 +98,6 @@ def main():
|
|||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
except SystemExit:
|
except SystemExit:
|
||||||
# Allow sys.exit() to work as intended
|
|
||||||
pass
|
pass
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("--- CRITICAL APPLICATION STARTUP ERROR ---")
|
print("--- CRITICAL APPLICATION STARTUP ERROR ---")
|
||||||
|
|||||||
@ -9,6 +9,7 @@ STYLE_ORIGINAL_NAME = "original_name"
|
|||||||
STYLE_DATE_BASED = "date_based"
|
STYLE_DATE_BASED = "date_based"
|
||||||
STYLE_DATE_POST_TITLE = "date_post_title"
|
STYLE_DATE_POST_TITLE = "date_post_title"
|
||||||
STYLE_POST_TITLE_GLOBAL_NUMBERING = "post_title_global_numbering"
|
STYLE_POST_TITLE_GLOBAL_NUMBERING = "post_title_global_numbering"
|
||||||
|
STYLE_POST_ID = "post_id" # Add this line
|
||||||
MANGA_DATE_PREFIX_DEFAULT = ""
|
MANGA_DATE_PREFIX_DEFAULT = ""
|
||||||
|
|
||||||
# --- Download Scopes ---
|
# --- Download Scopes ---
|
||||||
@ -94,6 +95,7 @@ FOLDER_NAME_STOP_WORDS = {
|
|||||||
"me", "my", "net", "not", "of", "on", "or", "org", "our",
|
"me", "my", "net", "not", "of", "on", "or", "org", "our",
|
||||||
"s", "she", "so", "the", "their", "they", "this",
|
"s", "she", "so", "the", "their", "they", "this",
|
||||||
"to", "ve", "was", "we", "were", "with", "www", "you", "your",
|
"to", "ve", "was", "we", "were", "with", "www", "you", "your",
|
||||||
|
# add more according to need
|
||||||
}
|
}
|
||||||
|
|
||||||
# Additional words to ignore specifically for creator-level downloads
|
# Additional words to ignore specifically for creator-level downloads
|
||||||
@ -107,4 +109,5 @@ CREATOR_DOWNLOAD_DEFAULT_FOLDER_IGNORE_WORDS = {
|
|||||||
"oct", "october", "nov", "november", "dec", "december",
|
"oct", "october", "nov", "november", "dec", "december",
|
||||||
"mon", "monday", "tue", "tuesday", "wed", "wednesday", "thu", "thursday",
|
"mon", "monday", "tue", "tuesday", "wed", "wednesday", "thu", "thursday",
|
||||||
"fri", "friday", "sat", "saturday", "sun", "sunday"
|
"fri", "friday", "sat", "saturday", "sun", "sunday"
|
||||||
|
# add more according to need
|
||||||
}
|
}
|
||||||
|
|||||||
@ -167,6 +167,7 @@ class PostProcessorWorker:
|
|||||||
if self .dynamic_filter_holder :
|
if self .dynamic_filter_holder :
|
||||||
return self .dynamic_filter_holder .get_filters ()
|
return self .dynamic_filter_holder .get_filters ()
|
||||||
return self .filter_character_list_objects_initial
|
return self .filter_character_list_objects_initial
|
||||||
|
|
||||||
def _download_single_file (self ,file_info ,target_folder_path ,headers ,original_post_id_for_log ,skip_event ,
|
def _download_single_file (self ,file_info ,target_folder_path ,headers ,original_post_id_for_log ,skip_event ,
|
||||||
post_title ="",file_index_in_post =0 ,num_files_in_this_post =1 ,
|
post_title ="",file_index_in_post =0 ,num_files_in_this_post =1 ,
|
||||||
manga_date_file_counter_ref =None ,
|
manga_date_file_counter_ref =None ,
|
||||||
@ -273,6 +274,15 @@ class PostProcessorWorker:
|
|||||||
self .logger (f"⚠️ Manga Title+GlobalNum Mode: Counter ref not provided or malformed for '{api_original_filename }'. Using original. Ref: {manga_global_file_counter_ref }")
|
self .logger (f"⚠️ Manga Title+GlobalNum Mode: Counter ref not provided or malformed for '{api_original_filename }'. Using original. Ref: {manga_global_file_counter_ref }")
|
||||||
filename_to_save_in_main_path =cleaned_original_api_filename
|
filename_to_save_in_main_path =cleaned_original_api_filename
|
||||||
self .logger (f"⚠️ Manga mode (Title+GlobalNum Style Fallback): Using cleaned original filename '{filename_to_save_in_main_path }' for post {original_post_id_for_log }.")
|
self .logger (f"⚠️ Manga mode (Title+GlobalNum Style Fallback): Using cleaned original filename '{filename_to_save_in_main_path }' for post {original_post_id_for_log }.")
|
||||||
|
elif self.manga_filename_style == STYLE_POST_ID:
|
||||||
|
if original_post_id_for_log and original_post_id_for_log != 'unknown_id':
|
||||||
|
base_name = str(original_post_id_for_log)
|
||||||
|
# Always append the file index for consistency (e.g., xxxxxx_0, xxxxxx_1)
|
||||||
|
filename_to_save_in_main_path = f"{base_name}_{file_index_in_post}{original_ext}"
|
||||||
|
else:
|
||||||
|
# Fallback if post_id is somehow not available
|
||||||
|
self.logger(f"⚠️ Manga mode (Post ID Style): Post ID missing. Using cleaned original filename '{cleaned_original_api_filename}'.")
|
||||||
|
filename_to_save_in_main_path = cleaned_original_api_filename
|
||||||
elif self .manga_filename_style ==STYLE_DATE_POST_TITLE :
|
elif self .manga_filename_style ==STYLE_DATE_POST_TITLE :
|
||||||
published_date_str =self .post .get ('published')
|
published_date_str =self .post .get ('published')
|
||||||
added_date_str =self .post .get ('added')
|
added_date_str =self .post .get ('added')
|
||||||
@ -717,7 +727,6 @@ class PostProcessorWorker:
|
|||||||
if data_to_write_io and hasattr (data_to_write_io ,'close'):
|
if data_to_write_io and hasattr (data_to_write_io ,'close'):
|
||||||
data_to_write_io .close ()
|
data_to_write_io .close ()
|
||||||
|
|
||||||
|
|
||||||
def process (self ):
|
def process (self ):
|
||||||
if self ._check_pause (f"Post processing for ID {self .post .get ('id','N/A')}"):return 0 ,0 ,[],[],[],None
|
if self ._check_pause (f"Post processing for ID {self .post .get ('id','N/A')}"):return 0 ,0 ,[],[],[],None
|
||||||
if self .check_cancel ():return 0 ,0 ,[],[],[],None
|
if self .check_cancel ():return 0 ,0 ,[],[],[],None
|
||||||
|
|||||||
@ -22,13 +22,17 @@ def get_app_icon_object():
|
|||||||
if _app_icon_cache and not _app_icon_cache.isNull():
|
if _app_icon_cache and not _app_icon_cache.isNull():
|
||||||
return _app_icon_cache
|
return _app_icon_cache
|
||||||
|
|
||||||
|
# Declare a single variable to hold the base directory path.
|
||||||
|
app_base_dir = ""
|
||||||
|
|
||||||
# Determine the project's base directory, whether running from source or as a bundled app
|
# Determine the project's base directory, whether running from source or as a bundled app
|
||||||
if getattr(sys, 'frozen', False):
|
if getattr(sys, 'frozen', False):
|
||||||
# The application is frozen (e.g., with PyInstaller)
|
# The application is frozen (e.g., with PyInstaller).
|
||||||
base_dir = os.path.dirname(sys.executable)
|
# The base directory is the one containing the executable.
|
||||||
|
app_base_dir = os.path.dirname(sys.executable)
|
||||||
else:
|
else:
|
||||||
# The application is running from a .py file
|
# The application is running from a .py file.
|
||||||
# This path navigates up from src/ui/ to the project root
|
# This path navigates up from src/ui/assets.py to the project root.
|
||||||
app_base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
app_base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||||
|
|
||||||
icon_path = os.path.join(app_base_dir, 'assets', 'Kemono.ico')
|
icon_path = os.path.join(app_base_dir, 'assets', 'Kemono.ico')
|
||||||
@ -36,6 +40,13 @@ def get_app_icon_object():
|
|||||||
if os.path.exists(icon_path):
|
if os.path.exists(icon_path):
|
||||||
_app_icon_cache = QIcon(icon_path)
|
_app_icon_cache = QIcon(icon_path)
|
||||||
else:
|
else:
|
||||||
|
# If the icon isn't found, especially in a frozen app, check the _MEIPASS directory as a fallback.
|
||||||
|
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
|
||||||
|
fallback_icon_path = os.path.join(sys._MEIPASS, 'assets', 'Kemono.ico')
|
||||||
|
if os.path.exists(fallback_icon_path):
|
||||||
|
_app_icon_cache = QIcon(fallback_icon_path)
|
||||||
|
return _app_icon_cache
|
||||||
|
|
||||||
print(f"Warning: Application icon not found at {icon_path}")
|
print(f"Warning: Application icon not found at {icon_path}")
|
||||||
_app_icon_cache = QIcon() # Return an empty icon as a fallback
|
_app_icon_cache = QIcon() # Return an empty icon as a fallback
|
||||||
|
|
||||||
|
|||||||
@ -92,171 +92,177 @@ class DownloaderApp (QWidget ):
|
|||||||
file_progress_signal =pyqtSignal (str ,object )
|
file_progress_signal =pyqtSignal (str ,object )
|
||||||
|
|
||||||
|
|
||||||
def __init__ (self ):
|
def __init__(self):
|
||||||
super ().__init__ ()
|
super().__init__()
|
||||||
self .settings =QSettings (CONFIG_ORGANIZATION_NAME ,CONFIG_APP_NAME_MAIN )
|
self.settings = QSettings(CONFIG_ORGANIZATION_NAME, CONFIG_APP_NAME_MAIN)
|
||||||
if getattr (sys ,'frozen',False ):
|
|
||||||
self .app_base_dir =os .path .dirname (sys .executable )
|
|
||||||
else :
|
|
||||||
self.app_base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
|
||||||
self .config_file =os .path .join (self .app_base_dir ,"appdata","Known.txt")
|
|
||||||
|
|
||||||
self .download_thread =None
|
# --- CORRECT PATH DEFINITION ---
|
||||||
self .thread_pool =None
|
# This block correctly determines the application's base directory whether
|
||||||
self .cancellation_event =threading .Event ()
|
# it's running from source or as a frozen executable.
|
||||||
self.session_file_path = os.path.join(self.app_base_dir, "appdata","session.json")
|
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
|
||||||
|
# Path for PyInstaller one-file bundle
|
||||||
|
self.app_base_dir = os.path.dirname(sys.executable)
|
||||||
|
else:
|
||||||
|
# Path for running from source code
|
||||||
|
self.app_base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||||
|
|
||||||
|
# All file paths will now correctly use the single, correct app_base_dir
|
||||||
|
self.config_file = os.path.join(self.app_base_dir, "appdata", "Known.txt")
|
||||||
|
self.session_file_path = os.path.join(self.app_base_dir, "appdata", "session.json")
|
||||||
|
self.persistent_history_file = os.path.join(self.app_base_dir, "appdata", "download_history.json")
|
||||||
|
|
||||||
|
self.download_thread = None
|
||||||
|
self.thread_pool = None
|
||||||
|
self.cancellation_event = threading.Event()
|
||||||
self.session_lock = threading.Lock()
|
self.session_lock = threading.Lock()
|
||||||
self.interrupted_session_data = None
|
self.interrupted_session_data = None
|
||||||
self.is_restore_pending = False
|
self.is_restore_pending = False
|
||||||
self .external_link_download_thread =None
|
self.external_link_download_thread = None
|
||||||
self .pause_event =threading .Event ()
|
self.pause_event = threading.Event()
|
||||||
self .active_futures =[]
|
self.active_futures = []
|
||||||
self .total_posts_to_process =0
|
self.total_posts_to_process = 0
|
||||||
self .dynamic_character_filter_holder =DynamicFilterHolder ()
|
self.dynamic_character_filter_holder = DynamicFilterHolder()
|
||||||
self .processed_posts_count =0
|
self.processed_posts_count = 0
|
||||||
self .creator_name_cache ={}
|
self.creator_name_cache = {}
|
||||||
self .log_signal .emit (f"ℹ️ App base directory: {self .app_base_dir }")
|
self.log_signal.emit(f"ℹ️ App base directory: {self.app_base_dir}")
|
||||||
|
self.log_signal.emit(f"ℹ️ Persistent history file path set to: {self.persistent_history_file}")
|
||||||
|
|
||||||
self.app_base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
# --- The rest of your __init__ method continues from here ---
|
||||||
self.persistent_history_file = os.path.join(self.app_base_dir, "appdata", "download_history.json")
|
self.last_downloaded_files_details = deque(maxlen=3)
|
||||||
self .last_downloaded_files_details =deque (maxlen =3 )
|
self.download_history_candidates = deque(maxlen=8)
|
||||||
self .download_history_candidates =deque (maxlen =8 )
|
self.final_download_history_entries = []
|
||||||
self .log_signal .emit (f"ℹ️ Persistent history file path set to: {self .persistent_history_file }")
|
self.favorite_download_queue = deque()
|
||||||
self .final_download_history_entries =[]
|
self.is_processing_favorites_queue = False
|
||||||
self .favorite_download_queue =deque ()
|
self.download_counter = 0
|
||||||
self .is_processing_favorites_queue =False
|
self.permanently_failed_files_for_dialog = []
|
||||||
self .download_counter =0
|
self.last_link_input_text_for_queue_sync = ""
|
||||||
self .favorite_download_queue =deque ()
|
self.is_fetcher_thread_running = False
|
||||||
self .permanently_failed_files_for_dialog =[]
|
self._restart_pending = False
|
||||||
self .last_link_input_text_for_queue_sync =""
|
self.download_history_log = deque(maxlen=50)
|
||||||
self .is_fetcher_thread_running =False
|
self.skip_counter = 0
|
||||||
self ._restart_pending =False
|
self.all_kept_original_filenames = []
|
||||||
self .is_processing_favorites_queue =False
|
self.cancellation_message_logged_this_session = False
|
||||||
self .download_history_log =deque (maxlen =50 )
|
self.favorite_scope_toggle_button = None
|
||||||
self .skip_counter =0
|
self.favorite_download_scope = FAVORITE_SCOPE_SELECTED_LOCATION
|
||||||
self .all_kept_original_filenames =[]
|
self.manga_mode_checkbox = None
|
||||||
self .cancellation_message_logged_this_session =False
|
self.selected_cookie_filepath = None
|
||||||
self .favorite_scope_toggle_button =None
|
self.retryable_failed_files_info = []
|
||||||
self .favorite_download_scope =FAVORITE_SCOPE_SELECTED_LOCATION
|
self.is_paused = False
|
||||||
|
self.worker_to_gui_queue = queue.Queue()
|
||||||
self .manga_mode_checkbox =None
|
self.gui_update_timer = QTimer(self)
|
||||||
|
self.actual_gui_signals = PostProcessorSignals()
|
||||||
self .selected_cookie_filepath =None
|
self.worker_signals = PostProcessorSignals()
|
||||||
self .retryable_failed_files_info =[]
|
self.prompt_mutex = QMutex()
|
||||||
|
self._add_character_response = None
|
||||||
self .is_paused =False
|
self._original_scan_content_tooltip = ("If checked, the downloader will scan the HTML content of posts for image URLs (from <img> tags or direct links).\n"
|
||||||
self .worker_to_gui_queue =queue .Queue ()
|
|
||||||
self .gui_update_timer =QTimer (self )
|
|
||||||
self .actual_gui_signals =PostProcessorSignals ()
|
|
||||||
|
|
||||||
self .worker_signals =PostProcessorSignals ()
|
|
||||||
self .prompt_mutex =QMutex ()
|
|
||||||
self ._add_character_response =None
|
|
||||||
|
|
||||||
self ._original_scan_content_tooltip =("If checked, the downloader will scan the HTML content of posts for image URLs (from <img> tags or direct links).\n"
|
|
||||||
"now This includes resolving relative paths from <img> tags to full URLs.\n"
|
"now This includes resolving relative paths from <img> tags to full URLs.\n"
|
||||||
"Relative paths in <img> tags (e.g., /data/image.jpg) will be resolved to full URLs.\n"
|
"Relative paths in <img> tags (e.g., /data/image.jpg) will be resolved to full URLs.\n"
|
||||||
"Useful for cases where images are in the post description but not in the API's file/attachment list.")
|
"Useful for cases where images are in the post description but not in the API's file/attachment list.")
|
||||||
|
self.downloaded_files = set()
|
||||||
|
self.downloaded_files_lock = threading.Lock()
|
||||||
|
self.downloaded_file_hashes = set()
|
||||||
|
self.downloaded_file_hashes_lock = threading.Lock()
|
||||||
|
self.show_external_links = False
|
||||||
|
self.external_link_queue = deque()
|
||||||
|
self._is_processing_external_link_queue = False
|
||||||
|
self._current_link_post_title = None
|
||||||
|
self.extracted_links_cache = []
|
||||||
|
self.manga_rename_toggle_button = None
|
||||||
|
self.favorite_mode_checkbox = None
|
||||||
|
self.url_or_placeholder_stack = None
|
||||||
|
self.url_input_widget = None
|
||||||
|
self.url_placeholder_widget = None
|
||||||
|
self.favorite_action_buttons_widget = None
|
||||||
|
self.favorite_mode_artists_button = None
|
||||||
|
self.favorite_mode_posts_button = None
|
||||||
|
self.standard_action_buttons_widget = None
|
||||||
|
self.bottom_action_buttons_stack = None
|
||||||
|
self.main_log_output = None
|
||||||
|
self.external_log_output = None
|
||||||
|
self.log_splitter = None
|
||||||
|
self.main_splitter = None
|
||||||
|
self.reset_button = None
|
||||||
|
self.progress_log_label = None
|
||||||
|
self.log_verbosity_toggle_button = None
|
||||||
|
self.missed_character_log_output = None
|
||||||
|
self.log_view_stack = None
|
||||||
|
self.current_log_view = 'progress'
|
||||||
|
self.link_search_input = None
|
||||||
|
self.link_search_button = None
|
||||||
|
self.export_links_button = None
|
||||||
|
self.radio_only_links = None
|
||||||
|
self.radio_only_archives = None
|
||||||
|
self.missed_title_key_terms_count = {}
|
||||||
|
self.missed_title_key_terms_examples = {}
|
||||||
|
self.logged_summary_for_key_term = set()
|
||||||
|
self.STOP_WORDS = set(["a", "an", "the", "is", "was", "were", "of", "for", "with", "in", "on", "at", "by", "to", "and", "or", "but", "i", "you", "he", "she", "it", "we", "they", "my", "your", "his", "her", "its", "our", "their", "com", "net", "org", "www"])
|
||||||
|
self.already_logged_bold_key_terms = set()
|
||||||
|
self.missed_key_terms_buffer = []
|
||||||
|
self.char_filter_scope_toggle_button = None
|
||||||
|
self.skip_words_scope = SKIP_SCOPE_POSTS
|
||||||
|
self.char_filter_scope = CHAR_SCOPE_TITLE
|
||||||
|
self.manga_filename_style = self.settings.value(MANGA_FILENAME_STYLE_KEY, STYLE_POST_TITLE, type=str)
|
||||||
|
self.current_theme = self.settings.value(THEME_KEY, "dark", type=str)
|
||||||
|
self.only_links_log_display_mode = LOG_DISPLAY_LINKS
|
||||||
|
self.mega_download_log_preserved_once = False
|
||||||
|
self.allow_multipart_download_setting = False
|
||||||
|
self.use_cookie_setting = False
|
||||||
|
self.scan_content_images_setting = self.settings.value(SCAN_CONTENT_IMAGES_KEY, False, type=bool)
|
||||||
|
self.cookie_text_setting = ""
|
||||||
|
self.current_selected_language = self.settings.value(LANGUAGE_KEY, "en", type=str)
|
||||||
|
|
||||||
self .downloaded_files =set ()
|
print(f"ℹ️ Known.txt will be loaded/saved at: {self.config_file}")
|
||||||
self .downloaded_files_lock =threading .Lock ()
|
|
||||||
self .downloaded_file_hashes =set ()
|
|
||||||
self .downloaded_file_hashes_lock =threading .Lock ()
|
|
||||||
|
|
||||||
self .show_external_links =False
|
try:
|
||||||
self .external_link_queue =deque ()
|
base_path_for_icon = ""
|
||||||
self ._is_processing_external_link_queue =False
|
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
|
||||||
self ._current_link_post_title =None
|
base_path_for_icon = sys._MEIPASS
|
||||||
self .extracted_links_cache =[]
|
else:
|
||||||
self .manga_rename_toggle_button =None
|
base_path_for_icon = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||||
self .favorite_mode_checkbox =None
|
|
||||||
self .url_or_placeholder_stack =None
|
|
||||||
self .url_input_widget =None
|
|
||||||
self .url_placeholder_widget =None
|
|
||||||
self .favorite_action_buttons_widget =None
|
|
||||||
self .favorite_mode_artists_button =None
|
|
||||||
self .favorite_mode_posts_button =None
|
|
||||||
self .standard_action_buttons_widget =None
|
|
||||||
self .bottom_action_buttons_stack =None
|
|
||||||
self .main_log_output =None
|
|
||||||
self .external_log_output =None
|
|
||||||
self .log_splitter =None
|
|
||||||
self .main_splitter =None
|
|
||||||
self .reset_button =None
|
|
||||||
self .progress_log_label =None
|
|
||||||
self .log_verbosity_toggle_button =None
|
|
||||||
|
|
||||||
self .missed_character_log_output =None
|
icon_path_for_window = os.path.join(base_path_for_icon, 'assets', 'Kemono.ico')
|
||||||
self .log_view_stack =None
|
|
||||||
self .current_log_view ='progress'
|
|
||||||
|
|
||||||
self .link_search_input =None
|
if os.path.exists(icon_path_for_window):
|
||||||
self .link_search_button =None
|
self.setWindowIcon(QIcon(icon_path_for_window))
|
||||||
self .export_links_button =None
|
else:
|
||||||
self .radio_only_links =None
|
if getattr(sys, 'frozen', False):
|
||||||
self .radio_only_archives =None
|
executable_dir = os.path.dirname(sys.executable)
|
||||||
self .missed_title_key_terms_count ={}
|
fallback_icon_path = os.path.join(executable_dir, 'assets', 'Kemono.ico')
|
||||||
self .missed_title_key_terms_examples ={}
|
if os.path.exists(fallback_icon_path):
|
||||||
self .logged_summary_for_key_term =set ()
|
self.setWindowIcon(QIcon(fallback_icon_path))
|
||||||
self .STOP_WORDS =set (["a","an","the","is","was","were","of","for","with","in","on","at","by","to","and","or","but","i","you","he","she","it","we","they","my","your","his","her","its","our","their","com","net","org","www"])
|
else:
|
||||||
self .already_logged_bold_key_terms =set ()
|
self.log_signal.emit(f"⚠️ Main window icon 'assets/Kemono.ico' not found at {icon_path_for_window} or {fallback_icon_path}")
|
||||||
self .missed_key_terms_buffer =[]
|
else:
|
||||||
self .char_filter_scope_toggle_button =None
|
self.log_signal.emit(f"⚠️ Main window icon 'assets/Kemono.ico' not found at {icon_path_for_window}")
|
||||||
self .skip_words_scope =SKIP_SCOPE_POSTS
|
except Exception as e_icon_app:
|
||||||
self .char_filter_scope =CHAR_SCOPE_TITLE
|
self.log_signal.emit(f"❌ Error setting main window icon in DownloaderApp init: {e_icon_app}")
|
||||||
self .manga_filename_style =self .settings .value (MANGA_FILENAME_STYLE_KEY ,STYLE_POST_TITLE ,type =str )
|
|
||||||
self .current_theme =self .settings .value (THEME_KEY ,"dark",type =str )
|
|
||||||
self .only_links_log_display_mode =LOG_DISPLAY_LINKS
|
|
||||||
self .mega_download_log_preserved_once =False
|
|
||||||
self .allow_multipart_download_setting =False
|
|
||||||
self .use_cookie_setting =False
|
|
||||||
self .scan_content_images_setting =self .settings .value (SCAN_CONTENT_IMAGES_KEY ,False ,type =bool )
|
|
||||||
self .cookie_text_setting =""
|
|
||||||
self .current_selected_language =self .settings .value (LANGUAGE_KEY ,"en",type =str )
|
|
||||||
|
|
||||||
print (f"ℹ️ Known.txt will be loaded/saved at: {self .config_file }")
|
self.url_label_widget = None
|
||||||
try :
|
self.download_location_label_widget = None
|
||||||
if getattr (sys ,'frozen',False )and hasattr (sys ,'_MEIPASS'):
|
self.remove_from_filename_label_widget = None
|
||||||
|
self.skip_words_label_widget = None
|
||||||
base_dir_for_icon =sys ._MEIPASS
|
self.setWindowTitle("Kemono Downloader v5.5.0")
|
||||||
else :
|
self.init_ui()
|
||||||
app_base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
self._connect_signals()
|
||||||
icon_path_for_window =os .path .join (app_base_dir ,'assets','Kemono.ico')
|
self.log_signal.emit("ℹ️ Local API server functionality has been removed.")
|
||||||
if os .path .exists (icon_path_for_window ):
|
self.log_signal.emit("ℹ️ 'Skip Current File' button has been removed.")
|
||||||
self .setWindowIcon (QIcon (icon_path_for_window ))
|
if hasattr(self, 'character_input'):
|
||||||
else :
|
self.character_input.setToolTip(self._tr("character_input_tooltip", "Enter character names (comma-separated)..."))
|
||||||
self .log_signal .emit (f"⚠️ Main window icon 'assets/Kemono.ico' not found at {icon_path_for_window } (tried in DownloaderApp init)")
|
self.log_signal.emit(f"ℹ️ Manga filename style loaded: '{self.manga_filename_style}'")
|
||||||
except Exception as e_icon_app :
|
self.log_signal.emit(f"ℹ️ Skip words scope loaded: '{self.skip_words_scope}'")
|
||||||
self .log_signal .emit (f"❌ Error setting main window icon in DownloaderApp init: {e_icon_app }")
|
self.log_signal.emit(f"ℹ️ Character filter scope set to default: '{self.char_filter_scope}'")
|
||||||
|
self.log_signal.emit(f"ℹ️ Multi-part download defaults to: {'Enabled' if self.allow_multipart_download_setting else 'Disabled'}")
|
||||||
self .url_label_widget =None
|
self.log_signal.emit(f"ℹ️ Cookie text defaults to: Empty on launch")
|
||||||
self .download_location_label_widget =None
|
self.log_signal.emit(f"ℹ️ 'Use Cookie' setting defaults to: Disabled on launch")
|
||||||
|
self.log_signal.emit(f"ℹ️ Scan post content for images defaults to: {'Enabled' if self.scan_content_images_setting else 'Disabled'}")
|
||||||
self .remove_from_filename_label_widget =None
|
self.log_signal.emit(f"ℹ️ Application language loaded: '{self.current_selected_language.upper()}' (UI may not reflect this yet).")
|
||||||
self .skip_words_label_widget =None
|
self._retranslate_main_ui()
|
||||||
|
self._load_persistent_history()
|
||||||
self .setWindowTitle ("Kemono Downloader v5.5.0")
|
self._load_saved_download_location()
|
||||||
|
self._update_button_states_and_connections()
|
||||||
self .init_ui ()
|
|
||||||
self ._connect_signals ()
|
|
||||||
self .log_signal .emit ("ℹ️ Local API server functionality has been removed.")
|
|
||||||
self .log_signal .emit ("ℹ️ 'Skip Current File' button has been removed.")
|
|
||||||
if hasattr (self ,'character_input'):
|
|
||||||
self .character_input .setToolTip (self ._tr ("character_input_tooltip","Enter character names (comma-separated)..."))
|
|
||||||
self .log_signal .emit (f"ℹ️ Manga filename style loaded: '{self .manga_filename_style }'")
|
|
||||||
self .log_signal .emit (f"ℹ️ Skip words scope loaded: '{self .skip_words_scope }'")
|
|
||||||
self .log_signal .emit (f"ℹ️ Character filter scope set to default: '{self .char_filter_scope }'")
|
|
||||||
self .log_signal .emit (f"ℹ️ Multi-part download defaults to: {'Enabled'if self .allow_multipart_download_setting else 'Disabled'}")
|
|
||||||
self .log_signal .emit (f"ℹ️ Cookie text defaults to: Empty on launch")
|
|
||||||
self .log_signal .emit (f"ℹ️ 'Use Cookie' setting defaults to: Disabled on launch")
|
|
||||||
self .log_signal .emit (f"ℹ️ Scan post content for images defaults to: {'Enabled'if self .scan_content_images_setting else 'Disabled'}")
|
|
||||||
self .log_signal .emit (f"ℹ️ Application language loaded: '{self .current_selected_language .upper ()}' (UI may not reflect this yet).")
|
|
||||||
self ._retranslate_main_ui ()
|
|
||||||
self ._load_persistent_history ()
|
|
||||||
self ._load_saved_download_location ()
|
|
||||||
self._update_button_states_and_connections() # Initial button state setup
|
|
||||||
self._check_for_interrupted_session()
|
self._check_for_interrupted_session()
|
||||||
|
|
||||||
|
|
||||||
def get_checkbox_map(self):
|
def get_checkbox_map(self):
|
||||||
"""Returns a mapping of checkbox attribute names to their corresponding settings key."""
|
"""Returns a mapping of checkbox attribute names to their corresponding settings key."""
|
||||||
return {
|
return {
|
||||||
@ -851,20 +857,40 @@ class DownloaderApp (QWidget ):
|
|||||||
|
|
||||||
self .character_list .addItems ([entry ["name"]for entry in KNOWN_NAMES ])
|
self .character_list .addItems ([entry ["name"]for entry in KNOWN_NAMES ])
|
||||||
|
|
||||||
def save_known_names (self ):
|
def save_known_names(self):
|
||||||
|
"""
|
||||||
|
Saves the current list of known names (KNOWN_NAMES) to the config file.
|
||||||
|
This version includes a fix to ensure the destination directory exists
|
||||||
|
before attempting to write the file, preventing crashes in new installations.
|
||||||
|
"""
|
||||||
global KNOWN_NAMES
|
global KNOWN_NAMES
|
||||||
try :
|
try:
|
||||||
with open (self .config_file ,'w',encoding ='utf-8')as f :
|
# --- FIX STARTS HERE ---
|
||||||
for entry in KNOWN_NAMES :
|
# Get the directory path from the full file path.
|
||||||
if entry ["is_group"]:
|
config_dir = os.path.dirname(self.config_file)
|
||||||
f .write (f"({', '.join (sorted (entry ['aliases'],key =str .lower ))})\n")
|
# Create the directory if it doesn't exist. 'exist_ok=True' prevents
|
||||||
else :
|
# an error if the directory is already there.
|
||||||
f .write (entry ["name"]+'\n')
|
os.makedirs(config_dir, exist_ok=True)
|
||||||
if hasattr (self ,'log_signal'):self .log_signal .emit (f"💾 Saved {len (KNOWN_NAMES )} known entries to {self .config_file }")
|
# --- FIX ENDS HERE ---
|
||||||
except Exception as e :
|
|
||||||
log_msg =f"❌ Error saving config '{self .config_file }': {e }"
|
with open(self.config_file, 'w', encoding='utf-8') as f:
|
||||||
if hasattr (self ,'log_signal'):self .log_signal .emit (log_msg )
|
for entry in KNOWN_NAMES:
|
||||||
QMessageBox .warning (self ,"Config Save Error",f"Could not save list to {self .config_file }:\n{e }")
|
if entry["is_group"]:
|
||||||
|
# For groups, write the aliases in a sorted, comma-separated format inside parentheses.
|
||||||
|
f.write(f"({', '.join(sorted(entry['aliases'], key=str.lower))})\n")
|
||||||
|
else:
|
||||||
|
# For single entries, write the name on its own line.
|
||||||
|
f.write(entry["name"] + '\n')
|
||||||
|
|
||||||
|
if hasattr(self, 'log_signal'):
|
||||||
|
self.log_signal.emit(f"💾 Saved {len(KNOWN_NAMES)} known entries to {self.config_file}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# If any error occurs during saving, log it and show a warning popup.
|
||||||
|
log_msg = f"❌ Error saving config '{self.config_file}': {e}"
|
||||||
|
if hasattr(self, 'log_signal'):
|
||||||
|
self.log_signal.emit(log_msg)
|
||||||
|
QMessageBox.warning(self, "Config Save Error", f"Could not save list to {self.config_file}:\n{e}")
|
||||||
|
|
||||||
def closeEvent (self ,event ):
|
def closeEvent (self ,event ):
|
||||||
self .save_known_names ()
|
self .save_known_names ()
|
||||||
@ -1526,25 +1552,28 @@ class DownloaderApp (QWidget ):
|
|||||||
self .final_download_history_entries =[]
|
self .final_download_history_entries =[]
|
||||||
self ._save_persistent_history ()
|
self ._save_persistent_history ()
|
||||||
|
|
||||||
def _save_persistent_history (self ):
|
|
||||||
|
def _save_persistent_history(self):
|
||||||
"""Saves download history to a persistent file."""
|
"""Saves download history to a persistent file."""
|
||||||
self .log_signal .emit (f"📜 Attempting to save history to: {self .persistent_history_file }")
|
self.log_signal.emit(f"📜 Attempting to save history to: {self.persistent_history_file}")
|
||||||
try :
|
try:
|
||||||
history_dir =os .path .dirname (self .persistent_history_file )
|
history_dir = os.path.dirname(self.persistent_history_file)
|
||||||
self .log_signal .emit (f" History directory: {history_dir }")
|
self.log_signal.emit(f" History directory: {history_dir}")
|
||||||
if not os .path .exists (history_dir ):
|
if not os.path.exists(history_dir):
|
||||||
os .makedirs (history_dir ,exist_ok =True )
|
os.makedirs(history_dir, exist_ok=True)
|
||||||
self .log_signal .emit (f" Created history directory: {history_dir }")
|
self.log_signal.emit(f" Created history directory: {history_dir}")
|
||||||
|
|
||||||
history_data = {
|
history_data = {
|
||||||
"last_downloaded_files": list(self.last_downloaded_files_details),
|
"last_downloaded_files": list(self.last_downloaded_files_details),
|
||||||
"first_processed_posts": self.final_download_history_entries
|
"first_processed_posts": self.final_download_history_entries
|
||||||
}
|
}
|
||||||
with open (self .persistent_history_file ,'w',encoding ='utf-8')as f :
|
with open(self.persistent_history_file, 'w', encoding='utf-8') as f:
|
||||||
json .dump (history_data ,f ,indent =2 )
|
json.dump(history_data, f, indent=2)
|
||||||
self .log_signal .emit (f"✅ Saved {len (self .final_download_history_entries )} history entries to: {self .persistent_history_file }")
|
self.log_signal.emit(f"✅ Saved {len(self.final_download_history_entries)} history entries to: {self.persistent_history_file}")
|
||||||
except Exception as e :
|
except Exception as e:
|
||||||
self .log_signal .emit (f"❌ Error saving persistent history to {self .persistent_history_file }: {e }")
|
self.log_signal.emit(f"❌ Error saving persistent history to {self.persistent_history_file}: {e}")
|
||||||
|
|
||||||
|
|
||||||
def _load_creator_name_cache_from_json (self ):
|
def _load_creator_name_cache_from_json (self ):
|
||||||
"""Loads creator id-name-service mappings from creators.json into self.creator_name_cache."""
|
"""Loads creator id-name-service mappings from creators.json into self.creator_name_cache."""
|
||||||
self .log_signal .emit ("ℹ️ Attempting to load creators.json for creator name cache.")
|
self .log_signal .emit ("ℹ️ Attempting to load creators.json for creator name cache.")
|
||||||
@ -2752,6 +2781,8 @@ class DownloaderApp (QWidget ):
|
|||||||
elif self .manga_filename_style ==STYLE_DATE_BASED :
|
elif self .manga_filename_style ==STYLE_DATE_BASED :
|
||||||
self .manga_rename_toggle_button .setText (self ._tr ("manga_style_date_based_text","Name: Date Based"))
|
self .manga_rename_toggle_button .setText (self ._tr ("manga_style_date_based_text","Name: Date Based"))
|
||||||
|
|
||||||
|
elif self .manga_filename_style ==STYLE_POST_ID: # Add this block
|
||||||
|
self .manga_rename_toggle_button .setText (self ._tr ("manga_style_post_id_text","Name: Post ID"))
|
||||||
|
|
||||||
elif self .manga_filename_style ==STYLE_DATE_POST_TITLE :
|
elif self .manga_filename_style ==STYLE_DATE_POST_TITLE :
|
||||||
self .manga_rename_toggle_button .setText (self ._tr ("manga_style_date_post_title_text","Name: Date + Title"))
|
self .manga_rename_toggle_button .setText (self ._tr ("manga_style_date_post_title_text","Name: Date + Title"))
|
||||||
@ -2763,6 +2794,8 @@ class DownloaderApp (QWidget ):
|
|||||||
self .manga_rename_toggle_button .setToolTip ("Click to cycle Manga Filename Style (when Manga Mode is active for a creator feed).")
|
self .manga_rename_toggle_button .setToolTip ("Click to cycle Manga Filename Style (when Manga Mode is active for a creator feed).")
|
||||||
|
|
||||||
|
|
||||||
|
# In main_window.py
|
||||||
|
|
||||||
def _toggle_manga_filename_style (self ):
|
def _toggle_manga_filename_style (self ):
|
||||||
current_style =self .manga_filename_style
|
current_style =self .manga_filename_style
|
||||||
new_style =""
|
new_style =""
|
||||||
@ -2775,6 +2808,8 @@ class DownloaderApp (QWidget ):
|
|||||||
elif current_style ==STYLE_POST_TITLE_GLOBAL_NUMBERING :
|
elif current_style ==STYLE_POST_TITLE_GLOBAL_NUMBERING :
|
||||||
new_style =STYLE_DATE_BASED
|
new_style =STYLE_DATE_BASED
|
||||||
elif current_style ==STYLE_DATE_BASED :
|
elif current_style ==STYLE_DATE_BASED :
|
||||||
|
new_style =STYLE_POST_ID # Change this line
|
||||||
|
elif current_style ==STYLE_POST_ID: # Add this block
|
||||||
new_style =STYLE_POST_TITLE
|
new_style =STYLE_POST_TITLE
|
||||||
else :
|
else :
|
||||||
self .log_signal .emit (f"⚠️ Unknown current manga filename style: {current_style }. Resetting to default ('{STYLE_POST_TITLE }').")
|
self .log_signal .emit (f"⚠️ Unknown current manga filename style: {current_style }. Resetting to default ('{STYLE_POST_TITLE }').")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user