diff --git a/downloader_utils.py b/downloader_utils.py index ffe6823..383e66d 100644 --- a/downloader_utils.py +++ b/downloader_utils.py @@ -11,6 +11,10 @@ from concurrent .futures import ThreadPoolExecutor ,Future ,CancelledError ,as_c import html from PyQt5 .QtCore import QObject ,pyqtSignal ,QThread ,QMutex ,QMutexLocker from urllib .parse import urlparse +try : + from mega import Mega +except ImportError : + print ("ERROR: mega.py library not found. Please install it: pip install mega.py") try : from PIL import Image except ImportError : @@ -2094,4 +2098,87 @@ class DownloadThread (QThread ): def receive_add_character_result (self ,result ): with QMutexLocker (self .prompt_mutex ): self ._add_character_response =result - self .logger (f" (DownloadThread) Received character prompt response: {'Yes (added/confirmed)'if result else 'No (declined/failed)'}") \ No newline at end of file + self .logger (f" (DownloadThread) Received character prompt response: {'Yes (added/confirmed)'if result else 'No (declined/failed)'}") + +def download_mega_file (mega_link ,download_path =".",logger_func =print ): + """ + Downloads a file from a public Mega.nz link. + + Args: + mega_link (str): The public Mega.nz link to the file. + download_path (str, optional): The directory to save the downloaded file. + Defaults to the current directory. + logger_func (callable, optional): Function to use for logging. Defaults to print. + """ + logger_func ("Initializing Mega client...") + try : + mega_client =Mega () + except NameError : + logger_func ("ERROR: Mega class not available. mega.py library might not be installed correctly.") + raise ImportError ("Mega class not found. Is mega.py installed?") + + m =mega_client .login () + + logger_func (f"Attempting to download from: {mega_link }") + + try : + + # Pre-flight check for link validity and attributes + logger_func(f" Verifying Mega link and fetching attributes: {mega_link}") + file_attributes = m.get_public_url_info(mega_link) + + if not file_attributes or not isinstance(file_attributes, dict): + logger_func(f"❌ Error: Could not retrieve valid file information for the Mega link. Link might be invalid, expired, or a folder. Info received: {file_attributes}") + raise ValueError(f"Invalid or inaccessible Mega link. get_public_url_info returned: {file_attributes}") + + expected_filename = file_attributes.get('name') # Changed from 'n' + file_size = file_attributes.get('size') # Changed from 's' + + if not expected_filename: + logger_func(f"⚠️ Critical: File name ('name') not found in Mega link attributes. Attributes: {file_attributes}") # Updated log + raise ValueError(f"File name ('name') not found in Mega link attributes: {file_attributes}") # Updated ValueError + + logger_func(f" Link verified. Expected filename: '{expected_filename}'. Size: {file_size if file_size is not None else 'Unknown'} bytes.") + + if not os .path .exists (download_path ): + logger_func (f"Download path '{download_path }' does not exist. Creating it...") + os .makedirs (download_path ,exist_ok =True ) + + logger_func(f"Starting download of '{expected_filename}' to '{download_path}'...") + + # m.download_url returns a tuple (filepath, filename) on success for mega.py 1.0.8 + download_result = m.download_url(mega_link, dest_path=download_path, dest_filename=None) + + if download_result and isinstance(download_result, tuple) and len(download_result) == 2: + saved_filepath, saved_filename = download_result + # Ensure saved_filepath is an absolute path if dest_path was relative + if not os.path.isabs(saved_filepath) and dest_path: + saved_filepath = os.path.join(os.path.abspath(dest_path), saved_filename) + + logger_func(f"File downloaded successfully! Saved as: {saved_filepath}") + if not os.path.exists(saved_filepath): + logger_func(f"⚠️ Warning: mega.py reported success but file '{saved_filepath}' not found on disk.") + # Optionally, verify filename if needed, though saved_filename should be correct + if saved_filename != expected_filename: + logger_func(f" Note: Saved filename '{saved_filename}' differs from initially expected '{expected_filename}'. This is usually fine.") + else : + logger_func(f"Download failed. The download_url method returned: {download_result}") + raise Exception(f"Mega download_url did not return expected result or failed. Result: {download_result}") + + except PermissionError : + logger_func (f"Error: Permission denied to write to '{download_path }'. Please check permissions.") + raise + except FileNotFoundError : + logger_func (f"Error: The specified download path '{download_path }' is invalid or a component was not found.") + raise + except requests .exceptions .RequestException as e : + logger_func (f"Error during request to Mega (network issue, etc.): {e }") + raise + except ValueError as ve: # Catch our custom ValueError from pre-flight + logger_func(f"ValueError during Mega processing (likely invalid link): {ve}") + raise + except Exception as e : + if isinstance(e, TypeError) and "'bool' object is not subscriptable" in str(e): + logger_func(" This specific TypeError occurred despite pre-flight checks. This might indicate a deeper issue with the mega.py library or a very transient API problem for this link.") + traceback .print_exc () + raise \ No newline at end of file diff --git a/main.py b/main.py index 7167303..70dc84a 100644 --- a/main.py +++ b/main.py @@ -60,7 +60,8 @@ try : FILE_DOWNLOAD_STATUS_FAILED_RETRYABLE_LATER , STYLE_DATE_BASED , STYLE_POST_TITLE_GLOBAL_NUMBERING , - CREATOR_DOWNLOAD_DEFAULT_FOLDER_IGNORE_WORDS + CREATOR_DOWNLOAD_DEFAULT_FOLDER_IGNORE_WORDS , + download_mega_file ) print ("Successfully imported names from downloader_utils.") @@ -95,6 +96,7 @@ except ImportError as e : STYLE_DATE_BASED ="date_based" STYLE_POST_TITLE_GLOBAL_NUMBERING ="post_title_global_numbering" CREATOR_DOWNLOAD_DEFAULT_FOLDER_IGNORE_WORDS =set () + def download_mega_file (*args ,**kwargs ):pass except Exception as e : print (f"--- UNEXPECTED IMPORT ERROR ---") @@ -136,6 +138,78 @@ FAVORITE_SCOPE_SELECTED_LOCATION ="selected_location" FAVORITE_SCOPE_ARTIST_FOLDERS ="artist_folders" CONFIRM_ADD_ALL_SKIP_ADDING =2 CONFIRM_ADD_ALL_CANCEL_DOWNLOAD =3 +LOG_DISPLAY_LINKS = "links" +LOG_DISPLAY_DOWNLOAD_PROGRESS = "download_progress" + +class DownloadMegaLinksDialog (QDialog ): + """A dialog to select and initiate download for extracted Mega links.""" + + download_requested =pyqtSignal (list ) + + def __init__ (self ,mega_links_data ,parent =None ): + + + super ().__init__ (parent ) + self .mega_links_data =mega_links_data + self .setWindowTitle ("Download Selected Mega Links") + self .setMinimumSize (500 ,400 ) + + layout =QVBoxLayout (self ) + label =QLabel (f"Found {len (self .mega_links_data )} Mega link(s). Select which ones to download:") + label .setAlignment (Qt .AlignCenter ) + label .setWordWrap (True ) + layout .addWidget (label ) + + self .links_list_widget =QListWidget () + self .links_list_widget .setSelectionMode (QAbstractItemView .NoSelection ) + for link_info in self .mega_links_data : + display_text =f"{link_info ['title']} - {link_info ['link_text']} ({link_info ['url']})" + item =QListWidgetItem (display_text ) + item .setData (Qt .UserRole ,link_info ) + item .setFlags (item .flags ()|Qt .ItemIsUserCheckable ) + item .setCheckState (Qt .Checked ) + self .links_list_widget .addItem (item ) + layout .addWidget (self .links_list_widget ) + + button_layout =QHBoxLayout () + self .select_all_button =QPushButton ("Select All") + self .select_all_button .clicked .connect (lambda :self ._set_all_items_checked (Qt .Checked )) + button_layout .addWidget (self .select_all_button ) + + self .deselect_all_button =QPushButton ("Deselect All") + self .deselect_all_button .clicked .connect (lambda :self ._set_all_items_checked (Qt .Unchecked )) + button_layout .addWidget (self .deselect_all_button ) + button_layout .addStretch () + + self .download_button =QPushButton ("Download Selected") + self .download_button .clicked .connect (self ._handle_download_selected ) + self .download_button .setDefault (True ) + button_layout .addWidget (self .download_button ) + + self .cancel_button =QPushButton ("Cancel") + self .cancel_button .clicked .connect (self .reject ) + button_layout .addWidget (self .cancel_button ) + layout .addLayout (button_layout ) + + if parent and hasattr (parent ,'get_dark_theme')and parent .current_theme =="dark": + self .setStyleSheet (parent .get_dark_theme ()) + + def _set_all_items_checked (self ,check_state ): + for i in range (self .links_list_widget .count ()): + self .links_list_widget .item (i ).setCheckState (check_state ) + + def _handle_download_selected (self ): + selected_links =[] + for i in range (self .links_list_widget .count ()): + item =self .links_list_widget .item (i ) + if item .checkState ()==Qt .Checked : + selected_links .append (item .data (Qt .UserRole )) + + if selected_links : + self .download_requested .emit (selected_links ) + self .accept () + else : + QMessageBox .information (self ,"No Selection","Please select at least one Mega link to download.") class ConfirmAddAllDialog (QDialog ): """A dialog to confirm adding multiple new names to Known.txt.""" @@ -2205,6 +2279,42 @@ class TourDialog (QDialog ): except Exception as e : print (f"[Tour] CRITICAL ERROR in run_tour_if_needed: {e }") return QDialog .Rejected + +class MegaDownloadThread (QThread ): + """A QThread to handle downloading multiple Mega links sequentially.""" + progress_signal =pyqtSignal (str ) + file_complete_signal =pyqtSignal (str ,bool ) + finished_signal =pyqtSignal () + + def __init__ (self ,tasks_to_download ,download_base_path ,parent_logger_func ,parent =None ): + super ().__init__ (parent ) + self .tasks =tasks_to_download + self .download_base_path =download_base_path + self .parent_logger_func =parent_logger_func + self .is_cancelled =False + + def run (self ): + self .progress_signal .emit (f"ℹ️ Starting Mega download thread for {len (self .tasks )} link(s).") + for i ,task_info in enumerate (self .tasks ): + if self .is_cancelled : + self .progress_signal .emit ("Mega download cancelled by user.") + break + + full_mega_url =task_info ['url'] + post_title =task_info ['title'] + self .progress_signal .emit (f"Mega Download ({i +1 }/{len (self .tasks )}): Starting '{post_title }' from {full_mega_url }") + try : + + download_mega_file (full_mega_url ,self .download_base_path ,logger_func =self .parent_logger_func ) + self .file_complete_signal .emit (full_mega_url ,True ) + except Exception as e : + self .progress_signal .emit (f"❌ Error downloading Mega link '{full_mega_url }' (from post '{post_title }'): {e }") + self .file_complete_signal .emit (full_mega_url ,False ) + self .finished_signal .emit () + + def cancel (self ): + self .is_cancelled =True + class DynamicFilterHolder : def __init__ (self ,initial_filters =None ): self .lock =threading .Lock () @@ -2240,6 +2350,7 @@ class DownloaderApp (QWidget ): self .download_thread =None self .thread_pool =None self .cancellation_event =threading .Event () + self .mega_download_thread =None self .pause_event =threading .Event () self .active_futures =[] self .total_posts_to_process =0 @@ -2325,6 +2436,8 @@ class DownloaderApp (QWidget ): 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 # New state variable + 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 ) @@ -2429,6 +2542,14 @@ class DownloaderApp (QWidget ): if self .export_links_button :self .export_links_button .clicked .connect (self ._export_links_to_file ) if self .manga_mode_checkbox :self .manga_mode_checkbox .toggled .connect (self .update_ui_for_manga_mode ) + + + if hasattr (self ,'download_extracted_links_button'): + self .download_extracted_links_button .clicked .connect (self ._show_download_extracted_links_dialog ) + + if hasattr(self, 'log_display_mode_toggle_button'): + self.log_display_mode_toggle_button.clicked.connect(self._toggle_log_display_mode) + if self .manga_rename_toggle_button :self .manga_rename_toggle_button .clicked .connect (self ._toggle_manga_filename_style ) if hasattr (self ,'link_input'): @@ -3241,6 +3362,20 @@ class DownloaderApp (QWidget ): self .export_links_button .setEnabled (False ) self .export_links_button .setVisible (False ) export_button_layout .addWidget (self .export_links_button ) + + self .download_extracted_links_button =QPushButton ("Download") + self .download_extracted_links_button .setToolTip ("Download extracted links (placeholder).") + self .download_extracted_links_button .setFixedWidth (100 ) + self .download_extracted_links_button .setStyleSheet ("padding: 4px 8px; margin-top: 5px;") + self .download_extracted_links_button .setEnabled (False ) + self .download_extracted_links_button .setVisible (False ) + export_button_layout .addWidget (self .download_extracted_links_button ) + self.log_display_mode_toggle_button = QPushButton("🔗 Links View") + self.log_display_mode_toggle_button.setToolTip("Toggle log display mode for 'Only Links'") + self.log_display_mode_toggle_button.setFixedWidth(120) + self.log_display_mode_toggle_button.setStyleSheet("padding: 4px 8px; margin-top: 5px;") + self.log_display_mode_toggle_button.setVisible(False) # Initially hidden + export_button_layout.addWidget(self.log_display_mode_toggle_button) right_layout .addLayout (export_button_layout ) @@ -3294,19 +3429,130 @@ class DownloaderApp (QWidget ): if hasattr (self ,'link_input'): self .last_link_input_text_for_queue_sync =self .link_input .text () + def _show_download_extracted_links_dialog (self ): + """Shows the placeholder dialog for downloading extracted links.""" + if not (self .radio_only_links and self .radio_only_links .isChecked ()): + self .log_signal .emit ("ℹ️ Download extracted links button clicked, but not in 'Only Links' mode.") + return - def _get_domain_for_service (self ,service_name ): - """Determines the base domain for a given service platform.""" - service_lower =service_name .lower () + mega_links_to_show =[] + for link_data_tuple in self .extracted_links_cache : - coomer_primary_services ={'onlyfans','fansly','manyvids','candfans'} + if link_data_tuple [3 ]=='mega': + mega_links_to_show .append ({ + 'title':link_data_tuple [0 ], + 'link_text':link_data_tuple [1 ], + 'url':link_data_tuple [2 ], + 'platform':link_data_tuple [3 ], + 'key':link_data_tuple [4 ] + }) + if not mega_links_to_show : + QMessageBox .information (self ,"No Mega Links","No Mega links were found in the extracted links.") + return + dialog =DownloadMegaLinksDialog (mega_links_to_show ,self ) + dialog .download_requested .connect (self ._handle_mega_links_download_request ) + dialog .exec_ () - if service_lower in coomer_primary_services : - return "coomer.su" + def _handle_mega_links_download_request (self ,selected_links_info ): + if not selected_links_info : + self .log_signal .emit ("ℹ️ No Mega links selected for download from dialog.") + return + + if self.radio_only_links and self.radio_only_links.isChecked() and \ + self.only_links_log_display_mode == LOG_DISPLAY_DOWNLOAD_PROGRESS: + self.main_log_output.clear() + self.log_signal.emit("ℹ️ Displaying Mega download progress (extracted links hidden)...") + self.mega_download_log_preserved_once = False # Ensure no append logic triggers + + current_main_dir =self .dir_input .text ().strip () + download_dir_for_mega ="" + + if current_main_dir and os .path .isdir (current_main_dir ): + + download_dir_for_mega =current_main_dir + self .log_signal .emit (f"ℹ️ Using existing main download location for Mega links: {download_dir_for_mega }") else : - return "kemono.su" + + if not current_main_dir : + self .log_signal .emit ("ℹ️ Main download location is empty. Prompting for Mega download folder.") + else : + self .log_signal .emit (f"⚠️ Main download location '{current_main_dir }' is not a valid directory. Prompting for Mega download folder.") + + + suggestion_path =current_main_dir if current_main_dir else QStandardPaths .writableLocation (QStandardPaths .DownloadLocation ) + + chosen_dir =QFileDialog .getExistingDirectory ( + self , + "Select Download Folder for Mega Links", + suggestion_path + ) + + if not chosen_dir : + self .log_signal .emit ("ℹ️ Mega links download cancelled - no download directory selected from prompt.") + return + download_dir_for_mega =chosen_dir + + + + self .log_signal .emit (f"ℹ️ Preparing to download {len (selected_links_info )} selected Mega link(s) to: {download_dir_for_mega }") + if not os .path .exists (download_dir_for_mega ): + self .log_signal .emit (f"❌ Critical Error: Selected Mega download directory '{download_dir_for_mega }' does not exist.") + return + + tasks_for_thread =[] + for item in selected_links_info : + full_url =item ['url'] + key =item ['key'] + + if key : + url_parts =urlparse (full_url ) + if key not in url_parts .fragment : + if url_parts .fragment : + base_url_no_fragment =full_url .split ('#')[0 ] + full_url =f"{base_url_no_fragment }#{key }" + else : + full_url =f"{full_url }#{key }" + + tasks_for_thread .append ({'url':full_url ,'title':item ['title']}) + + if self .mega_download_thread and self .mega_download_thread .isRunning (): + QMessageBox .warning (self ,"Busy","Another Mega download is already in progress.") + return + + self .mega_download_thread =MegaDownloadThread (tasks_for_thread ,download_dir_for_mega ,self .log_signal .emit ,self ) + self .mega_download_thread .finished .connect (self ._on_mega_download_thread_finished ) + + + + self .set_ui_enabled (False ) + self .progress_label .setText (f"Downloading Mega Links (0/{len (tasks_for_thread )})...") + self .mega_download_thread .start () + + def _on_mega_download_thread_finished (self ): + self .log_signal .emit ("✅ Mega download thread finished.") + self .progress_label .setText ("Mega downloads complete. Ready for new task.") + + self.mega_download_log_preserved_once = True # Mark that a mega download just finished + self.log_signal.emit("INTERNAL: mega_download_log_preserved_once SET to True.") # Debug + + if self.radio_only_links and self.radio_only_links.isChecked(): + self.log_signal.emit(HTML_PREFIX + "

