Initial OnlyScavs: keys, ratings, grid icons

This commit is contained in:
serversdwn
2026-01-25 08:34:42 +00:00
commit 3c6a816942
6 changed files with 513 additions and 0 deletions

14
README.md Normal file
View File

@@ -0,0 +1,14 @@
##Place hoder, for this here personalized tarkov DB im building.
##Items to be tracked = ##
**Weight Management Mostly**
--Weapons and weapons parts
--helmets + armor and rigs
--backpacks
**Keys**
--full list w/ locations and whats behind the lock
-- vendor price
-- My personal 0-4 priority scale.
-- Flag useless keys.

110
app.py Normal file
View File

@@ -0,0 +1,110 @@
from flask import Flask, render_template, request, redirect, url_for
import sqlite3
app = Flask(__name__)
DB_PATH = "tarkov.db"
def get_db():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
@app.route("/")
def index():
conn = get_db()
maps = conn.execute("""
SELECT id, name
FROM maps
ORDER BY name
""").fetchall()
map_filter = request.args.get("map_id", type=int)
key_map_rows = conn.execute("""
SELECT key_id, map_id
FROM key_maps
""").fetchall()
key_maps = {}
for row in key_map_rows:
key_maps.setdefault(row["key_id"], set()).add(row["map_id"])
key_query = """
SELECT
k.id,
k.name,
k.icon_url,
k.grid_image_url,
k.wiki_url,
r.priority,
r.reason,
COALESCE(r.used_in_quest, 0) AS used_in_quest
FROM keys k
"""
params = []
if map_filter:
key_query += """
JOIN key_maps kmf
ON k.id = kmf.key_id
AND kmf.map_id = ?
"""
params.append(map_filter)
key_query += """
LEFT JOIN key_ratings r ON k.id = r.key_id
ORDER BY
CASE WHEN r.priority IS NULL THEN 1 ELSE 0 END,
r.priority DESC,
k.name
"""
keys = conn.execute(key_query, params).fetchall()
conn.close()
key_maps = {k: sorted(v) for k, v in key_maps.items()}
return render_template(
"index.html",
keys=keys,
maps=maps,
key_maps=key_maps,
map_filter=map_filter,
)
@app.route("/rate", methods=["POST"])
def rate_key():
key_id = request.form["key_id"]
priority = request.form["priority"]
reason = request.form.get("reason", "")
used_in_quest = 1 if request.form.get("used_in_quest") == "on" else 0
map_filter = request.form.get("map_id")
map_ids = []
for value in request.form.getlist("map_ids"):
try:
map_ids.append(int(value))
except ValueError:
continue
conn = get_db()
conn.execute("""
INSERT INTO key_ratings (key_id, priority, reason, used_in_quest)
VALUES (?, ?, ?, ?)
ON CONFLICT(key_id) DO UPDATE SET
priority = excluded.priority,
reason = excluded.reason,
used_in_quest = excluded.used_in_quest,
updated_at = CURRENT_TIMESTAMP
""", (key_id, priority, reason, used_in_quest))
conn.execute("DELETE FROM key_maps WHERE key_id = ?", (key_id,))
if map_ids:
conn.executemany(
"INSERT OR IGNORE INTO key_maps (key_id, map_id) VALUES (?, ?)",
[(key_id, map_id) for map_id in map_ids],
)
conn.commit()
conn.close()
if map_filter:
return redirect(url_for("index", map_id=map_filter))
return redirect(url_for("index"))
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)

125
import_keys.py Normal file
View File

