From 25d597cd7f531845a4f7a02f45733cb001913ae5 Mon Sep 17 00:00:00 2001 From: Dayonixe Date: Sun, 16 Nov 2025 12:15:10 +0100 Subject: [PATCH] Added estimated game duration --- README.md | 18 +++++--- src/.env.example | 3 +- src/export_data.py | 100 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 4f24734..4c771b9 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,14 @@ Team : Théo Pirouelle laguage-python +![TestsResult](https://github.com/Dayonixe/SteamToNotion/actions/workflows/python-tests.yml/badge.svg) + --- ## Preamble To use the script, you'll need a Notion API key and a Notion database ID. -You'll need to create a `.env` file containing the variables `NOTION_TOKEN` and `NOTION_DATABASE_ID` with your API key and database ID for the script to work correctly. +You'll need to create a `.env` file containing the variables `NOTION_TOKEN`, `NOTION_DATABASE_ID` and `RAWG_API_KEY` with your API key and database ID for the script to work correctly. > [!IMPORTANT] > To use the application, you need to be connected to the internet so that it can call the Steam and Notion APIs. @@ -26,6 +28,7 @@ You'll need to create a `.env` file containing the variables `NOTION_TOKEN` and > | Library | Version | > | --- | --- | > | requests | 2.32.5 | +> | pytest | 9.0.1 | Please also remember to install Python. The code was developed and works with Python 3.11. @@ -36,15 +39,18 @@ Please also remember to install Python. The code was developed and works with Py In the Notion database configured in the `.env`, the following properties are required for the script to work: - Platform (select): For the page to be processed, the selected platform must be ‘Steam’. - ID (number): Similarly, for the page to be processed, there must be a game ID (or at least the game title as the page title). -- Price (number) - Released (checkbox) +- Release date (date) - Genres (multi_select) +- Price (number) +- Estimated duration (number) - Metacritic (number) -- Release date (date) To easily find the game ID, simply go to the game's page in the Steam store and retrieve the ID from the page's URL. For example, in the URL `https://store.steampowered.com/app/3097560/Liars_Bar/`, the game ID is `3097560`. +It is important to note that the estimated duration of a game is calculated in the script because there is no API that returns the real duration of a game. The value is therefore extracted from RAWG.io, and then, based on the tags and genres of the game, a multiplier ratio calculation is applied to this value. + To run the script in a Linux or PowerShell terminal: ```bash python[3] [path/to/]export_data.py @@ -53,17 +59,17 @@ python[3] [path/to/]export_data.py The script will run and you should see the following lines displayed, for example: ``` 🔍 Récupération Steam pour app_id = 1172620 -📥 Données récupérées : {'app_id': 1172620, 'name': 'Sea of Thieves: 2025 Edition', 'price': 39.99, 'released': True, 'release_date': '2020-06-03', 'genres': ['Action', 'Adventure'], 'metacritic_score': None, 'cover_image': 'https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/1172620/library_hero.jpg', 'icon_image': 'https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/1172620/logo.png'} +📥 Données récupérées : {'app_id': 1172620, 'name': 'Sea of Thieves: 2025 Edition', 'price': 39.99, 'released': True, 'release_date': '2020-06-03', 'genres': ['Action', 'Adventure'], 'hltb_time': None, 'metacritic_score': None, 'cover_image': 'https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/1172620/library_hero.jpg', 'icon_image': 'https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/1172620/logo.png'} ✅ Page mise à jour : xxxxxxxxxxxxxxxx 🔍 Récupération Steam pour app_id = 1203180 -📥 Données récupérées : {'app_id': 1203180, 'name': 'Breakwaters: Crystal Tides', 'price': 16.79, 'released': True, 'release_date': '2021-12-09', 'genres': ['Action', 'Adventure', 'Indie', 'Simulation', 'Early Access'], 'metacritic_score': None, 'cover_image': 'https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/1203180/library_hero.jpg', 'icon_image': 'https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/1203180/logo.png'} +📥 Données récupérées : {'app_id': 1203180, 'name': 'Breakwaters: Crystal Tides', 'price': 16.79, 'released': True, 'release_date': '2021-12-09', 'genres': ['Action', 'Adventure', 'Indie', 'Simulation', 'Early Access'], 'hltb_time': 13.2, 'metacritic_score': None, 'cover_image': 'https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/1203180/library_hero.jpg', 'icon_image': 'https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/1203180/logo.png'} ✅ Page mise à jour : xxxxxxxxxxxxxxxx ⏭️ Page ignorée (xxxxxxxxxxxxxxxx) — Platform = Epic Games 🔍 Récupération Steam pour app_id = 113200 -📥 Données récupérées : {'app_id': 113200, 'name': 'The Binding of Isaac', 'price': 4.99, 'released': True, 'release_date': '2011-09-28', 'genres': ['Action', 'Adventure', 'Indie', 'RPG'], 'metacritic_score': 84, 'cover_image': 'https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/113200/library_hero.jpg', 'icon_image': 'https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/113200/logo.png'} +📥 Données récupérées : {'app_id': 113200, 'name': 'The Binding of Isaac', 'price': 4.99, 'released': True, 'release_date': '2011-09-28', 'genres': ['Action', 'Adventure', 'Indie', 'RPG'], 'hltb_time': 10.0, 'metacritic_score': 84, 'cover_image': 'https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/113200/library_hero.jpg', 'icon_image': 'https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/113200/logo.png'} ✅ Page mise à jour : xxxxxxxxxxxxxxxx [...] diff --git a/src/.env.example b/src/.env.example index f9accb4..50576b5 100644 --- a/src/.env.example +++ b/src/.env.example @@ -1,2 +1,3 @@ NOTION_TOKEN=secret_xxxxxx -NOTION_DATABASE_ID=xxxxxxxxxxxxxxxx \ No newline at end of file +NOTION_DATABASE_ID=xxxxxxxxxxxxxxxx +RAWG_API_KEY=xxxxxxxxxxxxxxxx \ No newline at end of file diff --git a/src/export_data.py b/src/export_data.py index 51d8488..29045e9 100644 --- a/src/export_data.py +++ b/src/export_data.py @@ -150,6 +150,95 @@ def search_app_id_by_name(name: str): return best_app_id +def get_rawg_data(game_name: str): + """ + Recherche les données d'un jeu dans RAWG + :param game_name: Nom du jeu + :return: playtime, genres, tags + """ + url = ( + f"https://api.rawg.io/api/games" + f"?search={game_name}" + f"&key={RAWG_API_KEY}" + ) + + try: + res = requests.get(url).json() + except Exception as e: + print(f"⚠️ Erreur RAWG : {e}") + return None, [], [] + + if "results" not in res or len(res["results"]) == 0: + print(f"⚠️ No res") + return None, [], [] + + game = res["results"][0] # meilleur match automatique RAWG + + playtime = game.get("playtime") # durée médiane communautaire + + genres = [g["name"] for g in game.get("genres", [])] + tags = [t["name"] for t in game.get("tags", [])] + + return playtime, genres, tags + + +def estimate_hltb_from_rawg(rawg_playtime, genres, tags): + """ + Calcul une estimation de la durée d'un jeu à partir de la durée extraite de RAWG + :param rawg_playtime: Durée du jeu de RAWG + :param genres: Liste des genres du jeu de RAWG + :param tags: Liste des tags du jeu de RAWG + :return: Estimation de la durée + """ + if rawg_playtime is None or rawg_playtime <= 0: + return None + + genres = [g.lower() for g in genres] + tags = [t.lower() for t in tags] + + # Ratio basé sur RAWG -> HLTB + ratio = 2.5 + + # Protection sur les jeux courts + if rawg_playtime <= 2: + return round(rawg_playtime * 1.2, 1) + elif rawg_playtime <= 3: + return round(rawg_playtime * 1.5, 1) + elif rawg_playtime <= 5: + return round(rawg_playtime * 2.0, 1) + + # Ratios selon genre principal + if any(g in genres for g in ["rpg", "role-playing"]): + ratio = 5.0 + elif any(g in genres for g in ["strategy"]): + ratio = 3.0 + elif any(g in genres for g in ["adventure"]): + ratio = 2.5 + elif any(g in genres for g in ["action"]): + ratio = 2.2 + elif any(g in genres for g in ["indie"]): + ratio = 1.8 + elif any(g in genres for g in ["platformer"]): + ratio = 1.5 + elif any(g in genres for g in ["roguelike"]): + ratio = 2.5 + + # survival sandbox long / survival horror court + if any(t in tags for t in ["open world", "sandbox", "crafting", "exploration", "base building"]): + ratio *= 3.5 + elif any(t in tags for t in ["survival", "horror"]): + ratio *= 1.2 + + # Simulation / factory games + if any(g in genres for g in ["simulation"]): + ratio = max(ratio, 3.5) + + if any(t in tags for t in ["automation", "factory", "management"]): + ratio = max(ratio, 4.0) + + return round(rawg_playtime * ratio, 1) + + ###################################################### # Fonctions Principales # @@ -238,6 +327,13 @@ def get_steam_game_details(app_id: int): if "genres" in info: genres = [g["description"] for g in info["genres"]] + # Durée du jeu + if released: + rawg_playtime, rawg_genres, rawg_tags = get_rawg_data(name) + hltb_time = estimate_hltb_from_rawg(rawg_playtime, rawg_genres, rawg_tags) + else: + hltb_time = None + # Note du jeu metacritic_score = None if "metacritic" in info and "score" in info["metacritic"]: @@ -254,6 +350,7 @@ def get_steam_game_details(app_id: int): "released": released, "release_date": release_date, "genres": genres, + "hltb_time": hltb_time, "metacritic_score": metacritic_score, "cover_image": wallpaper, "icon_image": icon @@ -286,6 +383,9 @@ def update_notion_page(page_id, game): "Genres": { "multi_select": [{"name": genre} for genre in game["genres"]] }, + "Estimated duration": { + "number": float(game["hltb_time"]) if game["hltb_time"] is not None else None + }, "Metacritic": { "number": int(game["metacritic_score"]) if game["metacritic_score"] is not None else None }