remove metacritic because i cannot get it done (no real API) - sorry
This commit is contained in:
parent
4522d51f47
commit
49fdd243d0
381
setup.sh
381
setup.sh
|
@ -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
|
||||||
|
|
||||||
if steam_deal:
|
app.logger.info(f"💶 Current Best: {game.current_price}€")
|
||||||
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:
|
||||||
|
|
Loading…
Reference in New Issue