@@ -0,0 +1,125 @@
import requests
import sqlite3
import sys
DB_PATH = "tarkov.db"
API_URL = "https://api.tarkov.dev/graphql"
GRAPHQL_QUERY = """
query {
items(types: [keys]) {
id
name
shortName
weight
wikiLink
gridImageLink
properties {
... on ItemPropertiesKey {
uses
}
}
}
}
"""
def fetch_keys():
response = requests.post(
API_URL,
json={"query": GRAPHQL_QUERY},
timeout=30
)
response.raise_for_status()
data = response.json()
if "errors" in data:
raise RuntimeError(data["errors"])
return data["data"]["items"]
def upsert_keys(conn, keys):
inserted = 0
updated = 0
skipped = 0
cursor = conn.cursor()
for k in keys:
api_id = k.get("id")
name = k.get("name")
short_name = k.get("shortName")
weight = k.get("weight")
wiki_url = k.get("wikiLink")
grid_image_url = k.get("gridImageLink")
uses = None
props = k.get("properties")
if props and "uses" in props:
uses = props["uses"]
if not api_id or not name:
skipped += 1
continue
cursor.execute(
"""
SELECT id FROM keys WHERE api_id = ?
""",
(api_id,)
)
row = cursor.fetchone()
if row:
cursor.execute(
"""
UPDATE keys
SET name = ?, short_name = ?, weight_kg = ?, uses = ?, wiki_url = ?, grid_image_url = ?
WHERE api_id = ?
""",
(name, short_name, weight, uses, wiki_url, grid_image_url, api_id)
)
updated += 1
else:
cursor.execute(
"""
INSERT INTO keys (api_id, name, short_name, weight_kg, uses)
VALUES (?, ?, ?, ?, ?)
""",
(api_id, name, short_name, weight, uses, icon_url, wiki_url)
)
inserted += 1
conn.commit()
return inserted, updated, skipped
def main():
print("Fetching keys from Tarkov.dev...")
try:
keys = fetch_keys()
except Exception as e:
print("ERROR: Failed to fetch keys")
print(e)
sys.exit(1)
print(f"Fetched {len(keys)} keys")
conn = sqlite3.connect(DB_PATH)
conn.execute("PRAGMA foreign_keys = ON")
try:
inserted, updated, skipped = upsert_keys(conn, keys)
except Exception as e:
conn.rollback()
print("ERROR: Database operation failed")
print(e)
sys.exit(1)
finally:
conn.close()
print("Import complete")
print(f"Inserted: {inserted}")
print(f"Updated: {updated}")
print(f"Skipped: {skipped}")
if __name__ == "__main__":
main()

27
migrations_v1.sql Normal file
View File

@@ -0,0 +1,27 @@
-- V1: maps tagging + used_in_quest flag
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS maps (
id INTEGER PRIMARY KEY,
name TEXT UNIQUE
);
CREATE TABLE IF NOT EXISTS key_maps (
key_id INTEGER NOT NULL,
map_id INTEGER NOT NULL,
PRIMARY KEY (key_id, map_id),
FOREIGN KEY (key_id) REFERENCES keys(id),
FOREIGN KEY (map_id) REFERENCES maps(id)
);
ALTER TABLE key_ratings ADD COLUMN used_in_quest INTEGER DEFAULT 0;
INSERT OR IGNORE INTO maps (name) VALUES
('Customs'),
('Factory'),
('Shoreline'),
('Interchange'),
('Reserve'),
('Woods'),
('Labs'),
('Streets');

BIN
tarkov.db Normal file

Binary file not shown.

237
templates/index.html Normal file
View File

