2025-07-01 22:48:58 +05:30
# --- Standard Library Imports ---
import html
import re
# --- Third-Party Library Imports ---
import requests
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
# Corrected Import: Get the icon from the new assets utility module
from . . assets import get_app_icon_object
from . . . utils . network_utils import prepare_cookies_for_request
from . CookieHelpDialog import CookieHelpDialog
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.su "
else :
return " kemono.su "
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 : 1 px solid #4A4A4A; /* Slightly softer line */
padding - top : 4 px ;
padding - bottom : 4 px ;
} """ )
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 ) :
2025-07-13 10:22:06 -07:00
if self . cookies_config [ ' use_cookie ' ] :
# Check if we can load cookies for at least one of the services.
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.su " )
if not kemono_cookies and not coomer_cookies :
# If cookies are enabled but none could be loaded, show help and stop.
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 # Stop further execution
2025-07-01 22:48:58 +05:30
kemono_fav_url = " https://kemono.su/api/v1/account/favorites?type=artist "
coomer_fav_url = " https://coomer.su/api/v1/account/favorites?type=artist "
self . all_fetched_artists = [ ]
fetched_any_successfully = False
errors_occurred = [ ]
any_cookies_loaded_successfully_for_any_source = False
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 ( 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 ' ] :
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 = source [ ' 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 " )
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 )
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 )
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.su or Coomer.su. " ) )
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