diff --git a/database/db_controller.py b/database/db_controller.py index 5b58741..2872c73 100644 --- a/database/db_controller.py +++ b/database/db_controller.py @@ -60,7 +60,7 @@ class DBController: "generation": generation, "sprite_url": sprite_url, "is_baby_form": False, - "storable_in_home": True, # Example additional field + "storable_in_home": False, } with self.lock: @@ -98,3 +98,26 @@ class DBController: results = self.cursor.fetchall() return [dict(row) for row in results] + + def update_home_status(self, pfic, status): + self.update_pokemon_field(pfic, "storable_in_home", status) + pass + + def update_pokemon_field(self, pfic, field_name, new_value): + # Fetch the existing record + self.cursor.execute('SELECT data FROM pokemon_forms WHERE PFIC = ?', (pfic,)) + result = self.cursor.fetchone() + + if result: + # Load the JSON data and update the field + data = json.loads(result[0]) + data[field_name] = new_value + + # Update the record with the modified JSON + updated_data_str = json.dumps(data) + self.cursor.execute(''' + UPDATE pokemon_forms + SET data = ? + WHERE PFIC = ? + ''', (updated_data_str, pfic)) + self.conn.commit() diff --git a/ui/main_window_controller.py b/ui/main_window_controller.py index ac0b0bf..7e20b75 100644 --- a/ui/main_window_controller.py +++ b/ui/main_window_controller.py @@ -3,7 +3,8 @@ from PyQt6.QtWidgets import QMenu from PyQt6.QtGui import QAction import os -from ui.workers import GatherPokemonFormsWorker +from ui.workers.gather_home_storage_status_worker import GatherHomeStorageStatus +from ui.workers.gather_pokemon_forms_worker import GatherPokemonFormsWorker from db import db @@ -97,6 +98,15 @@ class MainWindowController: db.save_changes() def gather_home_storage_info(self): + worker = GatherHomeStorageStatus() + worker.signals.finished.connect(self.on_home_status_gathered) + self.thread_pool.start(worker) + pass + + def on_home_status_gathered(self, data): + print("Works Done!") + for pfic in data: + db.update_home_status(pfic, True) pass def gather_evolution_info(self): diff --git a/ui/workers/gather_home_storage_status_worker.py b/ui/workers/gather_home_storage_status_worker.py new file mode 100644 index 0000000..23a2fb2 --- /dev/null +++ b/ui/workers/gather_home_storage_status_worker.py @@ -0,0 +1,148 @@ +from PyQt6.QtCore import QObject, pyqtSignal, QRunnable +from bs4 import BeautifulSoup +from cache import cache + +from utility.data import regions, default_forms +from utility.functions import get_objects_by_number, compare_pokemon_forms +from db import db + +class GatherHomeStorageStatusWorkerSignals(QObject): + finished = pyqtSignal(list) + +class GatherHomeStorageStatus(QRunnable): + def __init__(self): + super().__init__() + self.signals = GatherHomeStorageStatusWorkerSignals() + self.base_url = "https://www.serebii.net/pokemonhome/" + + def run(self): + try: + gathered_data = self.gather_home_storage_data() + self.signals.finished.emit(gathered_data) + except Exception as e: + print(f"Error gathering Pokémon home storage status: {e}") + + def gather_home_storage_data(self): + all_pokemon_forms = db.get_list_of_pokemon_forms() + pokemon_storable_in_home = [] + pfics_that_can_go_to_home = [] + for region in regions: + pokemon_storable_in_home.extend(self.scrape_region_for_pokemon(region)) + + for pokemon_form in all_pokemon_forms: + storable_in_home = False + name = pokemon_form["name"] + national_dex = pokemon_form["national_dex"] + working_form = pokemon_form["form_name"] + + if working_form and name in working_form: + working_form = working_form.replace(name, "").strip() + + # serebii doesn't list gender in the table so we have to assume based on form name. + if working_form and ("male" in working_form.lower() or "working_form" in working_form.lower()): + working_form = None + + if name == "Unown" and (working_form != "!" and working_form != "?"): + working_form = None + + if name == "Tauros" and working_form == "Combat Breed": + working_form = "Paldean Form" + + # serebii just gave up on Alcremie. It has 36 uniquie forms all storable in home. + if name == "Alcremie": + working_form = None + + pokemon_by_national_dex = get_objects_by_number(pokemon_storable_in_home, f"{national_dex:04d}") + for pokemon in pokemon_by_national_dex: + if working_form: + parts = pokemon['name'].split(" ") + if len(parts) > 1 and parts[0] == working_form: + storable_in_home = True + + brackets = self.extract_bracketed_text(pokemon['name']) + if brackets: + for bracket in brackets: + if name in bracket: + bracket = bracket.replace(name, "").strip() + if compare_pokemon_forms(working_form, bracket): + storable_in_home = True + break + if storable_in_home == False and working_form and working_form in default_forms: + working_form = None + + if working_form == None and name.lower() in pokemon['name'].lower(): + storable_in_home = True + break + if storable_in_home: + pfics_that_can_go_to_home.append(pokemon_form["pfic"]) + return pfics_that_can_go_to_home + + + def scrape_region_for_pokemon(self, region): + cached_entry = cache.get(f"home_{region}") + if cached_entry != None: + return cached_entry + + url = f"{self.base_url}{region}pokemon.shtml" + response = cache.fetch_url(url) + if not response: + return [] + + soup = BeautifulSoup(response, 'html.parser') + table = soup.find('table', class_='dextable') + if table == None: + return [] + + pokemon_list = [] + + rows = table.find_all('tr')[2:] # Skip the header row and the game intro row + for row in rows: + cells = row.find_all('td') + if len(cells) <= 5: # Ensure we have enough cells to check depositability. if only 5 then its not depositable in any game. + continue + + number = cells[0].text.strip().lstrip('#') + name = cells[2].text.strip() + + # Get the image URL + img_url = cells[1].find('img')['src'] + full_img_url = f"https://www.serebii.net{img_url}" + + pokemon_list.append({ + 'number': number, + 'name': name, + 'image_url': full_img_url + }) + + cache.set(f"home_{region}", pokemon_list) + + return pokemon_list + + def extract_bracketed_text(self, string): + results = [] + stack = [] + start_index = -1 + + for i, char in enumerate(string): + if char == '(': + if not stack: + start_index = i + stack.append(i) + elif char == ')': + if stack: + stack.pop() + if not stack: + results.append(string[start_index + 1:i]) + start_index = -1 + else: + #logger.warning(f"Warning: Unmatched closing parenthesis at position {i}") + pass + + # Handle any remaining unclosed brackets + if stack: + #logger.warning(f"Warning: {len(stack)} unmatched opening parentheses") + for unmatched_start in stack: + results.append(string[unmatched_start + 1:]) + + return results + diff --git a/ui/workers.py b/ui/workers/gather_pokemon_forms_worker.py similarity index 100% rename from ui/workers.py rename to ui/workers/gather_pokemon_forms_worker.py diff --git a/utility/data.py b/utility/data.py index 3973a11..b53f354 100644 --- a/utility/data.py +++ b/utility/data.py @@ -10,6 +10,7 @@ pokemon_generations = { 9: {"min": 906, "max": 1025}, } +regions = ["kanto", "johto", "hoenn", "sinnoh", "unova", "kalos", "alola", "galar", "paldea", "hisui", "unknown"] regional_descriptors = ["kantonian", "johtonian", "hoennian", "sinnohan", "unovan", "kalosian", "alolan", "galarian", "hisuian", "paldean"] yellow = { @@ -248,3 +249,57 @@ main_line_games = [ scarlet, violet, ] +# If a pokemon is in this form then its generally* not refered to as a form +# *I say generally as some do and some don't +default_forms = [ + "Male", + "Normal Forme", + "Hero of Many Battles", + "Altered Forme", + "Land Forme", + "Standard Mode", + "Ordinary Forme", + "Aria Forme", + "Natural Form", + "Shield Forme", + "Neutral Mode", + "Hoopa Confined", + "Solo Form", + "Type: Normal", + "Red Core", + "Disguised Form", + "Ice Face", + "Full Belly Mode", + "Zero Form", + "Curly Form", + "Apex Build", + "Ultimate Mode", + "Teal Mask", + "Normal Form", + "Plant Cloak", + "Overcast Form", + "West Sea", + "Normal", + "Red-Striped Form", + "Spring Form", + "Incarnate Forme", + "Meadow Pattern", + "Red Flower", + "Average Size", + "50% Forme", + "Confined", + "Baile Style", + "Midday Form", + "Amped Form", + "Vanilla Cream Strawberry Sweet", + "Single Strike Style", + "Green Plumage", + "Two-Segment Form", + "Standard Form", + "Counterfeit Form", + "Unremarkable Form", + "Antique Form", + "Phony Form", + "Masterpiece Form", + "Chest Form" +] \ No newline at end of file diff --git a/utility/functions.py b/utility/functions.py index d917bb4..840551e 100644 --- a/utility/functions.py +++ b/utility/functions.py @@ -1,5 +1,6 @@ from .data import pokemon_generations, main_line_games import unicodedata +import re def format_pokemon_id(national_dex: int, region_code: int, form_index: int, gender_code: int) -> str: return f"{national_dex:04d}-{region_code:02d}-{form_index:03d}-{gender_code}" @@ -49,4 +50,29 @@ def find_game_generation(game_name: str) -> int: for game in main_line_games: if game_name == game["Name"].lower() or game_name in (name.lower() for name in game["AltNames"]): return game["Generation"] - return None \ No newline at end of file + return None + +def sanitize_filename(filename): + # Define a dictionary of symbol replacements + symbol_replacements = { + '?': 'questionmark', + '*': 'asterisk', + ':': 'colon', + '/': 'slash', + '\\': 'backslash', + '|': 'pipe', + '<': 'lessthan', + '>': 'greaterthan', + '"': 'quote', + ' ': '_' + } + + # Replace symbols with their word equivalents + for symbol, word in symbol_replacements.items(): + filename = filename.replace(symbol, word) + + # Remove any remaining invalid characters + return re.sub(r'[<>:"/\\|?*]', '', filename) + +def get_objects_by_number(array, target_number): + return [obj for obj in array if obj['number'] == target_number] \ No newline at end of file