From d280cf2531a1ff473ce856bf9187f7782b14094c Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sun, 15 Jun 2025 15:48:16 +0500 Subject: [PATCH] feat(dev-scripts): parse all ppdb topics from our forum Signed-off-by: Boris Yumankulov --- .gitea/workflows/generate-appid.yml | 2 + dev-scripts/get_id.py | 124 +++++++++++++++++++--------- 2 files changed, 86 insertions(+), 40 deletions(-) diff --git a/.gitea/workflows/generate-appid.yml b/.gitea/workflows/generate-appid.yml index 32c0f8e..0ad8b1c 100644 --- a/.gitea/workflows/generate-appid.yml +++ b/.gitea/workflows/generate-appid.yml @@ -30,6 +30,8 @@ jobs: run: python dev-scripts/get_id.py env: STEAM_KEY: ${{ secrets.STEAM_KEY }} + LINUX_GAMING_API_KEY: ${{ secrets.LINUX_GAMING_API_KEY }} + LINUX_GAMING_API_USERNAME: ${{ secrets.LINUX_GAMING_API_USERNAME }} - name: Commit and push changes env: diff --git a/dev-scripts/get_id.py b/dev-scripts/get_id.py index 529696e..22eb70b 100755 --- a/dev-scripts/get_id.py +++ b/dev-scripts/get_id.py @@ -6,11 +6,20 @@ import asyncio import aiohttp import tarfile +# Получаем ключи и данные из переменных окружения +STEAM_KEY = os.environ.get('STEAM_KEY') +LINUX_GAMING_API_KEY = os.environ.get('LINUX_GAMING_API_KEY') +LINUX_GAMING_API_USERNAME = os.environ.get('LINUX_GAMING_API_USERNAME') -# Получаем ключ Steam из переменной окружения. -key = os.environ.get('STEAM_KEY') -base_url = "https://api.steampowered.com/IStoreService/GetAppList/v1/?" -category = "games" +# Конфигурация API +STEAM_BASE_URL = "https://api.steampowered.com/IStoreService/GetAppList/v1/?" +LINUX_GAMING_BASE_URL = "https://linux-gaming.ru" +CATEGORY_STEAM = "games" +CATEGORY_LINUX_GAMING = "ppdb" +LINUX_GAMING_HEADERS = { + "Api-Key": LINUX_GAMING_API_KEY, + "Api-Username": LINUX_GAMING_API_USERNAME +} def normalize_name(s): """ @@ -32,13 +41,11 @@ def normalize_name(s): if s.endswith(suffix): s = s[:-len(suffix)].strip() - # Удаляем служебные слова, которые не должны влиять на сопоставление keywords_to_remove = {"ultimate", "edition", "definitive", "complete", "remastered"} words = s.split() filtered_words = [word for word in words if word not in keywords_to_remove] return " ".join(filtered_words) - def process_steam_apps(steam_apps): """ Для каждого приложения из Steam добавляет ключ "normalized_name", @@ -49,16 +56,14 @@ def process_steam_apps(steam_apps): original = app.get("name", "") if not app.get("normalized_name"): app["normalized_name"] = normalize_name(original) - # Удаляем ненужные поля app.pop("name", None) app.pop("last_modified", None) app.pop("price_change_number", None) return steam_apps - async def get_app_list(session, last_appid, endpoint): """ - Получает часть списка приложений из API. + Получает часть списка приложений из API Steam. Если last_appid передан, добавляет его к URL для постраничной загрузки. """ url = endpoint @@ -68,7 +73,6 @@ async def get_app_list(session, last_appid, endpoint): response.raise_for_status() return await response.json() - async def fetch_games_json(session): """ Загружает JSON с данными из AreWeAntiCheatYet и извлекает поля normalized_name и status. @@ -79,21 +83,46 @@ async def fetch_games_json(session): response.raise_for_status() text = await response.text() data = json.loads(text) - # Извлекаем только поля normalized_name и status return [{"normalized_name": normalize_name(game["name"]), "status": game["status"]} for game in data] except Exception as error: print(f"Ошибка загрузки games.json: {error}") return [] +async def get_linux_gaming_topics(session, category_slug): + """ + Получает все темы из указанной категории linux-gaming.ru. + Сохраняет только нормализованное название (normalized_title) и slug. + """ + page = 0 + all_topics = [] + + while True: + page += 1 + url = f"{LINUX_GAMING_BASE_URL}/c/{category_slug}/l/latest.json?page={page}" + try: + async with session.get(url, headers=LINUX_GAMING_HEADERS) as response: + response.raise_for_status() + data = await response.json() + topics = data.get("topic_list", {}).get("topics", []) + if not topics: + break + for topic in topics: + all_topics.append({ + "normalized_title": normalize_name(topic["title"]), + "slug": topic["slug"] + }) + print(f"Обработано {len(topics)} тем на странице {page}, всего: {len(all_topics)}.") + except Exception as error: + print(f"Ошибка получения тем для страницы {page}: {error}") + break + return all_topics async def request_data(): """ - Получает данные списка приложений для категории "games" до тех пор, - пока не закончатся результаты, обрабатывает данные для добавления - нормализованных имён и записывает итоговый результат в JSON-файл. - Отдельно загружает games.json и сохраняет его в отдельный JSON-файл. + Получает данные из Steam, AreWeAntiCheatYet и linux-gaming.ru, + обрабатывает их и сохраняет в JSON-файлы и tar.xz архивы. """ - # Параметры запроса для игр. + # Параметры запроса для Steam game_param = "&include_games=true" dlc_param = "&include_dlc=false" software_param = "&include_software=false" @@ -101,13 +130,15 @@ async def request_data(): hardware_param = "&include_hardware=false" endpoint = ( - f"{base_url}key={key}" + f"{STEAM_BASE_URL}key={STEAM_KEY}" f"{game_param}{dlc_param}{software_param}{videos_param}{hardware_param}" f"&max_results=50000" ) output_json = [] total_parsed = 0 + linux_gaming_topics = [] + anticheat_games = [] try: async with aiohttp.ClientSession() as session: @@ -117,58 +148,62 @@ async def request_data(): while have_more_results: app_list = await get_app_list(session, last_appid_val, endpoint) apps = app_list['response']['apps'] - # Обрабатываем приложения для добавления нормализованных имён apps = process_steam_apps(apps) output_json.extend(apps) total_parsed += len(apps) have_more_results = app_list['response'].get('have_more_results', False) last_appid_val = app_list['response'].get('last_appid') + print(f"Обработано {len(apps)} игр Steam, всего: {total_parsed}.") - print(f"Обработано {len(apps)} игр, всего: {total_parsed}.") - - # Загружаем и сохраняем games.json отдельно + # Загружаем данные AreWeAntiCheatYet anticheat_games = await fetch_games_json(session) + # Загружаем данные linux-gaming.ru + if LINUX_GAMING_API_KEY and LINUX_GAMING_API_USERNAME: + linux_gaming_topics = await get_linux_gaming_topics(session, CATEGORY_LINUX_GAMING) + else: + print("Предупреждение: LINUX_GAMING_API_KEY или LINUX_GAMING_API_USERNAME не установлены.") + except Exception as error: - print(f"Ошибка получения данных для {category}: {error}") + print(f"Ошибка получения данных: {error}") return False repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) data_dir = os.path.join(repo_root, "data") os.makedirs(data_dir, exist_ok=True) - # Путь к JSON-файлам для Steam - output_json_full = os.path.join(data_dir, f"{category}_appid.json") - output_json_min = os.path.join(data_dir, f"{category}_appid_min.json") - - # Записываем полные данные Steam с отступами + # Сохранение данных Steam + output_json_full = os.path.join(data_dir, f"{CATEGORY_STEAM}_appid.json") + output_json_min = os.path.join(data_dir, f"{CATEGORY_STEAM}_appid_min.json") with open(output_json_full, "w", encoding="utf-8") as f: json.dump(output_json, f, ensure_ascii=False, indent=2) - - # Записываем минимизированные данные Steam with open(output_json_min, "w", encoding="utf-8") as f: json.dump(output_json, f, ensure_ascii=False, separators=(',',':')) - # Путь к JSON-файлам для AreWeAntiCheatYet + # Сохранение данных AreWeAntiCheatYet anticheat_json_full = os.path.join(data_dir, "anticheat_games.json") anticheat_json_min = os.path.join(data_dir, "anticheat_games_min.json") - - # Записываем полные данные AreWeAntiCheatYet с отступами with open(anticheat_json_full, "w", encoding="utf-8") as f: json.dump(anticheat_games, f, ensure_ascii=False, indent=2) - - # Записываем минимизированные данные AreWeAntiCheatYet with open(anticheat_json_min, "w", encoding="utf-8") as f: json.dump(anticheat_games, f, ensure_ascii=False, separators=(',',':')) - # Упаковка только минифицированных JSON в tar.xz архивы с максимальным сжатием + # Сохранение данных linux-gaming.ru + linux_gaming_json_full = os.path.join(data_dir, "linux_gaming_topics.json") + linux_gaming_json_min = os.path.join(data_dir, "linux_gaming_topics_min.json") + if linux_gaming_topics: + with open(linux_gaming_json_full, "w", encoding="utf-8") as f: + json.dump(linux_gaming_topics, f, ensure_ascii=False, indent=2) + with open(linux_gaming_json_min, "w", encoding="utf-8") as f: + json.dump(linux_gaming_topics, f, ensure_ascii=False, separators=(',',':')) + + # Упаковка минифицированных JSON в tar.xz архивы # Архив для Steam - steam_archive_path = os.path.join(data_dir, f"{category}_appid.tar.xz") + steam_archive_path = os.path.join(data_dir, f"{CATEGORY_STEAM}_appid.tar.xz") try: with tarfile.open(steam_archive_path, "w:xz", preset=9) as tar: tar.add(output_json_min, arcname=os.path.basename(output_json_min)) print(f"Упаковано минифицированное JSON Steam в архив: {steam_archive_path}") - # Удаляем исходный минифицированный файл после упаковки os.remove(output_json_min) except Exception as e: print(f"Ошибка при упаковке архива Steam: {e}") @@ -180,20 +215,29 @@ async def request_data(): with tarfile.open(anticheat_archive_path, "w:xz", preset=9) as tar: tar.add(anticheat_json_min, arcname=os.path.basename(anticheat_json_min)) print(f"Упаковано минифицированное JSON AreWeAntiCheatYet в архив: {anticheat_archive_path}") - # Удаляем исходный минифицированный файл после упаковки os.remove(anticheat_json_min) except Exception as e: print(f"Ошибка при упаковке архива AreWeAntiCheatYet: {e}") return False - return True + # Архив для linux-gaming.ru + if linux_gaming_topics: + linux_gaming_archive_path = os.path.join(data_dir, "linux_gaming_topics.tar.xz") + try: + with tarfile.open(linux_gaming_archive_path, "w:xz", preset=9) as tar: + tar.add(linux_gaming_json_min, arcname=os.path.basename(linux_gaming_json_min)) + print(f"Упаковано минифицированное JSON linux-gaming.ru в архив: {linux_gaming_archive_path}") + os.remove(linux_gaming_json_min) + except Exception as e: + print(f"Ошибка при упаковке архива linux-gaming.ru: {e}") + return False + return True async def run(): success = await request_data() if not success: exit(1) - if __name__ == "__main__": asyncio.run(run())