@@ -0,0 +1,237 @@
<!doctype html>
<html>
<head>
<title>OnlyScavs Key Ratings</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
:root {
--bg: #121212;
--panel: #1a1a1a;
--text: #eee;
--muted: #bbb;
--border: #333;
--accent: #9ccfff;
}
body {
font-family: sans-serif;
background: var(--bg);
color: var(--text);
margin: 0;
padding: 16px;
}
.page {
max-width: 980px;
margin: 0 auto;
}
.key {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 8px;
border-bottom: 1px solid var(--border);
}
.key-name {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.key-name strong {
line-height: 1.2;
word-break: break-word;
}
.key-name a {
color: var(--accent);
font-size: 0.9rem;
}
.filters {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
margin: 12px 0 20px;
}
.map-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
color: var(--muted);
font-size: 0.85rem;
}
.map-tag {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 999px;
padding: 2px 8px;
line-height: 1.2;
}
img {
width: 64px;
height: 64px;
border-radius: 6px;
background: var(--panel);
}
select, input {
background: #222;
color: var(--text);
border: 1px solid #444;
border-radius: 6px;
padding: 8px;
}
a {
color: var(--accent);
}
button {
background: #333;
color: var(--text);
border: 1px solid #555;
cursor: pointer;
padding: 8px 12px;
border-radius: 6px;
}
form {
display: flex;
align-items: center;
gap: 8px;
}
.key-form {
flex: 1;
flex-wrap: wrap;
align-items: flex-start;
}
input[name="reason"] {
min-width: 180px;
}
.map-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
width: 100%;
}
.map-checkbox,
.quest-flag {
display: flex;
align-items: center;
gap: 6px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 6px;
padding: 4px 8px;
}
.map-checkbox input,
.quest-flag input {
margin: 0;
}
@media (max-width: 720px) {
body {
padding: 12px;
}
.key {
align-items: flex-start;
gap: 12px;
}
form {
flex-wrap: wrap;
justify-content: flex-start;
gap: 8px;
}
select, input, button {
min-height: 40px;
font-size: 1rem;
}
input[name="reason"] {
flex: 1 1 100%;
min-width: 0;
}
}
@media (max-width: 480px) {
.key {
flex-direction: column;
align-items: stretch;
}
img {
width: 72px;
height: 72px;
}
}
</style>
</head>
<body>
<div class="page">
<h1>OnlyScavs Keys</h1>
<form method="get" class="filters">
<label for="map_id">Filter by map</label>
<select id="map_id" name="map_id">
<option value="">All maps</option>
{% for map in maps %}
<option value="{{ map.id }}" {% if map_filter == map.id %}selected{% endif %}>
{{ map.name }}
</option>
{% endfor %}
</select>
<button type="submit">Apply</button>
</form>
{% for key in keys %}
<div class="key">
<img
src="{{ key.grid_image_url }}"
loading="lazy"
>
<div class="key-name" style="flex:1">
<strong>{{ key.name }}</strong>
{% if key.wiki_url %}
<a href="{{ key.wiki_url }}" target="_blank">wiki</a>
{% endif %}
{% set selected_maps = key_maps.get(key.id, []) %}
{% if selected_maps %}
<div class="map-tags">
{% for map in maps %}
{% if map.id in selected_maps %}
<span class="map-tag">{{ map.name }}</span>
{% endif %}
{% endfor %}
</div>
{% endif %}
</div>
<form method="post" action="/rate" class="key-form">
<input type="hidden" name="key_id" value="{{ key.id }}">
{% if map_filter %}
<input type="hidden" name="map_id" value="{{ map_filter }}">
{% endif %}
<select name="priority">
{% for i, label in [(0,'IGNORE'),(1,'LOW'),(2,'MED'),(3,'HIGH'),(4,'SUPER')] %}
<option value="{{ i }}" {% if key.priority == i %}selected{% endif %}>
{{ label }}
</option>
{% endfor %}
</select>
<label class="quest-flag">
<input type="checkbox" name="used_in_quest" {% if key.used_in_quest %}checked{% endif %}>
<span>Used in quest?</span>
</label>
<input name="reason" placeholder="note…" value="{{ key.reason or '' }}">
<div class="map-list">
{% for map in maps %}
<label class="map-checkbox">
<input
type="checkbox"
name="map_ids"
value="{{ map.id }}"
{% if map.id in selected_maps %}checked{% endif %}
>
<span>{{ map.name }}</span>
</label>
{% endfor %}
</div>
<button type="submit">Save</button>
</form>
</div>
{% endfor %}
</div>
</body>
</html>