remove metacritic because i cannot get it done (no real API) - sorry

This commit is contained in:
nocci 2025-05-08 12:47:24 +02:00
parent 4522d51f47
commit 49fdd243d0
1 changed files with 202 additions and 185 deletions

387
setup.sh
View File

@ -7,7 +7,7 @@ GREEN='\033[1;32m'
YELLOW='\033[1;33m' YELLOW='\033[1;33m'
NC='\033[0m' NC='\033[0m'
# 1. Docker check (incl. Arch Linux) # Docker check (incl. Arch Linux)
if ! command -v docker &>/dev/null; then if ! command -v docker &>/dev/null; then
echo -e "${RED}❗ Docker is not installed.${NC}" echo -e "${RED}❗ Docker is not installed.${NC}"
read -p "Would you like to install Docker automatically now? [y/N]: " install_docker 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 rm get-docker.sh
fi fi
# Docker group membership prüfen # Docker group membership check
if ! groups | grep -q '\bdocker\b'; then if ! groups | grep -q '\bdocker\b'; then
echo -e "${YELLOW}⚠️ Your user is not in the docker group. Adding now...${NC}" echo -e "${YELLOW}⚠️ Your user is not in the docker group. Adding now...${NC}"
sudo usermod -aG docker $USER sudo usermod -aG docker $USER
@ -37,7 +37,7 @@ if ! command -v docker &>/dev/null; then
fi fi
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="" DOCKER_COMPOSE_CMD=""
if command -v docker-compose &>/dev/null; then if command -v docker-compose &>/dev/null; then
DOCKER_COMPOSE_CMD="docker-compose" DOCKER_COMPOSE_CMD="docker-compose"
@ -88,16 +88,7 @@ chmod -R a+rwX "$TRANSLATIONS_DIR" "$DATA_DIR"
cd $PROJECT_DIR cd $PROJECT_DIR
## UID/GID-Logic # requirements.txt
#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
cat <<EOL > requirements.txt cat <<EOL > requirements.txt
flask flask
flask-login flask-login
@ -122,7 +113,7 @@ redis
EOL EOL
# 3. create .env # create .env
SECRET_KEY=$(python3 -c 'import secrets; print(secrets.token_hex(24))') SECRET_KEY=$(python3 -c 'import secrets; print(secrets.token_hex(24))')
REDEEM_SECRET=$(python3 -c 'import secrets; print(secrets.token_hex(16))') REDEEM_SECRET=$(python3 -c 'import secrets; print(secrets.token_hex(16))')
REDEEM_CSRF=$(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_API_KEY="your-secret-key-here"
ITAD_COUNTRY="DE" 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="" APPRISE_URLS=""
### example for multiple notifications ### example for multiple notifications
@ -170,86 +161,91 @@ FLASK_DEBUG=1
DEBUGPY=1 DEBUGPY=1
EOL EOL
# 4. app.py (the main app) # app.py (the main app)
cat <<'PYTHON_END' > app.py 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 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 from zoneinfo import ZoneInfo
# 3rd-Provider-Modules
import pytz import pytz
import warnings import requests
from sqlalchemy.exc import LegacyAPIWarning from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
warnings.simplefilter("ignore", category=LegacyAPIWarning) from apscheduler.schedulers.background import BackgroundScheduler
from dotenv import load_dotenv
from flask import ( from flask import (
Flask, Flask,
Markup,
abort,
flash,
g,
jsonify,
make_response,
redirect,
render_template, render_template,
request, request,
redirect,
url_for,
flash,
session,
abort,
send_file, send_file,
jsonify, session,
Markup, url_for
make_response, )
g, from flask_login import (
abort 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 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_session import Session
from flask_sqlalchemy import SQLAlchemy
from flask_wtf import CSRFProtect, FlaskForm
from redis import Redis from redis import Redis
from time import sleep from reportlab.lib import colors
import random from reportlab.lib.pagesizes import A4, landscape, letter
import locale 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") @event.listens_for(Engine, "connect")
def enable_foreign_keys(dbapi_connection, connection_record): def enable_foreign_keys(dbapi_connection, connection_record):
@ -262,11 +258,11 @@ TZ = os.getenv('TZ', 'UTC')
os.environ['TZ'] = TZ os.environ['TZ'] = TZ
app = Flask(__name__) app = Flask(__name__)
# Auf UNIX-Systemen (Linux, Docker) wirksam machen # UNIX-Systems (Linux, Docker)
try: try:
time.tzset() time.tzset()
except AttributeError: except AttributeError:
pass # tzset gibt es auf Windows nicht pass # tzset not availabe on Windows
local_tz = pytz.timezone(TZ) local_tz = pytz.timezone(TZ)
# Load Languages # Load Languages
@ -326,19 +322,19 @@ convention = {
metadata = MetaData(naming_convention=convention) metadata = MetaData(naming_convention=convention)
load_dotenv(override=True) load_dotenv(override=True)
# Lade Umgebungsvariablen aus .env mit override # load variables from .env with override
load_dotenv(override=True) load_dotenv(override=True)
# App-Configuration # App-Configuration
app.config.update( app.config.update(
# WICHTIGSTE EINSTELLUNGEN # Most Important
SECRET_KEY=os.getenv('SECRET_KEY'), SECRET_KEY=os.getenv('SECRET_KEY'),
SQLALCHEMY_DATABASE_URI = 'sqlite:////app/data/games.db', SQLALCHEMY_DATABASE_URI = 'sqlite:////app/data/games.db',
SQLALCHEMY_TRACK_MODIFICATIONS = False, SQLALCHEMY_TRACK_MODIFICATIONS = False,
DEFAULT_LANGUAGE='en', DEFAULT_LANGUAGE='en',
ITAD_COUNTRY = os.getenv("ITAD_COUNTRY", "DE"), ITAD_COUNTRY = os.getenv("ITAD_COUNTRY", "DE"),
# SESSION-HANDLING (Produktion: Redis verwenden!) # SESSION-HANDLING (In Production: Use Redis!)
SESSION_TYPE='redis', SESSION_TYPE='redis',
SESSION_PERMANENT = False, SESSION_PERMANENT = False,
SESSION_USE_SIGNER = True, SESSION_USE_SIGNER = True,
@ -350,6 +346,12 @@ app.config.update(
SESSION_COOKIE_SAMESITE = 'Lax', SESSION_COOKIE_SAMESITE = 'Lax',
PERMANENT_SESSION_LIFETIME = timedelta(days=30), 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 # CSRF-PROTECTION
WTF_CSRF_ENABLED = True, WTF_CSRF_ENABLED = True,
WTF_CSRF_SECRET_KEY = os.getenv('CSRF_SECRET_KEY', os.urandom(32).hex()), 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)) interval_hours = int(os.getenv('CHECK_EXPIRING_KEYS_INTERVAL_HOURS', 12))
# Initialisation # Init
db = SQLAlchemy(app, metadata=metadata) db = SQLAlchemy(app, metadata=metadata)
migrate = Migrate(app, db) migrate = Migrate(app, db)
login_manager = LoginManager(app) login_manager = LoginManager(app)
@ -377,6 +379,10 @@ login_manager.login_view = 'login'
app.logger.addHandler(logging.StreamHandler()) app.logger.addHandler(logging.StreamHandler())
app.logger.setLevel(logging.DEBUG) app.logger.setLevel(logging.DEBUG)
@app.errorhandler(403)
def forbidden_error(error):
return render_template('403.html'), 403
@app.before_request @app.before_request
def set_language(): def set_language():
@ -465,14 +471,13 @@ class Game(db.Model):
steam_appid = db.Column(db.String(20)) steam_appid = db.Column(db.String(20))
platform = db.Column(db.String(50), default='pc') platform = db.Column(db.String(50), default='pc')
current_price = db.Column(db.Float) current_price = db.Column(db.Float)
current_price_shop = db.Column(db.String(100))
historical_low = db.Column(db.Float) historical_low = db.Column(db.Float)
release_date = db.Column(db.DateTime) release_date = db.Column(db.DateTime)
metacritic_score = db.Column(db.Integer)
release_date = db.Column(db.DateTime) release_date = db.Column(db.DateTime)
steam_description = db.Column(db.Text) steam_description = db.Column(db.Text)
itad_slug = db.Column(db.String(200)) itad_slug = db.Column(db.String(200))
# with users.id # with users.id
user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), nullable=False) 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) now = datetime.now(local_tz)
return now > self.expires.astimezone(local_tz) return now > self.expires.astimezone(local_tz)
class GameForm(FlaskForm): class GameForm(FlaskForm):
name = StringField('Name', [validators.DataRequired()]) name = StringField('Name', [validators.DataRequired()])
steam_key = StringField('Steam Key') steam_key = StringField('Steam Key')
@ -526,7 +530,6 @@ class GameForm(FlaskForm):
redeem_date = StringField('Einlösedatum') redeem_date = StringField('Einlösedatum')
steam_appid = StringField('Steam App ID') steam_appid = StringField('Steam App ID')
PLATFORM_CHOICES = [ PLATFORM_CHOICES = [
('pc', 'PC'), ('pc', 'PC'),
('xbox', 'XBox'), ('xbox', 'XBox'),
@ -541,7 +544,6 @@ STATUS_CHOICES = [
('geschenkt', 'Geschenkt') ('geschenkt', 'Geschenkt')
] ]
with app.app_context(): with app.app_context():
db.create_all() db.create_all()
@ -596,17 +598,17 @@ def fetch_steam_data(appid):
return None return None
def parse_steam_release_date(date_str): 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 import locale
from datetime import datetime from datetime import datetime
# Versuche deutsches Format # try german format
try: try:
locale.setlocale(locale.LC_TIME, "de_DE.UTF-8") locale.setlocale(locale.LC_TIME, "de_DE.UTF-8")
return datetime.strptime(date_str, "%d. %b. %Y") return datetime.strptime(date_str, "%d. %b. %Y")
except Exception: except Exception:
pass pass
# Fallback: Versuche englisches Format # Fallback: okay lets try the english one
try: try:
locale.setlocale(locale.LC_TIME, "en_US.UTF-8") locale.setlocale(locale.LC_TIME, "en_US.UTF-8")
return datetime.strptime(date_str, "%d %b, %Y") return datetime.strptime(date_str, "%d %b, %Y")
@ -631,7 +633,6 @@ def fetch_itad_slug(steam_appid: int) -> str | None:
return None return None
def fetch_itad_game_id(steam_appid: int) -> str | None: def fetch_itad_game_id(steam_appid: int) -> str | None:
api_key = os.getenv("ITAD_API_KEY") api_key = os.getenv("ITAD_API_KEY")
if not api_key: if not api_key:
@ -707,21 +708,37 @@ def set_lang(lang):
@app.route('/set-theme/<theme>') @app.route('/set-theme/<theme>')
def set_theme(theme): def set_theme(theme):
resp = make_response('', 204) 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 return resp
@app.route('/login', methods=['GET', 'POST']) @app.route('/login', methods=['GET', 'POST'])
def login(): 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': if request.method == 'POST':
username = request.form['username'] username = request.form.get('username')
password = request.form['password'] password = request.form.get('password')
remember = request.form.get('remember_me') == 'true'
user = User.query.filter_by(username=username).first() user = User.query.filter_by(username=username).first()
if user and check_password_hash(user.password, password): if user and check_password_hash(user.password, password):
login_user(user) # Pass remember=True to login_user and set duration
return redirect(url_for('index')) # The duration will be taken from app.config['REMEMBER_COOKIE_DURATION']
login_user(user, remember=remember)
flash(translate('Invalid credentials', session.get('lang', 'en')), 'danger')
# 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') return render_template('login.html')
@app.route('/register', methods=['GET', 'POST']) @app.route('/register', methods=['GET', 'POST'])
@ -1222,11 +1239,11 @@ def admin_audit_logs():
def update_game_data(game_id): def update_game_data(game_id):
game = Game.query.get_or_404(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() steam_appid = request.form.get('steam_appid', '').strip()
app.logger.info(f"🚀 Update gestartet für Game {game_id} mit AppID: {steam_appid}") 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 steam_data = None
if steam_appid: if steam_appid:
try: try:
@ -1256,12 +1273,12 @@ def update_game_data(game_id):
app.logger.error(f"💥 Kritischer Steam-Fehler: {str(e)}", exc_info=True) app.logger.error(f"💥 Kritischer Steam-Fehler: {str(e)}", exc_info=True)
flash(translate('Fehler bei Steam-Abfrage'), 'danger') flash(translate('Fehler bei Steam-Abfrage'), 'danger')
# ITAD-Slug abrufen und speichern # ITAD-Slug donings and such
itad_slug = fetch_itad_slug(steam_appid) itad_slug = fetch_itad_slug(steam_appid)
if itad_slug: if itad_slug:
game.itad_slug = itad_slug game.itad_slug = itad_slug
# 4. ITAD-Preisdaten # 4. ITAD-Prices
price_data = None price_data = None
if steam_appid: if steam_appid:
try: try:
@ -1273,20 +1290,24 @@ def update_game_data(game_id):
price_data = fetch_itad_prices(game.itad_game_id) price_data = fetch_itad_prices(game.itad_game_id)
if price_data: if price_data:
# Aktueller Steam-Preis # Best price right now
steam_deal = next( all_deals = price_data.get("deals", [])
(deal for deal in price_data.get("deals", []) if all_deals:
if deal.get("shop", {}).get("name", "").lower() == "steam"), best_deal = min(
None 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") 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: else:
app.logger.warning("⚠️ Keine ITAD-Preisdaten erhalten") app.logger.warning("⚠️ Keine ITAD-Preisdaten erhalten")
else: else:
@ -1296,16 +1317,6 @@ def update_game_data(game_id):
app.logger.error(f"💥 ITAD-API-Fehler: {str(e)}", exc_info=True) app.logger.error(f"💥 ITAD-API-Fehler: {str(e)}", exc_info=True)
flash(translate('Fehler bei Preisabfrage'), 'danger') 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: try:
db.session.commit() db.session.commit()
flash(translate('Externe Daten erfolgreich aktualisiert!'), 'success') flash(translate('Externe Daten erfolgreich aktualisiert!'), 'success')
@ -1608,7 +1619,6 @@ cat <<'HTML_END' > templates/index.html
<th>{{ _('Redeem by') }}</th> <th>{{ _('Redeem by') }}</th>
<th>{{ _('Shop') }}</th> <th>{{ _('Shop') }}</th>
<th>{{ _('Price') }}</th> <th>{{ _('Price') }}</th>
<th>{{ _('Metascore') }}</th>
<th>{{ _('Actions') }}</th> <th>{{ _('Actions') }}</th>
</tr> </tr>
</thead> </thead>
@ -1616,22 +1626,24 @@ cat <<'HTML_END' > templates/index.html
{% for game in games %} {% for game in games %}
<tr> <tr>
<td> <td>
{% if game.steam_appid %} <a href="{{ url_for('game_details', game_id=game.id) }}" title="{{ _('Details') }}">
<img src="https://cdn.cloudflare.steamstatic.com/steam/apps/{{ game.steam_appid }}/header.jpg" {% if game.steam_appid %}
alt="Steam Header" <img src="https://cdn.cloudflare.steamstatic.com/steam/apps/{{ game.steam_appid }}/header.jpg"
class="game-cover" alt="Steam Header"
{% if loop.first %}fetchpriority="high"{% endif %} class="game-cover"
width="368" {% if loop.first %}fetchpriority="high"{% endif %}
height="172" width="368"
loading="lazy"> height="172"
{% elif game.url and 'gog.com' in game.url %} loading="lazy">
<img src="{{ url_for('static', filename='gog_logo.webp') }}" {% elif game.url and 'gog.com' in game.url %}
alt="GOG Logo" <img src="{{ url_for('static', filename='gog_logo.webp') }}"
class="game-cover" alt="GOG Logo"
width="368" class="game-cover"
height="172" width="368"
loading="lazy"> height="172"
{% endif %} loading="lazy">
{% endif %}
</a>
</td> </td>
<td>{{ game.name }}</td> <td>{{ game.name }}</td>
<td class="font-monospace">{{ game.steam_key }}</td> <td class="font-monospace">{{ game.steam_key }}</td>
@ -1656,27 +1668,31 @@ cat <<'HTML_END' > templates/index.html
{% endif %} {% endif %}
</td> </td>
<td> <td>
{% if game.current_price %} {% if game.current_price is not none %}
<div class="text-center mb-2"> <div {% if game.historical_low is not none %}class="mb-2"{% endif %}>
<span class="badge bg-primary d-block">{{ _('Now') }}</span> <div class="text-body-secondary" style="font-size: 0.85em; line-height: 1.2;">
{{ "%.2f"|format(game.current_price) }} {{ _('Current Deal') }}
</div> </div>
</div> <div style="font-size: 1.05em; line-height: 1.2;">
{% endif %} {{ "%.2f"|format(game.current_price) }}
{% if game.historical_low %} {% if game.current_price_shop %}
<div class="text-center"> <span class="d-block text-body-secondary" style="font-size: 0.75em; line-height: 1.1;">({{ game.current_price_shop }})</span>
<span class="badge bg-secondary d-block">{{ _('Hist. Low') }}</span> {% endif %}
{{ "%.2f"|format(game.historical_low) }}
</div> </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 %} {% 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>
<td class="text-nowrap"> <td class="text-nowrap">
{% if game.status == 'geschenkt' %} {% if game.status == 'geschenkt' %}
@ -2358,19 +2374,6 @@ cat <<HTML_END > templates/game_details.html
<dt class="col-sm-3">{{ _('Current Price') }}</dt> <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> <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> </dl>
<a href="{{ url_for('edit_game', game_id=game.id) }}" class="btn btn-primary"> <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> </a>
</div> </div>
</div> </div>
<!-- Beschreibung UNTERHALB der Hauptzeile -->
{% if game.steam_description %} {% if game.steam_description %}
<div class="row mt-4"> <div class="row mt-4">
<div class="col-12"> <div class="col-12">
@ -2704,13 +2705,29 @@ body {
align-items: flex-start !important; 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-error { background-color: #f8d7da; border-color: #f5c6cb; color: #721c24; }
.alert-success { background-color: #d4edda; border-color: #c3e6cb; color: #155724; } .alert-success { background-color: #d4edda; border-color: #c3e6cb; color: #155724; }
.alert-info { background: #d9edf7; color: #31708f; } .alert-info { background: #d9edf7; color: #31708f; }
CSS_END CSS_END
# 7. directories and permissions # directories and permissions
mkdir -p ../data mkdir -p ../data
chmod -R a+rwX ../data chmod -R a+rwX ../data
find ../data -type d -exec chmod 775 {} \; find ../data -type d -exec chmod 775 {} \;
@ -2720,11 +2737,11 @@ find ../data -type f -exec chmod 664 {} \;
cat <<SCRIPT_END > entrypoint.sh cat <<SCRIPT_END > entrypoint.sh
#!/bin/bash #!/bin/bash
# Debug-Ausgaben hinzufügen # Debug-Output
echo "🔄 DEBUGPY-Value: '$DEBUGPY'" echo "🔄 DEBUGPY-Value: '$DEBUGPY'"
echo "🔄 FLASK_DEBUG-Value: '$FLASK_DEBUG'" 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 if [[ "$DEBUGPY" == "1" || "$FLASK_DEBUG" == "1" ]]; then
echo "🔄 Starting in DEBUG mode (Port 5678)..." 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 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 DOCKER_END
# 6. docker-compose.yml # create docker-compose.yml
cat <<COMPOSE_END > docker-compose.yml cat <<COMPOSE_END > docker-compose.yml
services: services:
redis: redis: