first commit

This commit is contained in:
archon0ne 2024-07-03 11:11:14 +02:00
commit 32964d1d1f
5 changed files with 566 additions and 0 deletions

227
app.py Normal file
View File

@ -0,0 +1,227 @@
from flask import Flask, render_template, request, redirect, url_for, jsonify
import json
import requests
import logging
import os
import traceback
app = Flask(__name__)
# Setup logging to file
logging.basicConfig(filename='log.log', level=logging.DEBUG, format='%(asctime)s %(levelname)s:%(message)s')
# Load the assort data
ASSORT_FILE_PATH = 'assort.json'
QUEST_ASSORT_FILE_PATH = 'questassort.json'
CACHE_FILE_PATH = 'item_cache.json'
RUBLE_TPL_ID = '5449016a4bdc2d6f028b456f'
try:
with open(ASSORT_FILE_PATH) as f:
assort_data = json.load(f)
logging.debug("Assort data loaded successfully")
except Exception as e:
logging.error(f"Error loading assort data: {e}")
logging.error(traceback.format_exc())
try:
with open(QUEST_ASSORT_FILE_PATH) as f:
quest_assort_data = json.load(f)
logging.debug("Quest assort data loaded successfully")
except Exception as e:
logging.error(f"Error loading quest assort data: {e}")
logging.error(traceback.format_exc())
# Load cache or initialize an empty dictionary
if os.path.exists(CACHE_FILE_PATH):
try:
with open(CACHE_FILE_PATH) as f:
item_cache = json.load(f)
logging.debug("Item cache loaded successfully")
except Exception as e:
logging.error(f"Error loading item cache: {e}")
logging.error(traceback.format_exc())
item_cache = {}
else:
item_cache = {}
logging.debug("Initialized empty item cache")
# Tarkov.dev API URL
TARKOV_API_URL = "https://api.tarkov.dev/graphql"
def get_ruble_image():
return get_item_details_cached(RUBLE_TPL_ID)
@app.route('/')
def index():
try:
items = get_main_items_with_details(assort_data)
ruble_image = get_ruble_image()
return render_template('index.html', items=items, ruble_image=ruble_image)
except Exception as e:
logging.error(f"Error in index route: {e}")
logging.error(traceback.format_exc())
return "Error loading items", 500
def get_main_items_with_details(assort_data):
items = []
for item in assort_data['items']:
if item['parentId'] == 'hideout':
item_copy = item.copy() # Create a copy of the item to avoid modifying the original
item_copy['details'] = get_item_details_cached(item_copy['_tpl'])
item_copy['parts'] = get_item_parts_with_details(item_copy['_id'], assort_data)
item_copy['barter_scheme'] = get_barter_scheme_with_details(item_copy['_id'], assort_data)
item_copy['quest_requirement'] = get_quest_requirement(item_copy['_id'])
item_copy['offer_name'] = item_copy['_id'] # Add offer name
items.append(item_copy)
logging.debug(f"Main items with details: {items}")
return items
def get_item_details_cached(tpl):
if tpl in item_cache:
logging.debug(f"Cache hit for tpl {tpl}")
return item_cache[tpl]
else:
logging.debug(f"Cache miss for tpl {tpl}, fetching from API")
item_details = get_items_details([tpl])
if tpl in item_details:
item_cache[tpl] = item_details[tpl]
save_item_cache()
return item_details.get(tpl, {})
def get_items_details(tpls):
queries = "\n".join([
f'item{index}: item(id: "{tpl}") {{ id name shortName description basePrice image512pxLink wikiLink }}'
for index, tpl in enumerate(tpls)
])
query = f"{{ {queries} }}"
try:
response = requests.post(TARKOV_API_URL, json={'query': query})
data = response.json()
logging.debug(f"Item details for tpls {tpls}: {data}")
return {tpl: data['data'][f'item{index}'] for index, tpl in enumerate(tpls)}
except Exception as e:
logging.error(f"Error fetching item details for tpls {tpls}: {e}")
logging.error(traceback.format_exc())
return {}
def get_item_parts_with_details(parent_id, assort_data):
parts = []
def fetch_parts(parent_id):
sub_parts = []
for item in assort_data['items']:
if item['parentId'] == parent_id:
part_copy = item.copy() # Create a copy of the part to avoid modifying the original
part_copy['details'] = get_item_details_cached(part_copy['_tpl'])
part_copy['parts'] = fetch_parts(part_copy['_id']) # Fetch sub-parts recursively
sub_parts.append(part_copy)
return sub_parts
parts = fetch_parts(parent_id)
logging.debug(f"Parts for parent_id {parent_id}: {parts}")
return parts
def get_barter_scheme_with_details(item_id, assort_data):
barter_scheme = []
if 'barter_scheme' in assort_data:
for scheme in assort_data['barter_scheme'].get(item_id, []):
scheme_details = []
for req in scheme:
req_copy = req.copy() # Create a copy of the req to avoid modifying the original
req_details = get_item_details_cached(req_copy['_tpl'])
req_copy['details'] = req_details
scheme_details.append(req_copy)
barter_scheme.append(scheme_details)
logging.debug(f"Barter scheme for item_id {item_id}: {barter_scheme}")
return barter_scheme
def get_quest_requirement(item_id):
for quest_type, quests in quest_assort_data.items():
if item_id in quests:
return {
"type": quest_type,
"quest": quests[item_id]
}
return None
@app.route('/edit/<item_id>', methods=['GET', 'POST'])
def edit_item(item_id):
try:
if request.method == 'POST':
# Handle form submission
new_barter_scheme = []
tpl_list = request.form.getlist('barter_tpl')
count_list = request.form.getlist('barter_count')
scheme = []
for i in range(len(tpl_list)):
scheme.append({
'_tpl': tpl_list[i],
'count': int(count_list[i])
})
new_barter_scheme.append(scheme)
update_barter_scheme(item_id, new_barter_scheme)
return redirect(url_for('index'))
item = next((item for item in assort_data['items'] if item['_id'] == item_id), None)
if item:
item_copy = item.copy() # Create a copy of the item to avoid modifying the original
item_copy['details'] = get_item_details_cached(item_copy['_tpl'])
item_copy['parts'] = get_item_parts_with_details(item_id, assort_data)
item_copy['barter_scheme'] = get_barter_scheme_with_details(item_id, assort_data)
item_copy['quest_requirement'] = get_quest_requirement(item_id)
ruble_image = get_ruble_image()
return render_template('edit.html', item=item_copy, ruble_image=ruble_image)
else:
return "Item not found", 404
except Exception as e:
logging.error(f"Error in edit_item route: {e}")
logging.error(traceback.format_exc())
return "Error editing item", 500
def update_barter_scheme(item_id, new_barter_scheme):
try:
assort_data['barter_scheme'][item_id] = new_barter_scheme
save_assort_data()
except Exception as e:
logging.error(f"Error updating barter scheme for item {item_id}: {e}")
logging.error(traceback.format_exc())
def save_assort_data():
try:
# Deep copy the assort data and remove 'details' key before saving
cleaned_assort_data = json.loads(json.dumps(assort_data)) # Deep copy
# Clean items
for item in cleaned_assort_data['items']:
if 'details' in item:
del item['details']
# Clean barter_scheme
for item_id, schemes in cleaned_assort_data.get('barter_scheme', {}).items():
for scheme in schemes:
for part in scheme:
if 'details' in part:
del part['details']
with open(ASSORT_FILE_PATH, 'w') as f:
json.dump(cleaned_assort_data, f, indent=2) # Use 2 spaces for indentation
logging.debug("Assort data saved successfully")
except Exception as e:
logging.error(f"Error saving assort data: {e}")
logging.error(traceback.format_exc())
def save_item_cache():
try:
with open(CACHE_FILE_PATH, 'w') as f:
json.dump(item_cache, f, indent=2) # Use 2 spaces for indentation
logging.debug("Item cache saved successfully")
except Exception as e:
logging.error(f"Error saving item cache: {e}")
logging.error(traceback.format_exc())
@app.route('/item_image/<tpl>')
def item_image(tpl):
details = get_item_details_cached(tpl)
return jsonify({'image512pxLink': details.get('image512pxLink', ''), 'name': details.get('name', '')})
if __name__ == '__main__':
app.run(debug=True)

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
Flask>=2.0.3
requests>=2.26.0

