neue Filter nach Kategorien erstellt
This commit is contained in:
1 parent
65089447b5
commit
3ea5612bed
3 files changed
+1095
-46
No files matched your search
@@ -3,6 +3,7 @@ import json
|
||||
import io
|
||||
import zipfile
|
||||
import asyncio
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from flask import Flask, render_template_string, request, session, redirect, url_for, send_from_directory, send_file, jsonify
|
||||
from flask_cors import CORS
|
||||
@@ -13,17 +14,89 @@ import config
|
||||
# Konfiguration
|
||||
PASSWORD = config.PASSWORD
|
||||
BASE_DIR = config.BASE_DIR
|
||||
JSON_DB_PATH = getattr(config, 'JSON_DB_PATH', 'toniebox.json')
|
||||
|
||||
# Logging Setup
|
||||
logging.basicConfig(
|
||||
filename='toniecloud.log',
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s [%(levelname)s] %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S',
|
||||
force=True
|
||||
)
|
||||
logger = logging.getLogger('tonieflix')
|
||||
|
||||
# Werkzeug-Zugriffslogs stummschalten
|
||||
logging.getLogger('werkzeug').setLevel(logging.WARNING)
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['SECRET_KEY'] = config.SECRET_KEY
|
||||
CORS(app, supports_credentials=True, allow_headers="*")
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# JSON Datenbank verarbeiten
|
||||
# ---------------------------------------------------------
|
||||
def load_metadata():
|
||||
"""Liest die JSON-Datei aus und ordnet Metadaten (Genre, Alter, Format) den Titeln zu."""
|
||||
metadata = {}
|
||||
if os.path.exists(JSON_DB_PATH):
|
||||
try:
|
||||
with open(JSON_DB_PATH, 'r', encoding='utf-8') as f:
|
||||
raw_data = json.load(f)
|
||||
|
||||
for item in raw_data:
|
||||
# Wir suchen spezifisch nach der Tabelle 'toniebox'
|
||||
if item.get('type') == 'table' and item.get('name') == 'toniebox':
|
||||
for row in item.get('data', []):
|
||||
title = row.get('title')
|
||||
if not title:
|
||||
continue
|
||||
|
||||
# Wir normalisieren den Titel (Kleinbuchstaben, keine Leerzeichen am Rand),
|
||||
# damit der Ordnername besser gematcht werden kann.
|
||||
title_key = title.lower().strip()
|
||||
|
||||
if title_key not in metadata:
|
||||
metadata[title_key] = {'genres': set(), 'formats': set(), 'agegroups': set()}
|
||||
|
||||
# Genres bereinigen (am | splitten und trimmen)
|
||||
if row.get('genre'):
|
||||
for g in str(row['genre']).split('|'):
|
||||
clean_g = g.strip()
|
||||
if clean_g:
|
||||
metadata[title_key]['genres'].add(clean_g)
|
||||
|
||||
# Formate bereinigen (am | splitten und trimmen)
|
||||
if row.get('format'):
|
||||
for fmt in str(row['format']).split('|'):
|
||||
clean_fmt = fmt.strip()
|
||||
if clean_fmt:
|
||||
metadata[title_key]['formats'].add(clean_fmt)
|
||||
|
||||
# Altersgruppen bereinigen (am | splitten und trimmen)
|
||||
if row.get('agegroup'):
|
||||
for age in str(row['agegroup']).split('|'):
|
||||
clean_age = age.strip()
|
||||
if clean_age:
|
||||
metadata[title_key]['agegroups'].add(clean_age)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Laden der JSON-Datenbank: {e}")
|
||||
|
||||
# Sets für die JSON-Serialisierung im Frontend in Listen umwandeln
|
||||
for key, val in metadata.items():
|
||||
val['genres'] = list(val['genres'])
|
||||
val['formats'] = list(val['formats'])
|
||||
val['agegroups'] = list(val['agegroups'])
|
||||
|
||||
return metadata
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Toniecloud API & Playwright Hilfsfunktionen
|
||||
# ---------------------------------------------------------
|
||||
async def get_tonie_api_token(username: str, password: str) -> str | None:
|
||||
"""Loggt sich automatisiert bei Tonies ein und gibt das Access-Token zurück."""
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch(headless=True)
|
||||
context = await browser.new_context(viewport={'width': 1280, 'height': 800})
|
||||
@@ -58,7 +131,7 @@ async def get_tonie_api_token(username: str, password: str) -> str | None:
|
||||
await browser.close()
|
||||
return access_token
|
||||
except Exception as e:
|
||||
print(f"Playwright Login-Fehler: {e}")
|
||||
logger.error(f"Playwright Login-Fehler: {e}")
|
||||
await browser.close()
|
||||
return None
|
||||
|
||||
@@ -167,6 +240,8 @@ def upload_audio_to_creative_tonie(access_token: str, household_id: str, tonie_i
|
||||
# ---------------------------------------------------------
|
||||
def get_books(single_book=None):
|
||||
books = []
|
||||
metadata_lookup = load_metadata()
|
||||
|
||||
if os.path.exists(BASE_DIR):
|
||||
items_to_check = [single_book] if single_book else sorted(os.listdir(BASE_DIR))
|
||||
for item in items_to_check:
|
||||
@@ -175,8 +250,20 @@ def get_books(single_book=None):
|
||||
files = os.listdir(item_path)
|
||||
tracks = sorted([f for f in files if f.lower().endswith(('.mp3', '.ogg'))])
|
||||
has_cover = any(f.lower() == 'cover.jpg' for f in files)
|
||||
|
||||
if tracks:
|
||||
books.append({'name': item, 'tracks': tracks, 'has_cover': has_cover})
|
||||
# Match der Metadaten basierend auf dem Ordnernamen
|
||||
item_key = item.lower().strip()
|
||||
meta = metadata_lookup.get(item_key, {'genres': [], 'formats': [], 'agegroups': []})
|
||||
|
||||
books.append({
|
||||
'name': item,
|
||||
'tracks': tracks,
|
||||
'has_cover': has_cover,
|
||||
'genres': meta['genres'],
|
||||
'formats': meta['formats'],
|
||||
'agegroups': meta['agegroups']
|
||||
})
|
||||
return books
|
||||
|
||||
|
||||
@@ -195,16 +282,21 @@ TEMPLATE = """
|
||||
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: var(--bg); color: var(--text); margin: 0; padding: 20px; padding-bottom: 160px; }
|
||||
h1 { text-align: center; color: var(--primary); font-size: 3rem; margin-bottom: 10px; font-weight: 800; }
|
||||
|
||||
.top-controls { text-align: center; margin-bottom: 30px; display: flex; justify-content: center; gap: 15px; flex-wrap: wrap; }
|
||||
input[type="text"] { padding: 12px 20px; width: 80%; max-width: 400px; border: 1px solid #333; background: #222; color: #fff; border-radius: 25px; font-size: 1rem; outline: none; }
|
||||
.top-controls { text-align: center; margin-bottom: 20px; display: flex; justify-content: center; gap: 15px; flex-wrap: wrap; }
|
||||
input[type="text"] { padding: 12px 20px; width: 100%; max-width: 400px; border: 1px solid #333; background: #222; color: #fff; border-radius: 25px; font-size: 1rem; outline: none; }
|
||||
input[type="text"]:focus { border-color: var(--primary); }
|
||||
|
||||
.filter-bar { display: flex; justify-content: center; gap: 10px; margin-bottom: 30px; flex-wrap: wrap; }
|
||||
.filter-bar select { padding: 10px 15px; border-radius: 20px; background: #222; color: #fff; border: 1px solid #333; font-size: 0.9rem; outline: none; cursor: pointer; }
|
||||
.filter-bar select:focus { border-color: var(--primary); }
|
||||
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 20px; max-width: 1200px; margin: 0 auto; }
|
||||
.card { background: var(--card); border-radius: 12px; overflow: hidden; box-shadow: 0 4px 10px rgba(0,0,0,0.5); transition: transform 0.2s; display: flex; flex-direction: column; }
|
||||
.card:hover { transform: scale(1.03); }
|
||||
.card-clickable { cursor: pointer; flex-grow: 1; }
|
||||
.card img { width: 100%; aspect-ratio: 1; object-fit: cover; background: #333; display: block; }
|
||||
.card .title { padding: 12px 10px 5px 10px; text-align: center; font-weight: bold; font-size: 1rem; }
|
||||
.card .meta-info { font-size: 0.75rem; color: #888; text-align: center; padding: 0 10px 5px 10px; }
|
||||
.card-actions { padding: 10px; text-align: center; display: flex; flex-direction: column; gap: 6px; }
|
||||
.btn-direct, .btn-tonie { background: #333; color: #ccc; border: 1px solid #444; padding: 6px 14px; border-radius: 15px; font-size: 0.85rem; cursor: pointer; transition: 0.2s; }
|
||||
.btn-direct:hover, .btn-tonie:hover { background: var(--primary); color: white; border-color: var(--primary); }
|
||||
@@ -230,28 +322,17 @@ TEMPLATE = """
|
||||
.logout:hover { color: #fff; }
|
||||
|
||||
/* Modal für Tonie-Upload */
|
||||
#tonieModal {
|
||||
display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
||||
background: rgba(0,0,0,0.8); z-index: 2000; justify-content: center; align-items: center;
|
||||
}
|
||||
.modal-content {
|
||||
background: #1f1f1f; padding: 30px; border-radius: 12px; width: 90%; max-width: 500px;
|
||||
box-shadow: 0 5px 20px rgba(0,0,0,0.8); text-align: center; position: relative; max-height: 90vh; overflow-y: auto;
|
||||
}
|
||||
#tonieModal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 2000; justify-content: center; align-items: center; }
|
||||
.modal-content { background: #1f1f1f; padding: 30px; border-radius: 12px; width: 90%; max-width: 500px; box-shadow: 0 5px 20px rgba(0,0,0,0.8); text-align: center; position: relative; max-height: 90vh; overflow-y: auto; }
|
||||
.modal-close { position: absolute; top: 15px; right: 15px; font-size: 1.5rem; cursor: pointer; color: #aaa; }
|
||||
.modal-close:hover { color: #fff; }
|
||||
.tonie-list { display: flex; flex-direction: column; gap: 10px; margin-top: 20px; text-align: left; }
|
||||
.tonie-item {
|
||||
display: flex; align-items: center; gap: 15px; background: #2a2a2a; padding: 10px; border-radius: 8px; cursor: pointer; transition: 0.2s;
|
||||
}
|
||||
.tonie-item { display: flex; align-items: center; gap: 15px; background: #2a2a2a; padding: 10px; border-radius: 8px; cursor: pointer; transition: 0.2s; }
|
||||
.tonie-item:hover { background: #3a3a3a; border: 1px solid var(--primary); }
|
||||
.tonie-item img { width: 50px; height: 50px; border-radius: 6px; object-fit: cover; }
|
||||
.checkbox-container { margin: 15px 0; text-align: left; display: flex; align-items: center; gap: 10px; font-size: 0.9rem; }
|
||||
|
||||
#loadingOverlay {
|
||||
display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
||||
background: rgba(0,0,0,0.85); z-index: 3000; flex-direction: column; justify-content: center; align-items: center; color: white;
|
||||
}
|
||||
#loadingOverlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.85); z-index: 3000; flex-direction: column; justify-content: center; align-items: center; color: white; }
|
||||
.spinner { border: 4px solid #333; border-top: 4px solid var(--primary); border-radius: 50%; width: 50px; height: 50px; animation: spin 1s linear infinite; margin-bottom: 20px; }
|
||||
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
||||
</style>
|
||||
@@ -269,9 +350,37 @@ TEMPLATE = """
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if not single_mode %}
|
||||
<div class="filter-bar">
|
||||
<select id="filterGenre">
|
||||
<option value="">🎭 Alle Genres</option>
|
||||
{% for g in all_genres %}
|
||||
<option value="{{ g }}">{{ g }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select id="filterAge">
|
||||
<option value="">👶 Alle Altersgruppen</option>
|
||||
{% for a in all_agegroups %}
|
||||
<option value="{{ a }}">{{ a }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select id="filterFormat">
|
||||
<option value="">🎧 Alle Formate</option>
|
||||
{% for f in all_formats %}
|
||||
<option value="{{ f }}">{{ f }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="grid" id="bookGrid">
|
||||
{% for book in books %}
|
||||
<div class="card" data-title="{{ book.name.lower() }}">
|
||||
<div class="card"
|
||||
data-title="{{ book.name.lower() }}"
|
||||
data-genres='{{ book.genres|tojson }}'
|
||||
data-agegroups='{{ book.agegroups|tojson }}'
|
||||
data-formats='{{ book.formats|tojson }}'>
|
||||
|
||||
<div class="card-clickable" data-name="{{ book.name }}" data-tracks='{{ book.tracks|tojson }}'>
|
||||
{% if book.has_cover %}
|
||||
<img src="{{ url_for('serve_cover', book_name=book.name) }}" alt="Cover">
|
||||
@@ -279,6 +388,12 @@ TEMPLATE = """
|
||||
<img src="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' fill='%23333' viewBox='0 0 1 1'></svg>" alt="Kein Cover">
|
||||
{% endif %}
|
||||
<div class="title">{{ book.name }}</div>
|
||||
{% if book.genres or book.formats or book.agegroups %}
|
||||
<div class="meta-info">
|
||||
{{ book.formats|join(', ') }} • {{ book.agegroups|join(', ') }}<br>
|
||||
{{ book.genres|join(', ') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<button class="btn-direct" data-name="{{ book.name }}">🔗 Direktlink</button>
|
||||
@@ -290,32 +405,28 @@ TEMPLATE = """
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Tonie Auswahl Modal -->
|
||||
<!-- Tonie Auswahl Modal & Loading (unverändert) -->
|
||||
<div id="tonieModal">
|
||||
<div class="modal-content">
|
||||
<span class="modal-close" onclick="closeTonieModal()">×</span>
|
||||
<h2 style="color: var(--primary); margin-top: 0;">Kreativ-Tonie auswählen</h2>
|
||||
<p id="modalBookTitle" style="color: #aaa; font-size: 0.9rem;"></p>
|
||||
|
||||
<div class="checkbox-container">
|
||||
<input type="checkbox" id="deleteAllChapters" style="transform: scale(1.2);">
|
||||
<label for="deleteAllChapters">Vorhandene Kapitel auf dem Tonie löschen</label>
|
||||
</div>
|
||||
|
||||
<div class="tonie-list" id="tonieListContainer">
|
||||
<p style="text-align: center; color: #aaa;">Lade Kreativ-Tonies...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ladebildschirm Upload -->
|
||||
<div id="loadingOverlay">
|
||||
<div class="spinner"></div>
|
||||
<h2 id="loadingText">Lade Hörbuch auf Kreativ-Tonie hoch...</h2>
|
||||
<p style="color: #aaa;">Bitte Fenster nicht schließen.</p>
|
||||
</div>
|
||||
|
||||
<!-- Player Bar -->
|
||||
<div class="player-bar" id="playerBar" style="display: none;">
|
||||
<div class="player-info" id="playerInfo">Wird geladen...</div>
|
||||
<div class="player-controls">
|
||||
@@ -330,7 +441,11 @@ TEMPLATE = """
|
||||
const audioPlayer = document.getElementById('audioPlayer');
|
||||
const playerBar = document.getElementById('playerBar');
|
||||
const playerInfo = document.getElementById('playerInfo');
|
||||
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
const filterGenre = document.getElementById('filterGenre');
|
||||
const filterAge = document.getElementById('filterAge');
|
||||
const filterFormat = document.getElementById('filterFormat');
|
||||
const cards = document.querySelectorAll('.card');
|
||||
|
||||
const prevBtn = document.getElementById('prevBtn');
|
||||
@@ -345,18 +460,38 @@ TEMPLATE = """
|
||||
|
||||
let selectedBookNameForTonie = '';
|
||||
|
||||
// Suche
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
const term = e.target.value.toLowerCase();
|
||||
cards.forEach(card => {
|
||||
const title = card.getAttribute('data-title');
|
||||
card.style.display = title.includes(term) ? 'flex' : 'none';
|
||||
});
|
||||
// Kombinierte Such- und Filterlogik
|
||||
function applyFilters() {
|
||||
const term = searchInput ? searchInput.value.toLowerCase() : '';
|
||||
const selGenre = filterGenre ? filterGenre.value : '';
|
||||
const selAge = filterAge ? filterAge.value : '';
|
||||
const selFormat = filterFormat ? filterFormat.value : '';
|
||||
|
||||
cards.forEach(card => {
|
||||
const title = card.getAttribute('data-title');
|
||||
const genres = JSON.parse(card.getAttribute('data-genres') || '[]');
|
||||
const agegroups = JSON.parse(card.getAttribute('data-agegroups') || '[]');
|
||||
const formats = JSON.parse(card.getAttribute('data-formats') || '[]');
|
||||
|
||||
const matchText = title.includes(term);
|
||||
const matchGenre = selGenre === '' || genres.includes(selGenre);
|
||||
const matchAge = selAge === '' || agegroups.includes(selAge);
|
||||
const matchFormat = selFormat === '' || formats.includes(selFormat);
|
||||
|
||||
if (matchText && matchGenre && matchAge && matchFormat) {
|
||||
card.style.display = 'flex';
|
||||
} else {
|
||||
card.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Event-Listener für Karten
|
||||
if (searchInput) searchInput.addEventListener('input', applyFilters);
|
||||
if (filterGenre) filterGenre.addEventListener('change', applyFilters);
|
||||
if (filterAge) filterAge.addEventListener('change', applyFilters);
|
||||
if (filterFormat) filterFormat.addEventListener('change', applyFilters);
|
||||
|
||||
// Event-Listener für Karten (unverändert)
|
||||
document.querySelectorAll('.card-clickable').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
const name = el.getAttribute('data-name');
|
||||
@@ -415,9 +550,7 @@ TEMPLATE = """
|
||||
});
|
||||
}
|
||||
|
||||
function closeTonieModal() {
|
||||
tonieModal.style.display = 'none';
|
||||
}
|
||||
function closeTonieModal() { tonieModal.style.display = 'none'; }
|
||||
|
||||
function uploadToTonie(tonieId) {
|
||||
const deleteAll = document.getElementById('deleteAllChapters').checked;
|
||||
@@ -568,12 +701,15 @@ def tonie_login():
|
||||
if request.method == 'POST':
|
||||
email = request.form.get('tonie_email')
|
||||
pwd = request.form.get('tonie_password')
|
||||
# Playwright Token holen (Asynchroner Aufruf im Sync Flask)
|
||||
|
||||
token = asyncio.run(get_tonie_api_token(email, pwd))
|
||||
if token:
|
||||
session['tonie_token'] = token
|
||||
session['tonie_email'] = email
|
||||
logger.info(f"Toniecloud-Login erfolgreich für E-Mail: {email} (IP: {request.remote_addr})")
|
||||
return redirect(url_for('index'))
|
||||
else:
|
||||
logger.warning(f"Toniecloud-Login fehlgeschlagen für E-Mail: {email} (IP: {request.remote_addr})")
|
||||
error = "Login fehlgeschlagen. Bitte Zugangsdaten prüfen."
|
||||
return render_template_string(TONIE_LOGIN_TEMPLATE, error=error)
|
||||
|
||||
@@ -584,11 +720,32 @@ def logout():
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return render_template_string(TEMPLATE, books=get_books(), auto_play="", single_mode=False)
|
||||
books = get_books()
|
||||
|
||||
# Eindeutige Listen für Dropdowns sammeln
|
||||
all_genres = set()
|
||||
all_formats = set()
|
||||
all_agegroups = set()
|
||||
|
||||
for b in books:
|
||||
all_genres.update(b['genres'])
|
||||
all_formats.update(b['formats'])
|
||||
all_agegroups.update(b['agegroups'])
|
||||
|
||||
return render_template_string(
|
||||
TEMPLATE,
|
||||
books=books,
|
||||
auto_play="",
|
||||
single_mode=False,
|
||||
all_genres=sorted(list(all_genres)),
|
||||
all_formats=sorted(list(all_formats)),
|
||||
all_agegroups=sorted(list(all_agegroups))
|
||||
)
|
||||
|
||||
@app.route('/play/<path:book_name>')
|
||||
def play_book(book_name):
|
||||
return render_template_string(TEMPLATE, books=get_books(single_book=book_name), auto_play=book_name, single_mode=True)
|
||||
# Im Single Mode brauchen wir keine Dropdown-Daten
|
||||
return render_template_string(TEMPLATE, books=get_books(single_book=book_name), auto_play=book_name, single_mode=True, all_genres=[], all_formats=[], all_agegroups=[])
|
||||
|
||||
@app.route('/cover/<path:book_name>')
|
||||
def serve_cover(book_name):
|
||||
@@ -634,10 +791,13 @@ def api_upload_to_tonie():
|
||||
book_name = data.get('book_name')
|
||||
tonie_id = data.get('tonie_id')
|
||||
delete_all = data.get('delete_all', False)
|
||||
user_email = session.get('tonie_email', 'Unbekannt')
|
||||
|
||||
if not book_name or not tonie_id:
|
||||
return jsonify({"success": False, "error": "Fehlende Parameter."}), 400
|
||||
|
||||
logger.info(f"Start Upload von '{book_name}' auf Tonie {tonie_id} durch Benutzer: {user_email} (IP: {request.remote_addr})")
|
||||
|
||||
try:
|
||||
household_id = get_household_id(token)
|
||||
book_path = os.path.join(BASE_DIR, book_name)
|
||||
@@ -649,13 +809,9 @@ def api_upload_to_tonie():
|
||||
if not files:
|
||||
return jsonify({"success": False, "error": "Keine Audiospuren im Hörbuch gefunden."}), 400
|
||||
|
||||
# Jede Spur nacheinander hochladen
|
||||
for index, track in enumerate(files):
|
||||
file_path = os.path.join(book_path, track)
|
||||
# Titel des Kapitels (Dateiname ohne Endung oder schöner Name)
|
||||
title = os.path.splitext(track)[0]
|
||||
|
||||
# Beim ersten Track wird ggf. der 'delete_all_chapters' Schalter angewendet, danach hängen wir an
|
||||
should_delete = delete_all if index == 0 else False
|
||||
|
||||
upload_audio_to_creative_tonie(
|
||||
@@ -667,10 +823,12 @@ def api_upload_to_tonie():
|
||||
delete_all_chapters=should_delete
|
||||
)
|
||||
|
||||
logger.info(f"Upload von '{book_name}' auf Tonie {tonie_id} durch {user_email} erfolgreich abgeschlossen.")
|
||||
return jsonify({"success": True})
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Upload von '{book_name}' durch {user_email}: {str(e)}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=5005)
|
||||
app.run(host='0.0.0.0', port=5005)
|
||||
Reference in new issue
Block a user