2 Commits

Author SHA1 Message Date
Gitea Actions
326b2d7411 chore: update steam apps list 2025-06-15T10:52:34Z 2025-06-15 10:52:34 +00:00
d280cf2531 feat(dev-scripts): parse all ppdb topics from our forum
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-15 15:48:16 +05:00
8 changed files with 7451 additions and 499 deletions

View File

@@ -30,6 +30,8 @@ jobs:
run: python dev-scripts/get_id.py run: python dev-scripts/get_id.py
env: env:
STEAM_KEY: ${{ secrets.STEAM_KEY }} 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 - name: Commit and push changes
env: env:

View File

@@ -1573,7 +1573,7 @@
}, },
{ {
"normalized_name": "dune awakening", "normalized_name": "dune awakening",
"status": "Broken" "status": "Running"
}, },
{ {
"normalized_name": "warcraft iii reforged", "normalized_name": "warcraft iii reforged",
@@ -2337,7 +2337,7 @@
}, },
{ {
"normalized_name": "punishing gray raven", "normalized_name": "punishing gray raven",
"status": "Broken" "status": "Running"
}, },
{ {
"normalized_name": "brainbread 2", "normalized_name": "brainbread 2",
@@ -3951,10 +3951,6 @@
"normalized_name": "outpost infinity siege", "normalized_name": "outpost infinity siege",
"status": "Running" "status": "Running"
}, },
{
"normalized_name": "avatar frontiers of pandora",
"status": "Broken"
},
{ {
"normalized_name": "v rising", "normalized_name": "v rising",
"status": "Running" "status": "Running"
@@ -4406,5 +4402,17 @@
{ {
"normalized_name": "elden ring nightreign", "normalized_name": "elden ring nightreign",
"status": "Running" "status": "Running"
},
{
"normalized_name": "steel hunters",
"status": "Running"
},
{
"normalized_name": "reverse 1999",
"status": "Running"
},
{
"normalized_name": "ragnarok origin roo",
"status": "Running"
} }
] ]

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -6,11 +6,20 @@ import asyncio
import aiohttp import aiohttp
import tarfile 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 из переменной окружения. # Конфигурация API
key = os.environ.get('STEAM_KEY') STEAM_BASE_URL = "https://api.steampowered.com/IStoreService/GetAppList/v1/?"
base_url = "https://api.steampowered.com/IStoreService/GetAppList/v1/?" LINUX_GAMING_BASE_URL = "https://linux-gaming.ru"
category = "games" 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): def normalize_name(s):
""" """
@@ -32,13 +41,11 @@ def normalize_name(s):
if s.endswith(suffix): if s.endswith(suffix):
s = s[:-len(suffix)].strip() s = s[:-len(suffix)].strip()
# Удаляем служебные слова, которые не должны влиять на сопоставление
keywords_to_remove = {"ultimate", "edition", "definitive", "complete", "remastered"} keywords_to_remove = {"ultimate", "edition", "definitive", "complete", "remastered"}
words = s.split() words = s.split()
filtered_words = [word for word in words if word not in keywords_to_remove] filtered_words = [word for word in words if word not in keywords_to_remove]
return " ".join(filtered_words) return " ".join(filtered_words)
def process_steam_apps(steam_apps): def process_steam_apps(steam_apps):
""" """
Для каждого приложения из Steam добавляет ключ "normalized_name", Для каждого приложения из Steam добавляет ключ "normalized_name",
@@ -49,16 +56,14 @@ def process_steam_apps(steam_apps):
original = app.get("name", "") original = app.get("name", "")
if not app.get("normalized_name"): if not app.get("normalized_name"):
app["normalized_name"] = normalize_name(original) app["normalized_name"] = normalize_name(original)
# Удаляем ненужные поля
app.pop("name", None) app.pop("name", None)
app.pop("last_modified", None) app.pop("last_modified", None)
app.pop("price_change_number", None) app.pop("price_change_number", None)
return steam_apps return steam_apps
async def get_app_list(session, last_appid, endpoint): async def get_app_list(session, last_appid, endpoint):
""" """
Получает часть списка приложений из API. Получает часть списка приложений из API Steam.
Если last_appid передан, добавляет его к URL для постраничной загрузки. Если last_appid передан, добавляет его к URL для постраничной загрузки.
""" """
url = endpoint url = endpoint
@@ -68,7 +73,6 @@ async def get_app_list(session, last_appid, endpoint):
response.raise_for_status() response.raise_for_status()
return await response.json() return await response.json()
async def fetch_games_json(session): async def fetch_games_json(session):
""" """
Загружает JSON с данными из AreWeAntiCheatYet и извлекает поля normalized_name и status. Загружает JSON с данными из AreWeAntiCheatYet и извлекает поля normalized_name и status.
@@ -79,21 +83,46 @@ async def fetch_games_json(session):
response.raise_for_status() response.raise_for_status()
text = await response.text() text = await response.text()
data = json.loads(text) data = json.loads(text)
# Извлекаем только поля normalized_name и status
return [{"normalized_name": normalize_name(game["name"]), "status": game["status"]} for game in data] return [{"normalized_name": normalize_name(game["name"]), "status": game["status"]} for game in data]
except Exception as error: except Exception as error:
print(f"Ошибка загрузки games.json: {error}") print(f"Ошибка загрузки games.json: {error}")
return [] 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(): async def request_data():
""" """
Получает данные списка приложений для категории "games" до тех пор, Получает данные из Steam, AreWeAntiCheatYet и linux-gaming.ru,
пока не закончатся результаты, обрабатывает данные для добавления обрабатывает их и сохраняет в JSON-файлы и tar.xz архивы.
нормализованных имён и записывает итоговый результат в JSON-файл.
Отдельно загружает games.json и сохраняет его в отдельный JSON-файл.
""" """
# Параметры запроса для игр. # Параметры запроса для Steam
game_param = "&include_games=true" game_param = "&include_games=true"
dlc_param = "&include_dlc=false" dlc_param = "&include_dlc=false"
software_param = "&include_software=false" software_param = "&include_software=false"
@@ -101,13 +130,15 @@ async def request_data():
hardware_param = "&include_hardware=false" hardware_param = "&include_hardware=false"
endpoint = ( 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"{game_param}{dlc_param}{software_param}{videos_param}{hardware_param}"
f"&max_results=50000" f"&max_results=50000"
) )
output_json = [] output_json = []
total_parsed = 0 total_parsed = 0
linux_gaming_topics = []
anticheat_games = []
try: try:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
@@ -117,58 +148,62 @@ async def request_data():
while have_more_results: while have_more_results:
app_list = await get_app_list(session, last_appid_val, endpoint) app_list = await get_app_list(session, last_appid_val, endpoint)
apps = app_list['response']['apps'] apps = app_list['response']['apps']
# Обрабатываем приложения для добавления нормализованных имён
apps = process_steam_apps(apps) apps = process_steam_apps(apps)
output_json.extend(apps) output_json.extend(apps)
total_parsed += len(apps) total_parsed += len(apps)
have_more_results = app_list['response'].get('have_more_results', False) have_more_results = app_list['response'].get('have_more_results', False)
last_appid_val = app_list['response'].get('last_appid') last_appid_val = app_list['response'].get('last_appid')
print(f"Обработано {len(apps)} игр Steam, всего: {total_parsed}.")
print(f"Обработано {len(apps)} игр, всего: {total_parsed}.") # Загружаем данные AreWeAntiCheatYet
# Загружаем и сохраняем games.json отдельно
anticheat_games = await fetch_games_json(session) 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: except Exception as error:
print(f"Ошибка получения данных для {category}: {error}") print(f"Ошибка получения данных: {error}")
return False return False
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
data_dir = os.path.join(repo_root, "data") data_dir = os.path.join(repo_root, "data")
os.makedirs(data_dir, exist_ok=True) os.makedirs(data_dir, exist_ok=True)
# Путь к JSON-файлам для Steam # Сохранение данных Steam
output_json_full = os.path.join(data_dir, f"{category}_appid.json") output_json_full = os.path.join(data_dir, f"{CATEGORY_STEAM}_appid.json")
output_json_min = os.path.join(data_dir, f"{category}_appid_min.json") output_json_min = os.path.join(data_dir, f"{CATEGORY_STEAM}_appid_min.json")
# Записываем полные данные Steam с отступами
with open(output_json_full, "w", encoding="utf-8") as f: with open(output_json_full, "w", encoding="utf-8") as f:
json.dump(output_json, f, ensure_ascii=False, indent=2) json.dump(output_json, f, ensure_ascii=False, indent=2)
# Записываем минимизированные данные Steam
with open(output_json_min, "w", encoding="utf-8") as f: with open(output_json_min, "w", encoding="utf-8") as f:
json.dump(output_json, f, ensure_ascii=False, separators=(',',':')) 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_full = os.path.join(data_dir, "anticheat_games.json")
anticheat_json_min = os.path.join(data_dir, "anticheat_games_min.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: with open(anticheat_json_full, "w", encoding="utf-8") as f:
json.dump(anticheat_games, f, ensure_ascii=False, indent=2) json.dump(anticheat_games, f, ensure_ascii=False, indent=2)
# Записываем минимизированные данные AreWeAntiCheatYet
with open(anticheat_json_min, "w", encoding="utf-8") as f: with open(anticheat_json_min, "w", encoding="utf-8") as f:
json.dump(anticheat_games, f, ensure_ascii=False, separators=(',',':')) 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
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: try:
with tarfile.open(steam_archive_path, "w:xz", preset=9) as tar: with tarfile.open(steam_archive_path, "w:xz", preset=9) as tar:
tar.add(output_json_min, arcname=os.path.basename(output_json_min)) tar.add(output_json_min, arcname=os.path.basename(output_json_min))
print(f"Упаковано минифицированное JSON Steam в архив: {steam_archive_path}") print(f"Упаковано минифицированное JSON Steam в архив: {steam_archive_path}")
# Удаляем исходный минифицированный файл после упаковки
os.remove(output_json_min) os.remove(output_json_min)
except Exception as e: except Exception as e:
print(f"Ошибка при упаковке архива Steam: {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: with tarfile.open(anticheat_archive_path, "w:xz", preset=9) as tar:
tar.add(anticheat_json_min, arcname=os.path.basename(anticheat_json_min)) tar.add(anticheat_json_min, arcname=os.path.basename(anticheat_json_min))
print(f"Упаковано минифицированное JSON AreWeAntiCheatYet в архив: {anticheat_archive_path}") print(f"Упаковано минифицированное JSON AreWeAntiCheatYet в архив: {anticheat_archive_path}")
# Удаляем исходный минифицированный файл после упаковки
os.remove(anticheat_json_min) os.remove(anticheat_json_min)
except Exception as e: except Exception as e:
print(f"Ошибка при упаковке архива AreWeAntiCheatYet: {e}") print(f"Ошибка при упаковке архива AreWeAntiCheatYet: {e}")
return False 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(): async def run():
success = await request_data() success = await request_data()
if not success: if not success:
exit(1) exit(1)
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(run()) asyncio.run(run())