new folder structure / add de translation
This commit is contained in:
parent
8aba6f5129
commit
4a0a5bac3f
35 changed files with 696 additions and 1124 deletions
|
@ -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():
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue