Initial OnlyScavs: keys, ratings, grid icons
This commit is contained in:
14
README.md
Normal file
14
README.md
Normal 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
110
app.py
Normal 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
125
import_keys.py
Normal 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
27
migrations_v1.sql
Normal 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');
|
||||||
237
templates/index.html
Normal file
237
templates/index.html
Normal 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>
|
||||||
Reference in New Issue
Block a user