441 lines
18 KiB
Python
Executable File
441 lines
18 KiB
Python
Executable File
#
|
|
# update_gameset_database.py
|
|
#
|
|
# Auteur: Michel Bats (BatsSoft)
|
|
# Gemaakt met: Gemini 2.5 Flash
|
|
# Versie: 2025.5.0
|
|
#
|
|
# Beschrijving:
|
|
# Dit script is ontworpen om Gameset-informatie, inclusief Spotify Track ID's, te verzamelen
|
|
# van specifieke pagina's op joragames.nl. Het verwerkt een gedefinieerde lijst van gamesets
|
|
# (SKU's), haalt de relevante gegevens op, en slaat deze op in een lokaal JSON-bestand
|
|
# (gameset_database.json). Het script zorgt ervoor dat de 'updated_on' timestamp alleen
|
|
# wordt bijgewerkt wanneer er daadwerkelijk een inhoudelijke wijziging in de database is.
|
|
# Bovendien is er een kleine vertraging ingebouwd tussen HTTP-requests om overbelasting
|
|
# van de server te voorkomen en het risico op blokkering te minimaliseren, wat essentieel is
|
|
# voor een verantwoord gebruik in een productieomgeving.
|
|
#
|
|
# Gebruik:
|
|
# Voer het script uit via de commandoregel:
|
|
# python3 update_gameset_database.py
|
|
#
|
|
# Vereisten:
|
|
# - Python 3.5+ (compatibel gemaakt voor Python 3.9)
|
|
# - De 'requests' bibliotheek (installeer met: pip install requests)
|
|
#
|
|
# Output:
|
|
# - gameset_database.json: Een JSON-bestand met de gestructureerde gameset-data,
|
|
# inclusief gameset_name, gameset_language en Spotify Track ID's voor elke kaart.
|
|
#
|
|
# Afhankelijkheden:
|
|
# - requests: Voor het uitvoeren van HTTP-requests.
|
|
# - re: Voor reguliere expressies om data uit HTML te extraheren.
|
|
# - json: Voor het lezen en schrijven van JSON-bestanden.
|
|
# - os: Voor bestandssysteembewerkingen (controleren of een bestand bestaat).
|
|
# - time: Voor het implementeren van vertragingen.
|
|
# - typing.Union: Voor type hints in Python < 3.10.
|
|
#
|
|
|
|
import requests
|
|
import re
|
|
import json
|
|
import os
|
|
import time
|
|
from typing import Union
|
|
|
|
|
|
def get_spotify_track_id(url: str) -> Union[tuple, None]:
|
|
"""
|
|
Laadt een URL in en extraheert een Spotify Track ID met behulp van een reguliere expressie.
|
|
Extraheert tevens de SKU en het CardNumber direct uit de URL.
|
|
|
|
Args:
|
|
url (str): De volledige URL van de kaartpagina, bijv. "https://joragames.nl/nineties/00040".
|
|
|
|
Returns:
|
|
Union[tuple, None]: Een tuple (spotify_id, sku, card_number) als de ID en URL-delen worden gevonden.
|
|
Retourneert None als de ID niet wordt gevonden, de URL ongeldig is, of bij een netwerkfout.
|
|
"""
|
|
try:
|
|
# Verstuur een GET-request naar de URL met een timeout van 10 seconden.
|
|
# Een timeout voorkomt dat het script oneindig blijft hangen bij unresponsive servers.
|
|
response = requests.get(url, timeout=10)
|
|
|
|
# Controleer de HTTP-statuscode. Gooit een uitzondering voor 4xx (client error) of 5xx (server error).
|
|
response.raise_for_status()
|
|
|
|
html_content = response.text
|
|
|
|
# Regex om de Spotify Track ID te vinden in de JavaScript redirect.
|
|
# Zoekt naar 'window.location = "spotify:track:<ID>"'.
|
|
spotify_regex = r'window\.location\s*=\s*"spotify:track:([a-zA-Z0-9]+)"'
|
|
spotify_match = re.search(spotify_regex, html_content)
|
|
|
|
if spotify_match:
|
|
spotify_id = spotify_match.group(
|
|
1
|
|
) # Extraheer de gevangen groep (de ID zelf)
|
|
|
|
# Regex om SKU en CardNumber uit de URL te extraheren.
|
|
# Verwacht URL-formaat: https://joragames.nl/<sku>/<card_number>
|
|
url_parts_regex = r"https://joragames\.nl/([a-zA-Z0-9_-]+)/(\d+)"
|
|
url_match = re.search(url_parts_regex, url)
|
|
|
|
if url_match:
|
|
sku = url_match.group(1)
|
|
# Vul het kaartnummer aan met voorloopnullen tot 5 cijfers (bijv. '40' wordt '00040').
|
|
card_number = url_match.group(2).zfill(5)
|
|
return spotify_id, sku, card_number
|
|
else:
|
|
# Dit zou zelden moeten voorkomen als de URL-structuur consistent is.
|
|
print(
|
|
f"Waarschuwing: Kon SKU en CardNumber niet extraheren uit URL: {url}"
|
|
)
|
|
return None
|
|
else:
|
|
# Geen Spotify Track ID gevonden op deze pagina.
|
|
return None
|
|
except requests.exceptions.RequestException as e:
|
|
# Vang specifieke request exceptions op (bijv. verbindingsproblemen, DNS-fouten, timeouts).
|
|
print(f"Fout bij het laden van de URL '{url}': {e}")
|
|
return None
|
|
except Exception as e:
|
|
# Vang alle andere onverwachte fouten op die kunnen optreden.
|
|
print(f"Een onverwachte fout is opgetreden bij URL '{url}': {e}")
|
|
return None
|
|
|
|
|
|
def update_gameset_database(
|
|
spotify_data: tuple,
|
|
gameset_name_from_config: str,
|
|
gameset_language_from_config: str,
|
|
db_file: str = "gameset_database.json",
|
|
):
|
|
"""
|
|
Werkt het gameset_database.json-bestand bij met de gevonden Spotify-gegevens.
|
|
De 'updated_on' timestamp van de database wordt alleen bijgewerkt als er daadwerkelijk
|
|
een inhoudelijke wijziging is geweest (nieuwe kaart, gewijzigde Spotify ID, gewijzigde naam/taal).
|
|
|
|
Args:
|
|
spotify_data (tuple): Een tuple (spotify_id, sku, card_number) met de geëxtraheerde gegevens.
|
|
gameset_name_from_config (str): De naam van de gameset, zoals gedefinieerd in gamesets_config.
|
|
gameset_language_from_config (str): De taal van de gameset, zoals gedefinieerd in gamesets_config.
|
|
db_file (str, optional): De naam van het JSON-databasebestand. Standaard is "gameset_database.json".
|
|
"""
|
|
if not spotify_data:
|
|
# Geen geldige Spotify-gegevens om te verwerken.
|
|
return
|
|
|
|
spotify_id, sku, card_number = spotify_data
|
|
|
|
# Initialiseer de database structuur. updated_on krijgt een tijdelijke waarde.
|
|
database = {"updated_on": int(time.time()), "gamesets": []}
|
|
original_database_content = None # Slaat de string-representatie van de originele JSON op voor vergelijking.
|
|
|
|
# Probeer bestaande data in te lezen als het databasebestand bestaat.
|
|
if os.path.exists(db_file):
|
|
try:
|
|
with open(db_file, "r", encoding="utf-8") as f:
|
|
existing_data = json.load(f)
|
|
# Maak een gesorteerde JSON-string van de bestaande data voor accurate vergelijking.
|
|
original_database_content = json.dumps(
|
|
existing_data, sort_keys=True, indent=4, ensure_ascii=False
|
|
)
|
|
|
|
if "gamesets" in existing_data:
|
|
database["gamesets"] = existing_data["gamesets"]
|
|
if "updated_on" in existing_data:
|
|
# Behoud de bestaande 'updated_on' timestamp; deze wordt alleen bijgewerkt bij echte wijzigingen.
|
|
database["updated_on"] = existing_data["updated_on"]
|
|
except json.JSONDecodeError:
|
|
print(
|
|
f"Waarschuwing: Het bestand '{db_file}' is ongeldig of leeg. Begint met een lege database."
|
|
)
|
|
except Exception as e:
|
|
print(
|
|
f"Fout bij het lezen van '{db_file}': {e}. Begint met een lege database."
|
|
)
|
|
|
|
# Vlag om bij te houden of er inhoudelijke wijzigingen zijn aangebracht die een update rechtvaardigen.
|
|
is_modified = False
|
|
|
|
# Zoek de gameset op basis van SKU of maak een nieuwe aan als deze nog niet bestaat.
|
|
found_gameset = False
|
|
for gameset in database["gamesets"]:
|
|
if gameset["sku"] == sku:
|
|
found_gameset = True
|
|
|
|
# Controleer en update gameset_name indien nodig.
|
|
if (
|
|
gameset["gameset_data"].get("gameset_name", "")
|
|
!= gameset_name_from_config
|
|
):
|
|
gameset["gameset_data"]["gameset_name"] = gameset_name_from_config
|
|
is_modified = True
|
|
print(
|
|
f"Gameset '{sku}' naam bijgewerkt naar: '{gameset_name_from_config}'"
|
|
)
|
|
|
|
# Controleer en update gameset_language indien nodig.
|
|
# Standaardwaarde 'Netherlands' wordt gebruikt voor vergelijking als de sleutel ontbreekt.
|
|
if (
|
|
gameset["gameset_data"].get("gameset_language", "Netherlands")
|
|
!= gameset_language_from_config
|
|
):
|
|
gameset["gameset_data"][
|
|
"gameset_language"
|
|
] = gameset_language_from_config
|
|
is_modified = True
|
|
print(
|
|
f"Gameset '{sku}' taal bijgewerkt naar: '{gameset_language_from_config}'"
|
|
)
|
|
|
|
# Zoek de specifieke kaart binnen de gameset of voeg deze toe.
|
|
found_card = False
|
|
for card in gameset["gameset_data"]["cards"]:
|
|
if card["CardNumber"] == card_number:
|
|
if (
|
|
card["Spotify"] != spotify_id
|
|
): # Controleer of de Spotify ID daadwerkelijk is gewijzigd.
|
|
card["Spotify"] = spotify_id # Update de Spotify ID.
|
|
is_modified = True
|
|
print(
|
|
f"Kaart {card_number} voor SKU '{sku}' bijgewerkt met NIEUWE Spotify ID: {spotify_id}"
|
|
)
|
|
else:
|
|
print(
|
|
f"Kaart {card_number} voor SKU '{sku}' had al Spotify ID: {spotify_id} (geen wijziging nodig)."
|
|
)
|
|
found_card = True
|
|
break
|
|
if not found_card:
|
|
# Voeg een nieuwe kaart toe aan de gameset.
|
|
gameset["gameset_data"]["cards"].append(
|
|
{"CardNumber": card_number, "Spotify": spotify_id}
|
|
)
|
|
is_modified = True
|
|
print(
|
|
f"Nieuwe kaart {card_number} toegevoegd aan SKU '{sku}' met Spotify ID: {spotify_id}"
|
|
)
|
|
break
|
|
|
|
if not found_gameset:
|
|
# Als de gameset (SKU) nog niet bestaat, maak een volledig nieuwe gameset-structuur aan.
|
|
new_gameset = {
|
|
"sku": sku,
|
|
"gameset_data": {
|
|
"gameset_language": gameset_language_from_config,
|
|
"gameset_name": gameset_name_from_config,
|
|
"cards": [{"CardNumber": card_number, "Spotify": spotify_id}],
|
|
},
|
|
}
|
|
database["gamesets"].append(new_gameset)
|
|
is_modified = True
|
|
print(
|
|
f"Nieuwe gameset '{sku}' (naam: '{gameset_name_from_config}', taal: '{gameset_language_from_config}') en kaart {card_number} toegevoegd met Spotify ID: {spotify_id}"
|
|
)
|
|
|
|
# Sorteer de gamesets op 'sku' voor consistentie in het JSON-bestand.
|
|
database["gamesets"].sort(key=lambda x: x["sku"])
|
|
|
|
# Sorteer de kaarten binnen elke gameset op 'CardNumber'.
|
|
for gameset in database["gamesets"]:
|
|
if "cards" in gameset["gameset_data"] and gameset["gameset_data"]["cards"]:
|
|
gameset["gameset_data"]["cards"].sort(key=lambda x: x["CardNumber"])
|
|
|
|
# Genereer de gesorteerde JSON-string van de actuele databasestatus voor vergelijking.
|
|
current_database_content = json.dumps(
|
|
database, sort_keys=True, indent=4, ensure_ascii=False
|
|
)
|
|
|
|
# Bepaal of de 'updated_on' timestamp moet worden bijgewerkt.
|
|
# Dit gebeurt als:
|
|
# 1. Er een inhoudelijke wijziging is gedetecteerd (is_modified is True).
|
|
# 2. Het databasebestand voorheen leeg was (of niet bestond) en nu gevuld is.
|
|
# 3. De gesorteerde inhoud van de database is veranderd, zelfs als geen specifieke 'is_modified' vlag is getriggerd
|
|
# (bijv. door een wijziging in de sortering van bestaande data die eerder niet correct was).
|
|
if (
|
|
is_modified
|
|
or (original_database_content is None and database["gamesets"])
|
|
or (original_database_content != current_database_content)
|
|
):
|
|
database["updated_on"] = int(
|
|
time.time()
|
|
) # Werk timestamp bij naar de huidige tijd.
|
|
print(
|
|
f"Database is inhoudelijk gewijzigd of de structuur is geoptimaliseerd. 'updated_on' timestamp bijgewerkt naar {database['updated_on']}."
|
|
)
|
|
else:
|
|
print(
|
|
"Geen inhoudelijke wijziging in de database. 'updated_on' timestamp blijft ongewijzigd."
|
|
)
|
|
|
|
# Schrijf de (mogelijk bijgewerkte) database terug naar het bestand.
|
|
try:
|
|
with open(db_file, "w", encoding="utf-8") as f:
|
|
json.dump(database, f, indent=4, ensure_ascii=False)
|
|
print(f"Database opgeslagen in '{db_file}'.")
|
|
except Exception as e:
|
|
print(f"Fout bij het schrijven naar '{db_file}': {e}")
|
|
|
|
|
|
# --- Hoofdgedeelte van het script ---
|
|
if __name__ == "__main__":
|
|
# Definieer de gamesets die gescand moeten worden.
|
|
# Elke dictionary bevat:
|
|
# - "sku": De unieke identifier voor de gameset (bijv. "kerst").
|
|
# - "gameset_language": De taal van de gameset (bijv. "Netherlands").
|
|
# - "gameset_name": Een leesbare naam voor de gameset (bijv. "Kerst").
|
|
# - "total_cards": Het totale aantal kaarten in deze gameset. (bijv. 150)
|
|
# Belangrijk: Alle hier gedefinieerde sleutels zijn verplicht voor een correcte werking,
|
|
# conform de laatste specificaties.
|
|
gamesets_config = [
|
|
{
|
|
"sku": "apres-ski",
|
|
"gameset_language": "Netherlands",
|
|
"gameset_name": "Après-ski",
|
|
"total_cards": 150,
|
|
},
|
|
{
|
|
"sku": "cover",
|
|
"gameset_language": "Netherlands",
|
|
"gameset_name": "Cover of Original",
|
|
"total_cards": 150,
|
|
},
|
|
{
|
|
"sku": "disco",
|
|
"gameset_language": "Netherlands",
|
|
"gameset_name": "Disco",
|
|
"total_cards": 150,
|
|
},
|
|
{
|
|
"sku": "festival",
|
|
"gameset_language": "Netherlands",
|
|
"gameset_name": "Festival",
|
|
"total_cards": 150,
|
|
},
|
|
{
|
|
"sku": "fifties",
|
|
"gameset_language": "Netherlands",
|
|
"gameset_name": "Fifties en Sixties",
|
|
"total_cards": 150,
|
|
},
|
|
{
|
|
"sku": "films",
|
|
"gameset_language": "Netherlands",
|
|
"gameset_name": "Films en Series",
|
|
"total_cards": 150,
|
|
},
|
|
{
|
|
"sku": "hardstyle",
|
|
"gameset_language": "Netherlands",
|
|
"gameset_name": "Hardstyle",
|
|
"total_cards": 150,
|
|
},
|
|
{
|
|
"sku": "hollands",
|
|
"gameset_language": "Netherlands",
|
|
"gameset_name": "Hollandse Hits",
|
|
"total_cards": 150,
|
|
},
|
|
{
|
|
"sku": "kerst",
|
|
"gameset_language": "Netherlands",
|
|
"gameset_name": "Kerst",
|
|
"total_cards": 150,
|
|
},
|
|
{
|
|
"sku": "mix",
|
|
"gameset_language": "Netherlands",
|
|
"gameset_name": "Mix",
|
|
"total_cards": 150,
|
|
},
|
|
{
|
|
"sku": "nederhop",
|
|
"gameset_language": "Netherlands",
|
|
"gameset_name": "Nederlandse Hiphop",
|
|
"total_cards": 150,
|
|
},
|
|
{
|
|
"sku": "nineties",
|
|
"gameset_language": "Netherlands",
|
|
"gameset_name": "Nineties en Zeroes",
|
|
"total_cards": 150,
|
|
},
|
|
{
|
|
"sku": "piraat",
|
|
"gameset_language": "Netherlands",
|
|
"gameset_name": "Piratenhits",
|
|
"total_cards": 150,
|
|
},
|
|
{
|
|
"sku": "rock",
|
|
"gameset_language": "Netherlands",
|
|
"gameset_name": "Rock",
|
|
"total_cards": 150,
|
|
},
|
|
{
|
|
"sku": "seventies",
|
|
"gameset_language": "Netherlands",
|
|
"gameset_name": "Seventies en Eighties",
|
|
"total_cards": 150,
|
|
},
|
|
{
|
|
"sku": "songfestival",
|
|
"gameset_language": "Netherlands",
|
|
"gameset_name": "Songfestival",
|
|
"total_cards": 150,
|
|
},
|
|
{
|
|
"sku": "tens",
|
|
"gameset_language": "Netherlands",
|
|
"gameset_name": "Tens en Twenties",
|
|
"total_cards": 150,
|
|
},
|
|
]
|
|
|
|
# De basis-URL voor de gameset-pagina's. SKU en CardNumber worden hierin dynamisch ingevuld.
|
|
base_url = "https://joragames.nl/{sku}/{card_number}"
|
|
|
|
# Loop door elke gameset die gedefinieerd is in de configuratie.
|
|
for gameset_info in gamesets_config:
|
|
sku = gameset_info["sku"]
|
|
total_cards = gameset_info["total_cards"]
|
|
gameset_name = gameset_info["gameset_name"]
|
|
gameset_language = gameset_info["gameset_language"]
|
|
|
|
print(
|
|
f"\n--- Start verwerking voor gameset: {sku} (Naam: '{gameset_name}', Taal: '{gameset_language}', {total_cards} kaarten) ---"
|
|
)
|
|
|
|
# Loop door elk kaartnummer binnen de huidige gameset.
|
|
for i in range(1, total_cards + 1):
|
|
card_number_str = str(
|
|
i
|
|
) # Converteer het kaartnummer naar een string voor de URL.
|
|
url = base_url.format(sku=sku, card_number=card_number_str)
|
|
|
|
# Implementeer een kleine vertraging om de server niet te overbelasten en
|
|
# om geblokkeerd te worden door anti-scraping mechanismen.
|
|
time.sleep(0.1) # 100 milliseconden vertraging. Pas dit aan indien nodig.
|
|
|
|
# Haal de Spotify Track ID en gerelateerde informatie op van de huidige URL.
|
|
extracted_data = get_spotify_track_id(url)
|
|
|
|
if extracted_data:
|
|
update_gameset_database(extracted_data, gameset_name, gameset_language)
|
|
else:
|
|
pass
|
|
|
|
print(f"--- Einde verwerking voor gameset: {sku} ---")
|
|
|
|
# Finalisatie: controleer of het databasebestand is aangemaakt of bijgewerkt.
|
|
if os.path.exists("gameset_database.json"):
|
|
print(
|
|
"\n'gameset_database.json' is aanwezig en de 'updated_on' timestamp is indien nodig bijgewerkt."
|
|
)
|
|
else:
|
|
print(
|
|
"\nWaarschuwing: 'gameset_database.json' is niet aangemaakt. Controleer of er data is verwerkt en/of er fouten zijn opgetreden."
|
|
)
|