remove metacritic because i cannot get it done (no real API) - sorry
This commit is contained in:
parent
4522d51f47
commit
49fdd243d0
387
setup.sh
387
setup.sh
|
@ -7,7 +7,7 @@ GREEN='\033[1;32m'
|
|||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
# 1. Docker check (incl. Arch Linux)
|
||||
# Docker check (incl. Arch Linux)
|
||||
if ! command -v docker &>/dev/null; then
|
||||
echo -e "${RED}❗ Docker is not installed.${NC}"
|
||||
read -p "Would you like to install Docker automatically now? [y/N]: " install_docker
|
||||
|
@ -23,7 +23,7 @@ if ! command -v docker &>/dev/null; then
|
|||
rm get-docker.sh
|
||||
fi
|
||||
|
||||
# Docker group membership prüfen
|
||||
# Docker group membership check
|
||||
if ! groups | grep -q '\bdocker\b'; then
|
||||
echo -e "${YELLOW}⚠️ Your user is not in the docker group. Adding now...${NC}"
|
||||
sudo usermod -aG docker $USER
|
||||
|
@ -37,7 +37,7 @@ if ! command -v docker &>/dev/null; then
|
|||
fi
|
||||
fi
|
||||
|
||||
# 2. Check Docker compose (V1 und V2 Plugin, incl. Arch Support)
|
||||
# Check Docker compose (V1 und V2 Plugin, incl. Arch Support)
|
||||
DOCKER_COMPOSE_CMD=""
|
||||
if command -v docker-compose &>/dev/null; then
|
||||
DOCKER_COMPOSE_CMD="docker-compose"
|
||||
|
@ -88,16 +88,7 @@ chmod -R a+rwX "$TRANSLATIONS_DIR" "$DATA_DIR"
|
|||
|
||||
cd $PROJECT_DIR
|
||||
|
||||
## UID/GID-Logic
|
||||
#if [ "$(id -u)" -eq 0 ]; then
|
||||
# export UID=1000
|
||||
# export GID=1000
|
||||
#else
|
||||
# export UID=$(id -u)
|
||||
# export GID=$(id -g)
|
||||
#fi
|
||||
|
||||
# 2. requirements.txt
|
||||
# requirements.txt
|
||||
cat <<EOL > requirements.txt
|
||||
flask
|
||||
flask-login
|
||||
|
@ -122,7 +113,7 @@ redis
|
|||
EOL
|
||||
|
||||
|
||||
# 3. create .env
|
||||
# create .env
|
||||
SECRET_KEY=$(python3 -c 'import secrets; print(secrets.token_hex(24))')
|
||||
REDEEM_SECRET=$(python3 -c 'import secrets; print(secrets.token_hex(16))')
|
||||
REDEEM_CSRF=$(python3 -c 'import secrets; print(secrets.token_hex(16))')
|
||||
|
@ -154,7 +145,7 @@ CHECK_EXPIRING_KEYS_INTERVAL_HOURS=6
|
|||
ITAD_API_KEY="your-secret-key-here"
|
||||
ITAD_COUNTRY="DE"
|
||||
|
||||
# Apprise URLs (separate several with a line break, comma or space)
|
||||
# Apprise URLs (separate several with a comma or space)
|
||||
APPRISE_URLS=""
|
||||
|
||||
### example for multiple notifications
|
||||
|
@ -170,86 +161,91 @@ FLASK_DEBUG=1
|
|||
DEBUGPY=1
|
||||
EOL
|
||||
|
||||
# 4. app.py (the main app)
|
||||
# app.py (the main app)
|
||||
cat <<'PYTHON_END' > app.py
|
||||
import os, time
|
||||
# Standards
|
||||
import atexit
|
||||
import csv
|
||||
import io
|
||||
import locale
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import secrets
|
||||
import sqlite3
|
||||
import time
|
||||
import traceback
|
||||
from datetime import datetime, timedelta
|
||||
from functools import wraps
|
||||
from io import BytesIO
|
||||
from time import sleep
|
||||
from urllib.parse import urlparse
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
# 3rd-Provider-Modules
|
||||
import pytz
|
||||
import warnings
|
||||
from sqlalchemy.exc import LegacyAPIWarning
|
||||
warnings.simplefilter("ignore", category=LegacyAPIWarning)
|
||||
import requests
|
||||
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from dotenv import load_dotenv
|
||||
from flask import (
|
||||
Flask,
|
||||
Markup,
|
||||
abort,
|
||||
flash,
|
||||
g,
|
||||
jsonify,
|
||||
make_response,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
redirect,
|
||||
url_for,
|
||||
flash,
|
||||
session,
|
||||
abort,
|
||||
send_file,
|
||||
jsonify,
|
||||
Markup,
|
||||
make_response,
|
||||
g,
|
||||
abort
|
||||
session,
|
||||
url_for
|
||||
)
|
||||
from flask_login import (
|
||||
LoginManager,
|
||||
UserMixin,
|
||||
current_user,
|
||||
login_required,
|
||||
login_user,
|
||||
logout_user
|
||||
)
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_session import Session
|
||||
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from flask_wtf import CSRFProtect
|
||||
from flask import abort
|
||||
from flask import request, redirect
|
||||
from flask_wtf import FlaskForm
|
||||
from flask_wtf.csrf import CSRFProtect
|
||||
from wtforms import StringField, SelectField, TextAreaField, validators
|
||||
import io
|
||||
import warnings
|
||||
import re
|
||||
import io
|
||||
import csv
|
||||
import secrets
|
||||
import requests
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv(override=True)
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
|
||||
import atexit
|
||||
from flask_migrate import Migrate
|
||||
from sqlalchemy import MetaData, event, UniqueConstraint
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.lib.pagesizes import A4, landscape, letter
|
||||
from reportlab.platypus import (
|
||||
SimpleDocTemplate,
|
||||
Table,
|
||||
TableStyle,
|
||||
Paragraph,
|
||||
Image,
|
||||
Spacer
|
||||
)
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||
from reportlab.lib.utils import ImageReader
|
||||
from reportlab.lib.units import cm, inch, mm
|
||||
from io import BytesIO
|
||||
import reportlab.lib
|
||||
import traceback
|
||||
import logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
# logging.basicConfig(level=logging.INFO)
|
||||
logging.getLogger('apscheduler').setLevel(logging.WARNING)
|
||||
from sqlalchemy.engine import Engine
|
||||
import sqlite3
|
||||
from sqlalchemy.orm import joinedload
|
||||
from functools import wraps
|
||||
from flask_session import Session
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_wtf import CSRFProtect, FlaskForm
|
||||
from redis import Redis
|
||||
from time import sleep
|
||||
import random
|
||||
import locale
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.pagesizes import A4, landscape, letter
|
||||
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
|
||||
from reportlab.lib.units import cm, inch, mm
|
||||
from reportlab.lib.utils import ImageReader
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.platypus import (
|
||||
Image,
|
||||
Paragraph,
|
||||
SimpleDocTemplate,
|
||||
Spacer,
|
||||
Table,
|
||||
TableStyle
|
||||
)
|
||||
from sqlalchemy import MetaData, UniqueConstraint, event
|
||||
from sqlalchemy.engine import Engine
|
||||
from sqlalchemy.exc import IntegrityError, LegacyAPIWarning
|
||||
from sqlalchemy.orm import joinedload
|
||||
from werkzeug.security import check_password_hash, generate_password_hash
|
||||
from wtforms import SelectField, StringField, TextAreaField, validators
|
||||
|
||||
# Config
|
||||
load_dotenv(override=True)
|
||||
warnings.simplefilter("ignore", category=LegacyAPIWarning)
|
||||
|
||||
# Logging-Config
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logging.getLogger('apscheduler').setLevel(logging.WARNING)
|
||||
|
||||
|
||||
@event.listens_for(Engine, "connect")
|
||||
def enable_foreign_keys(dbapi_connection, connection_record):
|
||||
|
@ -262,11 +258,11 @@ TZ = os.getenv('TZ', 'UTC')
|
|||
os.environ['TZ'] = TZ
|
||||
app = Flask(__name__)
|
||||
|
||||
# Auf UNIX-Systemen (Linux, Docker) wirksam machen
|
||||
# UNIX-Systems (Linux, Docker)
|
||||
try:
|
||||
time.tzset()
|
||||
except AttributeError:
|
||||
pass # tzset gibt es auf Windows nicht
|
||||
pass # tzset not availabe on Windows
|
||||
local_tz = pytz.timezone(TZ)
|
||||
|
||||
# Load Languages
|
||||
|
@ -326,19 +322,19 @@ convention = {
|
|||
metadata = MetaData(naming_convention=convention)
|
||||
load_dotenv(override=True)
|
||||
|
||||
# Lade Umgebungsvariablen aus .env mit override
|
||||
# load variables from .env with override
|
||||
load_dotenv(override=True)
|
||||
|
||||
# App-Configuration
|
||||
app.config.update(
|
||||
# WICHTIGSTE EINSTELLUNGEN
|
||||
# Most Important
|
||||
SECRET_KEY=os.getenv('SECRET_KEY'),
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:////app/data/games.db',
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False,
|
||||
DEFAULT_LANGUAGE='en',
|
||||
ITAD_COUNTRY = os.getenv("ITAD_COUNTRY", "DE"),
|
||||
|
||||
# SESSION-HANDLING (Produktion: Redis verwenden!)
|
||||
# SESSION-HANDLING (In Production: Use Redis!)
|
||||
SESSION_TYPE='redis',
|
||||
SESSION_PERMANENT = False,
|
||||
SESSION_USE_SIGNER = True,
|
||||
|
@ -350,6 +346,12 @@ app.config.update(
|
|||
SESSION_COOKIE_SAMESITE = 'Lax',
|
||||
PERMANENT_SESSION_LIFETIME = timedelta(days=30),
|
||||
|
||||
# LOGIN COOKIE STUFF
|
||||
REMEMBER_COOKIE_DURATION=timedelta(days=30),
|
||||
REMEMBER_COOKIE_HTTPONLY=True,
|
||||
REMEMBER_COOKIE_SECURE=True if os.getenv('FORCE_HTTPS', 'False').lower() == 'true' else False,
|
||||
REMEMBER_COOKIE_SAMESITE='Lax',
|
||||
|
||||
# CSRF-PROTECTION
|
||||
WTF_CSRF_ENABLED = True,
|
||||
WTF_CSRF_SECRET_KEY = os.getenv('CSRF_SECRET_KEY', os.urandom(32).hex()),
|
||||
|
@ -367,7 +369,7 @@ Session(app)
|
|||
|
||||
interval_hours = int(os.getenv('CHECK_EXPIRING_KEYS_INTERVAL_HOURS', 12))
|
||||
|
||||
# Initialisation
|
||||
# Init
|
||||
db = SQLAlchemy(app, metadata=metadata)
|
||||
migrate = Migrate(app, db)
|
||||
login_manager = LoginManager(app)
|
||||
|
@ -377,6 +379,10 @@ login_manager.login_view = 'login'
|
|||
app.logger.addHandler(logging.StreamHandler())
|
||||
app.logger.setLevel(logging.DEBUG)
|
||||
|
||||
@app.errorhandler(403)
|
||||
def forbidden_error(error):
|
||||
return render_template('403.html'), 403
|
||||
|
||||
|
||||
@app.before_request
|
||||
def set_language():
|
||||
|
@ -465,14 +471,13 @@ class Game(db.Model):
|
|||
steam_appid = db.Column(db.String(20))
|
||||
platform = db.Column(db.String(50), default='pc')
|
||||
current_price = db.Column(db.Float)
|
||||
current_price_shop = db.Column(db.String(100))
|
||||
historical_low = db.Column(db.Float)
|
||||
release_date = db.Column(db.DateTime)
|
||||
metacritic_score = db.Column(db.Integer)
|
||||
release_date = db.Column(db.DateTime)
|
||||
steam_description = db.Column(db.Text)
|
||||
itad_slug = db.Column(db.String(200))
|
||||
|
||||
|
||||
# with users.id
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), nullable=False)
|
||||
|
||||
|
@ -511,7 +516,6 @@ class RedeemToken(db.Model):
|
|||
now = datetime.now(local_tz)
|
||||
return now > self.expires.astimezone(local_tz)
|
||||
|
||||
|
||||
class GameForm(FlaskForm):
|
||||
name = StringField('Name', [validators.DataRequired()])
|
||||
steam_key = StringField('Steam Key')
|
||||
|
@ -526,7 +530,6 @@ class GameForm(FlaskForm):
|
|||
redeem_date = StringField('Einlösedatum')
|
||||
steam_appid = StringField('Steam App ID')
|
||||
|
||||
|
||||
PLATFORM_CHOICES = [
|
||||
('pc', 'PC'),
|
||||
('xbox', 'XBox'),
|
||||
|
@ -541,7 +544,6 @@ STATUS_CHOICES = [
|
|||
('geschenkt', 'Geschenkt')
|
||||
]
|
||||
|
||||
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
|
||||
|
@ -596,17 +598,17 @@ def fetch_steam_data(appid):
|
|||
return None
|
||||
|
||||
def parse_steam_release_date(date_str):
|
||||
"""Parst Steam-Release-Daten im deutschen oder englischen Format."""
|
||||
"""Parsing Steam-Release-Date (the german us thingy, you know)"""
|
||||
import locale
|
||||
from datetime import datetime
|
||||
|
||||
# Versuche deutsches Format
|
||||
# try german format
|
||||
try:
|
||||
locale.setlocale(locale.LC_TIME, "de_DE.UTF-8")
|
||||
return datetime.strptime(date_str, "%d. %b. %Y")
|
||||
except Exception:
|
||||
pass
|
||||
# Fallback: Versuche englisches Format
|
||||
# Fallback: okay lets try the english one
|
||||
try:
|
||||
locale.setlocale(locale.LC_TIME, "en_US.UTF-8")
|
||||
return datetime.strptime(date_str, "%d %b, %Y")
|
||||
|
@ -631,7 +633,6 @@ def fetch_itad_slug(steam_appid: int) -> str | None:
|
|||
return None
|
||||
|
||||
|
||||
|
||||
def fetch_itad_game_id(steam_appid: int) -> str | None:
|
||||
api_key = os.getenv("ITAD_API_KEY")
|
||||
if not api_key:
|
||||
|
@ -707,21 +708,37 @@ def set_lang(lang):
|
|||
@app.route('/set-theme/<theme>')
|
||||
def set_theme(theme):
|
||||
resp = make_response('', 204)
|
||||
resp.set_cookie('theme', theme, max_age=60*60*24*365) # 1 Jahr Gültigkeit
|
||||
resp.set_cookie('theme', theme, max_age=60*60*24*365)
|
||||
return resp
|
||||
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if current_user.is_authenticated: # Prevent already logged-in users from accessing login page
|
||||
return redirect(url_for('index'))
|
||||
|
||||
if request.method == 'POST':
|
||||
username = request.form['username']
|
||||
password = request.form['password']
|
||||
username = request.form.get('username')
|
||||
password = request.form.get('password')
|
||||
remember = request.form.get('remember_me') == 'true'
|
||||
|
||||
user = User.query.filter_by(username=username).first()
|
||||
|
||||
|
||||
if user and check_password_hash(user.password, password):
|
||||
login_user(user)
|
||||
return redirect(url_for('index'))
|
||||
|
||||
flash(translate('Invalid credentials', session.get('lang', 'en')), 'danger')
|
||||
# Pass remember=True to login_user and set duration
|
||||
# The duration will be taken from app.config['REMEMBER_COOKIE_DURATION']
|
||||
login_user(user, remember=remember)
|
||||
|
||||
# Log activity
|
||||
log_activity(user.id, 'user_login', f"User '{user.username}' logged in.")
|
||||
|
||||
next_page = request.args.get('next')
|
||||
# Add security check for next_page to prevent open redirect
|
||||
if not next_page or urlparse(next_page).netloc != '':
|
||||
next_page = url_for('index')
|
||||
flash(translate('Logged in successfully.'), 'success')
|
||||
return redirect(next_page)
|
||||
else:
|
||||
flash(translate('Invalid username or password.'), 'danger')
|
||||
return render_template('login.html')
|
||||
|
||||
@app.route('/register', methods=['GET', 'POST'])
|
||||
|
@ -1222,11 +1239,11 @@ def admin_audit_logs():
|
|||
def update_game_data(game_id):
|
||||
game = Game.query.get_or_404(game_id)
|
||||
|
||||
# 1. Steam AppID aus dem Formular holen
|
||||
# 1. Getting Steam AppID
|
||||
steam_appid = request.form.get('steam_appid', '').strip()
|
||||
app.logger.info(f"🚀 Update gestartet für Game {game_id} mit AppID: {steam_appid}")
|
||||
|
||||
# 2. Steam-Daten abrufen
|
||||
# 2. Steam-Data
|
||||
steam_data = None
|
||||
if steam_appid:
|
||||
try:
|
||||
|
@ -1256,12 +1273,12 @@ def update_game_data(game_id):
|
|||
app.logger.error(f"💥 Kritischer Steam-Fehler: {str(e)}", exc_info=True)
|
||||
flash(translate('Fehler bei Steam-Abfrage'), 'danger')
|
||||
|
||||
# ITAD-Slug abrufen und speichern
|
||||
# ITAD-Slug donings and such
|
||||
itad_slug = fetch_itad_slug(steam_appid)
|
||||
if itad_slug:
|
||||
game.itad_slug = itad_slug
|
||||
|
||||
# 4. ITAD-Preisdaten
|
||||
# 4. ITAD-Prices
|
||||
price_data = None
|
||||
if steam_appid:
|
||||
try:
|
||||
|
@ -1273,20 +1290,24 @@ def update_game_data(game_id):
|
|||
price_data = fetch_itad_prices(game.itad_game_id)
|
||||
|
||||
if price_data:
|
||||
# Aktueller Steam-Preis
|
||||
steam_deal = next(
|
||||
(deal for deal in price_data.get("deals", [])
|
||||
if deal.get("shop", {}).get("name", "").lower() == "steam"),
|
||||
None
|
||||
)
|
||||
# Best price right now
|
||||
all_deals = price_data.get("deals", [])
|
||||
if all_deals:
|
||||
best_deal = min(
|
||||
all_deals,
|
||||
key=lambda deal: deal.get("price", {}).get("amount", float('inf'))
|
||||
)
|
||||
game.current_price = best_deal.get("price", {}).get("amount")
|
||||
game.current_price_shop = best_deal.get("shop", {}).get("name")
|
||||
app.logger.info(f"💶 Current Best: {game.current_price}€ at {game.current_price_shop}")
|
||||
else:
|
||||
game.current_price = None
|
||||
game.current_price_shop = None
|
||||
|
||||
app.logger.info(f"💶 Current Best: {game.current_price}€")
|
||||
|
||||
if steam_deal:
|
||||
game.current_price = steam_deal.get("price", {}).get("amount")
|
||||
app.logger.info(f"💶 Aktueller Preis: {game.current_price}€")
|
||||
|
||||
# Historisches Minimum
|
||||
game.historical_low = price_data.get("historyLow", {}).get("all", {}).get("amount")
|
||||
app.logger.info(f"📉 Historisches Minimum: {game.historical_low}€")
|
||||
app.logger.info(f"📉 Historical Low: {game.historical_low}€")
|
||||
else:
|
||||
app.logger.warning("⚠️ Keine ITAD-Preisdaten erhalten")
|
||||
else:
|
||||
|
@ -1296,16 +1317,6 @@ def update_game_data(game_id):
|
|||
app.logger.error(f"💥 ITAD-API-Fehler: {str(e)}", exc_info=True)
|
||||
flash(translate('Fehler bei Preisabfrage'), 'danger')
|
||||
|
||||
# 5. Metacritic-Score (Beispielimplementierung)
|
||||
try:
|
||||
if game.name:
|
||||
app.logger.info(f"🎮 Starte Metacritic-Abfrage für: {game.name}")
|
||||
# Hier echte API-Integration einfügen
|
||||
game.metacritic_score = random.randint(50, 100) # Mock-Daten
|
||||
except Exception as e:
|
||||
app.logger.error(f"💥 Metacritic-Fehler: {str(e)}")
|
||||
|
||||
# 6. Datenbank-Update
|
||||
try:
|
||||
db.session.commit()
|
||||
flash(translate('Externe Daten erfolgreich aktualisiert!'), 'success')
|
||||
|
@ -1608,7 +1619,6 @@ cat <<'HTML_END' > templates/index.html
|
|||
<th>{{ _('Redeem by') }}</th>
|
||||
<th>{{ _('Shop') }}</th>
|
||||
<th>{{ _('Price') }}</th>
|
||||
<th>{{ _('Metascore') }}</th>
|
||||
<th>{{ _('Actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -1616,22 +1626,24 @@ cat <<'HTML_END' > templates/index.html
|
|||
{% for game in games %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if game.steam_appid %}
|
||||
<img src="https://cdn.cloudflare.steamstatic.com/steam/apps/{{ game.steam_appid }}/header.jpg"
|
||||
alt="Steam Header"
|
||||
class="game-cover"
|
||||
{% if loop.first %}fetchpriority="high"{% endif %}
|
||||
width="368"
|
||||
height="172"
|
||||
loading="lazy">
|
||||
{% elif game.url and 'gog.com' in game.url %}
|
||||
<img src="{{ url_for('static', filename='gog_logo.webp') }}"
|
||||
alt="GOG Logo"
|
||||
class="game-cover"
|
||||
width="368"
|
||||
height="172"
|
||||
loading="lazy">
|
||||
{% endif %}
|
||||
<a href="{{ url_for('game_details', game_id=game.id) }}" title="{{ _('Details') }}">
|
||||
{% if game.steam_appid %}
|
||||
<img src="https://cdn.cloudflare.steamstatic.com/steam/apps/{{ game.steam_appid }}/header.jpg"
|
||||
alt="Steam Header"
|
||||
class="game-cover"
|
||||
{% if loop.first %}fetchpriority="high"{% endif %}
|
||||
width="368"
|
||||
height="172"
|
||||
loading="lazy">
|
||||
{% elif game.url and 'gog.com' in game.url %}
|
||||
<img src="{{ url_for('static', filename='gog_logo.webp') }}"
|
||||
alt="GOG Logo"
|
||||
class="game-cover"
|
||||
width="368"
|
||||
height="172"
|
||||
loading="lazy">
|
||||
{% endif %}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ game.name }}</td>
|
||||
<td class="font-monospace">{{ game.steam_key }}</td>
|
||||
|
@ -1656,27 +1668,31 @@ cat <<'HTML_END' > templates/index.html
|
|||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if game.current_price %}
|
||||
<div class="text-center mb-2">
|
||||
<span class="badge bg-primary d-block">{{ _('Now') }}</span>
|
||||
{{ "%.2f"|format(game.current_price) }} €
|
||||
{% if game.current_price is not none %}
|
||||
<div {% if game.historical_low is not none %}class="mb-2"{% endif %}>
|
||||
<div class="text-body-secondary" style="font-size: 0.85em; line-height: 1.2;">
|
||||
{{ _('Current Deal') }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if game.historical_low %}
|
||||
<div class="text-center">
|
||||
<span class="badge bg-secondary d-block">{{ _('Hist. Low') }}</span>
|
||||
{{ "%.2f"|format(game.historical_low) }} €
|
||||
<div style="font-size: 1.05em; line-height: 1.2;">
|
||||
{{ "%.2f"|format(game.current_price) }} €
|
||||
{% if game.current_price_shop %}
|
||||
<span class="d-block text-body-secondary" style="font-size: 0.75em; line-height: 1.1;">({{ game.current_price_shop }})</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Historical Low #}
|
||||
{% if game.historical_low is not none %}
|
||||
<div>
|
||||
<div class="text-body-secondary" style="font-size: 0.85em; line-height: 1.2;">
|
||||
{{ _('Hist. Low') }}
|
||||
</div>
|
||||
<div style="font-size: 1.05em; line-height: 1.2;">
|
||||
{{ "%.2f"|format(game.historical_low) }} €
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if game.metacritic_score %}
|
||||
<span class="badge {% if game.metacritic_score >= 75 %}bg-success{% elif game.metacritic_score >= 50 %}bg-warning{% else %}bg-danger{% endif %}">
|
||||
{{ game.metacritic_score }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-nowrap">
|
||||
{% if game.status == 'geschenkt' %}
|
||||
|
@ -2358,19 +2374,6 @@ cat <<HTML_END > templates/game_details.html
|
|||
<dt class="col-sm-3">{{ _('Current Price') }}</dt>
|
||||
<dd class="col-sm-9">{{ "%.2f €"|format(game.current_price) if game.current_price else 'N/A' }}</dd>
|
||||
|
||||
<dt class="col-sm-3">{{ _('Metascore') }}</dt>
|
||||
<dd class="col-sm-9">
|
||||
{% if game.metacritic_score %}
|
||||
<span class="badge
|
||||
{% if game.metacritic_score >= 75 %}bg-success
|
||||
{% elif game.metacritic_score >= 50 %}bg-warning
|
||||
{% else %}bg-danger{% endif %}">
|
||||
{{ game.metacritic_score }}
|
||||
</span>
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<a href="{{ url_for('edit_game', game_id=game.id) }}" class="btn btn-primary">
|
||||
|
@ -2378,8 +2381,6 @@ cat <<HTML_END > templates/game_details.html
|
|||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Beschreibung UNTERHALB der Hauptzeile -->
|
||||
{% if game.steam_description %}
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
|
@ -2704,13 +2705,29 @@ body {
|
|||
align-items: flex-start !important;
|
||||
}
|
||||
}
|
||||
|
||||
.card-body img,
|
||||
.steam-description img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 8px auto;
|
||||
}
|
||||
|
||||
td.font-monospace {
|
||||
word-break: break-all;
|
||||
/* or */
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.alert-error { background-color: #f8d7da; border-color: #f5c6cb; color: #721c24; }
|
||||
.alert-success { background-color: #d4edda; border-color: #c3e6cb; color: #155724; }
|
||||
.alert-info { background: #d9edf7; color: #31708f; }
|
||||
|
||||
CSS_END
|
||||
|
||||
|
||||
# 7. directories and permissions
|
||||
# directories and permissions
|
||||
mkdir -p ../data
|
||||
chmod -R a+rwX ../data
|
||||
find ../data -type d -exec chmod 775 {} \;
|
||||
|
@ -2720,11 +2737,11 @@ find ../data -type f -exec chmod 664 {} \;
|
|||
cat <<SCRIPT_END > entrypoint.sh
|
||||
#!/bin/bash
|
||||
|
||||
# Debug-Ausgaben hinzufügen
|
||||
# Debug-Output
|
||||
echo "🔄 DEBUGPY-Value: '$DEBUGPY'"
|
||||
echo "🔄 FLASK_DEBUG-Value: '$FLASK_DEBUG'"
|
||||
|
||||
# Debug-Modus aktivieren, wenn eine der Variablen gesetzt ist
|
||||
# Debug-Modus activate if .env told you so
|
||||
if [[ "$DEBUGPY" == "1" || "$FLASK_DEBUG" == "1" ]]; then
|
||||
echo "🔄 Starting in DEBUG mode (Port 5678)..."
|
||||
exec python -m debugpy --listen 0.0.0.0:5678 -m flask run --host=0.0.0.0 --port=5000
|
||||
|
@ -2842,7 +2859,7 @@ ENTRYPOINT ["/app/entrypoint.sh"]
|
|||
|
||||
DOCKER_END
|
||||
|
||||
# 6. docker-compose.yml
|
||||
# create docker-compose.yml
|
||||
cat <<COMPOSE_END > docker-compose.yml
|
||||
services:
|
||||
redis:
|
||||
|
|
Loading…
Reference in New Issue