From 3ea5612bed50897c352b5b17dc1125c6d313a7ed Mon Sep 17 00:00:00 2001 From: sandro Date: Tue, 1 Sep 2026 23:37:34 +0200 Subject: [PATCH] neue Filter nach Kategorien erstellt --- .gitignore | 1 + app.py | 250 +++++++++++--- toniebox.json | 890 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1095 insertions(+), 46 deletions(-) create mode 100644 toniebox.json diff --git a/.gitignore b/.gitignore index f35db13..b8d8d02 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ config.py api-call +toniecloud.log \ No newline at end of file diff --git a/app.py b/app.py index bb42c19..8a04a63 100644 --- a/app.py +++ b/app.py @@ -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); } } @@ -269,9 +350,37 @@ TEMPLATE = """ {% endif %} + {% if not single_mode %} +
+ + + +
+ {% endif %} +
{% for book in books %} -
+
+
{% if book.has_cover %} Cover @@ -279,6 +388,12 @@ TEMPLATE = """ Kein Cover {% endif %}
{{ book.name }}
+ {% if book.genres or book.formats or book.agegroups %} +
+ {{ book.formats|join(', ') }} • {{ book.agegroups|join(', ') }}
+ {{ book.genres|join(', ') }} +
+ {% endif %}
@@ -290,32 +405,28 @@ TEMPLATE = """ {% endfor %}
- +
-

Lade Hörbuch auf Kreativ-Tonie hoch...

Bitte Fenster nicht schließen.

-