# # 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:"'. 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// 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." )