14 changed files with 919 additions and 0 deletions
@ -0,0 +1,16 @@ |
|||||
|
{ |
||||
|
// Use IntelliSense to learn about possible attributes. |
||||
|
// Hover to view descriptions of existing attributes. |
||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 |
||||
|
"version": "0.2.0", |
||||
|
"configurations": [ |
||||
|
{ |
||||
|
"name": "Run OriginDex App", |
||||
|
"type": "debugpy", |
||||
|
"request": "launch", |
||||
|
"program": "${workspaceFolder}/main.py", |
||||
|
"console": "integratedTerminal", |
||||
|
"justMyCode": false |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
{ |
||||
|
"python.languageServer": "Pylance", |
||||
|
"python.analysis.diagnosticSeverityOverrides": { |
||||
|
"reportMissingModuleSource": "none", |
||||
|
"reportShadowedImports": "none" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,2 @@ |
|||||
|
from utility.cache_manager import CacheManager |
||||
|
cache = CacheManager() |
||||
@ -0,0 +1,23 @@ |
|||||
|
import sys |
||||
|
|
||||
|
from pathlib import Path |
||||
|
sys.path.append(str(Path(__file__).resolve().parent)) |
||||
|
|
||||
|
from PyQt6 import QtWidgets |
||||
|
from ui.main_window_view import PokemonUI |
||||
|
|
||||
|
from cache import cache |
||||
|
|
||||
|
def main(): |
||||
|
import sys |
||||
|
app = QtWidgets.QApplication(sys.argv) |
||||
|
ui = PokemonUI() |
||||
|
ui.show() |
||||
|
sys.exit(app.exec()) |
||||
|
|
||||
|
if __name__ == "__main__": |
||||
|
try: |
||||
|
main() |
||||
|
finally: |
||||
|
# Ensure the cache is closed at the end of the application |
||||
|
cache.close() |
||||
@ -0,0 +1,120 @@ |
|||||
|
from PyQt6.QtCore import Qt, QTimer, QThreadPool |
||||
|
from PyQt6.QtWidgets import QMenu |
||||
|
from PyQt6.QtGui import QAction |
||||
|
|
||||
|
from ui.workers import GatherPokemonFormsWorker |
||||
|
|
||||
|
class MainWindowController: |
||||
|
def __init__(self, view): |
||||
|
self.view = view |
||||
|
self.pokemon_data_cache = [] |
||||
|
self.filter_timer = QTimer() |
||||
|
self.filter_timer.setInterval(300) # 300 ms delay to wait for user to stop typing |
||||
|
self.filter_timer.setSingleShot(True) |
||||
|
self.filter_timer.timeout.connect(self.apply_filters) |
||||
|
self.thread_pool = QThreadPool() |
||||
|
|
||||
|
def initialize_pokemon_list(self, data): |
||||
|
self.pokemon_data_cache = data |
||||
|
self.view.update_pokemon_list(data) |
||||
|
|
||||
|
def filter_pokemon_list(self): |
||||
|
self.filter_timer.start() |
||||
|
|
||||
|
def apply_filters(self): |
||||
|
search_text = self.view.search_bar.text().lower() |
||||
|
show_only_home_storable = self.view.filter_home_storable.isChecked() |
||||
|
show_only_missing_encounters = self.view.highlight_no_encounters.isChecked() |
||||
|
|
||||
|
filtered_data = [] |
||||
|
for pfic, display_name in self.pokemon_data_cache: |
||||
|
# Check if the item matches the search text |
||||
|
text_match = search_text in display_name.lower() |
||||
|
|
||||
|
# Check if the item is storable in Home (if the filter is active) |
||||
|
home_storable = True |
||||
|
if show_only_home_storable: |
||||
|
# TODO: update the call to correctly filter the data, or better yet update the data at the source to include this info. |
||||
|
home_storable = True #event_system.call_sync('get_home_storable', pfic) |
||||
|
|
||||
|
# Check to see if the pokemon has encounters |
||||
|
has_encounters = True |
||||
|
if show_only_missing_encounters: |
||||
|
# TODO: reimplement this check. |
||||
|
has_encounters = True |
||||
|
|
||||
|
# If both conditions are met, add to filtered data |
||||
|
if text_match and home_storable: |
||||
|
filtered_data.append((pfic, display_name)) |
||||
|
|
||||
|
# Update the view with the filtered data |
||||
|
self.view.update_pokemon_list(filtered_data) |
||||
|
|
||||
|
def show_pokemon_context_menu(self, position): |
||||
|
item = self.view.pokemon_list.itemAt(position) |
||||
|
if item is not None: |
||||
|
context_menu = QMenu(self) |
||||
|
refresh_action = QAction("Refresh Encounters", self) |
||||
|
refresh_action.triggered.connect(lambda: self.refresh_pokemon_encounters(item)) |
||||
|
context_menu.addAction(refresh_action) |
||||
|
context_menu.exec(self.pokemon_list.viewport().mapToGlobal(position)) |
||||
|
|
||||
|
def on_pokemon_selected(self, item): |
||||
|
pfic = item.data(Qt.ItemDataRole.UserRole) |
||||
|
self.refresh_pokemon_details_panel(pfic) |
||||
|
|
||||
|
def edit_encounter(self): |
||||
|
pass |
||||
|
|
||||
|
def add_new_encounter(self): |
||||
|
pass |
||||
|
|
||||
|
def add_new_evolution(self): |
||||
|
pass |
||||
|
|
||||
|
def save_changes(self): |
||||
|
pass |
||||
|
|
||||
|
def export_database(self): |
||||
|
pass |
||||
|
|
||||
|
def gather_pokemon_forms(self): |
||||
|
worker = GatherPokemonFormsWorker() |
||||
|
worker.signals.finished.connect(self.on_forms_gathered) |
||||
|
self.thread_pool.start(worker) |
||||
|
|
||||
|
def on_forms_gathered(self, data): |
||||
|
# This method will be called in the main thread when the worker finishes |
||||
|
# Update the UI with the gathered forms |
||||
|
self.view.update_pokemon_forms(data) |
||||
|
print("Work's Done!", data) |
||||
|
|
||||
|
def gather_home_storage_info(self): |
||||
|
pass |
||||
|
|
||||
|
def gather_evolution_info(self): |
||||
|
pass |
||||
|
|
||||
|
def reinitialize_database(self): |
||||
|
pass |
||||
|
|
||||
|
def gather_encounter_info(self): |
||||
|
pass |
||||
|
|
||||
|
def gather_marks_info(self): |
||||
|
pass |
||||
|
|
||||
|
def load_shiftable_forms(self): |
||||
|
pass |
||||
|
|
||||
|
def on_exclusive_set_selected(self): |
||||
|
pass |
||||
|
|
||||
|
def add_new_exclusive_set(self): |
||||
|
pass |
||||
|
|
||||
|
def show_encounter_context_menu(self): |
||||
|
pass |
||||
|
|
||||
|
def add_encounter_to_set(self): |
||||
|
pass |
||||
@ -0,0 +1,229 @@ |
|||||
|
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QListWidget, QLineEdit, |
||||
|
QLabel, QCheckBox, QPushButton, QFormLayout, QListWidgetItem, QSplitter, QTreeWidget, |
||||
|
QTreeWidgetItem, QDialog, QDialogButtonBox, QComboBox, QMessageBox, QSpinBox, QMenu, QTabWidget, |
||||
|
QTextEdit) |
||||
|
from PyQt6.QtCore import Qt, QSize, QTimer, QMetaObject |
||||
|
from PyQt6.QtGui import QPixmap, QFontMetrics, QColor, QAction |
||||
|
from .main_window_controller import MainWindowController |
||||
|
|
||||
|
class PokemonUI(QWidget): |
||||
|
def __init__(self, parent=None): |
||||
|
super().__init__(parent) |
||||
|
self.controller = MainWindowController(self) |
||||
|
self.setup_ui() |
||||
|
|
||||
|
def setup_ui(self): |
||||
|
main_layout = QVBoxLayout(self) |
||||
|
|
||||
|
self.tab_widget = QTabWidget() |
||||
|
main_layout.addWidget(self.tab_widget) |
||||
|
self.setup_main_tab() |
||||
|
self.setup_db_operations_tab() |
||||
|
self.setup_manage_encounters_tab() |
||||
|
|
||||
|
self.save_button = QPushButton("Save Changes") |
||||
|
self.save_button.clicked.connect(self.controller.save_changes) |
||||
|
main_layout.addWidget(self.save_button) |
||||
|
|
||||
|
self.export_button = QPushButton("Export Database") |
||||
|
self.export_button.clicked.connect(self.controller.export_database) |
||||
|
main_layout.addWidget(self.export_button) |
||||
|
|
||||
|
def setup_main_tab(self): |
||||
|
main_tab = QWidget() |
||||
|
main_tab_layout = QHBoxLayout(main_tab) |
||||
|
self.tab_widget.addTab(main_tab, "Main") |
||||
|
|
||||
|
self.create_main_left_panel(main_tab_layout) |
||||
|
self.create_main_right_panel(main_tab_layout) |
||||
|
|
||||
|
def create_main_left_panel(self, main_tab_layout): |
||||
|
left_layout = QVBoxLayout() |
||||
|
search_layout = QHBoxLayout() |
||||
|
self.search_bar = QLineEdit() |
||||
|
self.search_bar.setPlaceholderText("Search Pokémon...") |
||||
|
self.search_bar.textChanged.connect(self.controller.filter_pokemon_list) |
||||
|
search_layout.addWidget(self.search_bar) |
||||
|
|
||||
|
left_layout.addLayout(search_layout) |
||||
|
|
||||
|
self.pokemon_list = QListWidget() |
||||
|
self.pokemon_list.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) |
||||
|
self.pokemon_list.customContextMenuRequested.connect(self.controller.show_pokemon_context_menu) |
||||
|
self.pokemon_list.currentItemChanged.connect(self.controller.on_pokemon_selected) |
||||
|
left_layout.addWidget(self.pokemon_list) |
||||
|
|
||||
|
self.highlight_no_encounters = QCheckBox("Highlight Pokémon without encounters") |
||||
|
self.highlight_no_encounters.stateChanged.connect(self.controller.filter_pokemon_list) |
||||
|
left_layout.addWidget(self.highlight_no_encounters) |
||||
|
|
||||
|
self.filter_home_storable = QCheckBox("Show only Home-storable Pokémon") |
||||
|
self.filter_home_storable.stateChanged.connect(self.controller.filter_pokemon_list) |
||||
|
left_layout.addWidget(self.filter_home_storable) |
||||
|
|
||||
|
main_tab_layout.addLayout(left_layout, 1) |
||||
|
|
||||
|
def create_main_right_panel(self, main_tab_layout): |
||||
|
right_layout = QVBoxLayout() |
||||
|
|
||||
|
# Left side of right panel: Text information |
||||
|
info_layout = QHBoxLayout() |
||||
|
text_layout = QVBoxLayout() |
||||
|
self.edit_form = QFormLayout() |
||||
|
self.name_label = QLabel() |
||||
|
self.form_name_label = QLabel() |
||||
|
self.national_dex_label = QLabel() |
||||
|
self.generation_label = QLabel() |
||||
|
self.home_checkbox = QCheckBox("Available in Home") |
||||
|
self.is_baby_form_checkbox = QCheckBox("Is Baby Form") |
||||
|
|
||||
|
self.edit_form.addRow("Name:", self.name_label) |
||||
|
self.edit_form.addRow("Form:", self.form_name_label) |
||||
|
self.edit_form.addRow("National Dex:", self.national_dex_label) |
||||
|
self.edit_form.addRow("Generation:", self.generation_label) |
||||
|
self.edit_form.addRow(self.home_checkbox) |
||||
|
self.edit_form.addRow(self.is_baby_form_checkbox) |
||||
|
|
||||
|
text_layout.addLayout(self.edit_form) |
||||
|
|
||||
|
# Right side of right panel: Image |
||||
|
image_layout = QVBoxLayout() |
||||
|
self.image_label = QLabel() |
||||
|
self.image_label.setAlignment(Qt.AlignmentFlag.AlignCenter) |
||||
|
self.image_label.setFixedSize(150, 150) |
||||
|
image_layout.addWidget(self.image_label) |
||||
|
image_layout.addStretch(1) |
||||
|
|
||||
|
info_layout.addLayout(text_layout) |
||||
|
info_layout.addLayout(image_layout) |
||||
|
|
||||
|
second_half_layout = QVBoxLayout() |
||||
|
# Evolution chain tree |
||||
|
self.evolution_tree = QTreeWidget() |
||||
|
self.evolution_tree.setHeaderLabels(["Pokémon", "Evolution Method"]) |
||||
|
self.evolution_tree.setColumnWidth(0, 200) |
||||
|
second_half_layout.addWidget(self.evolution_tree) |
||||
|
|
||||
|
# Add Locations tree |
||||
|
self.locations_tree = QTreeWidget() |
||||
|
self.locations_tree.setHeaderLabels(["Game/Location", "Details"]) |
||||
|
self.locations_tree.setColumnWidth(0, 200) |
||||
|
self.locations_tree.itemDoubleClicked.connect(self.controller.edit_encounter) |
||||
|
second_half_layout.addWidget(QLabel("Locations:")) |
||||
|
second_half_layout.addWidget(self.locations_tree) |
||||
|
|
||||
|
# Add New Encounter button |
||||
|
self.add_encounter_button = QPushButton("Add New Encounter") |
||||
|
self.add_encounter_button.clicked.connect(self.controller.add_new_encounter) |
||||
|
second_half_layout.addWidget(self.add_encounter_button) |
||||
|
|
||||
|
# Move buttons to the bottom |
||||
|
second_half_layout.addStretch(1) |
||||
|
|
||||
|
# Add New Evolution button |
||||
|
self.add_evolution_button = QPushButton("Add New Evolution") |
||||
|
self.add_evolution_button.clicked.connect(self.controller.add_new_evolution) |
||||
|
second_half_layout.addWidget(self.add_evolution_button) |
||||
|
|
||||
|
right_layout.addLayout(info_layout) |
||||
|
right_layout.addLayout(second_half_layout) |
||||
|
|
||||
|
main_tab_layout.addLayout(right_layout, 1) |
||||
|
|
||||
|
def setup_db_operations_tab(self): |
||||
|
db_tab = QWidget() |
||||
|
db_tab_layout = QVBoxLayout(db_tab) |
||||
|
self.tab_widget.addTab(db_tab, "Database Operations") |
||||
|
|
||||
|
gather_forms_btn = QPushButton("Gather Pokémon Forms") |
||||
|
gather_forms_btn.clicked.connect(self.controller.gather_pokemon_forms) |
||||
|
db_tab_layout.addWidget(gather_forms_btn) |
||||
|
|
||||
|
gather_home_btn = QPushButton("Gather Home Storage Info") |
||||
|
gather_home_btn.clicked.connect(self.controller.gather_home_storage_info) |
||||
|
db_tab_layout.addWidget(gather_home_btn) |
||||
|
|
||||
|
gather_evolutions_btn = QPushButton("Gather Evolution Information") |
||||
|
gather_evolutions_btn.clicked.connect(self.controller.gather_evolution_info) |
||||
|
db_tab_layout.addWidget(gather_evolutions_btn) |
||||
|
|
||||
|
gather_encounters_btn = QPushButton("Gather Encounter Information") |
||||
|
gather_encounters_btn.clicked.connect(self.controller.gather_encounter_info) |
||||
|
db_tab_layout.addWidget(gather_encounters_btn) |
||||
|
|
||||
|
gather_marks_btn = QPushButton("Gather Marks Information") |
||||
|
gather_marks_btn.clicked.connect(self.controller.gather_marks_info) |
||||
|
db_tab_layout.addWidget(gather_marks_btn) |
||||
|
|
||||
|
load_shiftable_forms_btn = QPushButton("Load Shiftable Forms") |
||||
|
load_shiftable_forms_btn.clicked.connect(self.controller.load_shiftable_forms) |
||||
|
db_tab_layout.addWidget(load_shiftable_forms_btn) |
||||
|
|
||||
|
self.progress_text = QTextEdit() |
||||
|
self.progress_text.setReadOnly(True) |
||||
|
self.progress_text.setMinimumHeight(200) # Set a minimum height |
||||
|
db_tab_layout.addWidget(self.progress_text) |
||||
|
|
||||
|
db_tab_layout.addStretch(1) |
||||
|
|
||||
|
reinit_db_btn = QPushButton("Clear and Reinitialize Database") |
||||
|
reinit_db_btn.clicked.connect(self.controller.reinitialize_database) |
||||
|
db_tab_layout.addWidget(reinit_db_btn) |
||||
|
|
||||
|
def setup_manage_encounters_tab(self): |
||||
|
manage_encounters = QWidget() |
||||
|
self.manage_encounters_tab = QHBoxLayout(manage_encounters) |
||||
|
self.tab_widget.addTab(manage_encounters, "Manage Encounters") |
||||
|
|
||||
|
left_layout = QVBoxLayout() |
||||
|
self.exclusive_set_list = QListWidget() |
||||
|
self.exclusive_set_list.currentItemChanged.connect(self.controller.on_exclusive_set_selected) |
||||
|
left_layout.addWidget(QLabel("Exclusive Encounter Sets:")) |
||||
|
left_layout.addWidget(self.exclusive_set_list) |
||||
|
|
||||
|
add_set_button = QPushButton("Add New Set") |
||||
|
add_set_button.clicked.connect(self.controller.add_new_exclusive_set) |
||||
|
left_layout.addWidget(add_set_button) |
||||
|
|
||||
|
# Right side: Set details and encounters |
||||
|
right_layout = QVBoxLayout() |
||||
|
self.set_name_label = QLabel() |
||||
|
self.set_description_label = QLabel() |
||||
|
self.set_game_label = QLabel() |
||||
|
right_layout.addWidget(self.set_name_label) |
||||
|
right_layout.addWidget(self.set_description_label) |
||||
|
right_layout.addWidget(self.set_game_label) |
||||
|
|
||||
|
self.encounter_list = QListWidget() |
||||
|
self.encounter_list.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) |
||||
|
self.encounter_list.customContextMenuRequested.connect(self.controller.show_encounter_context_menu) |
||||
|
right_layout.addWidget(QLabel("Encounters in Set:")) |
||||
|
right_layout.addWidget(self.encounter_list) |
||||
|
|
||||
|
add_encounter_button = QPushButton("Add Encounter to Set") |
||||
|
add_encounter_button.clicked.connect(self.controller.add_encounter_to_set) |
||||
|
right_layout.addWidget(add_encounter_button) |
||||
|
|
||||
|
self.manage_encounters_tab.addLayout(left_layout, 1) |
||||
|
self.manage_encounters_tab.addLayout(right_layout, 2) |
||||
|
|
||||
|
#self.load_exclusive_sets() |
||||
|
|
||||
|
def update_pokemon_list(self, data): |
||||
|
self.pokemon_list.clear() |
||||
|
|
||||
|
for pfic, display_name in data: |
||||
|
item = QListWidgetItem(display_name) |
||||
|
item.setData(Qt.ItemDataRole.UserRole, pfic) |
||||
|
self.pokemon_list.addItem(item) |
||||
|
|
||||
|
def update_pokemon_forms(self, data): |
||||
|
self.pokemon_list.clear() |
||||
|
|
||||
|
for pokemon in data: |
||||
|
display_name = f"{pokemon["national_dex"]:04d} - {pokemon["name"]}" |
||||
|
if pokemon["form_name"]: |
||||
|
display_name += f" ({pokemon["form_name"]})" |
||||
|
item = QListWidgetItem(display_name) |
||||
|
item.setData(Qt.ItemDataRole.UserRole, pokemon["pfic"]) |
||||
|
self.pokemon_list.addItem(item) |
||||
@ -0,0 +1,170 @@ |
|||||
|
from PyQt6.QtCore import QObject, pyqtSignal, QRunnable |
||||
|
from bs4 import BeautifulSoup |
||||
|
import re |
||||
|
|
||||
|
from cache import cache |
||||
|
from utility.functions import get_generation_from_national_dex, sanitise_pokemon_name_for_url, remove_accents, compare_pokemon_forms, find_game_generation, format_pokemon_id |
||||
|
|
||||
|
class GatherPokemonFormsWorkerSignals(QObject): |
||||
|
finished = pyqtSignal(list) |
||||
|
|
||||
|
class GatherPokemonFormsWorker(QRunnable): |
||||
|
def __init__(self): |
||||
|
super().__init__() |
||||
|
self.signals = GatherPokemonFormsWorkerSignals() |
||||
|
|
||||
|
def run(self): |
||||
|
try: |
||||
|
gathered_data = self.gather_forms_data() |
||||
|
self.signals.finished.emit(gathered_data) |
||||
|
except Exception as e: |
||||
|
print(f"Error gathering Pokémon forms: {e}") |
||||
|
|
||||
|
def gather_forms_data(self): |
||||
|
# Get the sprites page from pokemondb. |
||||
|
# This gives us every pokemon in its default form. |
||||
|
url = "https://pokemondb.net/sprites" |
||||
|
page_data = cache.fetch_url(url) |
||||
|
|
||||
|
if not page_data: |
||||
|
return None |
||||
|
|
||||
|
soup = BeautifulSoup(page_data, 'html.parser') |
||||
|
pokemon = soup.find_all('a', class_='infocard') |
||||
|
|
||||
|
# Loop through each card for the pokemon so we can extract out more information |
||||
|
pokemon_forms = [] |
||||
|
for index, mon in enumerate(pokemon): |
||||
|
new_forms = self.process_pokemon_entry(index+1, mon) |
||||
|
if new_forms: |
||||
|
pokemon_forms.extend(new_forms) |
||||
|
|
||||
|
return pokemon_forms |
||||
|
|
||||
|
def get_pokemon_sprites_page_data(self, pokemon_name: str): |
||||
|
url = f"https://pokemondb.net/sprites/{pokemon_name}" |
||||
|
return cache.fetch_url(url) |
||||
|
|
||||
|
def get_pokemon_dex_page(self, pokemon_name: str): |
||||
|
url = f"https://pokemondb.net/pokedex/{pokemon_name}" |
||||
|
return cache.fetch_url(url) |
||||
|
|
||||
|
def extract_form_name(self, soup): |
||||
|
if soup.find('small'): |
||||
|
smalls = soup.find_all('small') |
||||
|
form_name = "" |
||||
|
for small in smalls: |
||||
|
form_name += small.get_text(strip=True) + " " |
||||
|
form_name = form_name.strip() |
||||
|
return form_name |
||||
|
return "None" |
||||
|
|
||||
|
def process_pokemon_entry(self, national_dex_number, pokemon_soup, force_refresh = True): |
||||
|
found_forms = [] |
||||
|
generation = get_generation_from_national_dex(national_dex_number) |
||||
|
pokemon_name = pokemon_soup.get_text(strip=True) |
||||
|
print(f"Processing {pokemon_name}") |
||||
|
|
||||
|
url_name = sanitise_pokemon_name_for_url(pokemon_name) |
||||
|
|
||||
|
if force_refresh: |
||||
|
cache.purge(url_name) |
||||
|
|
||||
|
cached_entry = cache.get(url_name) |
||||
|
if cached_entry != None: |
||||
|
return cached_entry |
||||
|
|
||||
|
sprites_page_data = self.get_pokemon_sprites_page_data(url_name) |
||||
|
if not sprites_page_data: |
||||
|
return None |
||||
|
|
||||
|
form_pattern = re.compile(r'a(?:n)? (\w+) Form(?:,)? introduced in (?:the )?([\w\s:]+)(?:\/([\w\s:]+))?', re.IGNORECASE) |
||||
|
update_pattern = re.compile(r'a(?:n)? (\w+) form(?:,)? available in the latest update to ([\w\s:]+)(?:& ([\w\s:]+))?', re.IGNORECASE) |
||||
|
multiple_forms_pattern = re.compile(r'has (?:\w+) new (\w+) Form(?:s)?(?:,)? available in (?:the )?([\w\s:]+)(?:& ([\w\s:]+))?', re.IGNORECASE) |
||||
|
expansion_pass_pattern = re.compile(r'a(?:n)? (\w+) form(?:,)? introduced in the Crown Tundra Expansion Pass to ([\w\s:]+)(?:& ([\w\s:]+))?', re.IGNORECASE) |
||||
|
patterns = [form_pattern, update_pattern, multiple_forms_pattern, expansion_pass_pattern] |
||||
|
|
||||
|
sprites_soup = BeautifulSoup(sprites_page_data, 'html.parser') |
||||
|
generation_8_table = sprites_soup.find('h2', string='Generation 8') |
||||
|
if generation_8_table: |
||||
|
generation_8_table = generation_8_table.find_next('table') |
||||
|
|
||||
|
if generation_8_table: |
||||
|
generation_8_rows = generation_8_table.select('tbody > tr') |
||||
|
generation_8_rows = [row for row in generation_8_rows if "Home" in row.get_text(strip=True)] |
||||
|
for row in generation_8_rows: |
||||
|
sprites = row.find_all('span', class_='sprites-table-card') |
||||
|
if not sprites: |
||||
|
continue |
||||
|
form_index = 0 |
||||
|
for sprite in sprites: |
||||
|
sprite_img = sprite.find('img') |
||||
|
sprite_url = "missing" |
||||
|
if sprite_img: |
||||
|
sprite_url = sprite_img.get('src') |
||||
|
|
||||
|
if "shiny" in sprite_url: |
||||
|
continue |
||||
|
|
||||
|
form_name = self.extract_form_name(sprite) |
||||
|
#logger.info(f'{sprite_url}, {form_name}') |
||||
|
if form_name != "None": |
||||
|
form_index += 1 |
||||
|
gender = 0 |
||||
|
if form_name.startswith("Male"): |
||||
|
form_index -= 1 |
||||
|
gender = 1 |
||||
|
elif form_name.startswith("Female"): |
||||
|
form_index -= 1 |
||||
|
gender = 2 |
||||
|
|
||||
|
dex_page_data = self.get_pokemon_dex_page(url_name) |
||||
|
if dex_page_data: |
||||
|
dex_soup = BeautifulSoup(dex_page_data, 'html.parser') |
||||
|
|
||||
|
#Find a heading that has the pokemon name in it |
||||
|
dex_header = dex_soup.find('h1', string=pokemon_name) |
||||
|
if dex_header: |
||||
|
#The next <p> tag contains the generation number, in the format "{pokemon name} is a {type}(/{2nd_type}) type Pokémon introduced in Generation {generation number}." |
||||
|
generation_tag = dex_header.find_next('p') |
||||
|
dex_text = generation_tag.get_text() |
||||
|
pattern = r'^(.+?) is a (\w+)(?:/(\w+))? type Pokémon introduced in Generation (\d+)\.$' |
||||
|
match = re.match(pattern, dex_text) |
||||
|
if match: |
||||
|
name, type1, type2, gen = match.groups() |
||||
|
generation = int(gen) |
||||
|
|
||||
|
if form_name != "None": |
||||
|
next_tag = generation_tag.find_next('p') |
||||
|
if next_tag: |
||||
|
extra_text = next_tag.get_text() |
||||
|
extra_text = remove_accents(extra_text) |
||||
|
test_form = form_name.replace(pokemon_name, "").replace("Male", "").replace("Female", "").strip() |
||||
|
if pokemon_name == "Tauros" and (form_name == "Aqua Breed" or form_name == "Blaze Breed" or form_name == "Combat Breed"): |
||||
|
test_form = "Paldean" |
||||
|
for pattern in patterns: |
||||
|
matches = re.findall(pattern, extra_text) |
||||
|
generation_found = False |
||||
|
for i, (regional, game1, game2) in enumerate(matches, 1): |
||||
|
if compare_pokemon_forms(test_form, regional): |
||||
|
target_game = game1.replace("Pokemon", "").strip() |
||||
|
result = find_game_generation(target_game) |
||||
|
if result: |
||||
|
generation = result |
||||
|
generation_found = True |
||||
|
break |
||||
|
if generation_found: |
||||
|
break |
||||
|
|
||||
|
pokemon_form = { |
||||
|
"pfic":format_pokemon_id(national_dex_number, generation, form_index, gender), |
||||
|
"name":pokemon_name, |
||||
|
"form_name":form_name if form_name != "None" else None, |
||||
|
"sprite_url":sprite_url, |
||||
|
"national_dex":national_dex_number, |
||||
|
"generation":generation |
||||
|
} |
||||
|
found_forms.append(pokemon_form) |
||||
|
|
||||
|
cache.set(url_name, found_forms) |
||||
|
return found_forms |
||||
@ -0,0 +1,52 @@ |
|||||
|
import time |
||||
|
import requests |
||||
|
from typing import Any, Optional, Dict, List |
||||
|
import diskcache as dc |
||||
|
from threading import Lock |
||||
|
|
||||
|
class CacheManager: |
||||
|
def __init__(self, cache_dir='cache', max_connections: int = 10): |
||||
|
# Initialize the disk cache |
||||
|
self.cache = dc.Cache(cache_dir) |
||||
|
self.fetch_url_lock = Lock() |
||||
|
|
||||
|
def get(self, key: str) -> Optional[Any]: |
||||
|
# Fetch the value from the cache |
||||
|
return self.cache.get(key) |
||||
|
|
||||
|
def set(self, key: str, value: Any, expire: int = None): |
||||
|
# Store the value in the cache with optional expiry |
||||
|
self.cache.set(key, value, expire=expire) |
||||
|
|
||||
|
def purge(self, key: str): |
||||
|
self.cache.delete(key) |
||||
|
|
||||
|
def bulk_get(self, keys: List[str]) -> Dict[str, Any]: |
||||
|
# Use a dictionary comprehension to fetch multiple values |
||||
|
return {key: self.cache.get(key) for key in keys if key in self.cache} |
||||
|
|
||||
|
def fetch_url(self, url: str, force_refresh: bool = False, expiry: int = 86400*30) -> Optional[str]: |
||||
|
cache_key = f"url_{url}" |
||||
|
if not force_refresh: |
||||
|
cached_data = self.get(cache_key) |
||||
|
if cached_data: |
||||
|
cached_time = cached_data['timestamp'] |
||||
|
if time.time() - cached_time < expiry: |
||||
|
return cached_data['content'] |
||||
|
|
||||
|
# Fetch the URL if not in cache or if a refresh is requested |
||||
|
with self.fetch_url_lock: |
||||
|
print(f"Fetching URL: {url}") |
||||
|
response = requests.get(url) |
||||
|
if response.status_code == 200: |
||||
|
content = response.text |
||||
|
self.set(cache_key, { |
||||
|
'content': content, |
||||
|
'timestamp': time.time() |
||||
|
}, expire=expiry) |
||||
|
time.sleep(0.25) # Throttle requests to avoid being blocked |
||||
|
return content |
||||
|
return None |
||||
|
|
||||
|
def close(self): |
||||
|
self.cache.close() |
||||
@ -0,0 +1,250 @@ |
|||||
|
pokemon_generations = { |
||||
|
1: {"min": 1, "max": 151}, |
||||
|
2: {"min": 152, "max": 251}, |
||||
|
3: {"min": 252, "max": 386}, |
||||
|
4: {"min": 387, "max": 493}, |
||||
|
5: {"min": 494, "max": 649}, |
||||
|
6: {"min": 650, "max": 721}, |
||||
|
7: {"min": 722, "max": 809}, |
||||
|
8: {"min": 810, "max": 905}, |
||||
|
9: {"min": 906, "max": 1025}, |
||||
|
} |
||||
|
|
||||
|
regional_descriptors = ["kantonian", "johtonian", "hoennian", "sinnohan", "unovan", "kalosian", "alolan", "galarian", "hisuian", "paldean"] |
||||
|
|
||||
|
yellow = { |
||||
|
"Name": "Yellow", |
||||
|
"AltNames": ["Pokemon Yellow", "Pokémon Yellow", "Y"], |
||||
|
"Generation": 1 |
||||
|
} |
||||
|
|
||||
|
red = { |
||||
|
"Name": "Red", |
||||
|
"AltNames": ["Pokemon Red", "Pokémon Red", "R"], |
||||
|
"Generation": 1 |
||||
|
} |
||||
|
|
||||
|
blue = { |
||||
|
"Name": "Blue", |
||||
|
"AltNames": ["Pokemon Blue", "Pokémon Blue", "B"], |
||||
|
"Generation": 1 |
||||
|
} |
||||
|
|
||||
|
crystal = { |
||||
|
"Name": "Crystal", |
||||
|
"AltNames": ["Pokemon Crystal", "Pokémon Crystal", "C"], |
||||
|
"Generation": 2 |
||||
|
} |
||||
|
|
||||
|
gold = { |
||||
|
"Name": "Gold", |
||||
|
"AltNames": ["Pokemon Gold", "Pokémon Gold", "G"], |
||||
|
"Generation": 2 |
||||
|
} |
||||
|
|
||||
|
silver = { |
||||
|
"Name": "Silver", |
||||
|
"AltNames": ["Pokemon Silver", "Pokémon Silver", "S"], |
||||
|
"Generation": 2 |
||||
|
} |
||||
|
|
||||
|
emerald = { |
||||
|
"Name": "Emerald", |
||||
|
"AltNames": ["Pokemon Emerald", "Pokémon Emerald", "E"], |
||||
|
"Generation": 3 |
||||
|
} |
||||
|
|
||||
|
fire_red = { |
||||
|
"Name": "FireRed", |
||||
|
"AltNames": ["Pokemon FireRed", "Pokémon FireRed", "FR"], |
||||
|
"Generation": 3 |
||||
|
} |
||||
|
|
||||
|
leaf_green = { |
||||
|
"Name": "LeafGreen", |
||||
|
"AltNames": ["Pokemon LeafGreen", "Pokémon LeafGreen", "LG"], |
||||
|
"Generation": 3 |
||||
|
} |
||||
|
|
||||
|
ruby = { |
||||
|
"Name": "Ruby", |
||||
|
"AltNames": ["Pokemon Ruby", "Pokémon Ruby", "R"], |
||||
|
"Generation": 3 |
||||
|
} |
||||
|
|
||||
|
sapphire = { |
||||
|
"Name": "Sapphire", |
||||
|
"AltNames": ["Pokemon Sapphire", "Pokémon Sapphire", "S"], |
||||
|
"Generation": 3 |
||||
|
} |
||||
|
|
||||
|
platinum = { |
||||
|
"Name": "Platinum", |
||||
|
"AltNames": ["Pokemon Platinum", "Pokémon Platinum", "Pt"], |
||||
|
"Generation": 4 |
||||
|
} |
||||
|
|
||||
|
heart_gold = { |
||||
|
"Name": "HeartGold", |
||||
|
"AltNames": ["Pokemon HeartGold", "Pokémon HeartGold", "HG"], |
||||
|
"Generation": 4 |
||||
|
} |
||||
|
|
||||
|
soul_silver = { |
||||
|
"Name": "SoulSilver", |
||||
|
"AltNames": ["Pokemon SoulSilver", "Pokémon SoulSilver", "SS"], |
||||
|
"Generation": 4 |
||||
|
} |
||||
|
|
||||
|
diamond = { |
||||
|
"Name": "Diamond", |
||||
|
"AltNames": ["Pokemon Diamond", "Pokémon Diamond", "D"], |
||||
|
"Generation": 4 |
||||
|
} |
||||
|
|
||||
|
pearl = { |
||||
|
"Name": "Pearl", |
||||
|
"AltNames": ["Pokemon Pearl", "Pokémon Pearl", "P"], |
||||
|
"Generation": 4 |
||||
|
} |
||||
|
|
||||
|
black = { |
||||
|
"Name": "Black", |
||||
|
"AltNames": ["Pokemon Black", "Pokémon Black", "B"], |
||||
|
"Generation": 5 |
||||
|
} |
||||
|
|
||||
|
white = { |
||||
|
"Name": "White", |
||||
|
"AltNames": ["Pokemon White", "Pokémon White", "W"], |
||||
|
"Generation": 5 |
||||
|
} |
||||
|
|
||||
|
black_2 = { |
||||
|
"Name": "Black 2", |
||||
|
"AltNames": ["Pokemon Black 2", "Pokémon Black 2", "B2"], |
||||
|
"Generation": 5 |
||||
|
} |
||||
|
|
||||
|
white_2 = { |
||||
|
"Name": "White 2", |
||||
|
"AltNames": ["Pokemon White 2", "Pokémon White 2", "W2"], |
||||
|
"Generation": 5 |
||||
|
} |
||||
|
|
||||
|
x = { |
||||
|
"Name": "X", |
||||
|
"AltNames": ["Pokemon X", "Pokémon X"], |
||||
|
"Generation": 6 |
||||
|
} |
||||
|
|
||||
|
y = { |
||||
|
"Name": "Y", |
||||
|
"AltNames": ["Pokemon Y", "Pokémon Y"], |
||||
|
"Generation": 6 |
||||
|
} |
||||
|
|
||||
|
omega_ruby = { |
||||
|
"Name": "Omega Ruby", |
||||
|
"AltNames": ["Pokemon Omega Ruby", "Pokémon Omega Ruby", "OR"], |
||||
|
"Generation": 6 |
||||
|
} |
||||
|
|
||||
|
alpha_sapphire = { |
||||
|
"Name": "Alpha Sapphire", |
||||
|
"AltNames": ["Pokemon Alpha Sapphire", "Pokémon Alpha Sapphire", "AS"], |
||||
|
"Generation": 6 |
||||
|
} |
||||
|
|
||||
|
sun = { |
||||
|
"Name": "Sun", |
||||
|
"AltNames": ["Pokemon Sun", "Pokémon Sun"], |
||||
|
"Generation": 7 |
||||
|
} |
||||
|
|
||||
|
moon = { |
||||
|
"Name": "Moon", |
||||
|
"AltNames": ["Pokemon Moon", "Pokémon Moon"], |
||||
|
"Generation": 7 |
||||
|
} |
||||
|
|
||||
|
ultra_sun = { |
||||
|
"Name": "Ultra Sun", |
||||
|
"AltNames": ["Pokemon Ultra Sun", "Pokémon Ultra Sun", "US"], |
||||
|
"Generation": 7 |
||||
|
} |
||||
|
|
||||
|
ultra_moon = { |
||||
|
"Name": "Ultra Moon", |
||||
|
"AltNames": ["Pokemon Ultra Moon", "Pokémon Ultra Moon", "UM"], |
||||
|
"Generation": 7 |
||||
|
} |
||||
|
|
||||
|
sword = { |
||||
|
"Name": "Sword", |
||||
|
"AltNames": ["Pokemon Sword", "Pokémon Sword", "Expansion Pass", "Expansion Pass (Sword)"], |
||||
|
"Generation": 8 |
||||
|
} |
||||
|
|
||||
|
shield = { |
||||
|
"Name": "Shield", |
||||
|
"AltNames": ["Pokemon Shield", "Pokémon Shield", "Expansion Pass", "Expansion Pass (Shield)"], |
||||
|
"Generation": 8 |
||||
|
} |
||||
|
|
||||
|
brilliant_diamond = { |
||||
|
"Name": "Brilliant Diamond", |
||||
|
"AltNames": ["Pokemon Brilliant Diamond", "Pokémon Brilliant Diamond", "BD"], |
||||
|
"Generation": 8 |
||||
|
} |
||||
|
|
||||
|
shining_pearl = { |
||||
|
"Name": "Shining Pearl", |
||||
|
"AltNames": ["Pokemon Shining Pearl", "Pokémon Shining Pearl", "SP"], |
||||
|
"Generation": 8 |
||||
|
} |
||||
|
|
||||
|
legends_arceus = { |
||||
|
"Name": "Legends: Arceus", |
||||
|
"AltNames": ["Pokemon Legends: Arceus", "Pokémon Legends: Arceus", "LA", "Legends Arceus", "Arceus"], |
||||
|
"Generation": 8 |
||||
|
} |
||||
|
|
||||
|
scarlet = { |
||||
|
"Name": "Scarlet", |
||||
|
"AltNames": ["Pokemon Scarlet", "Pokémon Scarlet", "The Hidden Treasure of Area Zero", "The Hidden Treasure of Area Zero (Scarlet)", "The Teal Mask", "The Teal Mask (Scarlet)"], |
||||
|
"Generation": 9 |
||||
|
} |
||||
|
|
||||
|
violet = { |
||||
|
"Name": "Violet", |
||||
|
"AltNames": ["Pokemon Violet", "Pokémon Violet", "The Hidden Treasure of Area Zero", "The Hidden Treasure of Area Zero (Violet)", "The Teal Mask", "The Teal Mask (Violet)"], |
||||
|
"Generation": 9 |
||||
|
} |
||||
|
|
||||
|
lets_go_pikachu = { |
||||
|
"Name": "Lets Go Pikachu", |
||||
|
"AltNames": [], |
||||
|
"Generation": 8 |
||||
|
} |
||||
|
|
||||
|
lets_go_eevee = { |
||||
|
"Name": "Lets Go Eevee", |
||||
|
"AltNames": [], |
||||
|
"Generation": 8 |
||||
|
} |
||||
|
|
||||
|
main_line_games = [ |
||||
|
yellow, red, blue, |
||||
|
crystal, gold, silver, |
||||
|
emerald, fire_red, leaf_green, ruby, sapphire, |
||||
|
platinum, heart_gold, soul_silver, diamond, pearl, |
||||
|
black_2, white_2, black, white, |
||||
|
x, y, omega_ruby, alpha_sapphire, |
||||
|
ultra_sun, ultra_moon, sun, moon, lets_go_pikachu, lets_go_eevee, |
||||
|
sword, shield, |
||||
|
brilliant_diamond, shining_pearl, |
||||
|
legends_arceus, |
||||
|
scarlet, violet, |
||||
|
] |
||||
|
|
||||
@ -0,0 +1,48 @@ |
|||||
|
from .data import pokemon_generations, main_line_games |
||||
|
import unicodedata |
||||
|
|
||||
|
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}" |
||||
|
|
||||
|
def compare_pokemon_forms(a, b): |
||||
|
if a == None or b == None: |
||||
|
return False |
||||
|
|
||||
|
if a == b: |
||||
|
return True |
||||
|
|
||||
|
temp_a = a.lower().replace("forme", "").replace("form", "").replace("é", "e").strip() |
||||
|
temp_b = b.lower().replace("forme", "").replace("form", "").replace("é", "e").strip() |
||||
|
|
||||
|
# Common spelling mistakes |
||||
|
temp_a = temp_a.replace("deputante", "debutante").replace("p'au", "pa'u").replace("blood moon", "bloodmoon") |
||||
|
temp_b = temp_b.replace("deputante", "debutante").replace("p'au", "pa'u").replace("blood moon", "bloodmoon") |
||||
|
|
||||
|
if temp_a == temp_b: |
||||
|
return True |
||||
|
|
||||
|
return False |
||||
|
|
||||
|
def get_generation_from_national_dex(national_dex_number): |
||||
|
generation = 1 |
||||
|
for gen in pokemon_generations: |
||||
|
if pokemon_generations[gen]["min"] <= national_dex_number <= pokemon_generations[gen]["max"]: |
||||
|
generation = gen |
||||
|
break |
||||
|
return generation |
||||
|
|
||||
|
def sanitise_pokemon_name_for_url(pokemon_name): |
||||
|
pokemon_url_name = pokemon_name.replace("♀", "-f").replace("♂", "-m").replace("'", "").replace(".", "").replace('é', 'e').replace(':', '') |
||||
|
pokemon_url_name = pokemon_url_name.replace(" ", "-") |
||||
|
return pokemon_url_name |
||||
|
|
||||
|
def remove_accents(input_str): |
||||
|
nfkd_form = unicodedata.normalize('NFKD', input_str) |
||||
|
return u"".join([c for c in nfkd_form if not unicodedata.combining(c)]) |
||||
|
|
||||
|
def find_game_generation(game_name: str) -> int: |
||||
|
game_name = game_name.lower() |
||||
|
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 |
||||
Loading…
Reference in new issue