From 19b90bb4c78d6c865f92b0fad413fae10e9d3ee6 Mon Sep 17 00:00:00 2001 From: Michel Bats Date: Sun, 25 May 2025 18:21:30 +0200 Subject: [PATCH] Eerste commit --- .gitea/workflows/update.yaml | 33 +++ requirements.txt | 1 + update.py | 416 +++++++++++++++++++++++++++++++++++ 3 files changed, 450 insertions(+) create mode 100644 .gitea/workflows/update.yaml create mode 100644 requirements.txt create mode 100755 update.py diff --git a/.gitea/workflows/update.yaml b/.gitea/workflows/update.yaml new file mode 100644 index 0000000..30aa580 --- /dev/null +++ b/.gitea/workflows/update.yaml @@ -0,0 +1,33 @@ +name: update gameset_database.json + +on: + push: + schedule: + - cron: "0 0 1 */1 *" + +jobs: + Check_for_update: + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v4 + - name: install python3-pip + run: | + apt update + apt install python3-pip -y + - name: install requirements + run: | + pip3 install -r requirements.txt + - name: update database + run: | + python3 update.py + - name: git commit + run: | + git config user.name "${{ secrets.GIT_USERNAME }}" + git config user.email "${{ secrets.GIT_USEREMAIL }}" + mtime=$(stat -c "%y" ./gameset_database.json) + git add . + export GIT_COMMITTER_DATE=$mtime + export GIT_AUTHOR_DATE=$mtime + version=$(date -d "${mtime}" +%Y-%m-%d) + if [ -n "$(git diff --staged)" ]; then git commit --date "$mtime" -m "update $version" && git push; fi diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..663bd1f --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +requests \ No newline at end of file diff --git a/update.py b/update.py new file mode 100755 index 0000000..28e7583 --- /dev/null +++ b/update.py @@ -0,0 +1,416 @@ +# +# 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": "festival", + "gameset_language": "Netherlands", + "gameset_name": "Festival", + "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": "nineties", + "gameset_language": "Netherlands", + "gameset_name": "Nineties en Zeroes", + "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." + )