feat(dev-scripts): parse all ppdb topics from our forum
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
@ -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:
|
||||||
|
@ -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())
|
||||||
|
Reference in New Issue
Block a user