This commit is contained in:
Yuvi9587 2025-07-05 06:02:21 +05:30
parent b78d543f16
commit c8b77fb0d7
5 changed files with 259 additions and 206 deletions

29
main.py
View File

@ -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 ---")

View File

@ -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
} }

View File

@ -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

View File

@ -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

View File

@ -95,16 +95,25 @@ class DownloaderApp (QWidget ):
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 ):
# --- CORRECT PATH DEFINITION ---
# This block correctly determines the application's base directory whether
# it's running from source or as a frozen executable.
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
# Path for PyInstaller one-file bundle
self.app_base_dir = os.path.dirname(sys.executable) self.app_base_dir = os.path.dirname(sys.executable)
else: else:
# Path for running from source code
self.app_base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) 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.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.download_thread = None
self.thread_pool = None self.thread_pool = None
self.cancellation_event = threading.Event() self.cancellation_event = threading.Event()
self.session_file_path = os.path.join(self.app_base_dir, "appdata","session.json")
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
@ -116,53 +125,43 @@ class DownloaderApp (QWidget ):
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 .log_signal .emit (f" Persistent history file path set to: {self .persistent_history_file }")
self.final_download_history_entries = [] self.final_download_history_entries = []
self.favorite_download_queue = deque() self.favorite_download_queue = deque()
self.is_processing_favorites_queue = False self.is_processing_favorites_queue = False
self.download_counter = 0 self.download_counter = 0
self .favorite_download_queue =deque ()
self.permanently_failed_files_for_dialog = [] self.permanently_failed_files_for_dialog = []
self.last_link_input_text_for_queue_sync = "" self.last_link_input_text_for_queue_sync = ""
self.is_fetcher_thread_running = False self.is_fetcher_thread_running = False
self._restart_pending = False self._restart_pending = False
self .is_processing_favorites_queue =False
self.download_history_log = deque(maxlen=50) self.download_history_log = deque(maxlen=50)
self.skip_counter = 0 self.skip_counter = 0
self.all_kept_original_filenames = [] self.all_kept_original_filenames = []
self.cancellation_message_logged_this_session = False self.cancellation_message_logged_this_session = False
self.favorite_scope_toggle_button = None self.favorite_scope_toggle_button = None
self.favorite_download_scope = FAVORITE_SCOPE_SELECTED_LOCATION self.favorite_download_scope = FAVORITE_SCOPE_SELECTED_LOCATION
self.manga_mode_checkbox = None self.manga_mode_checkbox = None
self.selected_cookie_filepath = None self.selected_cookie_filepath = None
self.retryable_failed_files_info = [] self.retryable_failed_files_info = []
self.is_paused = False self.is_paused = False
self.worker_to_gui_queue = queue.Queue() self.worker_to_gui_queue = queue.Queue()
self.gui_update_timer = QTimer(self) self.gui_update_timer = QTimer(self)
self.actual_gui_signals = PostProcessorSignals() self.actual_gui_signals = PostProcessorSignals()
self.worker_signals = PostProcessorSignals() self.worker_signals = PostProcessorSignals()
self.prompt_mutex = QMutex() self.prompt_mutex = QMutex()
self._add_character_response = None 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" 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 = set()
self.downloaded_files_lock = threading.Lock() self.downloaded_files_lock = threading.Lock()
self.downloaded_file_hashes = set() self.downloaded_file_hashes = set()
self.downloaded_file_hashes_lock = threading.Lock() self.downloaded_file_hashes_lock = threading.Lock()
self.show_external_links = False self.show_external_links = False
self.external_link_queue = deque() self.external_link_queue = deque()
self._is_processing_external_link_queue = False self._is_processing_external_link_queue = False
@ -185,11 +184,9 @@ class DownloaderApp (QWidget ):
self.reset_button = None self.reset_button = None
self.progress_log_label = None self.progress_log_label = None
self.log_verbosity_toggle_button = None self.log_verbosity_toggle_button = None
self.missed_character_log_output = None self.missed_character_log_output = None
self.log_view_stack = None self.log_view_stack = None
self.current_log_view = 'progress' self.current_log_view = 'progress'
self.link_search_input = None self.link_search_input = None
self.link_search_button = None self.link_search_button = None
self.export_links_button = None self.export_links_button = None
@ -215,28 +212,36 @@ class DownloaderApp (QWidget ):
self.current_selected_language = self.settings.value(LANGUAGE_KEY, "en", type=str) self.current_selected_language = self.settings.value(LANGUAGE_KEY, "en", type=str)
print(f" Known.txt will be loaded/saved at: {self.config_file}") print(f" Known.txt will be loaded/saved at: {self.config_file}")
try :
if getattr (sys ,'frozen',False )and hasattr (sys ,'_MEIPASS'):
base_dir_for_icon =sys ._MEIPASS try:
base_path_for_icon = ""
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
base_path_for_icon = sys._MEIPASS
else: else:
app_base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) base_path_for_icon = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
icon_path_for_window =os .path .join (app_base_dir ,'assets','Kemono.ico')
icon_path_for_window = os.path.join(base_path_for_icon, 'assets', 'Kemono.ico')
if os.path.exists(icon_path_for_window): if os.path.exists(icon_path_for_window):
self.setWindowIcon(QIcon(icon_path_for_window)) self.setWindowIcon(QIcon(icon_path_for_window))
else: else:
self .log_signal .emit (f"⚠️ Main window icon 'assets/Kemono.ico' not found at {icon_path_for_window } (tried in DownloaderApp init)") if getattr(sys, 'frozen', False):
executable_dir = os.path.dirname(sys.executable)
fallback_icon_path = os.path.join(executable_dir, 'assets', 'Kemono.ico')
if os.path.exists(fallback_icon_path):
self.setWindowIcon(QIcon(fallback_icon_path))
else:
self.log_signal.emit(f"⚠️ Main window icon 'assets/Kemono.ico' not found at {icon_path_for_window} or {fallback_icon_path}")
else:
self.log_signal.emit(f"⚠️ Main window icon 'assets/Kemono.ico' not found at {icon_path_for_window}")
except Exception as e_icon_app: except Exception as e_icon_app:
self.log_signal.emit(f"❌ Error setting main window icon in DownloaderApp init: {e_icon_app}") self.log_signal.emit(f"❌ Error setting main window icon in DownloaderApp init: {e_icon_app}")
self.url_label_widget = None self.url_label_widget = None
self.download_location_label_widget = None self.download_location_label_widget = None
self.remove_from_filename_label_widget = None self.remove_from_filename_label_widget = None
self.skip_words_label_widget = None self.skip_words_label_widget = None
self.setWindowTitle("Kemono Downloader v5.5.0") self.setWindowTitle("Kemono Downloader v5.5.0")
self.init_ui() self.init_ui()
self._connect_signals() self._connect_signals()
self.log_signal.emit(" Local API server functionality has been removed.") self.log_signal.emit(" Local API server functionality has been removed.")
@ -254,9 +259,10 @@ class DownloaderApp (QWidget ):
self._retranslate_main_ui() self._retranslate_main_ui()
self._load_persistent_history() self._load_persistent_history()
self._load_saved_download_location() self._load_saved_download_location()
self._update_button_states_and_connections() # Initial button state setup self._update_button_states_and_connections()
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 {
@ -852,18 +858,38 @@ 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:
# --- FIX STARTS HERE ---
# Get the directory path from the full file path.
config_dir = os.path.dirname(self.config_file)
# Create the directory if it doesn't exist. 'exist_ok=True' prevents
# an error if the directory is already there.
os.makedirs(config_dir, exist_ok=True)
# --- FIX ENDS HERE ---
with open(self.config_file, 'w', encoding='utf-8') as f: with open(self.config_file, 'w', encoding='utf-8') as f:
for entry in KNOWN_NAMES: for entry in KNOWN_NAMES:
if entry["is_group"]: 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") f.write(f"({', '.join(sorted(entry['aliases'], key=str.lower))})\n")
else: else:
# For single entries, write the name on its own line.
f.write(entry["name"] + '\n') 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 }")
if hasattr(self, 'log_signal'):
self.log_signal.emit(f"💾 Saved {len(KNOWN_NAMES)} known entries to {self.config_file}")
except Exception as e: 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}" log_msg = f"❌ Error saving config '{self.config_file}': {e}"
if hasattr (self ,'log_signal'):self .log_signal .emit (log_msg ) 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}") 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 ):
@ -1526,6 +1552,7 @@ 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}")
@ -1545,6 +1572,8 @@ class DownloaderApp (QWidget ):
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 }').")