171 lines
5.1 KiB
Python
171 lines
5.1 KiB
Python
import json
|
||
import os
|
||
import threading
|
||
import time
|
||
import urllib.error
|
||
import urllib.parse
|
||
import urllib.request
|
||
|
||
from flask import Blueprint, jsonify
|
||
|
||
steam_bp = Blueprint("steam", __name__)
|
||
|
||
STEAM_API_KEY = os.environ.get("STEAM_API_KEY", "").strip()
|
||
STEAM_ID = os.environ.get("STEAM_ID", "76561199137306791").strip()
|
||
ACTIVITY_TTL = 60
|
||
RECENT_COUNT = 5
|
||
TOP_COUNT = 5
|
||
|
||
API_BASE = "https://api.steampowered.com"
|
||
STORE_APP = "https://store.steampowered.com/app"
|
||
PROFILE_URL = f"https://steamcommunity.com/profiles/{STEAM_ID}"
|
||
|
||
_CACHE_LOCK = threading.Lock()
|
||
_CACHE: dict = {"at": 0.0, "payload": None}
|
||
|
||
PERSONA_STATES = {
|
||
0: "не в сети",
|
||
1: "в сети",
|
||
2: "занят",
|
||
3: "отошёл",
|
||
4: "спит",
|
||
5: "ищет обмен",
|
||
6: "ищет поиграть",
|
||
7: "невидим",
|
||
}
|
||
|
||
|
||
def _get(path, **params):
|
||
params["key"] = STEAM_API_KEY
|
||
url = f"{API_BASE}{path}?{urllib.parse.urlencode(params)}"
|
||
req = urllib.request.Request(url, headers={"Accept": "application/json"})
|
||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||
return json.load(resp)
|
||
|
||
|
||
def _header(appid):
|
||
return f"https://cdn.akamai.steamstatic.com/steam/apps/{appid}/header.jpg"
|
||
|
||
|
||
def _store_url(appid):
|
||
return f"{STORE_APP}/{appid}/"
|
||
|
||
|
||
def _serialize(game):
|
||
appid = int(game["appid"])
|
||
minutes = int(game.get("playtime_forever") or 0)
|
||
two_weeks = game.get("playtime_2weeks")
|
||
last_played = int(game.get("rtime_last_played") or 0)
|
||
return {
|
||
"appid": appid,
|
||
"name": game.get("name") or f"App #{appid}",
|
||
"hours_total": round(minutes / 60, 1),
|
||
"hours_2weeks": round(int(two_weeks) / 60, 1) if two_weeks else None,
|
||
"last_played": last_played or None,
|
||
"header": _header(appid),
|
||
"url": _store_url(appid),
|
||
}
|
||
|
||
|
||
def _fetch():
|
||
recent_resp = _get(
|
||
"/IPlayerService/GetRecentlyPlayedGames/v1/",
|
||
steamid=STEAM_ID,
|
||
count=RECENT_COUNT,
|
||
)
|
||
owned_resp = _get(
|
||
"/IPlayerService/GetOwnedGames/v1/",
|
||
steamid=STEAM_ID,
|
||
include_appinfo=1,
|
||
include_played_free_games=1,
|
||
)
|
||
summary_resp = _get("/ISteamUser/GetPlayerSummaries/v2/", steamids=STEAM_ID)
|
||
|
||
recent = [
|
||
_serialize(game)
|
||
for game in (recent_resp.get("response") or {}).get("games") or []
|
||
]
|
||
|
||
owned = owned_resp.get("response") or {}
|
||
played = [
|
||
game
|
||
for game in owned.get("games") or []
|
||
if int(game.get("playtime_forever") or 0) > 0
|
||
]
|
||
played.sort(key=lambda game: int(game.get("playtime_forever") or 0), reverse=True)
|
||
top = [_serialize(game) for game in played[:TOP_COUNT]]
|
||
|
||
players = (summary_resp.get("response") or {}).get("players") or []
|
||
player = players[0] if players else {}
|
||
|
||
game_id = str(player.get("gameid") or "")
|
||
game_name = player.get("gameextrainfo") or ""
|
||
now = None
|
||
if game_id:
|
||
appid = int(game_id)
|
||
if not game_name:
|
||
known = next((g["name"] for g in recent + top if g["appid"] == appid), None)
|
||
game_name = known or ""
|
||
now = {
|
||
"appid": appid,
|
||
"name": game_name,
|
||
"header": _header(appid),
|
||
"url": _store_url(appid),
|
||
}
|
||
|
||
state = int(player.get("personastate") or 0)
|
||
return {
|
||
"ok": True,
|
||
"updated_at": int(time.time()),
|
||
"now": now,
|
||
"state": PERSONA_STATES.get(state, ""),
|
||
"profile": {
|
||
"url": player.get("profileurl") or PROFILE_URL,
|
||
"name": player.get("personaname") or "",
|
||
"public": int(player.get("communityvisibilitystate") or 0) == 3,
|
||
},
|
||
"recent": recent,
|
||
"top": top,
|
||
"game_count": int(owned.get("game_count") or 0) or None,
|
||
}
|
||
|
||
|
||
def _fetch_activity():
|
||
try:
|
||
return _fetch()
|
||
except urllib.error.HTTPError as exc:
|
||
if exc.code in (401, 403):
|
||
raise RuntimeError("Steam API: неверный или недействительный ключ") from exc
|
||
if exc.code == 429:
|
||
raise RuntimeError("Steam API: лимит запросов, попробуйте позже") from exc
|
||
raise RuntimeError(f"Steam API: HTTP {exc.code}") from exc
|
||
except urllib.error.URLError as exc:
|
||
raise RuntimeError(f"нет связи с Steam API: {exc.reason}") from exc
|
||
|
||
|
||
def _serve():
|
||
now = time.monotonic()
|
||
with _CACHE_LOCK:
|
||
if _CACHE["payload"] is not None and now - _CACHE["at"] < ACTIVITY_TTL:
|
||
return jsonify(_CACHE["payload"]), 200
|
||
|
||
try:
|
||
payload = _fetch_activity()
|
||
except (RuntimeError, OSError, ValueError, KeyError, AttributeError) as exc:
|
||
if _CACHE["payload"] is not None:
|
||
stale = dict(_CACHE["payload"])
|
||
stale["stale"] = True
|
||
return jsonify(stale), 200
|
||
return jsonify(ok=False, error=str(exc)), 502
|
||
|
||
_CACHE["at"] = now
|
||
_CACHE["payload"] = payload
|
||
return jsonify(payload), 200
|
||
|
||
|
||
@steam_bp.route("/api/steam/activity")
|
||
def steam_activity():
|
||
if not STEAM_API_KEY:
|
||
return jsonify(ok=False, error="STEAM_API_KEY is not set"), 503
|
||
return _serve()
|