test
This commit is contained in:
1 parent
d75cb0bf76
commit
160dabc59d
1 file changed
+226
-58
@@ -9,6 +9,9 @@ from flask import Flask, render_template_string, request, session, redirect, url
|
||||
from flask_cors import CORS
|
||||
from playwright.async_api import async_playwright
|
||||
import requests
|
||||
import mutagen
|
||||
from mutagen.mp3 import MP3
|
||||
from mutagen.oggvorbis import OggVorbis
|
||||
import config
|
||||
|
||||
# Konfiguration
|
||||
@@ -34,11 +37,25 @@ app.config['SECRET_KEY'] = config.SECRET_KEY
|
||||
CORS(app, supports_credentials=True, allow_headers="*")
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Hilfsfunktion: Exakte Track-Dauer ermitteln (Fehlertolerant)
|
||||
# ---------------------------------------------------------
|
||||
def get_track_duration(file_path: str) -> float:
|
||||
try:
|
||||
# Generischer Mutagen-Fallback, der mit MP3 und OGG/Vorbis/Opus umgehen kann
|
||||
audio = mutagen.File(file_path)
|
||||
if audio and hasattr(audio, 'info') and hasattr(audio.info, 'length'):
|
||||
return audio.info.length
|
||||
except Exception as e:
|
||||
logger.warning(f"Konnte Dauer für {file_path} nicht auslesen: {e}")
|
||||
log_to_db("WARNUNG", f"Konnte Dauer für {file_path} nicht auslesen: {e}")
|
||||
return 0.0
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 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:
|
||||
@@ -46,45 +63,35 @@ def load_metadata():
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
|
||||
if clean_age: metadata[title_key]['agegroups'].add(clean_age)
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Laden der JSON-Datenbank: {e}")
|
||||
log_to_db("WARNUNG", 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'])
|
||||
@@ -132,6 +139,7 @@ async def get_tonie_api_token(username: str, password: str) -> str | None:
|
||||
return access_token
|
||||
except Exception as e:
|
||||
logger.error(f"Playwright Login-Fehler: {e}")
|
||||
log_to_db("FEHLER", f"Playwright Login-Fehler: {e}")
|
||||
await browser.close()
|
||||
return None
|
||||
|
||||
@@ -162,7 +170,6 @@ def get_creative_tonies(access_token: str, household_id: str) -> list[dict]:
|
||||
def upload_audio_to_creative_tonie(access_token: str, household_id: str, tonie_id: str, file_path: str, title: str, delete_all_chapters: bool):
|
||||
headers = {"Authorization": f"Bearer {access_token}", "Content-Type": "application/json", "User-Agent": "Mozilla/5.0"}
|
||||
|
||||
# 1. S3 Berechtigung anfordern
|
||||
upload_req_resp = requests.post("https://api.tonie.cloud/v2/file", json={"headers": {}}, headers=headers)
|
||||
if upload_req_resp.status_code != 200:
|
||||
raise Exception(f"Upload-Request fehlgeschlagen: {upload_req_resp.text}")
|
||||
@@ -173,7 +180,6 @@ def upload_audio_to_creative_tonie(access_token: str, household_id: str, tonie_i
|
||||
file_id = amazon_data.get("fileId") or amazon_data.get("fileID") or amazon_data.get("file")
|
||||
s3_url = s3_request.get("url", "https://bxn-toniecloud-prod-upload.s3.amazonaws.com/")
|
||||
|
||||
# 2. Datei zu S3 hochladen
|
||||
if not Path(file_path).exists():
|
||||
raise FileNotFoundError(f"Datei {file_path} nicht gefunden.")
|
||||
|
||||
@@ -194,7 +200,6 @@ def upload_audio_to_creative_tonie(access_token: str, household_id: str, tonie_i
|
||||
if s3_resp.status_code not in (200, 204):
|
||||
raise Exception(f"S3-Upload fehlgeschlagen: {s3_resp.text}")
|
||||
|
||||
# 3. Tonie-Details abrufen & Kapitel aktualisieren
|
||||
tonie_detail_resp = requests.get(f"https://api.tonie.cloud/v2/households/{household_id}/creativetonies/{tonie_id}", headers=headers)
|
||||
if tonie_detail_resp.status_code != 200:
|
||||
raise Exception(f"Konnte Tonie-Details nicht abrufen: {tonie_detail_resp.text}")
|
||||
@@ -212,7 +217,6 @@ def upload_audio_to_creative_tonie(access_token: str, household_id: str, tonie_i
|
||||
if c_id and c_file and str(c_file).lower() != "none":
|
||||
cleaned_chapters.append({"id": str(c_id), "file": str(c_file), "title": str(c_title)})
|
||||
|
||||
# Neues Kapitel anhängen
|
||||
cleaned_chapters.append({
|
||||
"id": str(fields.get("key")),
|
||||
"file": str(file_id),
|
||||
@@ -236,7 +240,7 @@ def upload_audio_to_creative_tonie(access_token: str, household_id: str, tonie_i
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Bibliothek abrufen
|
||||
# Bibliothek abrufen (Nur Grundstruktur, superschnell)
|
||||
# ---------------------------------------------------------
|
||||
def get_books(single_book=None):
|
||||
books = []
|
||||
@@ -248,17 +252,16 @@ def get_books(single_book=None):
|
||||
item_path = os.path.join(BASE_DIR, item)
|
||||
if os.path.isdir(item_path):
|
||||
files = os.listdir(item_path)
|
||||
tracks = sorted([f for f in files if f.lower().endswith(('.mp3', '.ogg'))])
|
||||
track_files = 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:
|
||||
# Match der Metadaten basierend auf dem Ordnernamen
|
||||
if track_files:
|
||||
item_key = item.lower().strip()
|
||||
meta = metadata_lookup.get(item_key, {'genres': [], 'formats': [], 'agegroups': []})
|
||||
|
||||
books.append({
|
||||
'name': item,
|
||||
'tracks': tracks,
|
||||
'tracks': track_files,
|
||||
'has_cover': has_cover,
|
||||
'genres': meta['genres'],
|
||||
'formats': meta['formats'],
|
||||
@@ -290,7 +293,6 @@ TEMPLATE = """
|
||||
.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); }
|
||||
|
||||
/* Desktop Grid: max-width auf 1600px erhöht, minmax auf 180px für bis zu 7 Items */
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 20px; max-width: 1600px; 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); }
|
||||
@@ -302,20 +304,14 @@ TEMPLATE = """
|
||||
.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); }
|
||||
|
||||
/* Mobile Anpassungen (für Bildschirme schmaler als 600px) */
|
||||
@media (max-width: 600px) {
|
||||
body { padding: 10px; padding-bottom: 160px; }
|
||||
h1 { font-size: 2.2rem; }
|
||||
|
||||
/* Grid auf Handy: minmax auf 110px reduziert -> ergibt 2-3 Spalten */
|
||||
.grid { grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: 10px; }
|
||||
|
||||
/* Schriften und Abstände kompakter machen */
|
||||
.card .title { font-size: 0.85rem; padding: 8px 5px 2px 5px; }
|
||||
.card .meta-info { font-size: 0.65rem; padding: 0 5px 5px 5px; }
|
||||
.card-actions { padding: 8px; gap: 4px; }
|
||||
.btn-direct, .btn-tonie { font-size: 0.75rem; padding: 4px 8px; }
|
||||
|
||||
.filter-bar select { font-size: 0.8rem; padding: 8px 12px; }
|
||||
}
|
||||
|
||||
@@ -339,16 +335,24 @@ TEMPLATE = """
|
||||
.logout { position: absolute; top: 20px; right: 20px; color: #aaa; text-decoration: none; font-weight: bold; }
|
||||
.logout:hover { color: #fff; }
|
||||
|
||||
/* Modal für Tonie-Upload */
|
||||
/* Modal für Tonie-Upload & Kapitelübersicht */
|
||||
#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-content { background: #1f1f1f; padding: 30px; border-radius: 12px; width: 90%; max-width: 550px; box-shadow: 0 5px 20px rgba(0,0,0,0.8); text-align: left; 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-list { display: flex; flex-direction: column; gap: 8px; margin-top: 15px; }
|
||||
.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; }
|
||||
|
||||
.chapter-list { display: flex; flex-direction: column; gap: 6px; margin-top: 10px; max-height: 250px; overflow-y: auto; background: #141414; padding: 10px; border-radius: 8px; border: 1px solid #333; }
|
||||
.chapter-item { display: flex; align-items: center; justify-content: space-between; font-size: 0.85rem; padding: 4px 0; border-bottom: 1px solid #222; }
|
||||
.chapter-item label { display: flex; align-items: center; justify-content: space-between; cursor: pointer; width: 100%; gap: 10px; }
|
||||
.chapter-info-text { color: #888; font-size: 0.75rem; white-space: nowrap; }
|
||||
|
||||
.checkbox-container { margin: 15px 0; text-align: left; display: flex; align-items: center; gap: 10px; font-size: 0.9rem; }
|
||||
.time-info { font-size: 0.85rem; color: #aaa; margin-bottom: 10px; font-weight: bold; }
|
||||
|
||||
#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; }
|
||||
@@ -423,18 +427,36 @@ TEMPLATE = """
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Tonie Auswahl Modal & Loading (unverändert) -->
|
||||
<!-- Tonie Auswahl Modal -->
|
||||
<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>
|
||||
<h2 id="modalTitle" style="color: var(--primary); margin-top: 0;">Kreativ-Tonie auswählen</h2>
|
||||
<p id="modalBookTitle" style="color: #aaa; font-size: 0.9rem; margin-bottom: 15px;"></p>
|
||||
|
||||
<div id="tonieStep1">
|
||||
<div class="tonie-list" id="tonieListContainer">
|
||||
<p style="text-align: center; color: #aaa;">Lade Kreativ-Tonies...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tonie-list" id="tonieListContainer">
|
||||
<p style="text-align: center; color: #aaa;">Lade Kreativ-Tonies...</p>
|
||||
|
||||
<div id="tonieStep2" style="display: none;">
|
||||
<p style="font-size: 0.9rem; font-weight: bold; margin-bottom: 5px;">Kapitel auswählen (max. 90 Minuten):</p>
|
||||
<div class="time-info" id="timeInfo">Lade Spieldauer...</div>
|
||||
|
||||
<div class="checkbox-container" style="margin: 10px 0;">
|
||||
<input type="checkbox" id="deleteAllChapters" checked style="transform: scale(1.2);">
|
||||
<label for="deleteAllChapters" style="cursor: pointer;">Vorhandene Kapitel auf dem Tonie löschen</label>
|
||||
</div>
|
||||
|
||||
<div class="chapter-list" id="chapterListContainer">
|
||||
<p style="text-align: center; color: #aaa; padding: 10px;">Ermittle Kapitel-Längen...</p>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
||||
<button class="btn" style="background: #333; color: #fff; flex: 1;" onclick="backToTonieList()">⬅️ Zurück</button>
|
||||
<button class="btn btn-nav" style="flex: 1;" onclick="startUpload()">🚀 Hochladen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -471,14 +493,20 @@ TEMPLATE = """
|
||||
const downloadBtn = document.getElementById('downloadBtn');
|
||||
|
||||
const tonieModal = document.getElementById('tonieModal');
|
||||
const modalTitle = document.getElementById('modalTitle');
|
||||
const modalBookTitle = document.getElementById('modalBookTitle');
|
||||
const tonieListContainer = document.getElementById('tonieListContainer');
|
||||
const tonieStep1 = document.getElementById('tonieStep1');
|
||||
const tonieStep2 = document.getElementById('tonieStep2');
|
||||
const chapterListContainer = document.getElementById('chapterListContainer');
|
||||
const timeInfo = document.getElementById('timeInfo');
|
||||
|
||||
const loadingOverlay = document.getElementById('loadingOverlay');
|
||||
const loadingText = document.getElementById('loadingText');
|
||||
|
||||
let selectedBookNameForTonie = '';
|
||||
let selectedTonieId = '';
|
||||
|
||||
// Kombinierte Such- und Filterlogik
|
||||
function applyFilters() {
|
||||
const term = searchInput ? searchInput.value.toLowerCase() : '';
|
||||
const selGenre = filterGenre ? filterGenre.value : '';
|
||||
@@ -509,7 +537,6 @@ TEMPLATE = """
|
||||
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');
|
||||
@@ -534,8 +561,11 @@ TEMPLATE = """
|
||||
});
|
||||
|
||||
function openTonieModal(bookName) {
|
||||
modalTitle.innerText = "Kreativ-Tonie auswählen";
|
||||
modalBookTitle.innerText = `Hörbuch: "${bookName}"`;
|
||||
tonieModal.style.display = 'flex';
|
||||
tonieStep1.style.display = 'block';
|
||||
tonieStep2.style.display = 'none';
|
||||
tonieListContainer.innerHTML = '<p style="text-align: center; color: #aaa;">Lade Kreativ-Tonies...</p>';
|
||||
|
||||
fetch('/api/tonies')
|
||||
@@ -552,7 +582,7 @@ TEMPLATE = """
|
||||
let html = '';
|
||||
data.forEach(t => {
|
||||
html += `
|
||||
<div class="tonie-item" onclick="uploadToTonie('${t.id}')">
|
||||
<div class="tonie-item" onclick="selectTonie('${t.id}')">
|
||||
<img src="${t.imageUrl || 'https://via.placeholder.com/50'}" alt="Tonie">
|
||||
<div>
|
||||
<div style="font-weight: bold;">${t.name}</div>
|
||||
@@ -568,18 +598,107 @@ TEMPLATE = """
|
||||
});
|
||||
}
|
||||
|
||||
function formatDuration(seconds) {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
return `${mins}:${secs < 10 ? '0' : ''}${secs} Min.`;
|
||||
}
|
||||
|
||||
function selectTonie(tonieId) {
|
||||
selectedTonieId = tonieId;
|
||||
modalTitle.innerText = "Kapitel auswählen";
|
||||
tonieStep1.style.display = 'none';
|
||||
tonieStep2.style.display = 'block';
|
||||
chapterListContainer.innerHTML = '<p style="text-align: center; color: #aaa; padding: 10px;">Ermittle Kapitel-Längen...</p>';
|
||||
timeInfo.innerText = "Lade Spieldauer...";
|
||||
|
||||
// Kapitel und Dauern erst jetzt asynchron vom Server holen
|
||||
fetch(`/api/book-details?book_name=${encodeURIComponent(selectedBookNameForTonie)}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
chapterListContainer.innerHTML = `<p style="color: red; text-align: center;">${data.error}</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
let accumulatedSeconds = 0;
|
||||
let html = '';
|
||||
|
||||
data.tracks.forEach((trackObj, index) => {
|
||||
const durationSec = trackObj.duration || 0;
|
||||
accumulatedSeconds += durationSec;
|
||||
let isChecked = (accumulatedSeconds <= 5400 || index === 0) ? 'checked' : '';
|
||||
let durationFormatted = durationSec > 0 ? formatDuration(durationSec) : '';
|
||||
|
||||
html += `
|
||||
<div class="chapter-item">
|
||||
<label>
|
||||
<div style="display: flex; align-items: center; gap: 8px; overflow: hidden;">
|
||||
<input type="checkbox" class="chapter-checkbox" value="${trackObj.name}" data-duration="${durationSec}" ${isChecked} onchange="updateTotalTime()">
|
||||
<span style="word-break: break-all;">${index + 1}. ${trackObj.name}</span>
|
||||
</div>
|
||||
<span class="chapter-info-text">${durationFormatted}</span>
|
||||
</label>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
chapterListContainer.innerHTML = html;
|
||||
updateTotalTime();
|
||||
})
|
||||
.catch(err => {
|
||||
chapterListContainer.innerHTML = '<p style="color: red; text-align: center;">Fehler beim Laden der Kapitel.</p>';
|
||||
});
|
||||
}
|
||||
|
||||
function backToTonieList() {
|
||||
modalTitle.innerText = "Kreativ-Tonie auswählen";
|
||||
tonieStep2.style.display = 'none';
|
||||
tonieStep1.style.display = 'block';
|
||||
}
|
||||
|
||||
function updateTotalTime() {
|
||||
const checkboxes = document.querySelectorAll('.chapter-checkbox:checked');
|
||||
let totalSeconds = 0;
|
||||
checkboxes.forEach(cb => {
|
||||
totalSeconds += parseFloat(cb.getAttribute('data-duration') || 0);
|
||||
});
|
||||
|
||||
const totalMinutes = Math.round(totalSeconds / 60);
|
||||
timeInfo.innerText = `Ausgewählt: ${checkboxes.length} Kapitel (${totalMinutes} Min. Gesamtlänge)`;
|
||||
|
||||
if (totalMinutes > 90) {
|
||||
timeInfo.style.color = '#e50914';
|
||||
timeInfo.innerText += " ⚠️ Achtung: Über 90 Minuten Limit!";
|
||||
} else {
|
||||
timeInfo.style.color = '#2ed573';
|
||||
}
|
||||
}
|
||||
|
||||
function closeTonieModal() { tonieModal.style.display = 'none'; }
|
||||
|
||||
function uploadToTonie(tonieId) {
|
||||
function startUpload() {
|
||||
const checkboxes = document.querySelectorAll('.chapter-checkbox:checked');
|
||||
const selectedTracks = Array.from(checkboxes).map(cb => cb.value);
|
||||
|
||||
if (selectedTracks.length === 0) {
|
||||
alert('Bitte wähle mindestens ein Kapitel aus.');
|
||||
return;
|
||||
}
|
||||
|
||||
const deleteAll = document.getElementById('deleteAllChapters').checked;
|
||||
closeTonieModal();
|
||||
loadingOverlay.style.display = 'flex';
|
||||
loadingText.innerText = `Lade "${selectedBookNameForTonie}" auf den Kreativ-Tonie...`;
|
||||
loadingText.innerText = `Lade "${selectedBookNameForTonie}" (${selectedTracks.length} Kapitel) hoch...`;
|
||||
|
||||
fetch('/api/upload-to-tonie', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ book_name: selectedBookNameForTonie, tonie_id: tonieId, delete_all: deleteAll })
|
||||
body: JSON.stringify({
|
||||
book_name: selectedBookNameForTonie,
|
||||
tonie_id: selectedTonieId,
|
||||
delete_all: deleteAll,
|
||||
tracks: selectedTracks
|
||||
})
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
@@ -725,9 +844,11 @@ def tonie_login():
|
||||
session['tonie_token'] = token
|
||||
session['tonie_email'] = email
|
||||
logger.info(f"Toniecloud-Login erfolgreich für E-Mail: {email} (IP: {request.remote_addr})")
|
||||
log_to_db("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})")
|
||||
log_to_db("WARNUNG", 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)
|
||||
|
||||
@@ -740,7 +861,6 @@ def logout():
|
||||
def index():
|
||||
books = get_books()
|
||||
|
||||
# Eindeutige Listen für Dropdowns sammeln
|
||||
all_genres = set()
|
||||
all_formats = set()
|
||||
all_agegroups = set()
|
||||
@@ -762,7 +882,6 @@ def index():
|
||||
|
||||
@app.route('/play/<path:book_name>')
|
||||
def play_book(book_name):
|
||||
# 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>')
|
||||
@@ -799,6 +918,28 @@ def api_tonies():
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/book-details')
|
||||
def api_book_details():
|
||||
book_name = request.args.get('book_name')
|
||||
if not book_name:
|
||||
return jsonify({"error": "Kein Hörbuch angegeben."}), 400
|
||||
|
||||
book_path = os.path.join(BASE_DIR, book_name)
|
||||
if not os.path.isdir(book_path):
|
||||
return jsonify({"error": "Hörbuch-Ordner nicht gefunden."}), 404
|
||||
|
||||
try:
|
||||
files = sorted([f for f in os.listdir(book_path) if f.lower().endswith(('.mp3', '.ogg'))])
|
||||
tracks_data = []
|
||||
for f in files:
|
||||
f_path = os.path.join(book_path, f)
|
||||
duration = get_track_duration(f_path)
|
||||
tracks_data.append({"name": f, "duration": duration})
|
||||
|
||||
return jsonify({"tracks": tracks_data})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/upload-to-tonie', methods=['POST'])
|
||||
def api_upload_to_tonie():
|
||||
token = session.get('tonie_token')
|
||||
@@ -809,12 +950,14 @@ def api_upload_to_tonie():
|
||||
book_name = data.get('book_name')
|
||||
tonie_id = data.get('tonie_id')
|
||||
delete_all = data.get('delete_all', False)
|
||||
selected_tracks = data.get('tracks', [])
|
||||
user_email = session.get('tonie_email', 'Unbekannt')
|
||||
|
||||
if not book_name or not tonie_id:
|
||||
if not book_name or not tonie_id or not selected_tracks:
|
||||
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})")
|
||||
logger.info(f"Start Upload von '{book_name}' ({len(selected_tracks)} Kapitel) auf Tonie {tonie_id} durch Benutzer: {user_email} (IP: {request.remote_addr})")
|
||||
log_to_db("INFO", f"Start Upload von '{book_name}' ({len(selected_tracks)} Kapitel) auf Tonie {tonie_id} durch Benutzer: {user_email} (IP: {request.remote_addr})")
|
||||
|
||||
try:
|
||||
household_id = get_household_id(token)
|
||||
@@ -823,11 +966,7 @@ def api_upload_to_tonie():
|
||||
if not os.path.isdir(book_path):
|
||||
return jsonify({"success": False, "error": "Hörbuch-Ordner nicht gefunden."}), 404
|
||||
|
||||
files = sorted([f for f in os.listdir(book_path) if f.lower().endswith(('.mp3', '.ogg'))])
|
||||
if not files:
|
||||
return jsonify({"success": False, "error": "Keine Audiospuren im Hörbuch gefunden."}), 400
|
||||
|
||||
for index, track in enumerate(files):
|
||||
for index, track in enumerate(selected_tracks):
|
||||
file_path = os.path.join(book_path, track)
|
||||
title = os.path.splitext(track)[0]
|
||||
should_delete = delete_all if index == 0 else False
|
||||
@@ -842,11 +981,40 @@ def api_upload_to_tonie():
|
||||
)
|
||||
|
||||
logger.info(f"Upload von '{book_name}' auf Tonie {tonie_id} durch {user_email} erfolgreich abgeschlossen.")
|
||||
log_to_db("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)}")
|
||||
log_to_db("FEHLER", f"Fehler beim Upload von '{book_name}' durch {user_email}: {str(e)}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
|
||||
def log_to_db(typ: str, logsatz: str):
|
||||
try:
|
||||
database = "toniecloud"
|
||||
url = f"http://192.168.189.118:8000/{database}/execute"
|
||||
|
||||
# 1. SQL-Injection / Syntax-Fehler bei Quotes verhindern (' -> '')
|
||||
safe_typ = str(typ).replace("'", "''")
|
||||
safe_logsatz = str(logsatz).replace("'", "''")
|
||||
|
||||
payload = {
|
||||
"aufrufer" : "tonieflix",
|
||||
"sql": f"INSERT INTO flixlog (typ, logsatz) VALUES ('{safe_typ}', '{safe_logsatz}')"
|
||||
}
|
||||
|
||||
# 2. Timeout (z.B. 2 Sekunden), damit das Hauptprogramm nicht hängen bleibt
|
||||
response = requests.post(url, json=payload, timeout=2.0)
|
||||
|
||||
if response.status_code != 200:
|
||||
print(f"⚠️ Log fehlgeschlagen (Code {response.status_code}): {response.text}")
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
print("⚠️ Log-Timeout: Datenbank-Server antwortet nicht.")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Log-Fehler: {str(e)}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
log_to_db("INFO", "Starte Tonieflix")
|
||||
app.run(host='0.0.0.0', port=5005)
|
||||
Reference in new issue
Block a user