179
templates/edit.html Normal file
View File

@ -0,0 +1,179 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Edit Item</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
<style>
.price-box {
border: 1px solid #ccc;
padding: 10px;
margin-bottom: 15px;
display: flex;
align-items: center;
background-color: #f8f9fa;
}
.price-box img {
width: 30px;
margin-right: 10px;
}
.price-box span {
font-size: 1.2em;
font-weight: bold;
}
.price-box small {
display: block;
font-size: 0.8em;
color: #888;
}
.card {
margin-bottom: 15px;
}
.card-header img {
width: 45px;
margin-right: 10px;
}
.parts-tree {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.parts-tree > li {
margin-right: 10px;
}
.part-item {
display: flex;
flex-direction: row;
align-items: center;
background-color: #f8f9fa;
padding: 5px;
border-radius: 5px;
margin-bottom: 5px;
}
.part-item img {
width: 30px;
margin-right: 5px;
}
</style>
</head>
<body>
<div class="container">
<h1>Edit {{ item.details.name }}</h1>
<div class="form-group">
<label for="main-item">Main Item</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">
<img src="{{ item.details.image512pxLink }}" alt="{{ item.details.name }}" style="width: 50px;">
</span>
</div>
<input type="text" class="form-control" id="main-item" name="main-item" value="{{ item.details.name }}" readonly>
</div>
</div>
<div class="price-box">
<img src="{{ ruble_image.image512pxLink }}" alt="RUB">
<div>
<span>{{ item.details.basePrice }} RUB</span>
<small>Base Price (for reference)</small>
</div>
</div>
{% if item.parts|length > 0 %}
<h2>
<button class="btn btn-outline-primary-custom" type="button" data-toggle="collapse" data-target="#parts-{{ item._id }}" aria-expanded="false" aria-controls="parts-{{ item._id }}">
Parts
</button>
</h2>
<div id="parts-{{ item._id }}" class="collapse">
<ul class="parts-tree">
{% include 'parts.html' with context %}
</ul>
</div>
{% endif %}
<h2>Barter Scheme</h2>
<form method="POST" id="barter-form">
<div id="barter-scheme">
{% for scheme in item.barter_scheme %}
<div class="form-group barter-item" data-index="{{ loop.index0 }}">
{% for req in scheme %}
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">
<img src="{{ req.details.image512pxLink }}" alt="{{ req.details.name }}" style="width: 30px;" id="barter_img_{{ loop.index0 }}">
</span>
</div>
<input type="text" class="form-control barter_tpl" id="barter_tpl_{{ loop.index0 }}" name="barter_tpl" value="{{ req._tpl }}" required>
<input type="number" class="form-control barter_count" id="barter_count_{{ loop.index0 }}" name="barter_count" value="{{ req.count }}" required>
<div class="input-group-append">
<button class="btn btn-danger remove-barter-item" type="button">Remove</button>
</div>
</div>
{% endfor %}
</div>
{% endfor %}
</div>
<button type="button" class="btn btn-primary" id="add-barter-item">Add Barter Item</button>
<button type="submit" class="btn btn-success">Save</button>
<a href="{{ url_for('index') }}" class="btn btn-secondary">Cancel</a>
</form>
{% if item.quest_requirement %}
<h2>Quest Requirement</h2>
<p>
<strong>Quest Requirement:</strong>
{{ item.quest_requirement.quest }}
{% if item.quest_requirement.type == 'success' %}
<i class="fas fa-check-circle text-success"></i>
{% elif item.quest_requirement.type == 'fail' %}
<i class="fas fa-times-circle text-danger"></i>
{% elif item.quest_requirement.type == 'started' %}
<i class="fas fa-question-circle text-primary"></i>
{% endif %}
</p>
{% endif %}
</div>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
<script>
$(document).ready(function() {
let barterIndex = {{ item.barter_scheme|length }};
$('#add-barter-item').click(function() {
const newItem = `
<div class="form-group barter-item" data-index="${barterIndex}">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">
<img src="" alt="Barter Item" style="width: 30px;" id="barter_img_${barterIndex}">
</span>
</div>
<input type="text" class="form-control barter_tpl" id="barter_tpl_${barterIndex}" name="barter_tpl" required>
<input type="number" class="form-control barter_count" id="barter_count_${barterIndex}" name="barter_count" required>
<div class="input-group-append">
<button class="btn btn-danger remove-barter-item" type="button">Remove</button>
</div>
</div>`;
$('#barter-scheme').append(newItem);
barterIndex++;
});
$(document).on('click', '.remove-barter-item', function() {
if ($('.barter-item').length > 1) {
$(this).closest('.barter-item').remove();
} else {
alert("You must have at least one barter item.");
}
});
$(document).on('change', '.barter_tpl', function() {
const index = $(this).closest('.barter-item').data('index');
const tpl = $(this).val();
$.get(`/item_image/${tpl}`, function(data) {
$(`#barter_img_${index}`).attr('src', data.image512pxLink);
});
});
});
</script>
</body>
</html>

142
templates/index.html Normal file
View File

@ -0,0 +1,142 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Trader Assort</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
<style>
.price-box {
display: flex;
align-items: center;
flex-direction: column;
background-color: #f8f9fa;
padding: 5px;
border-radius: 5px;
margin-bottom: 15px;
}
.price-box img {
width: 30px;
margin-right: 10px;
}
.price-box span {
font-size: 1.2em;
font-weight: bold;
}
.price-box small {
font-size: 0.8em;
color: #888;
}
.card {
margin-bottom: 15px;
}
.card-header img {
width: 45px;
margin-right: 10px;
}
.barter-list {
list-style-type: none;
padding-left: 0;
display: flex;
flex-direction: row;
}
.barter-list li {
display: flex;
align-items: center;
}
.barter-list li img {
width: 45px;
margin-right: 10px;
}
.parts-tree {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.parts-tree > li {
margin-right: 10px;
}
.part-item {
display: flex;
flex-direction: row;
align-items: center;
background-color: #f8f9fa;
padding: 5px;
border-radius: 5px;
margin-bottom: 5px;
}
.part-item img {
width: 30px;
margin-right: 5px;
}
</style>
</head>
<body>
<div class="container">
<h1>Trader Assort</h1>
<div class="list-group">
{% for item in items %}
<div class="card">
<div class="card-header d-flex align-items-center">
<img src="{{ item.details.image512pxLink }}" alt="{{ item.details.name }}">
<strong>{{ item.details.name }}</strong>
<div class="ml-2 text-muted">{{ item.offer_name }}</div> <!-- Add offer name here -->
<div class="price-box ml-auto">
<div><img src="{{ ruble_image.image512pxLink }}" alt="RUB"><span>{{ item.details.basePrice }} RUB</span></div>
<small>Base Price (for reference)</small>
</div>
</div>
<div class="card-body">
<a href="{{ url_for('edit_item', item_id=item._id) }}" class="btn btn-primary btn-sm float-right">Edit</a>
{% if item.parts|length > 0 %}
<h2>
<button class="btn btn-outline-primary" type="button" data-toggle="collapse" data-target="#parts-{{ item._id }}" aria-expanded="false" aria-controls="parts-{{ item._id }}">
Parts
</button>
</h2>
<div id="parts-{{ item._id }}" class="collapse">
<ul class="parts-tree">
{% include 'parts.html' with context %}
</ul>
</div>
{% endif %}
{% if item.barter_scheme|length > 0 %}
<div><strong>Barter Requirements:</strong></div>
<ul class="barter-list">
{% for scheme in item.barter_scheme %}
{% for req in scheme %}
<li>
<span>{{ req.count }} ×</span>
<img src="{{ req.details.image512pxLink }}" alt="{{ req.details.name }}">
</li>
{% endfor %}
{% endfor %}
</ul>
{% endif %}
{% if item.quest_requirement %}
<p>
<strong>Quest Requirement:</strong>
{{ item.quest_requirement.quest }}
{% if item.quest_requirement.type == 'success' %}
<i class="fas fa-check-circle text-success"></i>
{% elif item.quest_requirement.type == 'fail' %}
<i class="fas fa-times-circle text-danger"></i>
{% elif item.quest_requirement.type == 'started' %}
<i class="fas fa-question-circle text-primary"></i>
{% endif %}
</p>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
</body>
</html>

16
templates/parts.html Normal file
View File

@ -0,0 +1,16 @@
{% macro render_parts(parts) %}
{% for part in parts %}
<li class="part-item">
<img src="{{ part.details.image512pxLink }}" alt="{{ part.details.name }}">
<div>
<strong>{{ part.details.shortName }}</strong>
{% if part.parts|length > 0 %}
<ul class="parts-tree">
{{ render_parts(part.parts) }}
</ul>
{% endif %}
</div>
</li>
{% endfor %}
{% endmacro %}
{{ render_parts(item.parts) }}