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'
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: