# --- Standard Library Imports --- import html import re # --- Third-Party Library Imports --- import cloudscraper # MODIFIED: Import cloudscraper from PyQt5.QtCore import QCoreApplication, Qt from PyQt5.QtWidgets import ( QApplication, QDialog, QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem, QMessageBox, QPushButton, QVBoxLayout ) # --- Local Application Imports --- from ...i18n.translator import get_translation from ..assets import get_app_icon_object from ...utils.network_utils import prepare_cookies_for_request from .CookieHelpDialog import CookieHelpDialog from ...utils.resolution import get_dark_theme class FavoriteArtistsDialog (QDialog ): """Dialog to display and select favorite artists.""" def __init__ (self ,parent_app ,cookies_config ): super ().__init__ (parent_app ) self .parent_app =parent_app self .cookies_config =cookies_config self .all_fetched_artists =[] app_icon =get_app_icon_object () if not app_icon .isNull (): self .setWindowIcon (app_icon ) self .selected_artist_urls =[] self .setModal (True ) self .setMinimumSize (500 ,500 ) 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.st" else: return "kemono.cr" def _tr (self ,key ,default_text =""): """Helper to get translation based on current app language.""" if callable (get_translation )and self .parent_app : return get_translation (self .parent_app .current_selected_language ,key ,default_text ) return default_text def _retranslate_ui (self ): self .setWindowTitle (self ._tr ("fav_artists_dialog_title","Favorite Artists")) self .status_label .setText (self ._tr ("fav_artists_loading_status","Loading favorite artists...")) self .search_input .setPlaceholderText (self ._tr ("fav_artists_search_placeholder","Search artists...")) self .select_all_button .setText (self ._tr ("fav_artists_select_all_button","Select All")) self .deselect_all_button .setText (self ._tr ("fav_artists_deselect_all_button","Deselect All")) self .download_button .setText (self ._tr ("fav_artists_download_selected_button","Download Selected")) self .cancel_button .setText (self ._tr ("fav_artists_cancel_button","Cancel")) def _init_ui (self ): main_layout =QVBoxLayout (self ) self .status_label =QLabel () self .status_label .setAlignment (Qt .AlignCenter ) main_layout .addWidget (self .status_label ) self .search_input =QLineEdit () self .search_input .textChanged .connect (self ._filter_artist_list_display ) main_layout .addWidget (self .search_input ) self .artist_list_widget =QListWidget () self .artist_list_widget .setStyleSheet (""" QListWidget::item { border-bottom: 1px solid #4A4A4A; /* Slightly softer line */ padding-top: 4px; padding-bottom: 4px; }""") main_layout .addWidget (self .artist_list_widget ) self .artist_list_widget .setAlternatingRowColors (True ) self .search_input .setVisible (False ) self .artist_list_widget .setVisible (False ) combined_buttons_layout =QHBoxLayout () self .select_all_button =QPushButton () self .select_all_button .clicked .connect (self ._select_all_items ) combined_buttons_layout .addWidget (self .select_all_button ) self .deselect_all_button =QPushButton () self .deselect_all_button .clicked .connect (self ._deselect_all_items ) combined_buttons_layout .addWidget (self .deselect_all_button ) self .download_button =QPushButton () self .download_button .clicked .connect (self ._accept_selection_action ) self .download_button .setEnabled (False ) self .download_button .setDefault (True ) combined_buttons_layout .addWidget (self .download_button ) self .cancel_button =QPushButton () self .cancel_button .clicked .connect (self .reject ) combined_buttons_layout .addWidget (self .cancel_button ) combined_buttons_layout .addStretch (1 ) main_layout .addLayout (combined_buttons_layout ) self ._retranslate_ui () if hasattr (self .parent_app ,'get_dark_theme')and self .parent_app .current_theme =="dark": self .setStyleSheet (self .parent_app .get_dark_theme ()) def _logger (self ,message ): """Helper to log messages, either to parent app or console.""" if hasattr (self .parent_app ,'log_signal')and self .parent_app .log_signal : self .parent_app .log_signal .emit (f"[FavArtistsDialog] {message }") else : print (f"[FavArtistsDialog] {message }") def _show_content_elements (self ,show ): """Helper to show/hide content-related widgets.""" self .search_input .setVisible (show ) self .artist_list_widget .setVisible (show ) def _fetch_favorite_artists (self ): # --- FIX: Use cloudscraper and add proper headers --- scraper = cloudscraper.create_scraper() # --- END FIX --- if self.cookies_config['use_cookie']: kemono_cookies = prepare_cookies_for_request( True, self.cookies_config['cookie_text'], self.cookies_config['selected_cookie_file'], self.cookies_config['app_base_dir'], self._logger, target_domain="kemono.cr" ) if not kemono_cookies: self._logger("No cookies for kemono.cr, trying fallback kemono.su...") kemono_cookies = prepare_cookies_for_request( True, self.cookies_config['cookie_text'], self.cookies_config['selected_cookie_file'], self.cookies_config['app_base_dir'], self._logger, target_domain="kemono.su" ) coomer_cookies = prepare_cookies_for_request( True, self.cookies_config['cookie_text'], self.cookies_config['selected_cookie_file'], self.cookies_config['app_base_dir'], self._logger, target_domain="coomer.st" ) if not coomer_cookies: self._logger("No cookies for coomer.st, trying fallback coomer.su...") coomer_cookies = prepare_cookies_for_request( True, self.cookies_config['cookie_text'], self.cookies_config['selected_cookie_file'], self.cookies_config['app_base_dir'], self._logger, target_domain="coomer.su" ) if not kemono_cookies and not coomer_cookies: self.status_label.setText(self._tr("fav_artists_cookies_required_status", "Error: Cookies enabled but could not be loaded for any source.")) self._logger("Error: Cookies enabled but no valid cookies were loaded. Showing help dialog.") cookie_help_dialog = CookieHelpDialog(self.parent_app, self) cookie_help_dialog.exec_() self.download_button.setEnabled(False) return self .all_fetched_artists =[] fetched_any_successfully =False errors_occurred =[] any_cookies_loaded_successfully_for_any_source =False api_sources = [ {"name": "Kemono.cr", "url": "https://kemono.cr/api/v1/account/favorites?type=artist", "domain": "kemono.cr"}, {"name": "Coomer.st", "url": "https://coomer.st/api/v1/account/favorites?type=artist", "domain": "coomer.st"} ] for source in api_sources : self ._logger (f"Attempting to fetch favorite artists from: {source ['name']} ({source ['url']})") self .status_label .setText (self ._tr ("fav_artists_loading_from_source_status","⏳ Loading favorites from {source_name}...").format (source_name =source ['name'])) QCoreApplication .processEvents () cookies_dict_for_source = None if self.cookies_config['use_cookie']: primary_domain = source['domain'] fallback_domain = "kemono.su" if "kemono" in primary_domain else "coomer.su" cookies_dict_for_source = prepare_cookies_for_request( True, self.cookies_config['cookie_text'], self.cookies_config['selected_cookie_file'], self.cookies_config['app_base_dir'], self._logger, target_domain=primary_domain ) if not cookies_dict_for_source: self._logger(f"Warning ({source['name']}): No cookies for '{primary_domain}'. Trying fallback '{fallback_domain}'...") cookies_dict_for_source = prepare_cookies_for_request( True, self.cookies_config['cookie_text'], self.cookies_config['selected_cookie_file'], self.cookies_config['app_base_dir'], self._logger, target_domain=fallback_domain ) if cookies_dict_for_source: any_cookies_loaded_successfully_for_any_source = True else: self._logger(f"Warning ({source['name']}): Cookies enabled but not loaded for this source. Fetch may fail.") try : # --- FIX: Add Referer and Accept headers --- headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Referer': f"https://{source['domain']}/favorites", 'Accept': 'text/css' } # --- END FIX --- # --- FIX: Use scraper instead of requests --- response = scraper.get(source['url'], headers=headers, cookies=cookies_dict_for_source, timeout=20) # --- END FIX --- 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") 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 Exception as e : error_msg =f"Error fetching favorites from {source ['name']}: {e }" self ._logger (error_msg ) errors_occurred .append (error_msg ) if self .cookies_config ['use_cookie']and not any_cookies_loaded_successfully_for_any_source : self .status_label .setText (self ._tr ("fav_artists_cookies_required_status","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.parent_app, self) cookie_help_dialog .exec_ () self .download_button .setEnabled (False ) if not fetched_any_successfully : errors_occurred .append ("Cookies enabled but could not be loaded for any API source.") 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 ()) self .all_fetched_artists .sort (key =lambda x :x ['name'].lower ()) self ._populate_artist_list_widget () if fetched_any_successfully and self .all_fetched_artists : self .status_label .setText (self ._tr ("fav_artists_found_status","Found {count} total favorite artist(s).").format (count =len (self .all_fetched_artists ))) self ._show_content_elements (True ) self .download_button .setEnabled (True ) elif not fetched_any_successfully and not errors_occurred : self .status_label .setText (self ._tr ("fav_artists_none_found_status","No favorite artists found on Kemono or Coomer.")) self ._show_content_elements (False ) self .download_button .setEnabled (False ) else : final_error_message =self ._tr ("fav_artists_failed_status","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 .download_button .setEnabled (False ) if fetched_any_successfully and not self .all_fetched_artists : self .status_label .setText (self ._tr ("fav_artists_no_favorites_after_processing","No favorite artists found after processing.")) def _populate_artist_list_widget (self ,artists_to_display =None ): self .artist_list_widget .clear () source_list =artists_to_display if artists_to_display is not None else self .all_fetched_artists for artist_data in source_list : item =QListWidgetItem (f"{artist_data ['name']} ({artist_data .get ('service','N/A').capitalize ()})") item .setFlags (item .flags ()|Qt .ItemIsUserCheckable ) item .setCheckState (Qt .Unchecked ) item .setData (Qt .UserRole ,artist_data ) self .artist_list_widget .addItem (item ) def _filter_artist_list_display (self ): search_text =self .search_input .text ().lower ().strip () if not search_text : self ._populate_artist_list_widget () return filtered_artists =[ artist for artist in self .all_fetched_artists if search_text in artist ['name'].lower ()or search_text in artist ['url'].lower () ] self ._populate_artist_list_widget (filtered_artists ) def _select_all_items (self ): for i in range (self .artist_list_widget .count ()): self .artist_list_widget .item (i ).setCheckState (Qt .Checked ) def _deselect_all_items (self ): for i in range (self .artist_list_widget .count ()): self .artist_list_widget .item (i ).setCheckState (Qt .Unchecked ) def _accept_selection_action (self ): self .selected_artists_data =[] for i in range (self .artist_list_widget .count ()): item =self .artist_list_widget .item (i ) if item .checkState ()==Qt .Checked : self .selected_artists_data .append (item .data (Qt .UserRole )) if not self .selected_artists_data : QMessageBox .information (self ,"No Selection","Please select at least one artist to download.") return self .accept () def get_selected_artists (self ): return self .selected_artists_data