new folder structure / add de translation

This commit is contained in:
nocci 2025-05-03 11:37:42 +02:00
parent 8aba6f5129
commit 4a0a5bac3f
35 changed files with 696 additions and 1124 deletions

View file

@ -1,16 +1,15 @@
import os
import logging
import warnings
from sqlalchemy.exc import LegacyAPIWarning
warnings.simplefilter("ignore", category=LegacyAPIWarning)
from flask import Flask, render_template, request, redirect, url_for, flash, make_response, session, abort, send_file, jsonify
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
from flask_babel import Babel, _
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime, timedelta
from flask_wtf import CSRFProtect
from flask import abort
from flask import request, redirect
import io
import warnings
import re
@ -41,8 +40,39 @@ from reportlab.lib.utils import ImageReader
from reportlab.lib.units import cm, inch, mm
from io import BytesIO
import reportlab.lib
import logging
logging.basicConfig()
app = Flask(__name__)
# Load Languages
import os
import json
TRANSLATION_DIR = os.path.join(os.path.dirname(__file__), 'translations')
SUPPORTED_LANGUAGES = ['de', 'en']
TRANSLATIONS = {}
for lang in SUPPORTED_LANGUAGES:
try:
with open(os.path.join(TRANSLATION_DIR, f'{lang}.json'), encoding='utf-8') as f:
TRANSLATIONS[lang] = json.load(f)
except Exception:
TRANSLATIONS[lang] = {}
def translate(key, lang=None, **kwargs):
if not lang:
lang = session.get('lang', 'en')
value = TRANSLATIONS.get(lang, {}).get(key)
if value is None and lang != 'en':
value = TRANSLATIONS.get('en', {}).get(key, key)
else:
value = value or key
return value.format(**kwargs) if kwargs and isinstance(value, str) else value
## DEBUG Translations
if app.debug:
print(f"Loaded translations for 'de': {TRANSLATIONS.get('de', {})}")
csrf = CSRFProtect(app)
convention = {
@ -62,14 +92,16 @@ load_dotenv(override=True)
# App-Configuration
app.config.update(
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,
BABEL_DEFAULT_LOCALE=os.getenv('BABEL_DEFAULT_LOCALE'),
BABEL_SUPPORTED_LOCALES=os.getenv('BABEL_SUPPORTED_LOCALES').split(','),
BABEL_TRANSLATION_DIRECTORIES=os.getenv('BABEL_TRANSLATION_DIRECTORIES'),
SESSION_COOKIE_SECURE=os.getenv('SESSION_COOKIE_SECURE') == 'True',
WTF_CSRF_ENABLED=os.getenv('CSRF_ENABLED') == 'True',
REGISTRATION_ENABLED=os.getenv('REGISTRATION_ENABLED', 'True').lower() == 'true'
SESSION_COOKIE_SECURE=os.getenv('SESSION_COOKIE_SECURE', 'False') == 'True',
SESSION_COOKIE_SAMESITE='Lax',
PERMANENT_SESSION_LIFETIME=timedelta(days=30),
SESSION_REFRESH_EACH_REQUEST=False,
WTF_CSRF_ENABLED=os.getenv('CSRF_ENABLED', 'True') == 'True',
REGISTRATION_ENABLED=os.getenv('REGISTRATION_ENABLED', 'True').lower() == 'true',
SEND_FILE_MAX_AGE_DEFAULT=int(os.getenv('SEND_FILE_MAX_AGE_DEFAULT', 0)),
TEMPLATES_AUTO_RELOAD=os.getenv('TEMPLATES_AUTO_RELOAD', 'True') == 'True'
)
interval_hours = int(os.getenv('CHECK_EXPIRING_KEYS_INTERVAL_HOURS', 12))
@ -79,24 +111,28 @@ db = SQLAlchemy(app, metadata=metadata)
migrate = Migrate(app, db)
login_manager = LoginManager(app)
login_manager.login_view = 'login'
babel = Babel(app)
# Logging
app.logger.addHandler(logging.StreamHandler())
app.logger.setLevel(logging.INFO)
@babel.localeselector
def get_locale():
if 'lang' in session and session['lang'] in app.config['BABEL_SUPPORTED_LOCALES']:
return session['lang']
return request.accept_languages.best_match(app.config['BABEL_SUPPORTED_LOCALES'])
@app.before_request
def enforce_https():
if os.getenv('FORCE_HTTPS', 'False').lower() == 'true':
if request.headers.get('X-Forwarded-Proto', 'http') != 'https' and not request.is_secure:
url = request.url.replace('http://', 'https://', 1)
app.logger.info(f"Redirecting to HTTPS: {url}")
return redirect(url, code=301)
@app.context_processor
def inject_template_vars():
return dict(
get_locale=get_locale,
theme='dark' if request.cookies.get('dark_mode') == 'true' else 'light'
)
def _(key, **kwargs):
lang = session.get('lang', 'en')
return translate(key, lang, **kwargs)
theme = request.cookies.get('theme', 'light')
return dict(_=_, theme=theme)
# DB Models
class User(db.Model, UserMixin):
@ -163,14 +199,15 @@ def index():
@app.route('/set-lang/<lang>')
def set_lang(lang):
if lang in app.config['BABEL_SUPPORTED_LOCALES']:
if lang in SUPPORTED_LANGUAGES:
session['lang'] = lang
return redirect(request.referrer or url_for('index'))
@app.route('/set-theme/<theme>')
def set_theme(theme):
resp = make_response('', 204)
resp.set_cookie('dark_mode', 'true' if theme == 'dark' else 'false', max_age=60*60*24*365)
# Von 'dark_mode' zu 'theme' ändern
resp.set_cookie('theme', theme, max_age=60*60*24*365)
return resp
@app.route('/login', methods=['GET', 'POST'])
@ -190,7 +227,7 @@ def login():
@app.route('/register', methods=['GET', 'POST'])
def register():
if not app.config['REGISTRATION_ENABLED']:
flash(_('Registrierungen sind deaktiviert'), 'danger')
flash(_('No new registrations. They are deactivated!'), 'danger')
return redirect(url_for('login'))
if request.method == 'POST':
@ -224,16 +261,16 @@ def change_password():
confirm_password = request.form['confirm_password']
if not check_password_hash(current_user.password, current_password):
flash(_('Aktuelles Passwort ist falsch'), 'danger')
flash(_('Current passwort is wrong'), 'danger')
return redirect(url_for('change_password'))
if new_password != confirm_password:
flash(_('Neue Passwörter stimmen nicht überein'), 'danger')
flash(_('New Passwords are not matching'), 'danger')
return redirect(url_for('change_password'))
current_user.password = generate_password_hash(new_password)
db.session.commit()
flash(_('Passwort erfolgreich geändert'), 'success')
flash(_('Password changed successfully'), 'success')
return redirect(url_for('index'))
return render_template('change_password.html')
@ -421,6 +458,12 @@ def export_pdf():
img = Image(img_data, width=3*cm, height=img_height)
except Exception:
img = Paragraph('', styles['Normal'])
elif game.url and 'gog.com' in game.url:
try:
img_path = os.path.join(app.root_path, 'static', 'gog_logo.webp')
img = Image(img_path, width=3*cm, height=img_height)
except Exception:
img = Paragraph('', styles['Normal'])
data.append([
img or '',
@ -429,7 +472,7 @@ def export_pdf():
game.redeem_date.strftime('%d.%m.%y') if game.redeem_date else ''
])
# Table format
# Table format (korrekte Einrückung)
table = Table(data, colWidths=col_widths, repeatRows=1)
table.setStyle(TableStyle([
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
@ -445,13 +488,14 @@ def export_pdf():
doc.build(elements)
buffer.seek(0)
return send_file(
return send_file(
buffer,
mimetype='application/pdf',
as_attachment=True,
download_name=f'game_export_{datetime.now().strftime("%Y%m%d")}.pdf'
)
@app.route('/import', methods=['GET', 'POST'])
@login_required
def import_games():
@ -491,15 +535,15 @@ def import_games():
db.session.commit()
flash(_('%(new)d neue Spiele importiert, %(dup)d Duplikate übersprungen', new=new_games, dup=duplicates), 'success')
flash(_('%(new)d new games imported, %(dup)d skipped duplicates', new=new_games, dup=duplicates), 'success')
except Exception as e:
db.session.rollback()
flash(_('Importfehler: %(error)s', error=str(e)), 'danger')
flash(_('Import error: %(error)s', error=str(e)), 'danger')
return redirect(url_for('index'))
flash(_('Bitte eine gültige CSV-Datei hochladen.'), 'danger')
flash(_('Please upload a valid CSV file.'), 'danger')
return render_template('import.html')
@ -557,81 +601,29 @@ def redeem_page(token):
redeem_token=redeem_token,
platform_link='https://store.steampowered.com/account/registerkey?key=' if game.steam_appid else 'https://www.gog.com/redeem')
# Benachrichtigungsfunktionen
def send_pushover_notification(user, game):
"""Sendet Pushover-Benachrichtigung für ablaufenden Key"""
if not app.config['PUSHOVER_APP_TOKEN'] or not app.config['PUSHOVER_USER_KEY']:
return False
payload = {
"token": os.getenv('PUSHOVER_APP_TOKEN'),
"user": os.getenv('PUSHOVER_USER_KEY'),
"title": "Steam-Key läuft ab!",
"message": f"Dein Key für '{game.name}' läuft in weniger als 48 Stunden ab!",
"url": url_for('edit_game', game_id=game.id, _external=True),
"url_title": "Zum Spiel",
"priority": 1
}
try:
response = requests.post(
'https://api.pushover.net/1/messages.json',
data=payload
)
return response.status_code == 200
except Exception as e:
app.logger.error(f"Pushover error: {str(e)}")
# Apprise Notifications
import apprise
def send_apprise_notification(user, game):
apprise_urls = os.getenv('APPRISE_URLS', '').strip()
if not apprise_urls:
app.logger.error("No APPRISE_URLS configured")
return False
def send_gotify_notification(user, game):
"""Sendet Gotify-Benachrichtigung für ablaufenden Key"""
if not GOTIFY_URL or not GOTIFY_TOKEN:
return False
payload = {
"title": "Steam-Key läuft ab!",
"message": f"Dein Key für '{game.name}' läuft in weniger als 48 Stunden ab!",
"priority": 5
}
try:
response = requests.post(
f"{GOTIFY_URL}/message?token={GOTIFY_TOKEN}",
json=payload
)
return response.status_code == 200
except Exception as e:
app.logger.error(f"Gotify error: {str(e)}")
return False
apobj = apprise.Apprise()
for url in apprise_urls.replace(',', '\n').splitlines():
if url.strip():
apobj.add(url.strip())
def send_matrix_notification(user, game):
"""Sendet Matrix-Benachrichtigung für ablaufenden Key"""
if not MATRIX_HOMESERVER or not MATRIX_ACCESS_TOKEN or not MATRIX_ROOM_ID:
return False
try:
from matrix_client.client import MatrixClient
client = MatrixClient(MATRIX_HOMESERVER, token=MATRIX_ACCESS_TOKEN)
room = client.join_room(MATRIX_ROOM_ID)
message = f"🎮 Dein Key für '{game.name}' läuft in weniger als 48 Stunden ab!"
room.send_text(message)
return True
except Exception as e:
app.logger.error(f"Matrix error: {str(e)}")
return False
edit_url = url_for('edit_game', game_id=game.id, _external=True)
result = apobj.notify(
title="Steam-Key läuft ab!",
body=f"Dein Key für '{game.name}' läuft in weniger als 48 Stunden ab!\n\nLink: {edit_url}",
)
return result
def send_notification(user, game):
"""Sendet Benachrichtigung über den bevorzugten Dienst des Benutzers"""
if user.notification_service == 'pushover':
return send_pushover_notification(user, game)
elif user.notification_service == 'gotify':
return send_gotify_notification(user, game)
elif user.notification_service == 'matrix':
return send_matrix_notification(user, game)
return False
return send_apprise_notification(user, game)
def check_expiring_keys():
with app.app_context():