This commit is contained in:
Yuvi9587 2025-06-06 04:55:51 +01:00
parent 3b010b8eeb
commit 401ccd9884
2 changed files with 413 additions and 143 deletions

View File

@ -117,9 +117,13 @@ def parse_cookie_string(cookie_string):
if name: # Ensure name is not empty
cookies[name] = value
return cookies if cookies else None
def load_cookies_from_netscape_file(filepath, logger_func):
"""Loads cookies from a Netscape-formatted cookies.txt file."""
def load_cookies_from_netscape_file(filepath, logger_func, target_domain_filter=None): # Added target_domain_filter
"""Loads cookies from a Netscape-formatted cookies.txt file.
If target_domain_filter is provided, only cookies for that domain (or its subdomains) are returned.
"""
cookies = {}
loaded_for_target_domain_count = 0
total_cookies_in_file = 0
try:
with open(filepath, 'r', encoding='utf-8') as f:
for line_num, line in enumerate(f, 1):
@ -127,13 +131,40 @@ def load_cookies_from_netscape_file(filepath, logger_func):
if not line or line.startswith('#'):
continue
parts = line.split('\t')
total_cookies_in_file +=1 # Count all valid lines
if len(parts) == 7:
cookie_domain_from_file = parts[0] # e.g., ".kemono.su" or "kemono.su"
name = parts[5]
value = parts[6]
if name: # Ensure name is not empty
cookies[name] = value
logger_func(f" 🍪 Loaded {len(cookies)} cookies from '{os.path.basename(filepath)}'.")
return cookies if cookies else None
if target_domain_filter:
# Standard cookie domain matching logic:
# A cookie set for ".example.com" is valid for "example.com" and "www.example.com".
# A cookie set for "example.com" is valid only for "example.com".
host_to_match = target_domain_filter.lower()
cookie_domain_norm = cookie_domain_from_file.lower()
is_match = False
if cookie_domain_norm.startswith('.'):
# Cookie domain is like ".example.com"
# Valid if host_to_match is "example.com" or "sub.example.com"
# i.e., host_to_match equals cookie_domain_norm[1:] OR host_to_match ends with cookie_domain_norm
if host_to_match == cookie_domain_norm[1:] or host_to_match.endswith(cookie_domain_norm):
is_match = True
else:
# Cookie domain is like "example.com"
# Valid only if host_to_match is "example.com"
if host_to_match == cookie_domain_norm:
is_match = True
if is_match:
cookies[name] = value
loaded_for_target_domain_count += 1
else: # No target_domain_filter, load all
cookies[name] = value
if target_domain_filter:
logger_func(f" 🍪 Scanned {total_cookies_in_file} cookies in '{os.path.basename(filepath)}'. Loaded {loaded_for_target_domain_count} for domain '{target_domain_filter}'.")
else:
logger_func(f" 🍪 Loaded {len(cookies)} cookies from '{os.path.basename(filepath)}' (no domain filter).")
return cookies if cookies else None # Return None if empty, even if filtered
except FileNotFoundError:
logger_func(f" 🍪 Cookie file '{os.path.basename(filepath)}' not found at expected location.")
return None
@ -328,35 +359,73 @@ def extract_post_info(url_string):
except Exception as e:
print(f"Debug: Exception during extract_post_info for URL '{url_string}': {e}")
return None, None, None
def prepare_cookies_for_request(use_cookie_flag, cookie_text_input, selected_cookie_file_path, app_base_dir, logger_func):
def prepare_cookies_for_request(use_cookie_flag, cookie_text_input, selected_cookie_file_path_from_ui, app_base_dir, logger_func, target_domain=None):
"""Prepares a cookie dictionary from text input or cookies.txt file."""
if not use_cookie_flag:
return None
if selected_cookie_file_path:
logger_func(f" 🍪 Attempting to load cookies from selected file: '{os.path.basename(selected_cookie_file_path)}'...")
cookies = load_cookies_from_netscape_file(selected_cookie_file_path, logger_func)
if cookies:
return cookies
else:
logger_func(f" ⚠️ Failed to load cookies from selected file: '{os.path.basename(selected_cookie_file_path)}'. Trying other methods.")
if app_base_dir: # Only proceed if app_base_dir is available
default_cookies_path = os.path.join(app_base_dir, "cookies.txt")
if os.path.exists(default_cookies_path): # Only attempt if it exists
if not selected_cookie_file_path: # Log attempt only if we didn't just try a selected file
logger_func(f" 🍪 No specific file selected. Attempting to load default '{os.path.basename(default_cookies_path)}' from app directory...")
cookies = load_cookies_from_netscape_file(default_cookies_path, logger_func)
attempted_paths = set() # Keep track of paths we've tried to load to avoid redundant attempts/logs
# Attempt 1: UI-selected file, if relevant for the target_domain
if selected_cookie_file_path_from_ui:
basename_selected = os.path.basename(selected_cookie_file_path_from_ui)
is_relevant_selection = False
if target_domain: # If a specific domain is targeted
if basename_selected == f"{target_domain}_cookies.txt" or basename_selected == "cookies.txt":
is_relevant_selection = True
else: # No target_domain, so any selected file is considered relevant (e.g. for direct file downloads)
is_relevant_selection = True
if is_relevant_selection:
logger_func(f" 🍪 Attempting to load cookies from UI-selected file: '{basename_selected}' for domain '{target_domain or 'any'}'...")
norm_selected_path = os.path.normpath(selected_cookie_file_path_from_ui)
attempted_paths.add(norm_selected_path)
cookies = load_cookies_from_netscape_file(selected_cookie_file_path_from_ui, logger_func, target_domain_filter=target_domain)
if cookies:
return cookies
elif not selected_cookie_file_path: # Log failure only if we tried default as primary file method
logger_func(f" ⚠️ Failed to load cookies from default file: '{os.path.basename(default_cookies_path)}'. Trying text input.")
else:
logger_func(f" ⚠️ Failed to load cookies from UI-selected file: '{basename_selected}'.")
else:
logger_func(f" UI-selected cookie file '{basename_selected}' is not specific to target domain '{target_domain}' or generic. Skipping it for this request, will try other sources.")
# Attempt 2: Domain-specific file in app_base_dir (e.g., <target_domain>_cookies.txt)
if app_base_dir and target_domain:
domain_specific_filename = f"{target_domain}_cookies.txt"
domain_specific_path = os.path.join(app_base_dir, domain_specific_filename)
norm_domain_specific_path = os.path.normpath(domain_specific_path)
if os.path.exists(domain_specific_path) and norm_domain_specific_path not in attempted_paths:
logger_func(f" 🍪 Attempting to load domain-specific cookies: '{domain_specific_filename}' for '{target_domain}' from app directory...")
attempted_paths.add(norm_domain_specific_path)
cookies = load_cookies_from_netscape_file(domain_specific_path, logger_func, target_domain_filter=target_domain)
if cookies:
return cookies
else:
logger_func(f" ⚠️ Failed to load cookies from '{domain_specific_filename}' in app directory.")
# Attempt 3: Default cookies.txt in app_base_dir
if app_base_dir:
default_cookies_filename = "cookies.txt"
default_cookies_path = os.path.join(app_base_dir, default_cookies_filename)
norm_default_path = os.path.normpath(default_cookies_path)
if os.path.exists(default_cookies_path) and norm_default_path not in attempted_paths:
logger_func(f" 🍪 Attempting to load default '{default_cookies_filename}' from app directory for domain '{target_domain or 'any'}'...")
attempted_paths.add(norm_default_path)
cookies = load_cookies_from_netscape_file(default_cookies_path, logger_func, target_domain_filter=target_domain)
if cookies:
return cookies
else:
logger_func(f" ⚠️ Failed to load cookies from default '{default_cookies_filename}' in app directory.")
# Attempt 4: UI text input
if cookie_text_input:
logger_func(" 🍪 Using cookies from UI text input (as file methods failed or were not applicable).")
logger_func(f" 🍪 Using cookies from UI text input for domain '{target_domain or 'any'}' (as file methods failed or were not applicable).")
cookies = parse_cookie_string(cookie_text_input)
if cookies:
return cookies
else:
logger_func(" ⚠️ UI cookie text input was provided but was empty or invalid.")
logger_func(" 🍪 Cookie usage enabled, but no valid cookies found from any source (selected file, default file, or text input).")
logger_func(f" 🍪 Cookie usage enabled for domain '{target_domain or 'any'}', but no valid cookies found from any source.")
return None
def fetch_posts_paginated(api_url_base, headers, offset, logger, cancellation_event=None, pause_event=None, cookies_dict=None):
if cancellation_event and cancellation_event.is_set(): # type: ignore
@ -442,7 +511,7 @@ def download_from_api(api_url_input, logger=print, start_page=None, end_page=Non
api_domain = "kemono.su" # Default domain if input is unusual
cookies_for_api = None
if use_cookie and app_base_dir: # app_base_dir is needed for cookies.txt path
cookies_for_api = prepare_cookies_for_request(use_cookie, cookie_text, selected_cookie_file, app_base_dir, logger)
cookies_for_api = prepare_cookies_for_request(use_cookie, cookie_text, selected_cookie_file, app_base_dir, logger, target_domain=api_domain)
if target_post_id:
direct_post_api_url = f"https://{api_domain}/api/v1/{service}/user/{user_id}/post/{target_post_id}"
logger(f" Attempting direct fetch for target post: {direct_post_api_url}")
@ -781,7 +850,7 @@ class PostProcessorWorker:
if self.use_cookie: # This flag comes from the checkbox
# self.logger(f" [DSF_DEBUG] Preparing cookies...")
cookies_to_use_for_file = prepare_cookies_for_request(self.use_cookie, self.cookie_text, self.selected_cookie_file, self.app_base_dir, self.logger)
# self.logger(f" [DSF_DEBUG] Cookies prepared.")
# self.logger(f" [DSF_DEBUG] Cookies prepared for file download (target_domain not applicable here as it's for API auth, not direct file links).")
api_original_filename = file_info.get('_original_name_for_log', file_info.get('name'))
# self.logger(f" [DSF_DEBUG] API original filename: '{api_original_filename}'")

429
main.py
View File

@ -876,13 +876,21 @@ class FavoriteArtistsDialog(QDialog):
self.setWindowTitle("Favorite Artists")
self.setModal(True) # type: ignore
self.setMinimumSize(500, 500) # Reduced minimum height
self.setMinimumSize(500, 500)
if hasattr(self.parent_app, 'get_dark_theme'):
self.setStyleSheet(self.parent_app.get_dark_theme())
self._init_ui()
self._fetch_favorite_artists()
def _get_domain_for_service(self, service_name):
service_lower = service_name.lower()
coomer_primary_services = {'onlyfans', 'fansly', 'manyvids', 'candfans'}
if service_lower in coomer_primary_services:
return "coomer.su"
else: # For patreon, fanbox, pixiv, etc., default to kemono.su
return "kemono.su"
def _init_ui(self):
main_layout = QVBoxLayout(self)
@ -908,7 +916,7 @@ class FavoriteArtistsDialog(QDialog):
self.search_input.setVisible(False)
self.artist_list_widget.setVisible(False)
self.status_label.setText("⏳ Loading favorite artists...") # Initial loading message
self.status_label.setText("⏳ Loading favorite artists...")
self.status_label.setAlignment(Qt.AlignCenter)
combined_buttons_layout = QHBoxLayout()
@ -948,76 +956,121 @@ class FavoriteArtistsDialog(QDialog):
self.artist_list_widget.setVisible(show)
def _fetch_favorite_artists(self):
fav_url = "https://kemono.su/api/v1/account/favorites?type=artist"
self._logger(f"Attempting to fetch favorite artists from: {fav_url}")
kemono_fav_url = "https://kemono.su/api/v1/account/favorites?type=artist"
coomer_fav_url = "https://coomer.su/api/v1/account/favorites?type=artist"
cookies_dict = prepare_cookies_for_request(
self.cookies_config['use_cookie'],
self.cookies_config['cookie_text'],
self.cookies_config['selected_cookie_file'],
self.cookies_config['app_base_dir'],
self._logger
)
if self.cookies_config['use_cookie'] and not cookies_dict:
self.status_label.setText("Error: Cookies enabled but could not be loaded. Cannot fetch favorites.")
self._show_content_elements(False)
self._logger("Error: Cookies enabled but could not be loaded. Showing help dialog.")
self.all_fetched_artists = []
fetched_any_successfully = False
errors_occurred = []
any_cookies_loaded_successfully_for_any_source = False # New flag
api_sources = [
{"name": "Kemono.su", "url": kemono_fav_url, "domain": "kemono.su"},
{"name": "Coomer.su", "url": coomer_fav_url, "domain": "coomer.su"}
]
for source in api_sources:
self._logger(f"Attempting to fetch favorite artists from: {source['name']} ({source['url']})")
self.status_label.setText(f"⏳ Loading favorites from {source['name']}...")
QCoreApplication.processEvents() # Update UI
cookies_dict_for_source = None
if self.cookies_config['use_cookie']:
cookies_dict_for_source = prepare_cookies_for_request(
True, # use_cookie_flag
self.cookies_config['cookie_text'], # cookie_text_input
self.cookies_config['selected_cookie_file'], # selected_cookie_file_path_from_ui
self.cookies_config['app_base_dir'], # app_base_dir
self._logger, # logger_func
target_domain=source['domain'] # target_domain
)
if cookies_dict_for_source:
any_cookies_loaded_successfully_for_any_source = True
else:
self._logger(f"Warning ({source['name']}): Cookies enabled but could not be loaded for this domain. Fetch might fail if cookies are required.")
try:
headers = {'User-Agent': 'Mozilla/5.0'}
response = requests.get(source['url'], headers=headers, cookies=cookies_dict_for_source, timeout=20)
response.raise_for_status()
artists_data_from_api = response.json()
if not isinstance(artists_data_from_api, list):
error_msg = f"Error ({source['name']}): API did not return a list of artists (got {type(artists_data_from_api)})."
self._logger(error_msg)
errors_occurred.append(error_msg)
continue
processed_artists_from_source = 0
for artist_entry in artists_data_from_api:
artist_id = artist_entry.get("id")
artist_name = html.unescape(artist_entry.get("name", "Unknown Artist").strip())
artist_service_platform = artist_entry.get("service") # e.g., "patreon", "onlyfans"
if artist_id and artist_name and artist_service_platform:
artist_page_domain = self._get_domain_for_service(artist_service_platform)
full_url = f"https://{artist_page_domain}/{artist_service_platform}/user/{artist_id}"
self.all_fetched_artists.append({
'name': artist_name,
'url': full_url,
'service': artist_service_platform,
'id': artist_id,
'_source_api': source['name']
})
processed_artists_from_source += 1
else:
self._logger(f"Warning ({source['name']}): Skipping favorite artist entry due to missing data: {artist_entry}")
if processed_artists_from_source > 0:
fetched_any_successfully = True
self._logger(f"Fetched {processed_artists_from_source} artists from {source['name']}.")
except requests.exceptions.RequestException as e:
error_msg = f"Error fetching favorites from {source['name']}: {e}"
self._logger(error_msg)
errors_occurred.append(error_msg)
except Exception as e:
error_msg = f"An unexpected error occurred with {source['name']}: {e}"
self._logger(error_msg)
errors_occurred.append(error_msg)
# Check if cookies were enabled but none loaded for any source
if self.cookies_config['use_cookie'] and not any_cookies_loaded_successfully_for_any_source:
self.status_label.setText("Error: Cookies enabled but could not be loaded for any source.")
self._logger("Error: Cookies enabled but no cookies loaded for any source. Showing help dialog.")
cookie_help_dialog = CookieHelpDialog(self)
cookie_help_dialog.exec_()
self.download_button.setEnabled(False)
return
try:
headers = {'User-Agent': 'Mozilla/5.0'}
response = requests.get(fav_url, headers=headers, cookies=cookies_dict, timeout=20)
response.raise_for_status()
if not fetched_any_successfully: # Add to errors if this was the sole reason for failure
errors_occurred.append("Cookies enabled but could not be loaded for any API source.")
artists_data = response.json()
unique_artists_map = {}
for artist in self.all_fetched_artists:
key = (artist['service'].lower(), str(artist['id']).lower())
if key not in unique_artists_map:
unique_artists_map[key] = artist
self.all_fetched_artists = list(unique_artists_map.values())
if not isinstance(artists_data, list):
self.status_label.setText("Error: API did not return a list of artists.")
self._show_content_elements(False)
self._logger(f"Error: Expected a list from API, got {type(artists_data)}")
QMessageBox.critical(self, "API Error", "The favorite artists API did not return the expected data format (list).")
return
self.all_fetched_artists.sort(key=lambda x: x['name'].lower())
self._populate_artist_list_widget()
self.all_fetched_artists = []
for artist_entry in artists_data:
artist_id = artist_entry.get("id")
artist_name = html.unescape(artist_entry.get("name", "Unknown Artist").strip())
artist_service = artist_entry.get("service")
if artist_id and artist_name and artist_service:
full_url = f"https://kemono.su/{artist_service}/user/{artist_id}"
self.all_fetched_artists.append({'name': artist_name, 'url': full_url, 'service': artist_service})
else:
self._logger(f"Warning: Skipping favorite artist entry due to missing data: {artist_entry}")
self.all_fetched_artists.sort(key=lambda x: x['name'].lower())
self._populate_artist_list_widget()
if self.all_fetched_artists:
self.status_label.setText(f"Found {len(self.all_fetched_artists)} favorite artist(s).")
self._show_content_elements(True)
self.download_button.setEnabled(True)
else:
self.status_label.setText("No favorite artists found.")
self._show_content_elements(False)
self.download_button.setEnabled(False)
except requests.exceptions.RequestException as e:
self.status_label.setText(f"Error fetching favorites: {e}")
if fetched_any_successfully and self.all_fetched_artists:
self.status_label.setText(f"Found {len(self.all_fetched_artists)} total favorite artist(s).")
self._show_content_elements(True)
self.download_button.setEnabled(True)
elif not fetched_any_successfully and not errors_occurred:
self.status_label.setText("No favorite artists found on Kemono.su or Coomer.su.")
self._show_content_elements(False)
self.download_button.setEnabled(False)
else:
final_error_message = "Failed to fetch favorites."
if errors_occurred:
final_error_message += " Errors: " + "; ".join(errors_occurred)
self.status_label.setText(final_error_message)
self._show_content_elements(False)
self._logger(f"Error fetching favorites: {e}")
QMessageBox.critical(self, "Fetch Error", f"Could not fetch favorite artists: {e}")
self.download_button.setEnabled(False)
except Exception as e:
self.status_label.setText(f"An unexpected error occurred: {e}")
self._show_content_elements(False)
self._logger(f"Unexpected error: {e}")
QMessageBox.critical(self, "Error", f"An unexpected error occurred: {e}")
self.download_button.setEnabled(False)
if fetched_any_successfully and not self.all_fetched_artists:
self.status_label.setText("No favorite artists found after processing.")
def _populate_artist_list_widget(self, artists_to_display=None):
self.artist_list_widget.clear()
@ -1070,65 +1123,147 @@ class FavoritePostsFetcherThread(QThread):
progress_bar_update = pyqtSignal(int, int) # value, maximum
finished = pyqtSignal(list, str) # list of posts, error message (or None)
def __init__(self, cookies_config, parent_logger_func): # Removed parent_get_domain_func
def __init__(self, cookies_config, parent_logger_func, target_domain_preference=None):
super().__init__()
self.cookies_config = cookies_config
self.parent_logger_func = parent_logger_func
self.target_domain_preference = target_domain_preference
self.cancellation_event = threading.Event()
def _logger(self, message):
self.parent_logger_func(f"[FavPostsFetcherThread] {message}")
def run(self):
fav_url = "https://kemono.su/api/v1/account/favorites?type=post"
self._logger(f"Attempting to fetch favorite posts from: {fav_url}")
kemono_fav_posts_url = "https://kemono.su/api/v1/account/favorites?type=post"
coomer_fav_posts_url = "https://coomer.su/api/v1/account/favorites?type=post"
all_fetched_posts_temp = []
errors_occurred = []
fetched_any_successfully = False
any_cookies_loaded_successfully_for_any_source = False # New flag
self.status_update.emit("Fetching list of favorite posts...")
self.progress_bar_update.emit(0, 0) # Indeterminate state for initial fetch
cookies_dict = prepare_cookies_for_request(
self.cookies_config['use_cookie'],
self.cookies_config['cookie_text'],
self.cookies_config['selected_cookie_file'],
self.cookies_config['app_base_dir'],
self._logger
)
api_sources = [
{"name": "Kemono.su", "url": kemono_fav_posts_url, "domain": "kemono.su"},
{"name": "Coomer.su", "url": coomer_fav_posts_url, "domain": "coomer.su"}
]
if self.cookies_config['use_cookie'] and not cookies_dict:
self.finished.emit([], "COOKIES_REQUIRED_BUT_NOT_FOUND")
api_sources_to_try = []
if self.target_domain_preference:
self._logger(f"Targeting specific domain for favorites: {self.target_domain_preference}")
for source_def in api_sources:
if source_def["domain"] == self.target_domain_preference:
api_sources_to_try.append(source_def)
break
if not api_sources_to_try: # Should not happen if preference is valid
self._logger(f"Warning: Preferred domain '{self.target_domain_preference}' not a recognized API source. Fetching from all.")
api_sources_to_try = api_sources
else: # No preference, try all (or if both cookies were available)
self._logger("No specific domain preference, or both domains have cookies. Will attempt to fetch from all sources.")
api_sources_to_try = api_sources
for source in api_sources_to_try:
if self.cancellation_event.is_set():
self.finished.emit([], "Cancelled by user during fetch.")
return
cookies_dict_for_source = None
if self.cookies_config['use_cookie']:
cookies_dict_for_source = prepare_cookies_for_request(
True, # use_cookie_flag
self.cookies_config['cookie_text'], # cookie_text_input
self.cookies_config['selected_cookie_file'], # selected_cookie_file_path_from_ui
self.cookies_config['app_base_dir'], # app_base_dir
self._logger, # logger_func
target_domain=source['domain'] # target_domain
)
if cookies_dict_for_source:
any_cookies_loaded_successfully_for_any_source = True
else:
self._logger(f"Warning ({source['name']}): Cookies enabled but could not be loaded for this domain. Fetch might fail if cookies are required.")
self._logger(f"Attempting to fetch favorite posts from: {source['name']} ({source['url']})")
self.status_update.emit(f"Fetching favorites from {source['name']}...")
QCoreApplication.processEvents()
try:
headers = {'User-Agent': 'Mozilla/5.0'}
response = requests.get(source['url'], headers=headers, cookies=cookies_dict_for_source, timeout=20)
response.raise_for_status()
posts_data_from_api = response.json()
if not isinstance(posts_data_from_api, list):
error_msg = f"Error ({source['name']}): API did not return a list of posts (got {type(posts_data_from_api)})."
self._logger(error_msg)
errors_occurred.append(error_msg)
continue
processed_posts_from_source = 0
for post_entry in posts_data_from_api:
post_id = post_entry.get("id")
post_title = html.unescape(post_entry.get("title", "Untitled Post").strip())
service = post_entry.get("service")
creator_id = post_entry.get("user")
added_date_str = post_entry.get("added", post_entry.get("published", ""))
if post_id and post_title and service and creator_id:
all_fetched_posts_temp.append({
'post_id': post_id, 'title': post_title, 'service': service,
'creator_id': creator_id, 'added_date': added_date_str,
'_source_api': source['name']
})
processed_posts_from_source +=1
else:
self._logger(f"Warning ({source['name']}): Skipping favorite post entry due to missing data: {post_entry}")
if processed_posts_from_source > 0:
fetched_any_successfully = True
self._logger(f"Fetched {processed_posts_from_source} posts from {source['name']}.")
except requests.exceptions.RequestException as e:
error_msg = f"Error fetching favorite posts from {source['name']}: {e}"
self._logger(error_msg)
errors_occurred.append(error_msg)
except Exception as e:
error_msg = f"An unexpected error occurred with {source['name']}: {e}"
self._logger(error_msg)
errors_occurred.append(error_msg)
if self.cancellation_event.is_set():
self.finished.emit([], "Cancelled by user after fetch attempts.")
return
try:
headers = {'User-Agent': 'Mozilla/5.0'}
response = requests.get(fav_url, headers=headers, cookies=cookies_dict, timeout=20)
response.raise_for_status()
posts_data_from_api = response.json()
# Check if cookies were globally enabled but none loaded for any source
if self.cookies_config['use_cookie'] and not any_cookies_loaded_successfully_for_any_source:
# If a specific domain was targeted and its cookies failed to load, this is also an issue.
if self.target_domain_preference and not any_cookies_loaded_successfully_for_any_source:
self.finished.emit([], f"COOKIES_REQUIRED_BUT_NOT_FOUND_FOR_{self.target_domain_preference.upper()}")
return
# General case: cookies enabled, but none loaded for any attempted source.
self.finished.emit([], "COOKIES_REQUIRED_BUT_NOT_FOUND") # Use existing error code
return
if not isinstance(posts_data_from_api, list):
self.finished.emit([], f"Error: API did not return a list of posts (got {type(posts_data_from_api)}).")
return
all_fetched_posts_temp = []
for post_entry in posts_data_from_api:
post_id = post_entry.get("id")
post_title = html.unescape(post_entry.get("title", "Untitled Post").strip())
service = post_entry.get("service")
creator_id = post_entry.get("user")
added_date_str = post_entry.get("added", post_entry.get("published", ""))
unique_posts_map = {}
for post in all_fetched_posts_temp:
key = (post['service'].lower(), str(post['creator_id']).lower(), str(post['post_id']).lower())
if key not in unique_posts_map:
unique_posts_map[key] = post
all_fetched_posts_temp = list(unique_posts_map.values())
if post_id and post_title and service and creator_id:
all_fetched_posts_temp.append({
'post_id': post_id, 'title': post_title, 'service': service,
'creator_id': creator_id, 'added_date': added_date_str
})
else:
self._logger(f"Warning: Skipping favorite post entry due to missing data: {post_entry}")
all_fetched_posts_temp.sort(key=lambda x: (x.get('service','').lower(), x.get('creator_id','').lower(), (x.get('added_date') or '')), reverse=False)
all_fetched_posts_temp.sort(key=lambda x: (x.get('_source_api','').lower(), x.get('service','').lower(), str(x.get('creator_id','')).lower(), (x.get('added_date') or '')), reverse=False)
if errors_occurred:
error_summary = "; ".join(errors_occurred)
if not fetched_any_successfully: # All attempted sources failed
self.finished.emit([], f"Failed to fetch favorites. Errors: {error_summary}")
else: # Some succeeded, some failed (only relevant if multiple sources were tried)
self.finished.emit(all_fetched_posts_temp, f"Fetched some favorites, but errors occurred with other sources: {error_summary}")
elif not all_fetched_posts_temp and not fetched_any_successfully and not self.target_domain_preference: # No specific target, tried all, found nothing, no errors
self.finished.emit([], "No favorite posts found on any platform.")
else:
self.finished.emit(all_fetched_posts_temp, None)
except requests.exceptions.RequestException as e:
self.finished.emit([], f"Error fetching favorite posts: {e}")
except Exception as e:
self.finished.emit([], f"An unexpected error occurred: {e}")
class PostListItemWidget(QWidget):
"""Custom widget for displaying a single post in the FavoritePostsDialog list."""
def __init__(self, post_data_dict, parent_dialog_ref, parent=None):
@ -1171,13 +1306,14 @@ class PostListItemWidget(QWidget):
class FavoritePostsDialog(QDialog):
"""Dialog to display and select favorite posts."""
def __init__(self, parent_app, cookies_config, known_names_list_ref):
def __init__(self, parent_app, cookies_config, known_names_list_ref, target_domain_preference=None):
super().__init__(parent_app)
self.parent_app = parent_app
self.cookies_config = cookies_config
self.all_fetched_posts = []
self.selected_posts_data = []
self.known_names_list_ref = known_names_list_ref # Store reference to global KNOWN_NAMES
self.target_domain_preference_for_this_fetch = target_domain_preference # Store the preference
self.creator_name_cache = {} # To store (service, id) -> name
self.displayable_grouped_posts = {} # For storing posts grouped by artist
self.fetcher_thread = None # For the worker thread
@ -1292,7 +1428,8 @@ class FavoritePostsDialog(QDialog):
self.fetcher_thread = FavoritePostsFetcherThread(
self.cookies_config,
self.parent_app.log_signal.emit, # Pass parent's logger
self.parent_app.log_signal.emit, # Pass parent's logger,
target_domain_preference=self.target_domain_preference_for_this_fetch # Pass the preference
) # Removed _get_domain_for_service
self.fetcher_thread.status_update.connect(self.status_label.setText)
self.fetcher_thread.finished.connect(self._on_fetch_completed)
@ -1310,16 +1447,34 @@ class FavoritePostsDialog(QDialog):
def _on_fetch_completed(self, fetched_posts_list, error_msg):
if error_msg:
if error_msg == "COOKIES_REQUIRED_BUT_NOT_FOUND":
specific_domain_msg_part = ""
if self.target_domain_preference_for_this_fetch:
specific_domain_msg_part = f" for {self.target_domain_preference_for_this_fetch}"
if error_msg.startswith("COOKIES_REQUIRED_BUT_NOT_FOUND"):
self.status_label.setText("Error: Cookies are required for favorite posts but could not be loaded.")
self._logger("Error: Cookies required for favorite posts but not found. Showing help dialog.")
self._logger(f"Error: Cookies required for favorite posts{specific_domain_msg_part} but not found. Showing help dialog.")
cookie_help_dialog = CookieHelpDialog(self)
cookie_help_dialog.exec_()
self.download_button.setEnabled(False) # Ensure it's disabled
else:
elif "401" in error_msg and ("UNAUTHORIZED" in error_msg.upper() or "Client Error" in error_msg):
self.status_label.setText("Error: Authorization failed. Check your cookies.")
self._logger(f"Error: 401 Unauthorized fetching favorites{specific_domain_msg_part}. Suggesting cookie check.")
QMessageBox.warning(self, "Authorization Failed (401)",
f"Could not fetch favorites{specific_domain_msg_part} due to an authorization error:\n\n{error_msg}\n\n"
"This usually means your cookies are missing, invalid, or expired for the site. Please check your cookie setup.")
cookie_help_dialog = CookieHelpDialog(self) # Offer help for 401 as well
cookie_help_dialog.exec_()
self.download_button.setEnabled(False) # Ensure it's disabled
else: # General error
self.status_label.setText(error_msg)
self._logger(error_msg) # Log to main app log
QMessageBox.critical(self, "Fetch Error", error_msg)
if self.target_domain_preference_for_this_fetch:
QMessageBox.critical(self, "Fetch Error",
f"Error fetching favorites from {self.target_domain_preference_for_this_fetch}:\n\n{error_msg}")
else:
QMessageBox.critical(self, "Fetch Error", error_msg)
self.download_button.setEnabled(False) # Ensure it's disabled on any error
self.progress_bar.setVisible(False) # Hide progress bar on error
return
@ -3140,6 +3295,20 @@ class DownloaderApp(QWidget):
if hasattr(self, 'link_input'): # Initialize last_link_input_text_for_queue_sync
self.last_link_input_text_for_queue_sync = self.link_input.text()
# Add this helper method
def _get_domain_for_service(self, service_name):
"""Determines the base domain for a given service platform."""
service_lower = service_name.lower()
# Services typically found on Coomer
coomer_primary_services = {'onlyfans', 'fansly', 'manyvids', 'candfans'}
# Add other services that are primarily hosted/namespaced under coomer.su
# For example, if coomer.su mirrors patreon posts under coomer.su/patreon, then 'patreon' might also map to coomer.su
# However, for constructing canonical URLs, 'patreon' should generally map to kemono.su.
if service_lower in coomer_primary_services:
return "coomer.su"
else: # For patreon, fanbox, pixiv, etc., default to kemono.su
return "kemono.su"
def _show_future_settings_dialog(self):
"""Shows the placeholder dialog for future settings."""
dialog = FutureSettingsDialog(self)
@ -6431,27 +6600,59 @@ class DownloaderApp(QWidget):
'app_base_dir': self.app_base_dir
}
global KNOWN_NAMES # Ensure we have access to the global
target_domain_preference_for_fetch = None # Default to fetch all
if cookies_config['use_cookie']:
temp_cookies_for_check = prepare_cookies_for_request(
self.log_signal.emit("Favorite Posts: 'Use Cookie' is checked. Determining target domain...")
kemono_cookies = prepare_cookies_for_request(
cookies_config['use_cookie'],
cookies_config['cookie_text'],
cookies_config['selected_cookie_file'],
cookies_config['app_base_dir'],
lambda msg: self.log_signal.emit(f"[FavPosts Cookie Check] {msg}")
lambda msg: self.log_signal.emit(f"[FavPosts Cookie Check - Kemono] {msg}"),
target_domain="kemono.su"
)
if temp_cookies_for_check is None:
coomer_cookies = prepare_cookies_for_request(
cookies_config['use_cookie'],
cookies_config['cookie_text'],
cookies_config['selected_cookie_file'],
cookies_config['app_base_dir'],
lambda msg: self.log_signal.emit(f"[FavPosts Cookie Check - Coomer] {msg}"),
target_domain="coomer.su"
)
kemono_ok = bool(kemono_cookies)
coomer_ok = bool(coomer_cookies)
if kemono_ok and not coomer_ok:
target_domain_preference_for_fetch = "kemono.su"
self.log_signal.emit(" ↳ Only Kemono.su cookies loaded. Will fetch favorites from Kemono.su only.")
elif coomer_ok and not kemono_ok:
target_domain_preference_for_fetch = "coomer.su"
self.log_signal.emit(" ↳ Only Coomer.su cookies loaded. Will fetch favorites from Coomer.su only.")
elif kemono_ok and coomer_ok:
target_domain_preference_for_fetch = None # Both available, fetch all
self.log_signal.emit(" ↳ Cookies for both Kemono.su and Coomer.su loaded. Will attempt to fetch from both.")
else: # Neither loaded
self.log_signal.emit(" ↳ No valid cookies loaded for Kemono.su or Coomer.su.")
cookie_help_dialog = CookieHelpDialog(self)
cookie_help_dialog.exec_()
return # Don't proceed to show FavoritePostsDialog if cookies are needed but not found
else: # 'Use Cookie' is not checked
self.log_signal.emit("Favorite Posts: 'Use Cookie' is NOT checked. Cookies are required.")
cookie_help_dialog = CookieHelpDialog(self)
cookie_help_dialog.exec_()
return
dialog = FavoritePostsDialog(self, cookies_config, KNOWN_NAMES) # Pass KNOWN_NAMES
dialog = FavoritePostsDialog(self, cookies_config, KNOWN_NAMES, target_domain_preference_for_fetch)
if dialog.exec_() == QDialog.Accepted:
selected_posts = dialog.get_selected_posts()
if selected_posts:
self.log_signal.emit(f" Queuing {len(selected_posts)} favorite post(s) for download.")
for post_data in selected_posts:
domain = "kemono.su" # Or determine from service/parent app settings
direct_post_url = f"https://{domain}/{post_data['service']}/user/{post_data['creator_id']}/post/{post_data['post_id']}"
domain = self._get_domain_for_service(post_data['service'])
direct_post_url = f"https://{domain}/{post_data['service']}/user/{str(post_data['creator_id'])}/post/{str(post_data['post_id'])}"
queue_item = {
'url': direct_post_url,