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