Public Access
feat(dev): add the local profile with recorded third-party answers
Profile "local" points Steam, Twitch/IGDB and speedrun.com to a WireMock container from compose.yaml (recordings in local/wiremock) and checks logins against the local Auth. The IGDB lookups now keep only the ids they asked for: an answer with another game made tags and release years drop the whole batch.
This commit is contained in:
1 parent
5a089b2082
commit
5c0c0c6a78
54 files changed
+1539
-8
No files matched your search
+10
-1
@@ -7,4 +7,13 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- POSTGRES_DB=tbp_core
|
- POSTGRES_DB=tbp_core
|
||||||
- POSTGRES_USER=tbp_user
|
- POSTGRES_USER=tbp_user
|
||||||
- POSTGRES_PASSWORD=local_secret_password
|
- POSTGRES_PASSWORD=local_secret_password
|
||||||
|
# Recorded answers of Steam, Twitch/IGDB and speedrun.com (local/wiremock). Starts only with the local profile.
|
||||||
|
wiremock:
|
||||||
|
image: wiremock/wiremock:3.9.1
|
||||||
|
container_name: tbp_core_mocks_local
|
||||||
|
profiles: ["local"]
|
||||||
|
ports:
|
||||||
|
- "8089:8080"
|
||||||
|
volumes:
|
||||||
|
- ./local/wiremock:/home/wiremock
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"game": 72,
|
||||||
|
"uid": "620"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"game": 113112,
|
||||||
|
"uid": "1145360"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"game": 17000,
|
||||||
|
"uid": "413150"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"game": 7046,
|
||||||
|
"uid": "427520"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"game": 26226,
|
||||||
|
"uid": "504230"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"game": 14593,
|
||||||
|
"uid": "367520"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"game": 233,
|
||||||
|
"uid": "220"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"game": 133236,
|
||||||
|
"uid": "892970"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 113112,
|
||||||
|
"name": "Hades",
|
||||||
|
"first_release_date": 1600387200,
|
||||||
|
"cover": {
|
||||||
|
"image_id": "co1r7f"
|
||||||
|
},
|
||||||
|
"platforms": [
|
||||||
|
{
|
||||||
|
"name": "PC (Microsoft Windows)",
|
||||||
|
"abbreviation": "PC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Nintendo Switch",
|
||||||
|
"abbreviation": "Switch"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total_rating_count": 1500,
|
||||||
|
"external_games": [
|
||||||
|
{
|
||||||
|
"external_game_source": 1,
|
||||||
|
"uid": "1145360"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 119388,
|
||||||
|
"name": "The Legend of Zelda: Tears of the Kingdom",
|
||||||
|
"first_release_date": 1683849600,
|
||||||
|
"cover": {
|
||||||
|
"image_id": "co1r7f"
|
||||||
|
},
|
||||||
|
"platforms": [
|
||||||
|
{
|
||||||
|
"name": "Nintendo Switch",
|
||||||
|
"abbreviation": "Switch"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total_rating_count": 900
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 26226,
|
||||||
|
"name": "Celeste",
|
||||||
|
"first_release_date": 1516838400,
|
||||||
|
"cover": {
|
||||||
|
"image_id": "co1r7f"
|
||||||
|
},
|
||||||
|
"platforms": [
|
||||||
|
{
|
||||||
|
"name": "PC (Microsoft Windows)",
|
||||||
|
"abbreviation": "PC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Nintendo Switch",
|
||||||
|
"abbreviation": "Switch"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total_rating_count": 1300,
|
||||||
|
"external_games": [
|
||||||
|
{
|
||||||
|
"external_game_source": 1,
|
||||||
|
"uid": "504230"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 119388,
|
||||||
|
"name": "The Legend of Zelda: Tears of the Kingdom",
|
||||||
|
"first_release_date": 1683849600,
|
||||||
|
"cover": {
|
||||||
|
"image_id": "co1r7f"
|
||||||
|
},
|
||||||
|
"platforms": [
|
||||||
|
{
|
||||||
|
"name": "Nintendo Switch",
|
||||||
|
"abbreviation": "Switch"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total_rating_count": 900
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 113112,
|
||||||
|
"name": "Hades",
|
||||||
|
"first_release_date": 1600387200,
|
||||||
|
"cover": {
|
||||||
|
"image_id": "co1r7f"
|
||||||
|
},
|
||||||
|
"platforms": [
|
||||||
|
{
|
||||||
|
"name": "PC (Microsoft Windows)",
|
||||||
|
"abbreviation": "PC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Nintendo Switch",
|
||||||
|
"abbreviation": "Switch"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total_rating_count": 1500,
|
||||||
|
"external_games": [
|
||||||
|
{
|
||||||
|
"external_game_source": 1,
|
||||||
|
"uid": "1145360"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 26226,
|
||||||
|
"name": "Celeste",
|
||||||
|
"first_release_date": 1516838400,
|
||||||
|
"cover": {
|
||||||
|
"image_id": "co1r7f"
|
||||||
|
},
|
||||||
|
"platforms": [
|
||||||
|
{
|
||||||
|
"name": "PC (Microsoft Windows)",
|
||||||
|
"abbreviation": "PC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Nintendo Switch",
|
||||||
|
"abbreviation": "Switch"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total_rating_count": 1300,
|
||||||
|
"external_games": [
|
||||||
|
{
|
||||||
|
"external_game_source": 1,
|
||||||
|
"uid": "504230"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 72,
|
||||||
|
"first_release_date": 1303171200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 113112,
|
||||||
|
"first_release_date": 1600387200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 17000,
|
||||||
|
"first_release_date": 1456444800
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7046,
|
||||||
|
"first_release_date": 1600387200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 26226,
|
||||||
|
"first_release_date": 1516838400
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 14593,
|
||||||
|
"first_release_date": 1487980800
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 233,
|
||||||
|
"first_release_date": 1100736000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 133236,
|
||||||
|
"first_release_date": 1612828800
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 119388,
|
||||||
|
"first_release_date": 1683849600
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 72,
|
||||||
|
"genres": [
|
||||||
|
{
|
||||||
|
"name": "Puzzle"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Platform"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Shooter"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"themes": [
|
||||||
|
{
|
||||||
|
"name": "Science fiction"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Comedy"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"game_modes": [
|
||||||
|
{
|
||||||
|
"name": "Single player"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Co-operative"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 113112,
|
||||||
|
"genres": [
|
||||||
|
{
|
||||||
|
"name": "Role-playing (RPG)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Hack and slash/Beat 'em up"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Indie"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"themes": [
|
||||||
|
{
|
||||||
|
"name": "Action"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Fantasy"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"game_modes": [
|
||||||
|
{
|
||||||
|
"name": "Single player"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 17000,
|
||||||
|
"genres": [
|
||||||
|
{
|
||||||
|
"name": "Role-playing (RPG)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Simulator"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Indie"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"themes": [
|
||||||
|
{
|
||||||
|
"name": "Sandbox"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"game_modes": [
|
||||||
|
{
|
||||||
|
"name": "Single player"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Multiplayer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Co-operative"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7046,
|
||||||
|
"genres": [
|
||||||
|
{
|
||||||
|
"name": "Simulator"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Strategy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Indie"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"themes": [
|
||||||
|
{
|
||||||
|
"name": "Science fiction"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Sandbox"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Survival"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"game_modes": [
|
||||||
|
{
|
||||||
|
"name": "Single player"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Multiplayer"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 26226,
|
||||||
|
"genres": [
|
||||||
|
{
|
||||||
|
"name": "Platform"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Indie"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"themes": [
|
||||||
|
{
|
||||||
|
"name": "Action"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"game_modes": [
|
||||||
|
{
|
||||||
|
"name": "Single player"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 14593,
|
||||||
|
"genres": [
|
||||||
|
{
|
||||||
|
"name": "Platform"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Adventure"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Indie"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"themes": [
|
||||||
|
{
|
||||||
|
"name": "Action"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Fantasy"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"game_modes": [
|
||||||
|
{
|
||||||
|
"name": "Single player"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 233,
|
||||||
|
"genres": [
|
||||||
|
{
|
||||||
|
"name": "Shooter"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"themes": [
|
||||||
|
{
|
||||||
|
"name": "Action"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Science fiction"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"game_modes": [
|
||||||
|
{
|
||||||
|
"name": "Single player"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 133236,
|
||||||
|
"genres": [
|
||||||
|
{
|
||||||
|
"name": "Adventure"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Indie"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Simulator"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"themes": [
|
||||||
|
{
|
||||||
|
"name": "Action"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Fantasy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Sandbox"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Survival"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"game_modes": [
|
||||||
|
{
|
||||||
|
"name": "Single player"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Multiplayer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Co-operative"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 119388,
|
||||||
|
"genres": [
|
||||||
|
{
|
||||||
|
"name": "Adventure"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Role-playing (RPG)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"themes": [
|
||||||
|
{
|
||||||
|
"name": "Action"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Fantasy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Open world"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"game_modes": [
|
||||||
|
{
|
||||||
|
"name": "Single player"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"game_id": 72,
|
||||||
|
"hastily": 21600,
|
||||||
|
"normally": 32400,
|
||||||
|
"completely": 79200,
|
||||||
|
"count": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"game_id": 113112,
|
||||||
|
"hastily": 72000,
|
||||||
|
"normally": 90000,
|
||||||
|
"completely": 342000,
|
||||||
|
"count": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"game_id": 17000,
|
||||||
|
"hastily": 144000,
|
||||||
|
"normally": 190800,
|
||||||
|
"completely": 576000,
|
||||||
|
"count": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"game_id": 7046,
|
||||||
|
"hastily": 108000,
|
||||||
|
"normally": 216000,
|
||||||
|
"completely": 900000,
|
||||||
|
"count": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"game_id": 26226,
|
||||||
|
"hastily": 28800,
|
||||||
|
"normally": 43200,
|
||||||
|
"completely": 144000,
|
||||||
|
"count": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"game_id": 14593,
|
||||||
|
"hastily": 97200,
|
||||||
|
"normally": 147600,
|
||||||
|
"completely": 226800,
|
||||||
|
"count": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"game_id": 233,
|
||||||
|
"hastily": 43200,
|
||||||
|
"normally": 50400,
|
||||||
|
"completely": 68400,
|
||||||
|
"count": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"game_id": 119388,
|
||||||
|
"hastily": 180000,
|
||||||
|
"normally": 216000,
|
||||||
|
"completely": 684000,
|
||||||
|
"count": 100
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": "o1y9j9v6",
|
||||||
|
"names": {
|
||||||
|
"international": "Celeste"
|
||||||
|
},
|
||||||
|
"released": 2018,
|
||||||
|
"weblink": "https://www.speedrun.com/celeste",
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"rel": "self"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": "y8dwozoj"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"achievementpercentages": {
|
||||||
|
"achievements": [
|
||||||
|
{
|
||||||
|
"name": "FIRST_STEPS",
|
||||||
|
"percent": 88.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CRYSTAL_HEART",
|
||||||
|
"percent": 35.2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "SUMMIT",
|
||||||
|
"percent": 30.6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "FAREWELL",
|
||||||
|
"percent": 5.9
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"achievementpercentages": {
|
||||||
|
"achievements": [
|
||||||
|
{
|
||||||
|
"name": "ACH_WAKE_UP",
|
||||||
|
"percent": 92.1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ACH_SURVIVE_CONTAINMENT",
|
||||||
|
"percent": 70.4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ACH_CO_OP",
|
||||||
|
"percent": 22.5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ACH_END_SP",
|
||||||
|
"percent": 48.3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
{
|
||||||
|
"response": {
|
||||||
|
"game_count": 8,
|
||||||
|
"games": [
|
||||||
|
{
|
||||||
|
"appid": 620,
|
||||||
|
"name": "Portal 2",
|
||||||
|
"playtime_forever": 1450,
|
||||||
|
"playtime_2weeks": null,
|
||||||
|
"img_icon_url": "",
|
||||||
|
"has_community_visible_stats": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appid": 1145360,
|
||||||
|
"name": "Hades",
|
||||||
|
"playtime_forever": 3200,
|
||||||
|
"playtime_2weeks": 240,
|
||||||
|
"img_icon_url": "",
|
||||||
|
"has_community_visible_stats": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appid": 413150,
|
||||||
|
"name": "Stardew Valley",
|
||||||
|
"playtime_forever": 5400,
|
||||||
|
"playtime_2weeks": null,
|
||||||
|
"img_icon_url": "",
|
||||||
|
"has_community_visible_stats": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appid": 427520,
|
||||||
|
"name": "Factorio",
|
||||||
|
"playtime_forever": 12000,
|
||||||
|
"playtime_2weeks": 600,
|
||||||
|
"img_icon_url": "",
|
||||||
|
"has_community_visible_stats": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appid": 504230,
|
||||||
|
"name": "Celeste",
|
||||||
|
"playtime_forever": 800,
|
||||||
|
"playtime_2weeks": null,
|
||||||
|
"img_icon_url": "",
|
||||||
|
"has_community_visible_stats": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appid": 367520,
|
||||||
|
"name": "Hollow Knight",
|
||||||
|
"playtime_forever": 0,
|
||||||
|
"playtime_2weeks": null,
|
||||||
|
"img_icon_url": "",
|
||||||
|
"has_community_visible_stats": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appid": 220,
|
||||||
|
"name": "Half-Life 2",
|
||||||
|
"playtime_forever": 780,
|
||||||
|
"playtime_2weeks": null,
|
||||||
|
"img_icon_url": "",
|
||||||
|
"has_community_visible_stats": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appid": 892970,
|
||||||
|
"name": "Valheim",
|
||||||
|
"playtime_forever": 90,
|
||||||
|
"playtime_2weeks": 90,
|
||||||
|
"img_icon_url": "",
|
||||||
|
"has_community_visible_stats": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"playerstats": {
|
||||||
|
"steamID": "76561198000000001",
|
||||||
|
"gameName": "Celeste",
|
||||||
|
"success": true,
|
||||||
|
"achievements": [
|
||||||
|
{
|
||||||
|
"apiname": "FIRST_STEPS",
|
||||||
|
"achieved": 1,
|
||||||
|
"unlocktime": 1700000000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"apiname": "CRYSTAL_HEART",
|
||||||
|
"achieved": 1,
|
||||||
|
"unlocktime": 1700003600
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"apiname": "SUMMIT",
|
||||||
|
"achieved": 0,
|
||||||
|
"unlocktime": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"apiname": "FAREWELL",
|
||||||
|
"achieved": 0,
|
||||||
|
"unlocktime": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"playerstats": {
|
||||||
|
"steamID": "76561198000000001",
|
||||||
|
"gameName": "Portal 2",
|
||||||
|
"success": true,
|
||||||
|
"achievements": [
|
||||||
|
{
|
||||||
|
"apiname": "ACH_WAKE_UP",
|
||||||
|
"achieved": 1,
|
||||||
|
"unlocktime": 1700000000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"apiname": "ACH_SURVIVE_CONTAINMENT",
|
||||||
|
"achieved": 1,
|
||||||
|
"unlocktime": 1700003600
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"apiname": "ACH_CO_OP",
|
||||||
|
"achieved": 0,
|
||||||
|
"unlocktime": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"apiname": "ACH_END_SP",
|
||||||
|
"achieved": 1,
|
||||||
|
"unlocktime": 1700010800
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"response": {
|
||||||
|
"players": [
|
||||||
|
{
|
||||||
|
"steamid": "76561198000000001",
|
||||||
|
"personaname": "Dev",
|
||||||
|
"profileurl": "https://steamcommunity.com/profiles/76561198000000001/",
|
||||||
|
"avatarfull": "https://avatars.steamstatic.com/fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb_full.jpg"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"game": {
|
||||||
|
"gameName": "Celeste",
|
||||||
|
"gameVersion": "1",
|
||||||
|
"availableGameStats": {
|
||||||
|
"achievements": [
|
||||||
|
{
|
||||||
|
"name": "FIRST_STEPS",
|
||||||
|
"displayName": "First Steps",
|
||||||
|
"description": "Complete the Prologue.",
|
||||||
|
"hidden": 0,
|
||||||
|
"icon": "https://cdn.cloudflare.steamstatic.com/steam/apps/504230/capsule_184x69.jpg",
|
||||||
|
"icongray": "https://cdn.cloudflare.steamstatic.com/steam/apps/504230/capsule_184x69.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CRYSTAL_HEART",
|
||||||
|
"displayName": "Heart of the Mountain",
|
||||||
|
"description": "Collect a Crystal Heart.",
|
||||||
|
"hidden": 0,
|
||||||
|
"icon": "https://cdn.cloudflare.steamstatic.com/steam/apps/504230/capsule_184x69.jpg",
|
||||||
|
"icongray": "https://cdn.cloudflare.steamstatic.com/steam/apps/504230/capsule_184x69.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "SUMMIT",
|
||||||
|
"displayName": "Summit",
|
||||||
|
"description": "Reach the summit and finish the story.",
|
||||||
|
"hidden": 0,
|
||||||
|
"icon": "https://cdn.cloudflare.steamstatic.com/steam/apps/504230/capsule_184x69.jpg",
|
||||||
|
"icongray": "https://cdn.cloudflare.steamstatic.com/steam/apps/504230/capsule_184x69.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "FAREWELL",
|
||||||
|
"displayName": "Farewell",
|
||||||
|
"description": "Complete Chapter 9.",
|
||||||
|
"hidden": 0,
|
||||||
|
"icon": "https://cdn.cloudflare.steamstatic.com/steam/apps/504230/capsule_184x69.jpg",
|
||||||
|
"icongray": "https://cdn.cloudflare.steamstatic.com/steam/apps/504230/capsule_184x69.jpg"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"game": {
|
||||||
|
"gameName": "Portal 2",
|
||||||
|
"gameVersion": "1",
|
||||||
|
"availableGameStats": {
|
||||||
|
"achievements": [
|
||||||
|
{
|
||||||
|
"name": "ACH_WAKE_UP",
|
||||||
|
"displayName": "Wake Up Call",
|
||||||
|
"description": "Survive the manual override of the relaxation center.",
|
||||||
|
"hidden": 0,
|
||||||
|
"icon": "https://cdn.cloudflare.steamstatic.com/steam/apps/620/capsule_184x69.jpg",
|
||||||
|
"icongray": "https://cdn.cloudflare.steamstatic.com/steam/apps/620/capsule_184x69.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ACH_SURVIVE_CONTAINMENT",
|
||||||
|
"displayName": "Pit Boss",
|
||||||
|
"description": "Show that pit who is boss.",
|
||||||
|
"hidden": 0,
|
||||||
|
"icon": "https://cdn.cloudflare.steamstatic.com/steam/apps/620/capsule_184x69.jpg",
|
||||||
|
"icongray": "https://cdn.cloudflare.steamstatic.com/steam/apps/620/capsule_184x69.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ACH_CO_OP",
|
||||||
|
"displayName": "Team Building",
|
||||||
|
"description": "Complete all test chambers in the Team Building co-op course.",
|
||||||
|
"hidden": 0,
|
||||||
|
"icon": "https://cdn.cloudflare.steamstatic.com/steam/apps/620/capsule_184x69.jpg",
|
||||||
|
"icongray": "https://cdn.cloudflare.steamstatic.com/steam/apps/620/capsule_184x69.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ACH_END_SP",
|
||||||
|
"displayName": "Lunacy",
|
||||||
|
"description": "Complete the single player campaign.",
|
||||||
|
"hidden": 0,
|
||||||
|
"icon": "https://cdn.cloudflare.steamstatic.com/steam/apps/620/capsule_184x69.jpg",
|
||||||
|
"icongray": "https://cdn.cloudflare.steamstatic.com/steam/apps/620/capsule_184x69.jpg"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"response": {
|
||||||
|
"store_items": [
|
||||||
|
{
|
||||||
|
"appid": 1030300,
|
||||||
|
"success": 1,
|
||||||
|
"name": "Hollow Knight: Silksong",
|
||||||
|
"best_purchase_option": {
|
||||||
|
"formatted_final_price": "19,50€",
|
||||||
|
"formatted_original_price": "19,50€",
|
||||||
|
"discount_pct": 0
|
||||||
|
},
|
||||||
|
"assets": {
|
||||||
|
"asset_url_format": "steam/apps/1030300/${FILENAME}",
|
||||||
|
"header": "header.jpg"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appid": 1145350,
|
||||||
|
"success": 1,
|
||||||
|
"name": "Hades II",
|
||||||
|
"best_purchase_option": {
|
||||||
|
"formatted_final_price": "22,49€",
|
||||||
|
"formatted_original_price": "29,99€",
|
||||||
|
"discount_pct": 25
|
||||||
|
},
|
||||||
|
"assets": {
|
||||||
|
"asset_url_format": "steam/apps/1145350/${FILENAME}",
|
||||||
|
"header": "header.jpg"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"response": {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"appid": 1030300,
|
||||||
|
"date_added": 1735689600
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appid": 1145350,
|
||||||
|
"date_added": 1735776000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"access_token": "local",
|
||||||
|
"expires_in": 5000000,
|
||||||
|
"token_type": "bearer"
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"urlPath": "/igdb/external_games"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"bodyFileName": "igdb-external-games.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"urlPath": "/igdb/games",
|
||||||
|
"bodyPatterns": [
|
||||||
|
{
|
||||||
|
"contains": "where id = 113112;"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"bodyFileName": "igdb-game-113112.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"urlPath": "/igdb/games",
|
||||||
|
"bodyPatterns": [
|
||||||
|
{
|
||||||
|
"contains": "where id = 119388;"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"bodyFileName": "igdb-game-119388.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"urlPath": "/igdb/games",
|
||||||
|
"bodyPatterns": [
|
||||||
|
{
|
||||||
|
"contains": "where id = 26226;"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"bodyFileName": "igdb-game-26226.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"urlPath": "/igdb/games",
|
||||||
|
"bodyPatterns": [
|
||||||
|
{
|
||||||
|
"contains": "search \""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"bodyFileName": "igdb-game-search.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"urlPath": "/igdb/games",
|
||||||
|
"bodyPatterns": [
|
||||||
|
{
|
||||||
|
"contains": "fields first_release_date;"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"bodyFileName": "igdb-game-years.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"urlPath": "/igdb/games"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"jsonBody": []
|
||||||
|
},
|
||||||
|
"priority": 10
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"urlPath": "/igdb/games",
|
||||||
|
"bodyPatterns": [
|
||||||
|
{
|
||||||
|
"contains": "genres.name"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"bodyFileName": "igdb-tags.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"urlPath": "/igdb/game_time_to_beats"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"bodyFileName": "igdb-time-to-beats.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"urlPath": "/speedrun/games",
|
||||||
|
"queryParameters": {
|
||||||
|
"name": {
|
||||||
|
"equalTo": "Celeste"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"bodyFileName": "speedrun-games-celeste.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"urlPath": "/speedrun/games"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"jsonBody": {
|
||||||
|
"data": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"priority": 10
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"urlPath": "/speedrun/runs",
|
||||||
|
"queryParameters": {
|
||||||
|
"game": {
|
||||||
|
"equalTo": "o1y9j9v6"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"bodyFileName": "speedrun-runs-celeste.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"urlPath": "/speedrun/runs"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"jsonBody": {
|
||||||
|
"data": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"priority": 10
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"urlPath": "/steam/ISteamUserStats/GetGlobalAchievementPercentagesForApp/v2/",
|
||||||
|
"queryParameters": {
|
||||||
|
"gameid": {
|
||||||
|
"equalTo": "504230"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"bodyFileName": "steam-global-percentages-504230.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"urlPath": "/steam/ISteamUserStats/GetGlobalAchievementPercentagesForApp/v2/",
|
||||||
|
"queryParameters": {
|
||||||
|
"gameid": {
|
||||||
|
"equalTo": "620"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"bodyFileName": "steam-global-percentages-620.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"urlPath": "/steam/ISteamUserStats/GetGlobalAchievementPercentagesForApp/v2/"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"jsonBody": {
|
||||||
|
"achievementpercentages": {
|
||||||
|
"achievements": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"priority": 10
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"urlPath": "/steam/IPlayerService/GetOwnedGames/v1/"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"bodyFileName": "steam-owned-games.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"urlPath": "/steam/ISteamUserStats/GetPlayerAchievements/v1/",
|
||||||
|
"queryParameters": {
|
||||||
|
"appid": {
|
||||||
|
"equalTo": "504230"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"bodyFileName": "steam-player-achievements-504230.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"urlPath": "/steam/ISteamUserStats/GetPlayerAchievements/v1/",
|
||||||
|
"queryParameters": {
|
||||||
|
"appid": {
|
||||||
|
"equalTo": "620"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"bodyFileName": "steam-player-achievements-620.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"urlPath": "/steam/ISteamUserStats/GetPlayerAchievements/v1/"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 400,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"jsonBody": {
|
||||||
|
"playerstats": {
|
||||||
|
"error": "Requested app has no stats",
|
||||||
|
"success": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"priority": 10
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"urlPath": "/steam/ISteamUser/GetPlayerSummaries/v0002/"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"bodyFileName": "steam-player-summary.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"urlPath": "/steam/ISteamUserStats/GetSchemaForGame/v2/",
|
||||||
|
"queryParameters": {
|
||||||
|
"appid": {
|
||||||
|
"equalTo": "504230"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"bodyFileName": "steam-schema-504230.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"urlPath": "/steam/ISteamUserStats/GetSchemaForGame/v2/",
|
||||||
|
"queryParameters": {
|
||||||
|
"appid": {
|
||||||
|
"equalTo": "620"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"bodyFileName": "steam-schema-620.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"urlPath": "/steam/ISteamUserStats/GetSchemaForGame/v2/"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 400,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"jsonBody": {}
|
||||||
|
},
|
||||||
|
"priority": 10
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"urlPath": "/steam/IStoreBrowseService/GetItems/v1/"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"bodyFileName": "steam-store-items.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"urlPath": "/steam/IWishlistService/GetWishlist/v1/"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"bodyFileName": "steam-wishlist.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"urlPath": "/twitch/oauth2/token"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"bodyFileName": "twitch-token.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -264,7 +264,46 @@ a second row when IGDB learns its Steam appid later and the user owns it on Stea
|
|||||||
|
|
||||||
## Run locally
|
## Run locally
|
||||||
|
|
||||||
Needs Java 25 and Docker.
|
### Local development without keys (profile `local`)
|
||||||
|
|
||||||
|
The whole stack runs on your machine with no API keys and no calls to Steam, Twitch/IGDB or speedrun.com. Needs
|
||||||
|
Java 25, Node 24 and Docker. Clone `ToBePlayed-Auth`, `ToBePlayed-Core` and `ToBePlayed-Frontend` next to each
|
||||||
|
other, then start them in this order (Core also starts without Auth and finds it later):
|
||||||
|
|
||||||
|
1. **Auth**: in `ToBePlayed-Auth`, `./mvnw spring-boot:run -Dspring-boot.run.profiles=local`
|
||||||
|
(IDE: active profile `local`). Port 8080.
|
||||||
|
2. **Core**: in `ToBePlayed-Core`, `./mvnw spring-boot:run -Dspring-boot.run.profiles=local`. Port 8090.
|
||||||
|
3. **Frontend**: in `ToBePlayed-Frontend`, `npm install`, then `npm run dev`, and open `http://localhost:5173`
|
||||||
|
(`localhost`, not `127.0.0.1`, so the browser sends the login cookies to every local port). The development
|
||||||
|
values must be `VITE_API_URL=http://localhost:8080` and `VITE_CORE_API_URL=http://localhost:8090`.
|
||||||
|
4. **Log in** with `dev@localhost` / `password123`. On the dashboard, start a Steam sync: the library fills with 8
|
||||||
|
games, achievements, time to beat, moods, a wishlist and one speedrun game from recorded answers.
|
||||||
|
|
||||||
|
What the `local` profile does:
|
||||||
|
|
||||||
|
- Spring Boot's Docker Compose support starts each service's `compose.yaml`: PostgreSQL for Auth (port 5432) and
|
||||||
|
for Core (port 5433), and in Core a WireMock container (port 8089, compose profile `local`) that answers in
|
||||||
|
place of Steam, Twitch/IGDB and speedrun.com from `ToBePlayed-Core/local/wiremock`.
|
||||||
|
- Auth makes a new signing key at each start (`LocalKeysEnvironmentPostProcessor`, only in this profile), so no
|
||||||
|
key is in the repo. After an Auth restart (also a devtools restart), log in again.
|
||||||
|
- Never use `local` on a server: it adds an admin with a public password. Auth refuses `local` together with
|
||||||
|
`prod`.
|
||||||
|
- Auth adds the dev user (Flyway callback in `db/local`): role `PLATFORM_ADMIN`, so user, auditor and admin pages
|
||||||
|
can all be tried ("View as" in the menu switches down), and a linked Steam ID that the recorded Steam answers
|
||||||
|
belong to. The invite code for more users is `local`.
|
||||||
|
- Core checks the login with Auth's local JWKS, like in production.
|
||||||
|
- The browser still loads game pictures from the Steam and IGDB image servers; offline they stay gray.
|
||||||
|
- "Sign in through Steam" still opens the real Steam page (no key needed). The dev user is already linked.
|
||||||
|
- A request to WireMock that has no recording answers 404 and shows up in Core's log.
|
||||||
|
|
||||||
|
To change the recorded answers, edit the JSON in `local/wiremock/mappings` (which request) and
|
||||||
|
`local/wiremock/__files` (the answer), then restart Core (it restarts the container). `LocalMocksParseTest` checks
|
||||||
|
that every answer still fits the adapters' response types. `http://localhost:8089/__admin/requests` lists what Core
|
||||||
|
asked.
|
||||||
|
|
||||||
|
### With the real services (profile `dev`)
|
||||||
|
|
||||||
|
Needs Java 25, Docker and your own keys.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./mvnw spring-boot:run -Dspring-boot.run.profiles=dev
|
./mvnw spring-boot:run -Dspring-boot.run.profiles=dev
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import java.util.Arrays;
|
|||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -275,13 +276,14 @@ public class IgdbApiService {
|
|||||||
List<Long> ids = List.copyOf(igdbIds);
|
List<Long> ids = List.copyOf(igdbIds);
|
||||||
Map<Long, TimeToBeat> result = new HashMap<>();
|
Map<Long, TimeToBeat> result = new HashMap<>();
|
||||||
for (int from = 0; from < ids.size(); from += BATCH) {
|
for (int from = 0; from < ids.size(); from += BATCH) {
|
||||||
String gameIds = ids.subList(from, Math.min(from + BATCH, ids.size())).stream()
|
List<Long> batchIds = ids.subList(from, Math.min(from + BATCH, ids.size()));
|
||||||
.map(String::valueOf).collect(Collectors.joining(","));
|
Set<Long> batch = new HashSet<>(batchIds);
|
||||||
|
String gameIds = batchIds.stream().map(String::valueOf).collect(Collectors.joining(","));
|
||||||
IgdbResponses.TimeToBeat[] times = query("/game_time_to_beats",
|
IgdbResponses.TimeToBeat[] times = query("/game_time_to_beats",
|
||||||
"fields game_id,hastily,normally,completely,count; where game_id = (" + gameIds + "); limit " + LIMIT + ";",
|
"fields game_id,hastily,normally,completely,count; where game_id = (" + gameIds + "); limit " + LIMIT + ";",
|
||||||
IgdbResponses.TimeToBeat[].class);
|
IgdbResponses.TimeToBeat[].class);
|
||||||
for (IgdbResponses.TimeToBeat time : times) {
|
for (IgdbResponses.TimeToBeat time : times) {
|
||||||
if (time.gameId() != null) {
|
if (time.gameId() != null && batch.contains(time.gameId())) {
|
||||||
result.putIfAbsent(time.gameId(),
|
result.putIfAbsent(time.gameId(),
|
||||||
new TimeToBeat(minutes(time.hastily()), minutes(time.normally()), minutes(time.completely())));
|
new TimeToBeat(minutes(time.hastily()), minutes(time.normally()), minutes(time.completely())));
|
||||||
}
|
}
|
||||||
@@ -353,8 +355,10 @@ public class IgdbApiService {
|
|||||||
"fields genres.name,themes.name,game_modes.name; where id = (" + gameIds + "); limit " + LIMIT + ";",
|
"fields genres.name,themes.name,game_modes.name; where id = (" + gameIds + "); limit " + LIMIT + ";",
|
||||||
IgdbResponses.GameTags[].class);
|
IgdbResponses.GameTags[].class);
|
||||||
Map<Long, GameTags> result = new HashMap<>();
|
Map<Long, GameTags> result = new HashMap<>();
|
||||||
|
// Only the asked ids, in this and the other lookups: callers index their maps by them, and an answer can
|
||||||
|
// hold more games.
|
||||||
for (IgdbResponses.GameTags game : games) {
|
for (IgdbResponses.GameTags game : games) {
|
||||||
if (game.id() != null) {
|
if (game.id() != null && igdbIds.contains(game.id())) {
|
||||||
result.put(game.id(), new GameTags(names(game.genres()), names(game.themes()), names(game.gameModes())));
|
result.put(game.id(), new GameTags(names(game.genres()), names(game.themes()), names(game.gameModes())));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -371,7 +375,7 @@ public class IgdbApiService {
|
|||||||
IgdbResponses.Game[].class);
|
IgdbResponses.Game[].class);
|
||||||
Map<Long, Integer> result = new HashMap<>();
|
Map<Long, Integer> result = new HashMap<>();
|
||||||
for (IgdbResponses.Game game : games) {
|
for (IgdbResponses.Game game : games) {
|
||||||
if (game.id() != null && game.firstReleaseDate() != null) {
|
if (game.id() != null && game.firstReleaseDate() != null && igdbIds.contains(game.id())) {
|
||||||
result.put(game.id(), year(game.firstReleaseDate()));
|
result.put(game.id(), year(game.firstReleaseDate()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -397,7 +401,8 @@ public class IgdbApiService {
|
|||||||
IgdbResponses.ExternalGame[].class);
|
IgdbResponses.ExternalGame[].class);
|
||||||
Map<Long, List<Long>> steamByIgdb = new HashMap<>();
|
Map<Long, List<Long>> steamByIgdb = new HashMap<>();
|
||||||
for (IgdbResponses.ExternalGame external : externals) {
|
for (IgdbResponses.ExternalGame external : externals) {
|
||||||
if (external.game() != null && external.uid() != null && external.uid().matches("\\d+")) {
|
if (external.game() != null && external.uid() != null && external.uid().matches("\\d+")
|
||||||
|
&& steamAppids.contains(Long.parseLong(external.uid()))) {
|
||||||
steamByIgdb.computeIfAbsent(external.game(), game -> new ArrayList<>()).add(Long.parseLong(external.uid()));
|
steamByIgdb.computeIfAbsent(external.game(), game -> new ArrayList<>()).add(Long.parseLong(external.uid()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Local development without keys or third parties: start with the profile "local" (alone), after Auth's local
|
||||||
|
# profile. Postgres and WireMock (local/wiremock) come from compose.yaml. See the readme, "Run locally".
|
||||||
|
spring:
|
||||||
|
docker:
|
||||||
|
compose:
|
||||||
|
profiles:
|
||||||
|
active: local
|
||||||
|
# Also when Postgres still runs from a dev run; else WireMock would not start.
|
||||||
|
start:
|
||||||
|
skip: never
|
||||||
|
|
||||||
|
security:
|
||||||
|
jwt:
|
||||||
|
jwks-url: "http://localhost:8080/.well-known/jwks.json"
|
||||||
|
|
||||||
|
app:
|
||||||
|
cors:
|
||||||
|
allowed-origins: "http://localhost:5173"
|
||||||
|
steam:
|
||||||
|
api-key: local
|
||||||
|
base-url: "http://localhost:8089/steam"
|
||||||
|
igdb:
|
||||||
|
client-id: local
|
||||||
|
client-secret: local
|
||||||
|
base-url: "http://localhost:8089/igdb"
|
||||||
|
token-base-url: "http://localhost:8089/twitch"
|
||||||
|
speedrun:
|
||||||
|
base-url: "http://localhost:8089/speedrun"
|
||||||
|
library:
|
||||||
|
# The image repair checks the real Steam CDN.
|
||||||
|
image-repair-cron: "-"
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package core_service.configuration;
|
||||||
|
|
||||||
|
import core_service.modules.igdb_adapter.api.dto.response.IgdbResponses;
|
||||||
|
import core_service.modules.speedrun_adapter.api.dto.response.SpeedrunResponses;
|
||||||
|
import core_service.modules.steam_adapter.api.dto.response.SteamGlobalAchievementPercentagesResponse;
|
||||||
|
import core_service.modules.steam_adapter.api.dto.response.SteamOwnedGamesResponse;
|
||||||
|
import core_service.modules.steam_adapter.api.dto.response.SteamPlayerAchievementsResponse;
|
||||||
|
import core_service.modules.steam_adapter.api.dto.response.SteamPlayerSummaryResponse;
|
||||||
|
import core_service.modules.steam_adapter.api.dto.response.SteamSchemaForGameResponse;
|
||||||
|
import core_service.modules.steam_adapter.api.dto.response.SteamStoreItemsResponse;
|
||||||
|
import core_service.modules.steam_adapter.api.dto.response.SteamWishlistResponse;
|
||||||
|
import core_service.shared.test.AbstractUnitTest;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import tools.jackson.databind.JsonNode;
|
||||||
|
import tools.jackson.databind.json.JsonMapper;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.RecordComponent;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The recorded answers of the local profile (local/wiremock) still fit the adapters' response types.
|
||||||
|
*/
|
||||||
|
class LocalMocksParseTest extends AbstractUnitTest {
|
||||||
|
|
||||||
|
private static final Path FILES = Path.of("local/wiremock/__files");
|
||||||
|
private static final Path MAPPINGS = Path.of("local/wiremock/mappings");
|
||||||
|
|
||||||
|
// By file name prefix; no prefix starts another one.
|
||||||
|
private static final Map<String, Class<?>> TYPES = Map.ofEntries(
|
||||||
|
Map.entry("steam-owned-games", SteamOwnedGamesResponse.class),
|
||||||
|
Map.entry("steam-player-summary", SteamPlayerSummaryResponse.class),
|
||||||
|
Map.entry("steam-wishlist", SteamWishlistResponse.class),
|
||||||
|
Map.entry("steam-store-items", SteamStoreItemsResponse.class),
|
||||||
|
Map.entry("steam-schema-", SteamSchemaForGameResponse.class),
|
||||||
|
Map.entry("steam-global-percentages-", SteamGlobalAchievementPercentagesResponse.class),
|
||||||
|
Map.entry("steam-player-achievements-", SteamPlayerAchievementsResponse.class),
|
||||||
|
Map.entry("twitch-token", IgdbResponses.Token.class),
|
||||||
|
Map.entry("igdb-external-games", IgdbResponses.ExternalGame[].class),
|
||||||
|
Map.entry("igdb-time-to-beats", IgdbResponses.TimeToBeat[].class),
|
||||||
|
Map.entry("igdb-tags", IgdbResponses.GameTags[].class),
|
||||||
|
Map.entry("igdb-game-", IgdbResponses.Game[].class),
|
||||||
|
Map.entry("speedrun-games-", SpeedrunResponses.Games.class),
|
||||||
|
Map.entry("speedrun-runs-", SpeedrunResponses.Runs.class));
|
||||||
|
|
||||||
|
private final JsonMapper jsonMapper = JsonMapper.builder().build();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void everyMapping_PointsToAnExistingAnswer() throws IOException {
|
||||||
|
List<Path> mappings;
|
||||||
|
try (Stream<Path> listed = Files.list(MAPPINGS)) {
|
||||||
|
mappings = listed.sorted().toList();
|
||||||
|
}
|
||||||
|
assertThat(mappings).isNotEmpty();
|
||||||
|
|
||||||
|
for (Path mapping : mappings) {
|
||||||
|
JsonNode response = jsonMapper.readTree(mapping.toFile()).path("response");
|
||||||
|
assertThat(response.has("bodyFileName") || response.has("jsonBody")).as(mapping.toString()).isTrue();
|
||||||
|
if (response.has("bodyFileName")) {
|
||||||
|
assertThat(FILES.resolve(response.get("bodyFileName").asString())).as(mapping.toString()).exists();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every top-level field of an answer, and the first field (the id) of every list element, is not null.
|
||||||
|
private static void assertFilled(String name, Object parsed) throws ReflectiveOperationException {
|
||||||
|
if (parsed instanceof Object[] elements) {
|
||||||
|
assertThat(elements).as(name).isNotEmpty();
|
||||||
|
for (Object element : elements) {
|
||||||
|
RecordComponent id = element.getClass().getRecordComponents()[0];
|
||||||
|
assertThat(id.getAccessor().invoke(element)).as(name + " " + id.getName()).isNotNull();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (RecordComponent component : parsed.getClass().getRecordComponents()) {
|
||||||
|
assertThat(component.getAccessor().invoke(parsed)).as(name + " " + component.getName()).isNotNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void everyRecordedAnswer_ParsesIntoItsResponseType() throws IOException, ReflectiveOperationException {
|
||||||
|
List<Path> files;
|
||||||
|
try (Stream<Path> listed = Files.list(FILES)) {
|
||||||
|
files = listed.sorted().toList();
|
||||||
|
}
|
||||||
|
assertThat(files).isNotEmpty();
|
||||||
|
|
||||||
|
for (Path file : files) {
|
||||||
|
String name = file.getFileName().toString();
|
||||||
|
Class<?> type = TYPES.entrySet().stream()
|
||||||
|
.filter(entry -> name.startsWith(entry.getKey()))
|
||||||
|
.map(Map.Entry::getValue)
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow(() -> new AssertionError("No response type for " + name));
|
||||||
|
assertFilled(name, jsonMapper.readValue(file.toFile(), type));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -127,6 +127,38 @@ class IgdbApiServiceTest extends AbstractUnitTest {
|
|||||||
assertThat(tags.get(620L).themes()).contains("Horror");
|
assertThat(tags.get(620L).themes()).contains("Horror");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getTimeToBeat_AnswersForAppsAndGamesNotAskedFor_AreLeftOut() {
|
||||||
|
expectToken();
|
||||||
|
igdb.expect(requestTo(containsString("/external_games")))
|
||||||
|
.andRespond(withSuccess("[{\"game\": 72, \"uid\": \"620\"}, {\"game\": 73, \"uid\": \"999\"}]",
|
||||||
|
MediaType.APPLICATION_JSON));
|
||||||
|
igdb.expect(requestTo(containsString("/game_time_to_beats")))
|
||||||
|
.andRespond(withSuccess("[{\"game_id\": 72, \"normally\": 3600}, {\"game_id\": 73, \"normally\": 7200}]",
|
||||||
|
MediaType.APPLICATION_JSON));
|
||||||
|
igdb.expect(requestTo(containsString("/game_time_to_beats")))
|
||||||
|
.andRespond(withSuccess("[{\"game_id\": 1025, \"normally\": 3600}, {\"game_id\": 1026, \"normally\": 7200}]",
|
||||||
|
MediaType.APPLICATION_JSON));
|
||||||
|
|
||||||
|
assertThat(service.getTimeToBeat(List.of(620L)).orElseThrow()).containsOnlyKeys(620L);
|
||||||
|
assertThat(service.getTimeToBeatByGameIds(List.of(1025L)).orElseThrow()).containsOnlyKeys(1025L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getGameTagsAndYears_AnswerWithAGameNotAskedFor_KeepTheAskedGames() {
|
||||||
|
expectToken();
|
||||||
|
for (int call = 0; call < 2; call++) {
|
||||||
|
igdb.expect(requestTo(containsString("/external_games")))
|
||||||
|
.andRespond(withSuccess("[{\"game\": 72, \"uid\": \"620\"}]", MediaType.APPLICATION_JSON));
|
||||||
|
igdb.expect(requestTo(containsString("/games")))
|
||||||
|
.andRespond(withSuccess("[{\"id\": 72, \"first_release_date\": 1149120000, \"themes\": [{\"name\": \"Horror\"}]},"
|
||||||
|
+ " {\"id\": 999, \"first_release_date\": 1493942400}]", MediaType.APPLICATION_JSON));
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(service.getGameTags(List.of(620L)).orElseThrow().get(620L).themes()).contains("Horror");
|
||||||
|
assertThat(service.getReleaseYears(List.of(620L))).contains(Map.of(620L, Set.of(2006)));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getReleaseYears_SteamAppLinkedToTwoIgdbGames_GetsBothYears() {
|
void getReleaseYears_SteamAppLinkedToTwoIgdbGames_GetsBothYears() {
|
||||||
expectToken();
|
expectToken();
|
||||||
|
|||||||
Reference in new issue
Block a user