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 + "