2023-03-11, add new feature to pause MAXBOT. fix bugs for ibon.

master
CHUN YU YAO 2023-03-13 00:34:59 +08:00
parent 73ec0403e8
commit 316fc727d8
3 changed files with 467 additions and 133 deletions

View File

@ -53,7 +53,11 @@ import argparse
import ssl import ssl
ssl._create_default_https_context = ssl._create_unverified_context ssl._create_default_https_context = ssl._create_unverified_context
CONST_APP_VERSION = u"MaxBot (2023.03.08)" CONST_APP_VERSION = u"MaxBot (2023.03.11)"
CONST_MAXBOT_CONFIG_FILE = "settings.json"
CONST_MAXBOT_LAST_URL_FILE = "MAXBOT_LAST_URL.txt"
CONST_MAXBOT_INT28_FILE = "MAXBOT_INT28_IDLE.txt"
CONST_HOMEPAGE_DEFAULT = "https://tixcraft.com" CONST_HOMEPAGE_DEFAULT = "https://tixcraft.com"
URL_GOOGLE_OAUTH = 'https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount?redirect_uri=https%3A%2F%2Fdevelopers.google.com%2Foauthplayground&prompt=consent&response_type=code&client_id=407408718192.apps.googleusercontent.com&scope=email&access_type=offline&flowName=GeneralOAuthFlow' URL_GOOGLE_OAUTH = 'https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount?redirect_uri=https%3A%2F%2Fdevelopers.google.com%2Foauthplayground&prompt=consent&response_type=code&client_id=407408718192.apps.googleusercontent.com&scope=email&access_type=offline&flowName=GeneralOAuthFlow'
@ -84,6 +88,7 @@ CONST_WEBDRIVER_TYPE_SELENIUM = "selenium"
#CONST_WEBDRIVER_TYPE_STEALTH = "stealth" #CONST_WEBDRIVER_TYPE_STEALTH = "stealth"
CONST_WEBDRIVER_TYPE_UC = "undetected_chromedriver" CONST_WEBDRIVER_TYPE_UC = "undetected_chromedriver"
def t_or_f(arg): def t_or_f(arg):
ret = False ret = False
ua = str(arg).upper() ua = str(arg).upper()
@ -120,9 +125,8 @@ def get_app_root():
return app_root return app_root
def get_config_dict(args): def get_config_dict(args):
config_json_filename = 'settings.json'
app_root = get_app_root() app_root = get_app_root()
config_filepath = os.path.join(app_root, config_json_filename) config_filepath = os.path.join(app_root, CONST_MAXBOT_CONFIG_FILE)
# allow assign config by command line. # allow assign config by command line.
if not args.input is None: if not args.input is None:
@ -178,6 +182,16 @@ def get_config_dict(args):
config_dict["ocr_captcha"]["force_submit"] = True config_dict["ocr_captcha"]["force_submit"] = True
return config_dict return config_dict
def write_last_url_to_file(url):
with open(CONST_MAXBOT_LAST_URL_FILE, "w") as text_file:
text_file.write("%s" % url)
def read_last_url_from_file():
ret = ""
with open(CONST_MAXBOT_LAST_URL_FILE, "r") as text_file:
ret = text_file.readline()
return ret
def format_keyword_string(keyword): def format_keyword_string(keyword):
if not keyword is None: if not keyword is None:
if len(keyword) > 0: if len(keyword) > 0:
@ -1718,9 +1732,10 @@ def tixcraft_area_auto_select(driver, url, config_dict):
# only when keyword#2 filled to query. # only when keyword#2 filled to query.
if area_keyword_2_enable: if area_keyword_2_enable:
is_need_refresh, areas = get_tixcraft_target_area(el, area_keyword_2, area_auto_select_mode, pass_1_seat_remaining_enable) if area_keyword_1 != area_keyword_2:
if show_debug_message: is_need_refresh, areas = get_tixcraft_target_area(el, area_keyword_2, area_auto_select_mode, pass_1_seat_remaining_enable)
print("is_need_refresh for keyword2:", is_need_refresh) if show_debug_message:
print("is_need_refresh for keyword2:", is_need_refresh)
if is_need_refresh: if is_need_refresh:
if areas is None: if areas is None:
@ -1729,9 +1744,10 @@ def tixcraft_area_auto_select(driver, url, config_dict):
# only when keyword#3 filled to query. # only when keyword#3 filled to query.
if area_keyword_3_enable: if area_keyword_3_enable:
is_need_refresh, areas = get_tixcraft_target_area(el, area_keyword_3, area_auto_select_mode, pass_1_seat_remaining_enable) if area_keyword_1 != area_keyword_3:
if show_debug_message: is_need_refresh, areas = get_tixcraft_target_area(el, area_keyword_3, area_auto_select_mode, pass_1_seat_remaining_enable)
print("is_need_refresh for keyword3:", is_need_refresh) if show_debug_message:
print("is_need_refresh for keyword3:", is_need_refresh)
if is_need_refresh: if is_need_refresh:
if areas is None: if areas is None:
@ -1740,9 +1756,10 @@ def tixcraft_area_auto_select(driver, url, config_dict):
# only when keyword#4 filled to query. # only when keyword#4 filled to query.
if area_keyword_4_enable: if area_keyword_4_enable:
is_need_refresh, areas = get_tixcraft_target_area(el, area_keyword_4, area_auto_select_mode, pass_1_seat_remaining_enable) if area_keyword_1 != area_keyword_4:
if show_debug_message: is_need_refresh, areas = get_tixcraft_target_area(el, area_keyword_4, area_auto_select_mode, pass_1_seat_remaining_enable)
print("is_need_refresh for keyword4:", is_need_refresh) if show_debug_message:
print("is_need_refresh for keyword4:", is_need_refresh)
area_target = None area_target = None
if areas is not None: if areas is not None:
@ -5133,10 +5150,15 @@ def ibon_activity_info(driver, config_dict):
return is_date_assign_by_bot return is_date_assign_by_bot
def ibon_area_auto_select(driver, area_auto_select_mode, area_keyword_1, area_keyword_1_and): def ibon_area_auto_select(driver, config_dict, area_keyword_1, area_keyword_1_and):
show_debug_message = True # debug. show_debug_message = True # debug.
show_debug_message = False # online show_debug_message = False # online
area_auto_select_mode = config_dict["tixcraft"]["area_auto_select"]["mode"]
if config_dict["advanced"]["verbose"]:
show_debug_message = True
is_price_assign_by_bot = False is_price_assign_by_bot = False
is_need_refresh = False is_need_refresh = False
@ -5179,9 +5201,22 @@ def ibon_area_auto_select(driver, area_auto_select_mode, area_keyword_1, area_ke
if row_is_enabled: if row_is_enabled:
try: try:
button_class_string = str(row.get_attribute('class')) button_class_string = str(row.get_attribute('class'))
if len(button_class_string) > 1: if not button_class_string is None:
if 'disabled' in button_class_string: if len(button_class_string) > 1:
row_is_enabled=False if 'disabled' in button_class_string:
row_is_enabled=False
if 'sold-out' in button_class_string:
row_is_enabled=False
except Exception as exc:
pass
if row_is_enabled:
row_is_enabled = False
try:
row_id_string = str(row.get_attribute('id'))
if not row_id_string is None:
if len(row_id_string) > 1:
row_is_enabled = True
except Exception as exc: except Exception as exc:
pass pass
@ -5284,7 +5319,7 @@ def ibon_area_auto_select(driver, area_auto_select_mode, area_keyword_1, area_ke
else: else:
is_need_refresh = True is_need_refresh = True
if show_debug_message: if show_debug_message:
print("matched_blocks is empty.") print("matched_blocks is empty, is_need_refresh")
if target_area is not None: if target_area is not None:
try: try:
@ -5309,13 +5344,15 @@ def ibon_performance(driver, config_dict):
show_debug_message = True # debug. show_debug_message = True # debug.
show_debug_message = False # online show_debug_message = False # online
if config_dict["advanced"]["verbose"]:
show_debug_message = True
is_price_assign_by_bot = False is_price_assign_by_bot = False
is_need_refresh = False is_need_refresh = False
auto_fill_ticket_number = True auto_fill_ticket_number = True
if auto_fill_ticket_number: if auto_fill_ticket_number:
# click price row. # click price row.
area_auto_select_mode = config_dict["tixcraft"]["area_auto_select"]["mode"]
area_keyword_1 = config_dict["tixcraft"]["area_auto_select"]["area_keyword_1"].strip() area_keyword_1 = config_dict["tixcraft"]["area_auto_select"]["area_keyword_1"].strip()
area_keyword_2 = config_dict["tixcraft"]["area_auto_select"]["area_keyword_2"].strip() area_keyword_2 = config_dict["tixcraft"]["area_auto_select"]["area_keyword_2"].strip()
area_keyword_3 = config_dict["tixcraft"]["area_auto_select"]["area_keyword_3"].strip() area_keyword_3 = config_dict["tixcraft"]["area_auto_select"]["area_keyword_3"].strip()
@ -5337,25 +5374,28 @@ def ibon_performance(driver, config_dict):
is_need_refresh = False is_need_refresh = False
if not is_price_assign_by_bot: if not is_price_assign_by_bot:
is_need_refresh, is_price_assign_by_bot = ibon_area_auto_select(driver, area_auto_select_mode, area_keyword_1, area_keyword_1_and) is_need_refresh, is_price_assign_by_bot = ibon_area_auto_select(driver, config_dict, area_keyword_1, area_keyword_1_and)
if is_need_refresh: if is_need_refresh:
if area_keyword_2_enable: if area_keyword_2_enable:
is_need_refresh, is_price_assign_by_bot = ibon_area_auto_select(driver, area_auto_select_mode, area_keyword_2, area_keyword_2_and) if area_keyword_1 != area_keyword_2:
if show_debug_message: is_need_refresh, is_price_assign_by_bot = ibon_area_auto_select(driver, config_dict, area_keyword_2, area_keyword_2_and)
print("is_need_refresh for keyword2:", is_need_refresh) if show_debug_message:
print("is_need_refresh for keyword2:", is_need_refresh)
if is_need_refresh: if is_need_refresh:
if area_keyword_3_enable: if area_keyword_3_enable:
is_need_refresh, is_price_assign_by_bot = ibon_area_auto_select(driver, area_auto_select_mode, area_keyword_3, area_keyword_3_and) if area_keyword_1 != area_keyword_3:
if show_debug_message: is_need_refresh, is_price_assign_by_bot = ibon_area_auto_select(driver, config_dict, area_keyword_3, area_keyword_3_and)
print("is_need_refresh for keyword3:", is_need_refresh) if show_debug_message:
print("is_need_refresh for keyword3:", is_need_refresh)
if is_need_refresh: if is_need_refresh:
if area_keyword_4_enable: if area_keyword_4_enable:
is_need_refresh, is_price_assign_by_bot = ibon_area_auto_select(driver, area_auto_select_mode, area_keyword_4, area_keyword_4_and) if area_keyword_1 != area_keyword_4:
if show_debug_message: is_need_refresh, is_price_assign_by_bot = ibon_area_auto_select(driver, config_dict, area_keyword_4, area_keyword_4_and)
print("is_need_refresh for keyword4:", is_need_refresh) if show_debug_message:
print("is_need_refresh for keyword4:", is_need_refresh)
if is_need_refresh: if is_need_refresh:
try: try:
@ -6179,40 +6219,163 @@ def cityline_main(driver, url, config_dict):
if len(url.split('/'))>=5: if len(url.split('/'))>=5:
cityline_shows_goto_cta(driver) cityline_shows_goto_cta(driver)
def guess_ibon_question(driver):
show_debug_message = True # debug.
show_debug_message = False # online
inferred_answer_string = None
answer_list = []
form_select = None
try:
form_select = driver.find_element(By.CSS_SELECTOR, 'div.editor-box > div > div.form-group > label')
except Exception as exc:
print("find verify textbox fail")
pass
question_text = None
if form_select is not None:
try:
question_text = form_select.text
except Exception as exc:
print("get text fail")
is_options_in_question = False
if inferred_answer_string is None:
if not question_text is None:
inferred_answer_string, answer_list = get_answer_list_from_question_string(None, question_text)
return inferred_answer_string, answer_list
def ibon_verification_question(driver, answer_index, config_dict): def ibon_verification_question(driver, answer_index, config_dict):
show_debug_message = True # debug. show_debug_message = True # debug.
show_debug_message = False # online show_debug_message = False # online
user_guess_string = config_dict["kktix"]["user_guess_string"] if config_dict["advanced"]["verbose"]:
show_debug_message = True
# part 1: check div. presale_code = config_dict["tixcraft"]["presale_code"]
question_div = None presale_code_delimiter = config_dict["tixcraft"]["presale_code_delimiter"]
try:
question_div = driver.find_element(By.CSS_SELECTOR, '')
except Exception as exc:
pass
#print("find input fail:", exc)
#captcha_text_div_text inferred_answer_string = None
captcha_text_div_text = None answer_list = []
captcha_password_inputbox = None is_retry_user_single_answer = False
try:
captcha_password_inputbox = driver.find_element(By.CSS_SELECTOR, '')
except Exception as exc:
pass
if not captcha_password_inputbox is None: if len(presale_code) > 0:
inferred_answer_string = None if len(presale_code_delimiter) > 0:
if presale_code_delimiter in presale_code:
if len(user_guess_string) > 0: answer_list = presale_code.split(presale_code_delimiter)
inferred_answer_string = user_guess_string if len(answer_list) > 0:
if answer_index < len(answer_list)-1:
inferred_answer_string = answer_list[answer_index+1]
else: else:
if not captcha_text_div_text is None: is_retry_user_single_answer = True
inferred_answer_string, answer_list = get_answer_list_from_question_string(None, captcha_text_div_text) if answer_index < 2:
inferred_answer_string = presale_code
if inferred_answer_string is None:
inferred_answer_string, answer_list = guess_ibon_question(driver)
if inferred_answer_string is None:
if not answer_list is None:
if len(answer_list) > 0:
if answer_index < len(answer_list)-1:
inferred_answer_string = answer_list[answer_index+1]
if show_debug_message:
print("answer_index:", answer_index)
print("inferred_answer_string:", inferred_answer_string)
print("answer_index:", answer_index)
print("is_retry_user_single_answer:", is_retry_user_single_answer)
form_input = None
try:
form_input = driver.find_element(By.CSS_SELECTOR, 'div.editor-box > div > div.form-group > input')
except Exception as exc:
print("find verify code fail")
pass
inputed_value = None
if form_input is not None:
try:
inputed_value = form_input.get_attribute('value')
except Exception as exc:
print("find verify code fail")
pass
if inputed_value is None:
inputed_value = ""
if not inferred_answer_string is None:
is_password_sent = False
if len(inputed_value)==0:
try:
# PS: sometime may send key twice...
form_input.clear()
form_input.send_keys(inferred_answer_string)
is_button_clicked = force_press_button(driver, By.CSS_SELECTOR,'div.editor-box > div > div.form-group > a.btn')
is_password_sent = True
# guess answer mode.
answer_index += 1
if show_debug_message:
print("sent password by bot:", inferred_answer_string)
except Exception as exc:
pass
else:
if inputed_value == inferred_answer_string:
if show_debug_message:
print("sent password by previous time.")
is_password_sent = True
try:
form_input.send_keys(Keys.ENTER)
except Exception as exc:
pass
if is_retry_user_single_answer:
# increase counter for waiting for stop retry.
answer_index += 1
else:
# guess answer mode.
if answer_index > -1:
# here not is first option.
inferred_answer_previous = None
if answer_index < len(answer_list)-1:
inferred_answer_previous = answer_list[answer_index]
if inputed_value == inferred_answer_previous:
try:
form_input.clear()
form_input.send_keys(inferred_answer_string)
is_button_clicked = force_press_button(driver, By.CSS_SELECTOR,'div.editor-box > div > div.form-group > a.btn')
is_password_sent = True
if show_debug_message:
print("sent password by bot:", inferred_answer_string, "at index:", answer_index+2)
answer_index += 1
except Exception as exc:
pass
if is_password_sent:
for i in range(3):
time.sleep(0.1)
alert_ret = check_pop_alert(driver)
if alert_ret:
if show_debug_message:
print("press accept button at time #", i+1)
break
else:
if len(inputed_value)==0:
try:
form_input.click()
except Exception as exc:
pass
return answer_index return answer_index
def ibon_ticket_agree(driver): def ibon_ticket_agree(driver):
# check agree # check agree
form_checkbox = None form_checkbox = None
@ -6278,70 +6441,77 @@ def ibon_main(driver, url, config_dict, ibon_dict):
if is_event_page: if is_event_page:
ibon_auto_signup(driver) ibon_auto_signup(driver)
#https://ticket.ibon.com.tw/ActivityInfo/Details/0000?pattern=entertainment is_match_target_feature = False
if '/ActivityInfo/Details/' in url:
is_event_page = False
if len(url.split('/'))==6:
is_event_page = True
if is_event_page: if not is_match_target_feature:
date_auto_select_enable = config_dict["tixcraft"]["date_auto_select"]["enable"] #https://ticket.ibon.com.tw/ActivityInfo/Details/0000?pattern=entertainment
if date_auto_select_enable: if '/ActivityInfo/Details/' in url:
ibon_activity_info(driver, config_dict) is_event_page = False
if len(url.split('/'))==6:
is_event_page = True
# validation question url: if is_event_page:
# https://orders.ibon.com.tw/application/UTK02/UTK0201_0.aspx?rn=1180872370&PERFORMANCE_ID=B04M7XZT&PRODUCT_ID=B04KS88E&SHOW_PLACE_MAP=True date_auto_select_enable = config_dict["tixcraft"]["date_auto_select"]["enable"]
if '/application/UTK02/' in url and '.aspx?rn=' in url: if date_auto_select_enable:
#PS: not sure, use 'kktix' block or 'tixcraft' block for this feature. is_match_target_feature = True
#auto_guess_options = config_dict["kktix"]["auto_guess_options"] ibon_activity_info(driver, config_dict)
auto_guess_options = True
if auto_guess_options:
#ibon_dict["answer_index"] = ibon_verification_question(driver, ibon_dict["answer_index"], config_dict)
pass
else:
ibon_dict["answer_index"] = -1
# https://orders.ibon.com.tw/application/UTK02/UTK0201_000.aspx?PERFORMANCE_ID=0000 if not is_match_target_feature:
if '/application/UTK02/' in url and '.aspx?PERFORMANCE_ID=' in url: # validation question url:
is_event_page = False # https://orders.ibon.com.tw/application/UTK02/UTK0201_0.aspx?rn=1180872370&PERFORMANCE_ID=B04M7XZT&PRODUCT_ID=B04KS88E&SHOW_PLACE_MAP=True
if len(url.split('/'))==6: if '/application/UTK02/' in url and '.aspx?rn=' in url:
is_event_page = True is_match_target_feature = True
ibon_dict["answer_index"] = ibon_verification_question(driver, ibon_dict["answer_index"], config_dict)
else:
ibon_dict["answer_index"] = -1
if is_event_page:
area_auto_select_enable = config_dict["tixcraft"]["area_auto_select"]["enable"]
if area_auto_select_enable:
if 'PERFORMANCE_PRICE_AREA_ID=' in url:
# step 2: assign ticket number.
ticket_number = str(config_dict["ticket_number"])
is_ticket_number_assigned = ibon_ticket_number_auto_select(driver, ticket_number)
if is_ticket_number_assigned:
click_ret = ibon_purchase_button_press(driver)
else:
is_sold_out = ibon_check_sold_out(driver)
if is_sold_out:
#is_button_clicked = force_press_button(driver, By.CSS_SELECTOR, 'a.btn.btn-primary')
driver.back()
driver.refresh()
else:
# step 1: select area.
ibon_performance(driver, config_dict)
#https://orders.ibon.com.tw/application/UTK02/UTK0206_.aspx if not is_match_target_feature:
if 'orders.ibon.com.tw/application/UTK02/UTK020' in url and '.aspx' in url: # https://orders.ibon.com.tw/application/UTK02/UTK0201_000.aspx?PERFORMANCE_ID=0000
is_event_page = False if '/application/UTK02/' in url and '.aspx?PERFORMANCE_ID=' in url:
if len(url.split('/'))==6: is_event_page = False
is_event_page = True if len(url.split('/'))==6:
is_event_page = True
if is_event_page: if is_event_page:
auto_check_agree = config_dict["auto_check_agree"] area_auto_select_enable = config_dict["tixcraft"]["area_auto_select"]["enable"]
if auto_check_agree: if area_auto_select_enable:
is_finish_checkbox_click = False if 'PERFORMANCE_PRICE_AREA_ID=' in url:
for i in range(3): # step 2: assign ticket number.
is_finish_checkbox_click = ibon_ticket_agree(driver) is_match_target_feature = True
ticket_number = str(config_dict["ticket_number"])
is_ticket_number_assigned = ibon_ticket_number_auto_select(driver, ticket_number)
if is_ticket_number_assigned:
click_ret = ibon_purchase_button_press(driver)
else:
is_sold_out = ibon_check_sold_out(driver)
if is_sold_out:
#is_button_clicked = force_press_button(driver, By.CSS_SELECTOR, 'a.btn.btn-primary')
driver.back()
driver.refresh()
if 'PRODUCT_ID=' in url:
# step 1: select area.
is_match_target_feature = True
ibon_performance(driver, config_dict)
if not is_match_target_feature:
#https://orders.ibon.com.tw/application/UTK02/UTK0206_.aspx
if 'orders.ibon.com.tw/application/UTK02/UTK020' in url and '.aspx' in url:
is_event_page = False
if len(url.split('/'))==6:
is_event_page = True
if is_event_page:
auto_check_agree = config_dict["auto_check_agree"]
if auto_check_agree:
is_finish_checkbox_click = False
for i in range(3):
is_finish_checkbox_click = ibon_ticket_agree(driver)
if is_finish_checkbox_click:
break
if is_finish_checkbox_click: if is_finish_checkbox_click:
break is_button_clicked = force_press_button(driver, By.CSS_SELECTOR, 'a.btn.btn-pink.continue')
if is_finish_checkbox_click:
is_button_clicked = force_press_button(driver, By.CSS_SELECTOR, 'a.btn.btn-pink.continue')
return ibon_dict return ibon_dict
@ -8799,8 +8969,15 @@ def main(args):
if len(url) > 0 : if len(url) > 0 :
if url != last_url: if url != last_url:
print(url) print(url)
write_last_url_to_file(url)
if os.path.exists(CONST_MAXBOT_INT28_FILE):
print("MAXBOT Paused.")
last_url = url last_url = url
if os.path.exists(CONST_MAXBOT_INT28_FILE):
time.sleep(0.2)
continue
tixcraft_family = False tixcraft_family = False
if 'tixcraft.com' in url: if 'tixcraft.com' in url:
tixcraft_family = True tixcraft_family = True
@ -8916,7 +9093,8 @@ if __name__ == "__main__":
#captcha_text_div_text = "以下哪位不是LOVELYZ成員? (請以半形輸入選項內的英文及數字,大小寫須符合),範例:E5e。 (A1a)智愛 (B2b)美珠 (C3c)JON (D4d)叡仁" #captcha_text_div_text = "以下哪位不是LOVELYZ成員? (請以半形輸入選項內的英文及數字,大小寫須符合),範例:E5e。 (A1a)智愛 (B2b)美珠 (C3c)JON (D4d)叡仁"
#captcha_text_div_text = "題請問此次 RAVI的SOLO專輯名稱為?(請以半形輸入法作答,大小寫需要一模一樣,範例:Tt Aa [ BOOK] 、 Bb [OOK BOOK.R] 、 Cc [R.OOK BOOK] 、 Dd [OOK R. BOOK]" #captcha_text_div_text = "題請問此次 RAVI的SOLO專輯名稱為?(請以半形輸入法作答,大小寫需要一模一樣,範例:Tt Aa [ BOOK] 、 Bb [OOK BOOK.R] 、 Cc [R.OOK BOOK] 、 Dd [OOK R. BOOK]"
#captcha_text_div_text = "請問下列哪個選項皆為河成雲的創作歌曲? Aa) Dont Forget、Candle Bb) Dont Forget、Forever+1 Cc) Dont Forget、Flowerbomb Dd) Dont Forget、One Love 請以半形輸入,大小寫含括號需一模一樣 【範例:答案為B需填入Bb)】" #captcha_text_div_text = "請問下列哪個選項皆為河成雲的創作歌曲? Aa) Dont Forget、Candle Bb) Dont Forget、Forever+1 Cc) Dont Forget、Flowerbomb Dd) Dont Forget、One Love 請以半形輸入,大小寫含括號需一模一樣 【範例:答案為B需填入Bb)】"
captcha_text_div_text = "魏如萱得過什麼獎?(1) 金馬獎 最佳女主角(2) 金鐘獎 戲劇節目女主角(3) 金曲獎 最佳華語女歌手(4) 走鐘獎 好好聽音樂獎 (請輸入半形數字)" #captcha_text_div_text = "魏如萱得過什麼獎?(1) 金馬獎 最佳女主角(2) 金鐘獎 戲劇節目女主角(3) 金曲獎 最佳華語女歌手(4) 走鐘獎 好好聽音樂獎 (請輸入半形數字)"
captcha_text_div_text = "Love in the Air 是由哪兩本小說改篇而成呢?(A)Love Strom & Love Sky (B)Love Rain & Love Cloud (C)Love Wind & Love Sun (D)Love Dry & Love Cold (請輸入選項大寫英文單字 範例E)"
inferred_answer_string, answer_list = get_answer_list_from_question_string(None, captcha_text_div_text) inferred_answer_string, answer_list = get_answer_list_from_question_string(None, captcha_text_div_text)
print("inferred_answer_string:", inferred_answer_string) print("inferred_answer_string:", inferred_answer_string)
print("answer_list:", answer_list) print("answer_list:", answer_list)

View File

@ -20,9 +20,10 @@ import json
import webbrowser import webbrowser
import base64 import base64
CONST_APP_VERSION = u"MaxBot (2023.03.08)" CONST_APP_VERSION = u"MaxBot (2023.03.11)"
CONST_LAUNCHER_CONFIG_FILENAME = "config_launcher.json" CONST_MAXBOT_LAUNCHER_FILE = "config_launcher.json"
CONST_MAXBOT_CONFIG_FILE = "settings.json"
translate={} translate={}
@ -146,7 +147,7 @@ def get_app_root():
def get_default_config(): def get_default_config():
config_dict={} config_dict={}
config_dict["list"] = ["settings.json"] config_dict["list"] = [CONST_MAXBOT_CONFIG_FILE]
config_dict["advanced"] = {} config_dict["advanced"] = {}
config_dict["advanced"]["language"] = "English" config_dict["advanced"]["language"] = "English"
@ -158,7 +159,7 @@ def load_json():
app_root = get_app_root() app_root = get_app_root()
# overwrite config path. # overwrite config path.
config_filepath = os.path.join(app_root, CONST_LAUNCHER_CONFIG_FILENAME) config_filepath = os.path.join(app_root, CONST_MAXBOT_LAUNCHER_FILE)
config_dict = None config_dict = None
if os.path.isfile(config_filepath): if os.path.isfile(config_filepath):
@ -170,7 +171,7 @@ def load_json():
def btn_restore_defaults_clicked(language_code): def btn_restore_defaults_clicked(language_code):
app_root = get_app_root() app_root = get_app_root()
config_filepath = os.path.join(app_root, CONST_LAUNCHER_CONFIG_FILENAME) config_filepath = os.path.join(app_root, CONST_MAXBOT_LAUNCHER_FILE)
config_dict = get_default_config() config_dict = get_default_config()
import json import json
@ -186,7 +187,7 @@ def btn_save_clicked(language_code):
def btn_save_act(language_code, slience_mode=True): def btn_save_act(language_code, slience_mode=True):
app_root = get_app_root() app_root = get_app_root()
config_filepath = os.path.join(app_root, CONST_LAUNCHER_CONFIG_FILENAME) config_filepath = os.path.join(app_root, CONST_MAXBOT_LAUNCHER_FILE)
config_dict = get_default_config() config_dict = get_default_config()

View File

@ -20,10 +20,14 @@ import json
import webbrowser import webbrowser
import pyperclip import pyperclip
import base64 import base64
import time
import threading
CONST_APP_VERSION = u"MaxBot (2023.03.08)" CONST_APP_VERSION = u"MaxBot (2023.03.11)"
CONST_SETTINGS_CONFIG_FILENAME = "settings.json" CONST_MAXBOT_CONFIG_FILE = "settings.json"
CONST_MAXBOT_LAST_URL_FILE = "MAXBOT_LAST_URL.txt"
CONST_MAXBOT_INT28_FILE = "MAXBOT_INT28_IDLE.txt"
CONST_FROM_TOP_TO_BOTTOM = u"from top to bottom" CONST_FROM_TOP_TO_BOTTOM = u"from top to bottom"
CONST_FROM_BOTTOM_TO_TOP = u"from bottom to top" CONST_FROM_BOTTOM_TO_TOP = u"from bottom to top"
@ -83,7 +87,7 @@ def load_translate():
en_us["date_keyword"] = 'Date Keyword' en_us["date_keyword"] = 'Date Keyword'
en_us["pass_date_is_sold_out"] = 'Pass date is sold out' en_us["pass_date_is_sold_out"] = 'Pass date is sold out'
en_us["auto_reload_coming_soon_page"] = 'Reload coming soon page' en_us["auto_reload_coming_soon_page"] = 'Reload coming soon page'
en_us["area_auto_select"] = 'Area Auto Select' en_us["area_auto_select"] = 'Area Auto Select'
#en_us["area_select_order"] = 'Area select order' #en_us["area_select_order"] = 'Area select order'
en_us["area_keyword_1"] = 'Area Keyword #1' en_us["area_keyword_1"] = 'Area Keyword #1'
@ -98,10 +102,20 @@ def load_translate():
en_us["headless"] = 'Headless mode' en_us["headless"] = 'Headless mode'
# Make the operation more talkative # Make the operation more talkative
en_us["verbose"] = 'Verbose mode' en_us["verbose"] = 'Verbose mode'
en_us["running_status"] = 'Running Status'
en_us["running_url"] = 'Running URL'
en_us["status_idle"] = 'Idle'
en_us["status_paused"] = 'Paused'
en_us["status_enabled"] = 'Enabled'
en_us["status_running"] = 'Running'
en_us["idle"] = 'Idle'
en_us["resume"] = 'Resume'
en_us["preference"] = 'Preference' en_us["preference"] = 'Preference'
en_us["advanced"] = 'Advanced' en_us["advanced"] = 'Advanced'
en_us["autofill"] = 'Autofill' en_us["autofill"] = 'Autofill'
en_us["runtime"] = 'Runtime'
en_us["about"] = 'About' en_us["about"] = 'About'
en_us["run"] = 'Run' en_us["run"] = 'Run'
@ -128,7 +142,7 @@ def load_translate():
en_us["hkticketing_password"] = 'HKTICKETING password' en_us["hkticketing_password"] = 'HKTICKETING password'
en_us["kham_password"] = 'KHAM password' en_us["kham_password"] = 'KHAM password'
en_us["save_password_alert"] = 'Saving passwords to config file may expose your passwords.' en_us["save_password_alert"] = 'Saving passwords to config file may expose your passwords.'
en_us["play_captcha_sound"] = 'Play sound when captcha' en_us["play_captcha_sound"] = 'Play sound when captcha'
en_us["captcha_sound_filename"] = 'captcha sound filename' en_us["captcha_sound_filename"] = 'captcha sound filename'
en_us["adblock_plus_enable"] = 'Adblock Plus Extension' en_us["adblock_plus_enable"] = 'Adblock Plus Extension'
@ -178,10 +192,20 @@ def load_translate():
zh_tw["webdriver_type"] = 'WebDriver類別' zh_tw["webdriver_type"] = 'WebDriver類別'
zh_tw["headless"] = '無圖形界面模式' zh_tw["headless"] = '無圖形界面模式'
zh_tw["verbose"] = '輸出詳細除錯訊息' zh_tw["verbose"] = '輸出詳細除錯訊息'
zh_tw["running_status"] = '執行狀態'
zh_tw["running_url"] = '執行網址'
zh_tw["status_idle"] = '閒置中'
zh_tw["status_paused"] = '已暫停'
zh_tw["status_enabled"] = '已啟用'
zh_tw["status_running"] = '執行中'
zh_tw["idle"] = '暫停搶票'
zh_tw["resume"] = '接續搶票'
zh_tw["preference"] = '偏好設定' zh_tw["preference"] = '偏好設定'
zh_tw["advanced"] = '進階設定' zh_tw["advanced"] = '進階設定'
zh_tw["autofill"] = '自動填表單' zh_tw["autofill"] = '自動填表單'
zh_tw["runtime"] = '執行階段'
zh_tw["about"] = '關於' zh_tw["about"] = '關於'
zh_tw["run"] = '搶票' zh_tw["run"] = '搶票'
@ -208,7 +232,7 @@ def load_translate():
zh_tw["hkticketing_password"] = 'HKTICKETING 密碼' zh_tw["hkticketing_password"] = 'HKTICKETING 密碼'
zh_tw["kham_password"] = '寬宏 密碼' zh_tw["kham_password"] = '寬宏 密碼'
zh_tw["save_password_alert"] = '將密碼保存到設定檔中可能會讓您的密碼被盜。' zh_tw["save_password_alert"] = '將密碼保存到設定檔中可能會讓您的密碼被盜。'
zh_tw["play_captcha_sound"] = '輸入驗證碼時播放音效' zh_tw["play_captcha_sound"] = '輸入驗證碼時播放音效'
zh_tw["captcha_sound_filename"] = '驗證碼用音效檔' zh_tw["captcha_sound_filename"] = '驗證碼用音效檔'
zh_tw["adblock_plus_enable"] = 'Adblock 瀏覽器擴充功能' zh_tw["adblock_plus_enable"] = 'Adblock 瀏覽器擴充功能'
@ -258,10 +282,20 @@ def load_translate():
zh_cn["webdriver_type"] = 'WebDriver类别' zh_cn["webdriver_type"] = 'WebDriver类别'
zh_cn["headless"] = '无图形界面模式' zh_cn["headless"] = '无图形界面模式'
zh_cn["verbose"] = '输出详细除错讯息' zh_cn["verbose"] = '输出详细除错讯息'
zh_cn["running_status"] = '執行狀態'
zh_cn["running_url"] = '執行網址'
zh_cn["status_idle"] = '閒置中'
zh_cn["status_paused"] = '已暫停'
zh_cn["status_enabled"] = '已启用'
zh_cn["status_running"] = '執行中'
zh_cn["idle"] = '暂停抢票'
zh_cn["resume"] = '接续抢票'
zh_cn["preference"] = '偏好设定' zh_cn["preference"] = '偏好设定'
zh_cn["advanced"] = '進階設定' zh_cn["advanced"] = '進階設定'
zh_cn["autofill"] = '自动填表单' zh_cn["autofill"] = '自动填表单'
zh_cn["runtime"] = '运行'
zh_cn["about"] = '关于' zh_cn["about"] = '关于'
zh_cn["copy"] = '复制' zh_cn["copy"] = '复制'
@ -289,7 +323,7 @@ def load_translate():
zh_cn["hkticketing_password"] = 'HKTICKETING 密码' zh_cn["hkticketing_password"] = 'HKTICKETING 密码'
zh_cn["kham_password"] = '宽宏 密码' zh_cn["kham_password"] = '宽宏 密码'
zh_cn["save_password_alert"] = '將密碼保存到文件中可能會暴露您的密碼。' zh_cn["save_password_alert"] = '將密碼保存到文件中可能會暴露您的密碼。'
zh_cn["play_captcha_sound"] = '输入验证码时播放音效' zh_cn["play_captcha_sound"] = '输入验证码时播放音效'
zh_cn["captcha_sound_filename"] = '验证码用音效档' zh_cn["captcha_sound_filename"] = '验证码用音效档'
zh_cn["adblock_plus_enable"] = 'Adblock 浏览器扩充功能' zh_cn["adblock_plus_enable"] = 'Adblock 浏览器扩充功能'
@ -339,10 +373,20 @@ def load_translate():
ja_jp["webdriver_type"] = 'WebDriverタイプ' ja_jp["webdriver_type"] = 'WebDriverタイプ'
ja_jp["headless"] = 'ヘッドレスモード' ja_jp["headless"] = 'ヘッドレスモード'
ja_jp["verbose"] = '詳細モード' ja_jp["verbose"] = '詳細モード'
ja_jp["running_status"] = 'スターテス'
ja_jp["running_url"] = '現在の URL'
ja_jp["status_idle"] = '閒置中'
ja_jp["status_paused"] = '一時停止'
ja_jp["status_enabled"] = '有効'
ja_jp["status_running"] = 'ランニング'
ja_jp["idle"] = 'アイドル'
ja_jp["resume"] = '再開する'
ja_jp["preference"] = '設定' ja_jp["preference"] = '設定'
ja_jp["advanced"] = '高度な設定' ja_jp["advanced"] = '高度な設定'
ja_jp["autofill"] = 'オートフィル' ja_jp["autofill"] = 'オートフィル'
ja_jp["runtime"] = 'ランタイム'
ja_jp["about"] = '情報' ja_jp["about"] = '情報'
ja_jp["run"] = 'チケットを取る' ja_jp["run"] = 'チケットを取る'
@ -493,11 +537,17 @@ def get_default_config():
return config_dict return config_dict
def read_last_url_from_file():
ret = ""
with open(CONST_MAXBOT_LAST_URL_FILE, "r") as text_file:
ret = text_file.readline()
return ret
def load_json(): def load_json():
app_root = get_app_root() app_root = get_app_root()
# overwrite config path. # overwrite config path.
config_filepath = os.path.join(app_root, CONST_SETTINGS_CONFIG_FILENAME) config_filepath = os.path.join(app_root, CONST_MAXBOT_CONFIG_FILE)
config_dict = None config_dict = None
if os.path.isfile(config_filepath): if os.path.isfile(config_filepath):
@ -509,7 +559,7 @@ def load_json():
def btn_restore_defaults_clicked(language_code): def btn_restore_defaults_clicked(language_code):
app_root = get_app_root() app_root = get_app_root()
config_filepath = os.path.join(app_root, CONST_SETTINGS_CONFIG_FILENAME) config_filepath = os.path.join(app_root, CONST_MAXBOT_CONFIG_FILE)
config_dict = get_default_config() config_dict = get_default_config()
import json import json
@ -520,6 +570,27 @@ def btn_restore_defaults_clicked(language_code):
global root global root
load_GUI(root, config_dict) load_GUI(root, config_dict)
def btn_idle_clicked(language_code):
app_root = get_app_root()
idle_filepath = os.path.join(app_root, CONST_MAXBOT_INT28_FILE)
with open(CONST_MAXBOT_INT28_FILE, "w") as text_file:
text_file.write("")
update_maxbot_runtime_status(language_code)
def btn_resume_clicked(language_code):
app_root = get_app_root()
idle_filepath = os.path.join(app_root, CONST_MAXBOT_INT28_FILE)
for i in range(10):
if os.path.exists(idle_filepath):
try:
os.remove(idle_filepath)
break
except Exception as exc:
pass
else:
break
update_maxbot_runtime_status(language_code)
def btn_launcher_clicked(language_code): def btn_launcher_clicked(language_code):
import subprocess import subprocess
@ -574,7 +645,7 @@ def btn_save_clicked(language_code):
def btn_save_act(language_code, slience_mode=False): def btn_save_act(language_code, slience_mode=False):
app_root = get_app_root() app_root = get_app_root()
config_filepath = os.path.join(app_root, CONST_SETTINGS_CONFIG_FILENAME) config_filepath = os.path.join(app_root, CONST_MAXBOT_CONFIG_FILE)
config_dict = get_default_config() config_dict = get_default_config()
@ -744,7 +815,7 @@ def btn_save_act(language_code, slience_mode=False):
config_dict["advanced"]["adblock_plus_enable"] = bool(chk_state_adblock_plus.get()) config_dict["advanced"]["adblock_plus_enable"] = bool(chk_state_adblock_plus.get())
config_dict["advanced"]["open_google_oauth_url"] = bool(chk_state_google_oauth.get()) config_dict["advanced"]["open_google_oauth_url"] = bool(chk_state_google_oauth.get())
config_dict["ocr_captcha"] = {} config_dict["ocr_captcha"] = {}
config_dict["ocr_captcha"]["enable"] = bool(chk_state_ocr_captcha.get()) config_dict["ocr_captcha"]["enable"] = bool(chk_state_ocr_captcha.get())
config_dict["ocr_captcha"]["force_submit"] = bool(chk_state_ocr_captcha_force_submit.get()) config_dict["ocr_captcha"]["force_submit"] = bool(chk_state_ocr_captcha_force_submit.get())
@ -759,7 +830,7 @@ def btn_save_act(language_code, slience_mode=False):
if not slience_mode: if not slience_mode:
#messagebox.showinfo(translate[language_code]["save"], translate[language_code]["done"]) #messagebox.showinfo(translate[language_code]["save"], translate[language_code]["done"])
file_to_save = asksaveasfilename(initialfile = CONST_SETTINGS_CONFIG_FILENAME, defaultextension=".json",filetypes=[("json Documents","*.json"),("All Files","*.*")]) file_to_save = asksaveasfilename(initialfile = CONST_MAXBOT_CONFIG_FILE, defaultextension=".json",filetypes=[("json Documents","*.json"),("All Files","*.*")])
if not file_to_save is None: if not file_to_save is None:
print("save as to:", file_to_save) print("save as to:", file_to_save)
with open(str(file_to_save), 'w') as outfile: with open(str(file_to_save), 'w') as outfile:
@ -833,7 +904,6 @@ def btn_preview_sound_clicked():
play_mp3_async(new_sound_filename) play_mp3_async(new_sound_filename)
def play_mp3_async(sound_filename): def play_mp3_async(sound_filename):
import threading
threading.Thread(target=play_mp3, args=(sound_filename,), daemon=True).start() threading.Thread(target=play_mp3, args=(sound_filename,), daemon=True).start()
def play_mp3(sound_filename): def play_mp3(sound_filename):
@ -981,7 +1051,7 @@ def applyNewLanguage():
lbl_kktix_area_keyword_2_and_text.config(text=translate[language_code]["and"]) lbl_kktix_area_keyword_2_and_text.config(text=translate[language_code]["and"])
lbl_auto_guess_options.config(text=translate[language_code]["auto_guess_options"]) lbl_auto_guess_options.config(text=translate[language_code]["auto_guess_options"])
lbl_user_guess_string.config(text=translate[language_code]["user_guess_string"]) lbl_user_guess_string.config(text=translate[language_code]["user_guess_string"])
lbl_date_auto_select.config(text=translate[language_code]["date_auto_select"]) lbl_date_auto_select.config(text=translate[language_code]["date_auto_select"])
lbl_date_auto_select_mode.config(text=translate[language_code]["date_select_order"]) lbl_date_auto_select_mode.config(text=translate[language_code]["date_select_order"])
lbl_date_keyword.config(text=translate[language_code]["date_keyword"]) lbl_date_keyword.config(text=translate[language_code]["date_keyword"])
@ -1028,7 +1098,8 @@ def applyNewLanguage():
tabControl.tab(0, text=translate[language_code]["preference"]) tabControl.tab(0, text=translate[language_code]["preference"])
tabControl.tab(1, text=translate[language_code]["advanced"]) tabControl.tab(1, text=translate[language_code]["advanced"])
tabControl.tab(2, text=translate[language_code]["autofill"]) tabControl.tab(2, text=translate[language_code]["autofill"])
tabControl.tab(3, text=translate[language_code]["about"]) tabControl.tab(3, text=translate[language_code]["runtime"])
tabControl.tab(4, text=translate[language_code]["about"])
global lbl_tixcraft_sid global lbl_tixcraft_sid
global lbl_ibon_ibonqware global lbl_ibon_ibonqware
@ -1385,11 +1456,11 @@ def PreferenctTab(root, config_dict, language_code, UI_PADDING_X):
,"https://ticketplus.com.tw/ (遠大)" ,"https://ticketplus.com.tw/ (遠大)"
,"http://www.urbtix.hk/ (城市)" ,"http://www.urbtix.hk/ (城市)"
,"https://www.cityline.com/ (買飛)" ,"https://www.cityline.com/ (買飛)"
,"https://premier.hkticketing.com/ (快達票)"
,"https://ticketing.galaxymacau.com/ (澳門銀河)" ,"https://ticketing.galaxymacau.com/ (澳門銀河)"
] ]
# 目前機器人已失效, 因為官方的 reCaptcha 可以檢測出機器人。 # 目前機器人已失效, 因為官方的 reCaptcha 可以檢測出機器人。
''' '''
,"https://premier.hkticketing.com/ (快達票)"
''' '''
combo_homepage.set(homepage) combo_homepage.set(homepage)
combo_homepage.bind("<<ComboboxSelected>>", callbackHomepageOnChange) combo_homepage.bind("<<ComboboxSelected>>", callbackHomepageOnChange)
@ -2222,6 +2293,85 @@ def AutofillTab(root, config_dict, language_code, UI_PADDING_X):
frame_group_header.grid(column=0, row=row_count, padx=UI_PADDING_X) frame_group_header.grid(column=0, row=row_count, padx=UI_PADDING_X)
def settings_timer(language_code):
while True:
update_maxbot_runtime_status(language_code)
time.sleep(0.5)
def update_maxbot_runtime_status(language_code):
is_paused = False
if os.path.exists(CONST_MAXBOT_INT28_FILE):
is_paused = True
try:
global lbl_maxbot_status_data
maxbot_status = translate[language_code]['status_enabled']
if is_paused:
maxbot_status = translate[language_code]['status_paused']
lbl_maxbot_status_data.config(text=maxbot_status)
global btn_idle
global btn_resume
if not is_paused:
btn_idle.grid(column=1, row=0)
btn_resume.grid_forget()
else:
btn_resume.grid(column=2, row=0)
btn_idle.grid_forget()
global lbl_maxbot_last_url_data
last_url = read_last_url_from_file()
lbl_maxbot_last_url_data.config(text=last_url)
except Exception as exc:
pass
def RuntimeTab(root, config_dict, language_code, UI_PADDING_X):
row_count = 0
frame_group_header = Frame(root)
group_row_count = 0
maxbot_status = ""
global lbl_maxbot_status
lbl_maxbot_status = Label(frame_group_header, text=translate[language_code]['running_status'])
lbl_maxbot_status.grid(column=0, row=group_row_count, sticky = E)
frame_maxbot_interrupt = Frame(frame_group_header)
global lbl_maxbot_status_data
lbl_maxbot_status_data = Label(frame_maxbot_interrupt, text=maxbot_status)
lbl_maxbot_status_data.grid(column=0, row=group_row_count, sticky = W)
global btn_idle
global btn_resume
btn_idle = ttk.Button(frame_maxbot_interrupt, text=translate[language_code]['idle'], command= lambda: btn_idle_clicked(language_code) )
btn_idle.grid(column=1, row=0)
btn_resume = ttk.Button(frame_maxbot_interrupt, text=translate[language_code]['resume'], command= lambda: btn_resume_clicked(language_code))
btn_resume.grid(column=2, row=0)
frame_maxbot_interrupt.grid(column=1, row=group_row_count, sticky = W)
group_row_count +=1
global lbl_maxbot_last_url
lbl_maxbot_last_url = Label(frame_group_header, text=translate[language_code]['running_url'])
lbl_maxbot_last_url.grid(column=0, row=group_row_count, sticky = E)
last_url = ""
global lbl_maxbot_last_url_data
lbl_maxbot_last_url_data = Label(frame_group_header, text=last_url)
lbl_maxbot_last_url_data.grid(column=1, row=group_row_count, sticky = W)
frame_group_header.grid(column=0, row=row_count, padx=UI_PADDING_X)
update_maxbot_runtime_status(language_code)
def AboutTab(root, language_code): def AboutTab(root, language_code):
row_count = 0 row_count = 0
@ -2336,8 +2486,6 @@ def get_action_bar(root, language_code):
btn_restore_defaults = ttk.Button(frame_action, text=translate[language_code]['restore_defaults'], command= lambda: btn_restore_defaults_clicked(language_code)) btn_restore_defaults = ttk.Button(frame_action, text=translate[language_code]['restore_defaults'], command= lambda: btn_restore_defaults_clicked(language_code))
btn_restore_defaults.grid(column=3, row=0) btn_restore_defaults.grid(column=3, row=0)
return frame_action return frame_action
def clearFrame(frame): def clearFrame(frame):
@ -2367,7 +2515,11 @@ def load_GUI(root, config_dict):
tabControl.add(tab3, text=translate[language_code]['autofill']) tabControl.add(tab3, text=translate[language_code]['autofill'])
tab4 = Frame(tabControl) tab4 = Frame(tabControl)
tabControl.add(tab4, text=translate[language_code]['about']) tabControl.add(tab4, text=translate[language_code]['runtime'])
tab5 = Frame(tabControl)
tabControl.add(tab5, text=translate[language_code]['about'])
tabControl.grid(column=0, row=row_count) tabControl.grid(column=0, row=row_count)
tabControl.select(tab1) tabControl.select(tab1)
@ -2380,7 +2532,9 @@ def load_GUI(root, config_dict):
PreferenctTab(tab1, config_dict, language_code, UI_PADDING_X) PreferenctTab(tab1, config_dict, language_code, UI_PADDING_X)
AdvancedTab(tab2, config_dict, language_code, UI_PADDING_X) AdvancedTab(tab2, config_dict, language_code, UI_PADDING_X)
AutofillTab(tab3, config_dict, language_code, UI_PADDING_X) AutofillTab(tab3, config_dict, language_code, UI_PADDING_X)
AboutTab(tab4, language_code) RuntimeTab(tab4, config_dict, language_code, UI_PADDING_X)
AboutTab(tab5, language_code)
threading.Thread(target=settings_timer, args=(language_code,), daemon=True).start()
def main(): def main():
@ -2402,6 +2556,7 @@ def main():
load_GUI(root, config_dict) load_GUI(root, config_dict)
GUI_SIZE_WIDTH = 510 GUI_SIZE_WIDTH = 510
GUI_SIZE_HEIGHT = 619 GUI_SIZE_HEIGHT = 619