--- End of Mega Download Log ---
") + # The _filter_links_log will be called by set_ui_enabled -> _handle_filter_mode_change + # No need to call it directly here as it might interfere with the flag. + + self .set_ui_enabled (True ) # Enable UI after potential log update + + # Reset the flag AFTER the UI update cycle (triggered by set_ui_enabled) is complete. + # This ensures the flag protects the log content throughout the entire UI update. + if self.mega_download_log_preserved_once: + self.mega_download_log_preserved_once = False + self.log_signal.emit("INTERNAL: mega_download_log_preserved_once RESET to False.") # Debug + + if self .mega_download_thread : + self .mega_download_thread .deleteLater () + self .mega_download_thread =None def _show_future_settings_dialog (self ): """Shows the placeholder dialog for future settings.""" @@ -3573,22 +3819,26 @@ class DownloaderApp (QWidget ): post_title ,link_text ,link_url ,platform ,decryption_key =link_data is_only_links_mode =self .radio_only_links and self .radio_only_links .isChecked () - 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 + max_link_text_len =50 # Consistent with _filter_links_log + display_text =(link_text [:max_link_text_len ].strip ()+"..." + if len (link_text )>max_link_text_len else link_text .strip ()) formatted_link_info =f"{display_text } - {link_url } - {platform }" - separator ="-"*45 if decryption_key : formatted_link_info +=f" (Decryption Key: {decryption_key })" if is_only_links_mode : if post_title !=self ._current_link_post_title : - self .log_signal .emit (HTML_PREFIX +"
"+separator +"
") - title_html =f'{post_title }
' + separator_html ="
"+"-"*45 +"
" + if self ._current_link_post_title is not None : + self .log_signal .emit (HTML_PREFIX +separator_html ) + title_html =f'{html.escape(post_title)}
' self .log_signal .emit (HTML_PREFIX +title_html ) self ._current_link_post_title =post_title - self .log_signal .emit (formatted_link_info ) + # Emit plain text link info; handle_main_log will .append() it + self .log_signal .emit (formatted_link_info) elif self .show_external_links : + separator ="-"*45 # This separator is for _append_to_external_log's internal use if it were to use it, not for HTML self ._append_to_external_log (formatted_link_info ,separator ) self ._is_processing_external_link_queue =False @@ -3712,6 +3962,15 @@ class DownloaderApp (QWidget ): self .export_links_button .setVisible (is_only_links ) self .export_links_button .setEnabled (is_only_links and bool (self .extracted_links_cache )) + if hasattr (self ,'download_extracted_links_button')and self .download_extracted_links_button : + self .download_extracted_links_button .setVisible (is_only_links ) + has_mega_links =any (link_info [3 ]=='mega'for link_info in self .extracted_links_cache ) + self .download_extracted_links_button .setEnabled (is_only_links and has_mega_links ) + + if hasattr(self, 'log_display_mode_toggle_button'): + self.log_display_mode_toggle_button.setVisible(is_only_links) + self._update_log_display_mode_button_text() # Ensure correct icon/tooltip + if self .download_btn : if is_only_links : self .download_btn .setText ("🔗 Extract Links") @@ -3721,8 +3980,8 @@ class DownloaderApp (QWidget ): file_download_mode_active =not is_only_links - if self .dir_input :self .dir_input .setEnabled (file_download_mode_active ) - if self .dir_button :self .dir_button .setEnabled (file_download_mode_active ) + + if self .use_subfolders_checkbox :self .use_subfolders_checkbox .setEnabled (file_download_mode_active ) if self .skip_words_input :self .skip_words_input .setEnabled (file_download_mode_active ) if self .skip_scope_toggle_button :self .skip_scope_toggle_button .setEnabled (file_download_mode_active ) @@ -3754,11 +4013,21 @@ class DownloaderApp (QWidget ): if is_only_links : self .progress_log_label .setText ("📜 Extracted Links Log:") 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 .clear ();self .main_log_output .setMinimumHeight (0 ) - if self .external_log_output :self .external_log_output .clear ();self .external_log_output .setMinimumHeight (0 ) + if self .log_splitter :self .log_splitter .setSizes ([self .height (),0 ]) # type: ignore + + # Conditional clear to preserve Mega download log in progress view + do_clear_log_in_filter_change = True + if self.mega_download_log_preserved_once and \ + self.only_links_log_display_mode == LOG_DISPLAY_DOWNLOAD_PROGRESS: + do_clear_log_in_filter_change = False + + if self .main_log_output and do_clear_log_in_filter_change: + self.log_signal.emit("INTERNAL: _handle_filter_mode_change - About to clear log.") # Debug + self .main_log_output .clear () + self.log_signal.emit("INTERNAL: _handle_filter_mode_change - Log cleared by _handle_filter_mode_change.") # Debug + + if self.main_log_output: self.main_log_output.setMinimumHeight(0) self .log_signal .emit ("="*20 +" Mode changed to: Only Links "+"="*20 ) - self ._filter_links_log () self ._try_process_next_external_link () elif is_only_archives : self .progress_log_label .setText ("📜 Progress Log (Archives Only):") @@ -3777,6 +4046,10 @@ class DownloaderApp (QWidget ): self .update_external_links_setting (self .external_links_checkbox .isChecked ()if self .external_links_checkbox else False ) self .log_signal .emit (f"="*20 +f" Mode changed to: {filter_mode_text } "+"="*20 ) + # Call _filter_links_log AFTER potential clear and other UI updates for "Only Links" + if is_only_links: + self._filter_links_log() + subfolders_on =self .use_subfolders_checkbox .isChecked ()if self .use_subfolders_checkbox else False manga_on =self .manga_mode_checkbox .isChecked ()if self .manga_mode_checkbox else False @@ -3804,36 +4077,71 @@ class DownloaderApp (QWidget ): if not (self .radio_only_links and self .radio_only_links .isChecked ()):return search_term =self .link_search_input .text ().lower ().strip ()if self .link_search_input else "" - self .main_log_output .clear () - current_title_for_display =None - separator ="-"*45 - for post_title ,link_text ,link_url ,platform ,decryption_key in self .extracted_links_cache : - 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 ()or - (decryption_key and search_term in decryption_key .lower ()) - ) + if self .download_extracted_links_button : + has_mega_links =any (link_info [3 ]=='mega'for link_info in self .extracted_links_cache ) + self .download_extracted_links_button .setEnabled (has_mega_links ) - if matches_search : - if post_title !=current_title_for_display : - self .main_log_output .insertHtml ("
"+separator +"
") - title_html =f'{post_title }
' - self .main_log_output .insertHtml (title_html ) - current_title_for_display =post_title + if self.mega_download_log_preserved_once and \ + self.only_links_log_display_mode == LOG_DISPLAY_DOWNLOAD_PROGRESS: + # Mega download just finished, in progress view. Preserve the log. + # Do nothing to the log content itself. + self.log_signal.emit("INTERNAL: _filter_links_log - Preserving Mega log (due to mega_download_log_preserved_once).") + elif self.only_links_log_display_mode == LOG_DISPLAY_DOWNLOAD_PROGRESS: + # This is when 'Progress View' is selected for the 'Only Links' log scope, + # AND a Mega download hasn't *just* finished (mega_download_log_preserved_once is False). + # We should clear the log and show the placeholder message. + self.log_signal.emit("INTERNAL: _filter_links_log - In Progress View. Clearing for placeholder.") + if self.main_log_output: self.main_log_output.clear() + self.log_signal.emit("INTERNAL: _filter_links_log - Cleared for progress placeholder.") + self.log_signal.emit("ℹ️ Switched to Mega download progress view. Extracted links are hidden.\n" + " Perform a Mega download to see its progress here, or switch back to 🔗 view.") + self.log_signal.emit("INTERNAL: _filter_links_log - Placeholder message emitted.") + + else: # self.only_links_log_display_mode == LOG_DISPLAY_LINKS + # In links view. Clear and show the (filtered) links. + self.log_signal.emit("INTERNAL: _filter_links_log - In links view branch. About to clear.") # Debug + if self.main_log_output: self.main_log_output.clear() + self.log_signal.emit("INTERNAL: _filter_links_log - Cleared for links view.") # Debug + + current_title_for_display = None + any_links_displayed_this_call = False + separator_html ="
"+"-"*45 +"
" - 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 }" - if decryption_key : - formatted_link_info +=f" (Decryption Key: {decryption_key })" - self .main_log_output .append (formatted_link_info ) + for post_title, link_text, link_url, platform, decryption_key in self.extracted_links_cache: + 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() or + (decryption_key and search_term in decryption_key.lower())) # type: ignore + if not matches_search: + continue + + any_links_displayed_this_call = True + if post_title != current_title_for_display: + if current_title_for_display is not None : # Add separator only if it's not the very first title + if self.main_log_output: self.main_log_output.insertHtml(separator_html) + + title_html = f'{html.escape(post_title)}
' + if self .main_log_output :self .main_log_output .insertHtml (title_html ) + current_title_for_display = post_title - if self .main_log_output .toPlainText ().strip (): - self .main_log_output .append ("") - self .main_log_output .verticalScrollBar ().setValue (0 ) + max_link_text_len = 50 + display_text = (link_text[:max_link_text_len].strip() + "..." if len(link_text) > max_link_text_len else link_text.strip()) + # Construct plain text link line + plain_link_info_line = f"{display_text} - {link_url} - {platform}" + if decryption_key: + plain_link_info_line += f" (Decryption Key: {decryption_key})" + if self .main_log_output : + self.main_log_output.append(plain_link_info_line) # Appending plain text for default styling & spacing + + if any_links_displayed_this_call: + if self.main_log_output: self.main_log_output.append("") # Extra blank line at the end + elif not search_term and self.main_log_output: # No links in cache at all + self.log_signal.emit(" (No links extracted yet or all filtered out in links view)") + + # self.mega_download_log_preserved_once is now reset in _on_mega_download_thread_finished + if self.main_log_output: self.main_log_output.verticalScrollBar().setValue(self.main_log_output.verticalScrollBar().maximum()) def _export_links_to_file (self ): @@ -5352,6 +5660,10 @@ class DownloaderApp (QWidget ): if self .cancellation_event .is_set (): self .log_signal .emit (" Cancellation detected after/during task submission loop.") + if self .mega_download_thread and self .mega_download_thread .isRunning (): + self .mega_download_thread .cancel () + + self .finished_signal .emit (self .download_counter ,self .skip_counter ,True ,self .all_kept_original_filenames ) if self .thread_pool :self .thread_pool .shutdown (wait =False ,cancel_futures =True );self .thread_pool =None @@ -5437,6 +5749,10 @@ class DownloaderApp (QWidget ): if not enabled : if self .bottom_action_buttons_stack : self .bottom_action_buttons_stack .setCurrentIndex (0 ) + + if self .mega_download_thread and self .mega_download_thread .isRunning (): + self .log_signal .emit ("ℹ️ Cancelling active Mega download due to UI state change.") + self .mega_download_thread .cancel () else : pass @@ -5574,7 +5890,12 @@ class DownloaderApp (QWidget ): self .favorite_download_queue .clear () self .is_processing_favorites_queue =False + self.only_links_log_display_mode = LOG_DISPLAY_LINKS # Reset on soft reset too + if hasattr (self ,'link_input'): + if self .download_extracted_links_button : + self .download_extracted_links_button .setEnabled (False ) + self .last_link_input_text_for_queue_sync =self .link_input .text () self .permanently_failed_files_for_dialog .clear () self .filter_character_list (self .character_search_input .text ()) @@ -5590,11 +5911,36 @@ class DownloaderApp (QWidget ): self .log_signal .emit ("✅ Soft UI reset complete. Preserved URL and Directory (if provided).") + def _update_log_display_mode_button_text(self): + if hasattr(self, 'log_display_mode_toggle_button'): + if self.only_links_log_display_mode == LOG_DISPLAY_LINKS: + self.log_display_mode_toggle_button.setText("🔗 Links View") + self.log_display_mode_toggle_button.setToolTip( + "Current View: Extracted Links.\n" + "After Mega download, Mega log is shown THEN links are appended.\n" + "Click to switch to 'Download Progress View'." + ) + else: # LOG_DISPLAY_DOWNLOAD_PROGRESS + self.log_display_mode_toggle_button.setText("⬇️ Progress View") + self.log_display_mode_toggle_button.setToolTip( + "Current View: Mega Download Progress.\n" + "After Mega download, ONLY Mega log is shown (links hidden).\n" + "Click to switch to 'Extracted Links View'." + ) + + def _toggle_log_display_mode(self): + self.only_links_log_display_mode = LOG_DISPLAY_DOWNLOAD_PROGRESS if self.only_links_log_display_mode == LOG_DISPLAY_LINKS else LOG_DISPLAY_LINKS + self._update_log_display_mode_button_text() + self._filter_links_log() # Refresh the log based on the new mode def cancel_download_button_action (self ): if not self .cancel_btn .isEnabled ()and not self .cancellation_event .is_set ():self .log_signal .emit ("ℹ️ No active download to cancel or already cancelling.");return self .log_signal .emit ("⚠️ Requesting cancellation of download process (soft reset)...") + if self .mega_download_thread and self .mega_download_thread .isRunning (): + self .log_signal .emit (" Cancelling active Mega download thread...") + self .mega_download_thread .cancel () + current_url =self .link_input .text () current_dir =self .dir_input .text () @@ -5675,6 +6021,9 @@ class DownloaderApp (QWidget ): self .log_signal .emit (f"ℹ️ Note during single-thread signal disconnection: {e }") if not self .download_thread .isRunning (): + + if self .download_thread : + self .download_thread .deleteLater () self .download_thread =None self .progress_label .setText (f"{status_message }: {total_downloaded } downloaded, {total_skipped } skipped.") @@ -5871,6 +6220,10 @@ class DownloaderApp (QWidget ): self .retry_thread_pool .shutdown (wait =True ) self .retry_thread_pool =None + if self .mega_download_thread and not self .mega_download_thread .isRunning (): + self .mega_download_thread .deleteLater () + self .mega_download_thread =None + self .active_retry_futures .clear () self .active_retry_futures_map .clear () self .files_for_current_retry_session .clear () @@ -5924,6 +6277,8 @@ class DownloaderApp (QWidget ): self .already_logged_bold_key_terms .clear () self .missed_key_terms_buffer .clear () self .favorite_download_queue .clear () + self .only_links_log_display_mode = LOG_DISPLAY_LINKS # Reset on full reset + self .mega_download_log_preserved_once =False self .permanently_failed_files_for_dialog .clear () self .favorite_download_scope =FAVORITE_SCOPE_SELECTED_LOCATION self ._update_favorite_scope_button_text () @@ -5976,6 +6331,11 @@ class DownloaderApp (QWidget ): if self .pause_event :self .pause_event .clear () self .is_paused =False self .missed_key_terms_buffer .clear () + if self .download_extracted_links_button : + self .only_links_log_display_mode = LOG_DISPLAY_LINKS + self .mega_download_log_preserved_once =False + self .download_extracted_links_button .setEnabled (False ) + if self .missed_character_log_output :self .missed_character_log_output .clear () self .permanently_failed_files_for_dialog .clear ()