Add current code
This commit is contained in:
parent
093f44edf5
commit
4b80b7353b
5 changed files with 591 additions and 0 deletions
315
server.py
Executable file
315
server.py
Executable file
|
@ -0,0 +1,315 @@
|
||||||
|
#!/bin/env python3
|
||||||
|
# This code is licensed under GNU GPLv3.
|
||||||
|
import fastapi
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import httpx
|
||||||
|
import typing
|
||||||
|
import hashlib
|
||||||
|
import pydantic
|
||||||
|
from pathlib import Path
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from fastapi.responses import Response, JSONResponse
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
|
|
||||||
|
class AppIDList(pydantic.BaseModel):
|
||||||
|
app_ids: list[int] | int
|
||||||
|
|
||||||
|
|
||||||
|
app = fastapi.FastAPI(
|
||||||
|
title="How much does The Sims 4 cost?",
|
||||||
|
description="A simple API to get the price of The Sims 4 (and technically other games) on Steam. Source @ /server.py",
|
||||||
|
license_info={
|
||||||
|
"name": "GNU AGPLv3",
|
||||||
|
"identifier": "AGPL-3.0"
|
||||||
|
},
|
||||||
|
contact={
|
||||||
|
"name": "[Matrix] @nex",
|
||||||
|
"url": "https://matrix.to/#/@nex:nexy7574.co.uk"
|
||||||
|
},
|
||||||
|
version="1.2.0",
|
||||||
|
)
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=False,
|
||||||
|
allow_methods=["GET", "HEAD", "POST"],
|
||||||
|
allow_headers=["*"]
|
||||||
|
)
|
||||||
|
app.state.session = httpx.AsyncClient(http2=False)
|
||||||
|
app.state.cache = {}
|
||||||
|
app.state.ratelimit = {}
|
||||||
|
app.state.etags = {}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post(
|
||||||
|
"/api/appinfo",
|
||||||
|
responses={
|
||||||
|
200: {
|
||||||
|
"description": "Successfully fetched all of the app info IDs.",
|
||||||
|
"headers": {
|
||||||
|
"X-Ratelimit-Count": {
|
||||||
|
"schema": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"description": "The number of requests you have sent to the API in the last 30 seconds."
|
||||||
|
},
|
||||||
|
"X-Ratelimit-Remaining": {
|
||||||
|
"schema": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"description": "The number of requests you have left in the next 30 seconds."
|
||||||
|
},
|
||||||
|
"X-Ratelimit-Reset": {
|
||||||
|
"schema": {
|
||||||
|
"type": "float"
|
||||||
|
},
|
||||||
|
"description": "The unix timestamp of when the ratelimit resets."
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
304: {
|
||||||
|
"description": "The ETag provided was valid and no data was returned.",
|
||||||
|
},
|
||||||
|
429: {
|
||||||
|
"description": "You hit the anti-abuse ratelimit. Check the `Retry-After` header for more information.",
|
||||||
|
"headers": {
|
||||||
|
"Retry-After": {
|
||||||
|
"schema": {
|
||||||
|
"type": "float"
|
||||||
|
},
|
||||||
|
"description": "The amount of seconds you should wait before retrying."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
"description": "There was an upstream error."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
async def appinfo(
|
||||||
|
req: fastapi.Request,
|
||||||
|
body: AppIDList,
|
||||||
|
cc: str = fastapi.Query(None),
|
||||||
|
user_agent: str = fastapi.Header(..., alias="User-Agent"),
|
||||||
|
if_none_match: str = fastapi.Header(None, alias="If-None-Match"),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
This API endpoint proxies the call to the steam web API, and returns the data in a more usable format.
|
||||||
|
This also gets around the CORS issue that prevents this being a serverless solution.
|
||||||
|
|
||||||
|
This endpoint:
|
||||||
|
- Caches the data internally for 24 hours, regardless of etag and cache control.
|
||||||
|
- Has a ratelimit of 10 requests per 20 seconds, shared across all endpoints.
|
||||||
|
"""
|
||||||
|
if req.client.host in app.state.ratelimit:
|
||||||
|
expires = app.state.ratelimit[req.client.host]["expires"]
|
||||||
|
count = app.state.ratelimit[req.client.host]["count"]
|
||||||
|
rl_remaining = 10 - count
|
||||||
|
now = time.time()
|
||||||
|
if expires >= now:
|
||||||
|
if count >= 10:
|
||||||
|
app.state.ratelimit[req.client.host]["expires"] += (2 * (1.125 ** count))
|
||||||
|
return Response(
|
||||||
|
status_code=429,
|
||||||
|
headers={"Retry-After": str(app.state.ratelimit[req.client.host]["expires"] - now)}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
app.state.ratelimit[req.client.host]["count"] += 1
|
||||||
|
else:
|
||||||
|
del app.state.ratelimit[req.client.host]
|
||||||
|
else:
|
||||||
|
expires = time.time() + 20
|
||||||
|
app.state.ratelimit[req.client.host] = {
|
||||||
|
"expires": expires,
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
rl_remaining = 9
|
||||||
|
count = 1
|
||||||
|
|
||||||
|
auto_cc = "N/A"
|
||||||
|
if if_none_match:
|
||||||
|
if_none_match = if_none_match.replace("W/", "").strip('"')
|
||||||
|
if not cc:
|
||||||
|
_ccr = await app.state.session.get(f"https://ipinfo.io/{req.client.host}/country")
|
||||||
|
cc = _ccr.text.strip().lower()
|
||||||
|
auto_cc = cc
|
||||||
|
app_ids = body.app_ids
|
||||||
|
if isinstance(app_ids, int):
|
||||||
|
app_ids = [app_ids]
|
||||||
|
|
||||||
|
multi_fetch = len(app_ids) > 1
|
||||||
|
cache_hit = "MISS"
|
||||||
|
|
||||||
|
if if_none_match in app.state.etags:
|
||||||
|
if app.state.etags[if_none_match] > time.time():
|
||||||
|
cache_hit = "FULL-HIT"
|
||||||
|
return Response(status_code=304)
|
||||||
|
else:
|
||||||
|
del app.state.etags[if_none_match]
|
||||||
|
|
||||||
|
_md5 = hashlib.md5((",".join(map(str, app_ids)) + cc).encode(), usedforsecurity=False).hexdigest()
|
||||||
|
|
||||||
|
values = {}
|
||||||
|
remaining = app_ids.copy()
|
||||||
|
for app_id in app_ids:
|
||||||
|
if int(app_id) in app.state.cache:
|
||||||
|
data, expires = app.state.cache[app_id]
|
||||||
|
if expires > time.time():
|
||||||
|
cache_hit = "PARTIAL-HIT"
|
||||||
|
values[app_id] = data
|
||||||
|
remaining.pop(remaining.index(app_id))
|
||||||
|
else:
|
||||||
|
del app.state.cache[app_id]
|
||||||
|
values[app_id] = None
|
||||||
|
else:
|
||||||
|
values[app_id] = None
|
||||||
|
|
||||||
|
if len(remaining) == 0:
|
||||||
|
return JSONResponse(
|
||||||
|
values,
|
||||||
|
headers={
|
||||||
|
"cache-control": "public, max-age=806400",
|
||||||
|
"X-Cache": "FULL-HIT",
|
||||||
|
"X-Ratelimit-Remaining": str(rl_remaining),
|
||||||
|
"X-Ratelimit-Count": str(count),
|
||||||
|
"X-Ratelimit-Reset": str(expires),
|
||||||
|
"X-Ratelimit-Reset-After": str(expires),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
elif len(remaining) == len(app_ids):
|
||||||
|
cache_hit = "MISS"
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"appids": ",".join(map(str, remaining))
|
||||||
|
}
|
||||||
|
if cc is not None:
|
||||||
|
params["cc"] = cc
|
||||||
|
|
||||||
|
if multi_fetch:
|
||||||
|
params["filters"] = "price_overview"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"User-Agent": user_agent,
|
||||||
|
}
|
||||||
|
|
||||||
|
result = original_response = await app.state.session.get(
|
||||||
|
"https://store.steampowered.com/api/appdetails",
|
||||||
|
params=params,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
if result.status_code == 429:
|
||||||
|
return Response(status_code=429)
|
||||||
|
elif result.status_code not in [304, 200]:
|
||||||
|
raise fastapi.HTTPException(result.status_code, result.text)
|
||||||
|
else:
|
||||||
|
app.state.etag = result.headers.get("etag")
|
||||||
|
app.state.last_modified = result.headers.get("last-modified")
|
||||||
|
|
||||||
|
result = result.json()
|
||||||
|
for _app_id, data in result.items():
|
||||||
|
if "data" in data:
|
||||||
|
app.state.cache[int(_app_id)] = (data["data"], time.time() + 86400)
|
||||||
|
values[int(_app_id)] = data["data"]
|
||||||
|
|
||||||
|
app.state.etags[_md5] = time.time() + 86400
|
||||||
|
return JSONResponse(
|
||||||
|
values,
|
||||||
|
original_response.status_code,
|
||||||
|
headers={
|
||||||
|
"ETag": _md5,
|
||||||
|
"cache-control": "public, max-age=806400",
|
||||||
|
"X-Cache": str(cache_hit),
|
||||||
|
"X-IP": str(req.client.host),
|
||||||
|
"X-Ratelimit-Remaining": str(rl_remaining),
|
||||||
|
"X-Ratelimit-Count": str(count),
|
||||||
|
"X-Ratelimit-Reset": str(expires),
|
||||||
|
"X-Ratelimit-Reset-After": str(expires),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
RL_HEADERS = {
|
||||||
|
"X-Ratelimit-Throttled": {
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"description": "Whether or not you are actively being throttled."
|
||||||
|
},
|
||||||
|
"X-Ratelimit-Count": {
|
||||||
|
"schema": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"description": "The number of requests you have sent to the API in the last 30 seconds."
|
||||||
|
},
|
||||||
|
"X-Ratelimit-Remaining": {
|
||||||
|
"schema": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"description": "The number of requests you have left in the next 30 seconds."
|
||||||
|
},
|
||||||
|
"X-Ratelimit-Reset": {
|
||||||
|
"schema": {
|
||||||
|
"type": "float"
|
||||||
|
},
|
||||||
|
"description": "The unix timestamp of when the ratelimit resets."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RL_DOC = {
|
||||||
|
204: {
|
||||||
|
"description": "Successfully checked the ratelimit.",
|
||||||
|
"headers": RL_HEADERS
|
||||||
|
},
|
||||||
|
429: {
|
||||||
|
"description": "You hit the anti-abuse ratelimit. Check the `Retry-After` header for more information.",
|
||||||
|
"headers": {
|
||||||
|
**RL_HEADERS,
|
||||||
|
"Retry-After": {
|
||||||
|
"schema": {
|
||||||
|
"type": "float"
|
||||||
|
},
|
||||||
|
"description": "The amount of seconds you should wait before retrying."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.get("/api/ratelimit", status_code=204, responses=RL_DOC)
|
||||||
|
@app.head("/api/ratelimit", status_code=204, responses=RL_DOC)
|
||||||
|
async def check_ratelimit(req: fastapi.Request):
|
||||||
|
"""
|
||||||
|
Checks the current ratelimit. See GET /api/appinfo for more info.
|
||||||
|
"""
|
||||||
|
headers = {
|
||||||
|
"X-Ratelimit-Throttled": "False",
|
||||||
|
"X-Ratelimit-Remaining": "10",
|
||||||
|
"X-Ratelimit-Count": "0",
|
||||||
|
"X-Ratelimit-Reset": "0",
|
||||||
|
}
|
||||||
|
if req.client.host in app.state.ratelimit:
|
||||||
|
expires = app.state.ratelimit[req.client.host]["expires"]
|
||||||
|
count = app.state.ratelimit[req.client.host]["count"]
|
||||||
|
rl_remaining = 10 - count
|
||||||
|
|
||||||
|
headers["X-Ratelimit-Remaining"] = str(rl_remaining)
|
||||||
|
headers["X-Ratelimit-Count"] = str(count)
|
||||||
|
headers["X-Ratelimit-Reset"] = str(expires)
|
||||||
|
headers["X-Ratelimit-Throttled"] = str(rl_remaining >= 10 and expires >= time.time())
|
||||||
|
if headers["X-Ratelimit-Throttled"] == "True":
|
||||||
|
headers["Retry-After"] = str(expires - time.time())
|
||||||
|
return Response(None, 204 if headers["X-Ratelimit-Throttled"] == "False" else 429, headers=headers)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
app.mount(
|
||||||
|
"/",
|
||||||
|
StaticFiles(directory=Path.cwd() / "static", html=True, check_dir=True, follow_symlink=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
import os
|
||||||
|
print("Starting in", Path.cwd())
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=1037, forwarded_allow_ips="*")
|
BIN
static/favicon.ico
Executable file
BIN
static/favicon.ico
Executable file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
35
static/index.html
Executable file
35
static/index.html
Executable file
|
@ -0,0 +1,35 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>How much does the entire The Sims 4 game cost right now?</title>
|
||||||
|
<meta name="description" content="See how much the entirety of the game 'The Sims 4' costs right now if you were to purchase it with all DLCs!">
|
||||||
|
<!-- OpenGraph tags -->
|
||||||
|
<meta property="og:title" content="How much does the entire The Sims 4 game cost right now?">
|
||||||
|
<meta property="og:description" content="See how much the entirety of the game 'The Sims 4' costs right now if you were to purchase it with all DLCs!">
|
||||||
|
<link rel="stylesheet" href="/style.css"/>
|
||||||
|
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
|
||||||
|
</head>
|
||||||
|
<body style="text-align: center">
|
||||||
|
<div id="retrying" hidden>
|
||||||
|
<div>
|
||||||
|
<h1>There was an issue loading the data.</h1>
|
||||||
|
<p>This page will reload in <span id="retry-after">0.0</span> seconds to try again.</p>
|
||||||
|
<p><i>Error: Ratelimited (HTTP_429)</i></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<main>
|
||||||
|
<h1>Currently <span id="name">The Sims 4</span> costs<span id="dlcs" hidden> (with all <span id="dlc-count"></span>DLCs)</span></h1>
|
||||||
|
<h2 id="price">£1000.00</h2>
|
||||||
|
|
||||||
|
<div hidden id="comparison">
|
||||||
|
<p>That's equivalent to...</p>
|
||||||
|
<p><i>Note: Prices may not be accurate. See script.js for more details.</i></p>
|
||||||
|
<ul></ul>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<noscript><p>(You need to enable JavaScript to load this)</p></noscript>
|
||||||
|
<script src="./script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
179
static/script.js
Executable file
179
static/script.js
Executable file
|
@ -0,0 +1,179 @@
|
||||||
|
// This code is licensed under GNU GPLv3.
|
||||||
|
const BASE_URL = "/api/appinfo";
|
||||||
|
var TS4_APP_ID = 1222670;
|
||||||
|
const APPID_REGEX = /#(\d{5,10})/
|
||||||
|
if(APPID_REGEX.test(window.location.hash)===true) {
|
||||||
|
let matches = APPID_REGEX.exec(window.location.hash);
|
||||||
|
TS4_APP_ID = parseInt(matches[1])
|
||||||
|
console.debug("Set app ID to " + TS4_APP_ID);
|
||||||
|
} else {
|
||||||
|
console.debug("No custom ID set or does not match regex.");
|
||||||
|
}
|
||||||
|
const PRICE_ELEMENT = document.getElementById("price");
|
||||||
|
const NAME_ELEMENT = document.getElementById("name");
|
||||||
|
|
||||||
|
/*
|
||||||
|
* These prices are gathered from a range of sources, such as Tesco, Amazon, and CeX.
|
||||||
|
* Some prices may no longer be correct. They do not include discounts.
|
||||||
|
*
|
||||||
|
* Last updated: 10/12/2023
|
||||||
|
*/
|
||||||
|
const PRICES = {
|
||||||
|
"Freddo (18g)": 0.25,
|
||||||
|
"Skittles (136G)": 1.25,
|
||||||
|
"Cadbury Fingers (114G)": 1.70,
|
||||||
|
"howmuchdoesthesims4cost.lol": 1.78,
|
||||||
|
"Pepsi Max (2L)": 2.00,
|
||||||
|
"Pringles (Salt & Vinegar, 185g)": 2.25,
|
||||||
|
"BLÅHAJ Soft toy, shark (40 cm)": 22.0,
|
||||||
|
"Tenda AC10 V3.0 AC1200 Dual Band Gigabit Wireless Cable Router": 27.99,
|
||||||
|
"KIOXIA EXERIA NVMe M.2 SSD (1TB)": 43.20,
|
||||||
|
"Seagate BarraCuda,Internal Hard Drive 2.5 Inch (1TB)": 49.99,
|
||||||
|
"Kingston NV2 NVMe PCIe 4.0 Internal SSD (1TB)": 59.99,
|
||||||
|
"NVIDIA GeForce GTX 1650": 117.96,
|
||||||
|
"AMD Radeon RX 5700 8GB (Second hand from CeX)": 150.00,
|
||||||
|
"TP-Link AX5400 WiFi 6 Router": 159.99,
|
||||||
|
"Intel Core i5-13400F": 191.99,
|
||||||
|
"iPhone SE (3rd generation/5G/2022)": 499.00,
|
||||||
|
"My custom PC (https://uk.pcpartpicker.com/user/nexy7574/saved/#view=cKYV4D)": 800.0,
|
||||||
|
"NVIDIA GeForce RTX 4090": 1519.00,
|
||||||
|
}
|
||||||
|
|
||||||
|
function compare(price, item) {
|
||||||
|
let n = Math.floor((price / PRICES[item]) * 100) / 100;
|
||||||
|
if(n>=1) {
|
||||||
|
n = Math.floor(n);
|
||||||
|
};
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(n, unit="$") {
|
||||||
|
PRICE_ELEMENT.textContent = unit + n;
|
||||||
|
console.debug("Updated price to: " + n);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function ids_to_body(ids) {
|
||||||
|
return JSON.stringify({"app_ids": ids})
|
||||||
|
}
|
||||||
|
|
||||||
|
function ratelimited(response) {
|
||||||
|
let retry_after = response.headers.get("retry-after");
|
||||||
|
console.warn("Rate-limited for " + retry_after + " seconds.");
|
||||||
|
setTimeout(() => {window.location.reload()}, retry_after * 1000);
|
||||||
|
setInterval(
|
||||||
|
() => {
|
||||||
|
let element = document.getElementById("retry-after");
|
||||||
|
let value = parseFloat(element.textContent);
|
||||||
|
value -= 0.1;
|
||||||
|
element.textContent = value.toFixed(1);
|
||||||
|
},
|
||||||
|
100
|
||||||
|
)
|
||||||
|
|
||||||
|
let element = document.getElementById("retrying");
|
||||||
|
element.hidden = false;
|
||||||
|
document.getElementById("retry-after").textContent = (retry_after * 1).toFixed(1);
|
||||||
|
document.querySelector("main").hidden = true;
|
||||||
|
document.querySelector("main").style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function get_app_info(id) {
|
||||||
|
const response = await fetch(BASE_URL, {method: "POST", body: ids_to_body([id]), headers: {"Content-Type": "application/json"}});
|
||||||
|
if(response.status === 429) {
|
||||||
|
ratelimited(response);
|
||||||
|
return {id: {"name": "<rate limited>"}}
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
console.debug(data);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function calculate_price(ids) {
|
||||||
|
console.debug("Calculating price for: " + ids);
|
||||||
|
const response = await fetch(BASE_URL, {method: "POST", body: ids_to_body(ids), headers: {"Content-Type": "application/json"}});
|
||||||
|
if(response.status === 429) {
|
||||||
|
ratelimited(response);
|
||||||
|
let o = {}
|
||||||
|
for (let id of ids) {
|
||||||
|
o[id] = {"price_overview": {"final": 0.0}}
|
||||||
|
}
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
console.debug("mass price data:", data);
|
||||||
|
|
||||||
|
let _price = 0.0;
|
||||||
|
let unit = "£";
|
||||||
|
for (let value of Object.values(data)) {
|
||||||
|
console.debug("Value:", value)
|
||||||
|
if (value.price_overview == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let new_unit = value.price_overview.final_formatted.slice(0, 1);
|
||||||
|
if((/\d/).test(new_unit)) {
|
||||||
|
//new_unit = value.price_overview.final_formatted.slice(-2, -1);
|
||||||
|
new_unit = value.price_overview.currency || '£'
|
||||||
|
}
|
||||||
|
unit = new_unit
|
||||||
|
_price += parseFloat(value.price_overview.final / 100);
|
||||||
|
}
|
||||||
|
return [_price, unit];
|
||||||
|
}
|
||||||
|
|
||||||
|
function find_unit(formatted) {
|
||||||
|
let unit = "£";
|
||||||
|
if((/\d/).test(formatted.slice(0, 1))) {
|
||||||
|
unit = formatted.slice(-2, -1);
|
||||||
|
}
|
||||||
|
return unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.debug("Fetching app info for base game");
|
||||||
|
let root;
|
||||||
|
try {
|
||||||
|
root = await get_app_info(TS4_APP_ID);
|
||||||
|
} catch (e) {
|
||||||
|
alert(`Error fetching information from Steam: ${e}`);
|
||||||
|
window.location.reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
console.debug("Fetching app info for DLCs");
|
||||||
|
let key = Object.keys(root)[0];
|
||||||
|
NAME_ELEMENT.textContent = root[key].name;
|
||||||
|
console.debug("Checking key: " + key);
|
||||||
|
let dlc_price = (root[key].price_overview || {final: 0.0}).final / 100;
|
||||||
|
let unit = find_unit((root[key].price_overview || {final_formatted: "£0.00"}).final_formatted);
|
||||||
|
if(root[key].dlc && root[key].dlc.length >= 0) {
|
||||||
|
document.getElementById("dlcs").hidden = false;
|
||||||
|
document.getElementById("dlc-count").textContent = root[key].dlc.length + " ";
|
||||||
|
let _result= await calculate_price(root[key].dlc)
|
||||||
|
dlc_price += _result[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if(root[key].header_image && root[key].capsule_image) {
|
||||||
|
let element2 = document.createElement("img");
|
||||||
|
element2.src = root[key].header_image;
|
||||||
|
element2.alt = root[key].name;
|
||||||
|
element2.style.height = "2em"
|
||||||
|
NAME_ELEMENT.textContent = null;
|
||||||
|
NAME_ELEMENT.appendChild(element2);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(dlc_price.toFixed(2), unit);
|
||||||
|
|
||||||
|
let element = document.getElementById("comparison");
|
||||||
|
element.hidden = false;
|
||||||
|
for(let item of Object.keys(PRICES)) {
|
||||||
|
let price = compare(dlc_price, item);
|
||||||
|
if(price >= 0.95) {
|
||||||
|
let li = document.createElement("li");
|
||||||
|
li.textContent = `${price}x ${item} (${unit}${PRICES[item]}/pc)`;
|
||||||
|
element.children[1].appendChild(li);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
62
static/style.css
Executable file
62
static/style.css
Executable file
|
@ -0,0 +1,62 @@
|
||||||
|
:root {
|
||||||
|
--var-background: #fff;
|
||||||
|
--var-color: #000;
|
||||||
|
--var-background-inverted: #000
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--var-background: #000;
|
||||||
|
--var-color: #fff;
|
||||||
|
--var-background-inverted: #fff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: var(--var-background);
|
||||||
|
color: var(--var-color);
|
||||||
|
text-align: center;
|
||||||
|
font-size: larger;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
html, body {
|
||||||
|
font-size: medium;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, span img {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
#comparison {
|
||||||
|
text-align: left;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#name {
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
#retrying[hidden=false] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 100;
|
||||||
|
background: var(--var-background-inverted);
|
||||||
|
color: inverted(var(--var-color));
|
||||||
|
}
|
Loading…
Reference in a new issue