Aussortiert
This commit is contained in:
-320
@@ -1,320 +0,0 @@
|
|||||||
import os
|
|
||||||
import json
|
|
||||||
import io
|
|
||||||
import zipfile
|
|
||||||
from flask import Flask, render_template_string, request, session, redirect, url_for, send_from_directory, send_file
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
# WICHTIG: Ändere diesen Key für den produktiven Einsatz
|
|
||||||
app.secret_key = '6a305032-9668-43c3-9a72-a407d5be988f'
|
|
||||||
|
|
||||||
# Konfiguration
|
|
||||||
PASSWORD = 'toniessindteuer'
|
|
||||||
BASE_DIR = '/mnt/NAS/Toniebox/'
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# HTML & JS Template (Frontend)
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
TEMPLATE = """
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Tonieflix</title>
|
|
||||||
<style>
|
|
||||||
:root { --primary: #ff4757; --bg: #f1f2f6; --card: #ffffff; --text: #2f3542; }
|
|
||||||
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; }
|
|
||||||
.search-container { text-align: center; margin-bottom: 30px; }
|
|
||||||
input[type="text"] { padding: 10px 20px; width: 80%; max-width: 400px; border: 2px solid #ddd; border-radius: 25px; font-size: 1rem; outline: none; }
|
|
||||||
input[type="text"]: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: 15px; overflow: hidden; box-shadow: 0 4px 6px rgba(0,0,0,0.1); cursor: pointer; transition: transform 0.2s; }
|
|
||||||
.card:hover { transform: translateY(-5px); }
|
|
||||||
.card img { width: 100%; aspect-ratio: 1; object-fit: cover; background: #ddd; }
|
|
||||||
.card .title { padding: 15px; text-align: center; font-weight: bold; font-size: 1.1rem; }
|
|
||||||
|
|
||||||
/* Player Bar */
|
|
||||||
.player-bar { position: fixed; bottom: 0; left: 0; right: 0; background: var(--card); padding: 15px 20px; box-shadow: 0 -2px 15px rgba(0,0,0,0.15); display: flex; flex-direction: column; align-items: center; gap: 12px; z-index: 1000;}
|
|
||||||
.player-info { font-weight: bold; color: var(--primary); font-size: 1.1rem; text-align: center; }
|
|
||||||
|
|
||||||
.player-controls { display: flex; align-items: center; gap: 15px; width: 100%; max-width: 800px; justify-content: center; flex-wrap: wrap; }
|
|
||||||
audio { flex-grow: 1; min-width: 250px; outline: none; }
|
|
||||||
|
|
||||||
.btn { border: none; padding: 10px 15px; border-radius: 25px; cursor: pointer; font-size: 1rem; font-weight: bold; transition: opacity 0.2s; }
|
|
||||||
.btn:hover { opacity: 0.8; }
|
|
||||||
.btn:disabled { background: #ccc; cursor: not-allowed; opacity: 1; }
|
|
||||||
|
|
||||||
.btn-nav { background: var(--primary); color: white; }
|
|
||||||
.btn-dl { background: #2ed573; color: white; text-decoration: none; display: inline-block; }
|
|
||||||
|
|
||||||
.logout { position: absolute; top: 20px; right: 20px; color: #57606f; text-decoration: none; font-weight: bold; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<a href="{{ url_for('logout') }}" class="logout">Abmelden</a>
|
|
||||||
<h1>🎧 Tonieflix</h1>
|
|
||||||
|
|
||||||
<div class="search-container">
|
|
||||||
<input type="text" id="searchInput" placeholder="Hörbuch suchen...">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid" id="bookGrid">
|
|
||||||
{% for book in books %}
|
|
||||||
<div class="card" data-title="{{ book.name.lower() }}" onclick='startBook({{ book|tojson }})'>
|
|
||||||
{% if book.has_cover %}
|
|
||||||
<img src="{{ url_for('serve_cover', book_name=book.name) }}" alt="Cover">
|
|
||||||
{% else %}
|
|
||||||
<img src="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' fill='%23ddd' viewBox='0 0 1 1'></svg>" alt="Kein Cover">
|
|
||||||
{% endif %}
|
|
||||||
<div class="title">{{ book.name }}</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="player-bar" id="playerBar" style="display: none;">
|
|
||||||
<div class="player-info" id="playerInfo">Wird geladen...</div>
|
|
||||||
<div class="player-controls">
|
|
||||||
<button class="btn btn-nav" id="prevBtn" onclick="prevTrack()">⏮ Zurück</button>
|
|
||||||
<audio id="audioPlayer" controls></audio>
|
|
||||||
<button class="btn btn-nav" id="nextBtn" onclick="nextTrack()">Weiter ⏭</button>
|
|
||||||
<a href="#" class="btn btn-dl" id="downloadBtn">⬇️ Buch als ZIP laden</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const audioPlayer = document.getElementById('audioPlayer');
|
|
||||||
const playerBar = document.getElementById('playerBar');
|
|
||||||
const playerInfo = document.getElementById('playerInfo');
|
|
||||||
const searchInput = document.getElementById('searchInput');
|
|
||||||
const cards = document.querySelectorAll('.card');
|
|
||||||
|
|
||||||
const prevBtn = document.getElementById('prevBtn');
|
|
||||||
const nextBtn = document.getElementById('nextBtn');
|
|
||||||
const downloadBtn = document.getElementById('downloadBtn');
|
|
||||||
|
|
||||||
let currentBookState = { bookName: '', tracks: [], trackIndex: 0 };
|
|
||||||
|
|
||||||
// Suchfunktion
|
|
||||||
searchInput.addEventListener('input', (e) => {
|
|
||||||
const term = e.target.value.toLowerCase();
|
|
||||||
cards.forEach(card => {
|
|
||||||
if (card.dataset.title.includes(term)) {
|
|
||||||
card.style.display = 'block';
|
|
||||||
} else {
|
|
||||||
card.style.display = 'none';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Status aus localStorage laden
|
|
||||||
window.addEventListener('load', () => {
|
|
||||||
const savedState = localStorage.getItem('toniboni_state');
|
|
||||||
if (savedState) {
|
|
||||||
currentBookState = JSON.parse(savedState);
|
|
||||||
playerBar.style.display = 'flex';
|
|
||||||
updatePlayerUI();
|
|
||||||
loadTrackInfo(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Hörbuch starten
|
|
||||||
function startBook(bookData) {
|
|
||||||
if (!bookData.tracks || bookData.tracks.length === 0) {
|
|
||||||
alert("Keine Audiodateien in diesem Ordner gefunden!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (currentBookState.bookName === bookData.name) {
|
|
||||||
audioPlayer.play();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentBookState = {
|
|
||||||
bookName: bookData.name,
|
|
||||||
tracks: bookData.tracks,
|
|
||||||
trackIndex: 0
|
|
||||||
};
|
|
||||||
playerBar.style.display = 'flex';
|
|
||||||
loadTrackInfo(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track laden & UI aktualisieren
|
|
||||||
function loadTrackInfo(autoPlay) {
|
|
||||||
const track = currentBookState.tracks[currentBookState.trackIndex];
|
|
||||||
audioPlayer.src = `/stream/${encodeURIComponent(currentBookState.bookName)}/${encodeURIComponent(track)}`;
|
|
||||||
|
|
||||||
updatePlayerUI();
|
|
||||||
|
|
||||||
const savedState = JSON.parse(localStorage.getItem('toniboni_state'));
|
|
||||||
if (savedState && savedState.bookName === currentBookState.bookName && savedState.trackIndex === currentBookState.trackIndex) {
|
|
||||||
audioPlayer.currentTime = savedState.currentTime || 0;
|
|
||||||
} else {
|
|
||||||
audioPlayer.currentTime = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (autoPlay) {
|
|
||||||
audioPlayer.play();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updatePlayerUI() {
|
|
||||||
const trackCount = currentBookState.tracks.length;
|
|
||||||
playerInfo.innerText = `${currentBookState.bookName} (Kapitel ${currentBookState.trackIndex + 1} von ${trackCount})`;
|
|
||||||
|
|
||||||
// Buttons de/aktivieren
|
|
||||||
prevBtn.disabled = currentBookState.trackIndex === 0;
|
|
||||||
nextBtn.disabled = currentBookState.trackIndex === trackCount - 1;
|
|
||||||
|
|
||||||
// Download-Link anpassen
|
|
||||||
downloadBtn.href = `/download/${encodeURIComponent(currentBookState.bookName)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigation
|
|
||||||
function prevTrack() {
|
|
||||||
if (currentBookState.trackIndex > 0) {
|
|
||||||
currentBookState.trackIndex--;
|
|
||||||
loadTrackInfo(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function nextTrack() {
|
|
||||||
if (currentBookState.trackIndex < currentBookState.tracks.length - 1) {
|
|
||||||
currentBookState.trackIndex++;
|
|
||||||
loadTrackInfo(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Speichern
|
|
||||||
audioPlayer.addEventListener('timeupdate', () => {
|
|
||||||
localStorage.setItem('toniboni_state', JSON.stringify({
|
|
||||||
...currentBookState,
|
|
||||||
currentTime: audioPlayer.currentTime
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Automatisch nächstes Kapitel
|
|
||||||
audioPlayer.addEventListener('ended', () => {
|
|
||||||
if (currentBookState.trackIndex < currentBookState.tracks.length - 1) {
|
|
||||||
nextTrack();
|
|
||||||
} else {
|
|
||||||
playerInfo.innerText = `${currentBookState.bookName} - Beendet!`;
|
|
||||||
localStorage.removeItem('toniboni_state');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"""
|
|
||||||
|
|
||||||
LOGIN_TEMPLATE = """
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Login - Toniboni</title>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<style>
|
|
||||||
body { font-family: sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; background: #f1f2f6; margin: 0; }
|
|
||||||
.login-box { background: white; padding: 40px; border-radius: 15px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); text-align: center; }
|
|
||||||
input[type="password"] { padding: 10px; margin: 10px 0; border: 1px solid #ddd; border-radius: 5px; width: 100%; box-sizing: border-box;}
|
|
||||||
button { background: #ff4757; color: white; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer; width: 100%; font-size: 1rem; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="login-box">
|
|
||||||
<h2 style="color: #ff4757;">Toniboni Login</h2>
|
|
||||||
<form method="POST">
|
|
||||||
<input type="password" name="password" placeholder="Passwort" required autofocus>
|
|
||||||
<button type="submit">Eintreten</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"""
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Backend Logik & Routen
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
|
|
||||||
@app.before_request
|
|
||||||
def require_login():
|
|
||||||
if request.endpoint not in ['login'] and not session.get('logged_in'):
|
|
||||||
return redirect(url_for('login'))
|
|
||||||
|
|
||||||
@app.route('/login', methods=['GET', 'POST'])
|
|
||||||
def login():
|
|
||||||
if request.method == 'POST':
|
|
||||||
if request.form.get('password') == PASSWORD:
|
|
||||||
session['logged_in'] = True
|
|
||||||
return redirect(url_for('index'))
|
|
||||||
else:
|
|
||||||
return "Falsches Passwort!", 403
|
|
||||||
return render_template_string(LOGIN_TEMPLATE)
|
|
||||||
|
|
||||||
@app.route('/logout')
|
|
||||||
def logout():
|
|
||||||
session.clear()
|
|
||||||
return redirect(url_for('login'))
|
|
||||||
|
|
||||||
@app.route('/')
|
|
||||||
def index():
|
|
||||||
books = []
|
|
||||||
if os.path.exists(BASE_DIR):
|
|
||||||
for item in sorted(os.listdir(BASE_DIR)):
|
|
||||||
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'))])
|
|
||||||
has_cover = any(f.lower() == 'cover.jpg' for f in files)
|
|
||||||
|
|
||||||
if tracks:
|
|
||||||
books.append({
|
|
||||||
'name': item,
|
|
||||||
'tracks': tracks,
|
|
||||||
'has_cover': has_cover
|
|
||||||
})
|
|
||||||
|
|
||||||
return render_template_string(TEMPLATE, books=books)
|
|
||||||
|
|
||||||
@app.route('/cover/<book_name>')
|
|
||||||
def serve_cover(book_name):
|
|
||||||
book_path = os.path.join(BASE_DIR, book_name)
|
|
||||||
return send_from_directory(book_path, 'cover.jpg')
|
|
||||||
|
|
||||||
@app.route('/stream/<book_name>/<track_name>')
|
|
||||||
def serve_audio(book_name, track_name):
|
|
||||||
book_path = os.path.join(BASE_DIR, book_name)
|
|
||||||
return send_from_directory(book_path, track_name)
|
|
||||||
|
|
||||||
@app.route('/download/<book_name>')
|
|
||||||
def download_book(book_name):
|
|
||||||
book_path = os.path.join(BASE_DIR, book_name)
|
|
||||||
|
|
||||||
# Sicherheitscheck: Existiert der Ordner?
|
|
||||||
if not os.path.isdir(book_path):
|
|
||||||
return "Hörbuch nicht gefunden", 404
|
|
||||||
|
|
||||||
# ZIP-Datei im Arbeitsspeicher erstellen
|
|
||||||
memory_file = io.BytesIO()
|
|
||||||
|
|
||||||
# ZIP_STORED nutzt keine Kompression (geht extrem schnell, gut für MP3s)
|
|
||||||
with zipfile.ZipFile(memory_file, 'w', zipfile.ZIP_STORED) as zf:
|
|
||||||
for root, dirs, files in os.walk(book_path):
|
|
||||||
for file in files:
|
|
||||||
file_path = os.path.join(root, file)
|
|
||||||
# Schreibe Datei ins ZIP (nutzt Dateinamen ohne den absoluten NAS-Pfad)
|
|
||||||
zf.write(file_path, arcname=file)
|
|
||||||
|
|
||||||
memory_file.seek(0)
|
|
||||||
|
|
||||||
# Datei an den Browser schicken
|
|
||||||
return send_file(
|
|
||||||
memory_file,
|
|
||||||
download_name=f"{book_name}.zip",
|
|
||||||
as_attachment=True,
|
|
||||||
mimetype='application/zip'
|
|
||||||
)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
app.run(host='0.0.0.0', port=5005)
|
|
||||||
-441
@@ -1,441 +0,0 @@
|
|||||||
import os
|
|
||||||
import json
|
|
||||||
import io
|
|
||||||
import zipfile
|
|
||||||
from flask import Flask, render_template_string, request, session, redirect, url_for, send_from_directory, send_file, jsonify
|
|
||||||
from flask_cors import CORS
|
|
||||||
|
|
||||||
# Konfiguration
|
|
||||||
PASSWORD = 'toniessindteuer'
|
|
||||||
BASE_DIR = '/mnt/NAS/Toniebox/'
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
app.config['SECRET_KEY'] = 'tonieflix-super-secret-key'
|
|
||||||
CORS(app, supports_credentials=True, allow_headers="*")
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Hilfsfunktionen
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
def get_books(single_book=None):
|
|
||||||
books = []
|
|
||||||
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:
|
|
||||||
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'))])
|
|
||||||
has_cover = any(f.lower() == 'cover.jpg' for f in files)
|
|
||||||
|
|
||||||
if tracks:
|
|
||||||
books.append({
|
|
||||||
'name': item,
|
|
||||||
'tracks': tracks,
|
|
||||||
'has_cover': has_cover
|
|
||||||
})
|
|
||||||
return books
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# HTML & JS Template (Frontend)
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
TEMPLATE = """
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Tonieflix</title>
|
|
||||||
<style>
|
|
||||||
:root { --primary: #e50914; --bg: #141414; --card: #1f1f1f; --text: #ffffff; }
|
|
||||||
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; }
|
|
||||||
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; }
|
|
||||||
input[type="text"]: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-actions { padding: 10px; text-align: center; }
|
|
||||||
.btn-direct { 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 { background: var(--primary); color: white; border-color: var(--primary); }
|
|
||||||
|
|
||||||
/* Player Bar - Höher gesetzt für Handys mit Gestensteuerung */
|
|
||||||
.player-bar {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 20px;
|
|
||||||
left: 15px;
|
|
||||||
right: 15px;
|
|
||||||
background: #181818;
|
|
||||||
border: 2px solid var(--primary);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 15px 20px;
|
|
||||||
box-shadow: 0 -4px 20px rgba(0,0,0,0.8);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
.player-info { font-weight: bold; color: var(--primary); font-size: 1.1rem; text-align: center; }
|
|
||||||
|
|
||||||
.player-controls { display: flex; align-items: center; gap: 15px; width: 100%; max-width: 800px; justify-content: center; flex-wrap: wrap; }
|
|
||||||
audio { flex-grow: 1; min-width: 250px; outline: none; }
|
|
||||||
|
|
||||||
.btn { border: none; padding: 10px 15px; border-radius: 25px; cursor: pointer; font-size: 1rem; font-weight: bold; transition: opacity 0.2s; }
|
|
||||||
.btn:hover { opacity: 0.8; }
|
|
||||||
.btn:disabled { background: #444; color: #777; cursor: not-allowed; opacity: 1; }
|
|
||||||
|
|
||||||
.btn-nav { background: var(--primary); color: white; text-decoration: none; display: inline-block; }
|
|
||||||
.btn-dl { background: #2ed573; color: white; text-decoration: none; display: inline-block; }
|
|
||||||
|
|
||||||
.logout { position: absolute; top: 20px; right: 20px; color: #aaa; text-decoration: none; font-weight: bold; }
|
|
||||||
.logout:hover { color: #fff; }
|
|
||||||
|
|
||||||
/* Autoplay Overlay */
|
|
||||||
#autoplayOverlay {
|
|
||||||
position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
|
|
||||||
background: rgba(0,0,0,0.9); z-index: 9999;
|
|
||||||
display: flex; flex-direction: column; justify-content: center; align-items: center;
|
|
||||||
color: white; cursor: pointer; display: none; text-align: center; padding: 20px; box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.play-pulse {
|
|
||||||
font-size: 6rem; margin-bottom: 20px; text-shadow: 0 0 20px var(--primary);
|
|
||||||
animation: pulse 1.5s infinite;
|
|
||||||
}
|
|
||||||
@keyframes pulse {
|
|
||||||
0% { transform: scale(1); }
|
|
||||||
50% { transform: scale(1.1); }
|
|
||||||
100% { transform: scale(1); }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<a href="{{ url_for('logout') }}" class="logout">Abmelden</a>
|
|
||||||
<h1>🎬 Tonieflix</h1>
|
|
||||||
|
|
||||||
<div class="top-controls">
|
|
||||||
{% if single_mode %}
|
|
||||||
<a href="{{ url_for('index') }}" class="btn btn-nav" style="font-size: 1.1rem; padding: 12px 25px;">📚 Gesamte Bibliothek laden</a>
|
|
||||||
{% else %}
|
|
||||||
<input type="text" id="searchInput" placeholder="Hörbuch suchen...">
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid" id="bookGrid">
|
|
||||||
{% for book in books %}
|
|
||||||
<div class="card" data-title="{{ book.name.lower() }}">
|
|
||||||
<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">
|
|
||||||
{% else %}
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
<div class="card-actions">
|
|
||||||
<button class="btn-direct" data-name="{{ book.name }}">🔗 Direktlink</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<p style="text-align: center; width: 100%; color: #aaa;">Keine Hörbücher gefunden.</p>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="player-bar" id="playerBar" style="display: none;">
|
|
||||||
<div class="player-info" id="playerInfo">Wird geladen...</div>
|
|
||||||
<div class="player-controls">
|
|
||||||
<button class="btn btn-nav" id="prevBtn">⏮</button>
|
|
||||||
<audio id="audioPlayer" controls></audio>
|
|
||||||
<button class="btn btn-nav" id="nextBtn">⏭</button>
|
|
||||||
<a href="#" class="btn btn-dl" id="downloadBtn">⬇️ ZIP</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Overlay für Browser, die Autoplay blockieren -->
|
|
||||||
<div id="autoplayOverlay">
|
|
||||||
<div class="play-pulse">▶️</div>
|
|
||||||
<h2>Tippe irgendwo auf den Bildschirm, um zu starten</h2>
|
|
||||||
<h3 id="overlayBookName" style="color: var(--primary); margin-top: 10px;"></h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const audioPlayer = document.getElementById('audioPlayer');
|
|
||||||
const playerBar = document.getElementById('playerBar');
|
|
||||||
const playerInfo = document.getElementById('playerInfo');
|
|
||||||
const searchInput = document.getElementById('searchInput');
|
|
||||||
const cards = document.querySelectorAll('.card');
|
|
||||||
|
|
||||||
const prevBtn = document.getElementById('prevBtn');
|
|
||||||
const nextBtn = document.getElementById('nextBtn');
|
|
||||||
const downloadBtn = document.getElementById('downloadBtn');
|
|
||||||
|
|
||||||
const overlay = document.getElementById('autoplayOverlay');
|
|
||||||
const overlayBookName = document.getElementById('overlayBookName');
|
|
||||||
|
|
||||||
const autoPlayBookName = {{ auto_play|tojson }};
|
|
||||||
let currentBookState = { bookName: '', tracks: [], trackIndex: 0 };
|
|
||||||
|
|
||||||
// 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';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Event-Listener
|
|
||||||
document.querySelectorAll('.card-clickable').forEach(el => {
|
|
||||||
el.addEventListener('click', () => {
|
|
||||||
const name = el.getAttribute('data-name');
|
|
||||||
const tracks = JSON.parse(el.getAttribute('data-tracks'));
|
|
||||||
startBook(name, tracks);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelectorAll('.btn-direct').forEach(btn => {
|
|
||||||
btn.addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
const name = btn.getAttribute('data-name');
|
|
||||||
copyDirectLink(name);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Overlay Klick (Startet Wiedergabe)
|
|
||||||
overlay.addEventListener('click', () => {
|
|
||||||
audioPlayer.play();
|
|
||||||
overlay.style.display = 'none';
|
|
||||||
});
|
|
||||||
|
|
||||||
prevBtn.addEventListener('click', prevTrack);
|
|
||||||
nextBtn.addEventListener('click', nextTrack);
|
|
||||||
|
|
||||||
// Direktlink kopieren
|
|
||||||
function copyDirectLink(bookName) {
|
|
||||||
const url = window.location.origin + '/play/' + encodeURIComponent(bookName);
|
|
||||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
||||||
navigator.clipboard.writeText(url).then(() => {
|
|
||||||
alert('Direktlink kopiert! URL:\\n\\n' + url);
|
|
||||||
}).catch(() => { prompt('Direktlink kopieren:', url); });
|
|
||||||
} else {
|
|
||||||
prompt('Direktlink kopieren:', url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Startet Hörbuch
|
|
||||||
function startBook(bookName, tracks) {
|
|
||||||
if (!tracks || tracks.length === 0) return;
|
|
||||||
|
|
||||||
if (currentBookState.bookName === bookName) {
|
|
||||||
audioPlayer.play().catch(e => console.warn(e));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentBookState = { bookName: bookName, tracks: tracks, trackIndex: 0 };
|
|
||||||
playerBar.style.display = 'flex';
|
|
||||||
loadTrackInfo(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadTrackInfo(autoPlay) {
|
|
||||||
const track = currentBookState.tracks[currentBookState.trackIndex];
|
|
||||||
audioPlayer.src = `/stream/${encodeURIComponent(currentBookState.bookName)}/${encodeURIComponent(track)}`;
|
|
||||||
|
|
||||||
updatePlayerUI();
|
|
||||||
|
|
||||||
const savedState = JSON.parse(localStorage.getItem('tonieflix_state'));
|
|
||||||
if (savedState && savedState.bookName === currentBookState.bookName && savedState.trackIndex === currentBookState.trackIndex) {
|
|
||||||
audioPlayer.currentTime = savedState.currentTime || 0;
|
|
||||||
} else {
|
|
||||||
audioPlayer.currentTime = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (autoPlay) {
|
|
||||||
const playPromise = audioPlayer.play();
|
|
||||||
if (playPromise !== undefined) {
|
|
||||||
playPromise.catch(error => {
|
|
||||||
console.warn("Autoplay blockiert vom Browser:", error);
|
|
||||||
overlayBookName.innerText = currentBookState.bookName;
|
|
||||||
overlay.style.display = 'flex';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updatePlayerUI() {
|
|
||||||
const trackCount = currentBookState.tracks.length;
|
|
||||||
playerInfo.innerText = `${currentBookState.bookName} (Kapitel ${currentBookState.trackIndex + 1} von ${trackCount})`;
|
|
||||||
|
|
||||||
prevBtn.disabled = currentBookState.trackIndex === 0;
|
|
||||||
nextBtn.disabled = currentBookState.trackIndex === trackCount - 1;
|
|
||||||
downloadBtn.href = `/download/${encodeURIComponent(currentBookState.bookName)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function prevTrack() {
|
|
||||||
if (currentBookState.trackIndex > 0) {
|
|
||||||
currentBookState.trackIndex--;
|
|
||||||
loadTrackInfo(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function nextTrack() {
|
|
||||||
if (currentBookState.trackIndex < currentBookState.tracks.length - 1) {
|
|
||||||
currentBookState.trackIndex++;
|
|
||||||
loadTrackInfo(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seite geladen
|
|
||||||
window.addEventListener('load', () => {
|
|
||||||
if (autoPlayBookName) {
|
|
||||||
const targetEl = document.querySelector(`.card-clickable[data-name="${CSS.escape(autoPlayBookName)}"]`);
|
|
||||||
if (targetEl) {
|
|
||||||
const tracks = JSON.parse(targetEl.getAttribute('data-tracks'));
|
|
||||||
startBook(autoPlayBookName, tracks);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const savedState = localStorage.getItem('tonieflix_state');
|
|
||||||
if (savedState) {
|
|
||||||
currentBookState = JSON.parse(savedState);
|
|
||||||
playerBar.style.display = 'flex';
|
|
||||||
updatePlayerUI();
|
|
||||||
loadTrackInfo(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
audioPlayer.addEventListener('timeupdate', () => {
|
|
||||||
localStorage.setItem('tonieflix_state', JSON.stringify({
|
|
||||||
...currentBookState,
|
|
||||||
currentTime: audioPlayer.currentTime
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
audioPlayer.addEventListener('ended', () => {
|
|
||||||
if (currentBookState.trackIndex < currentBookState.tracks.length - 1) {
|
|
||||||
nextTrack();
|
|
||||||
} else {
|
|
||||||
playerInfo.innerText = `${currentBookState.bookName} - Beendet!`;
|
|
||||||
localStorage.removeItem('tonieflix_state');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"""
|
|
||||||
|
|
||||||
LOGIN_TEMPLATE = """
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Login - Tonieflix</title>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<style>
|
|
||||||
body { font-family: sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; background: #141414; color: #fff; margin: 0; }
|
|
||||||
.login-box { background: #1f1f1f; padding: 40px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.8); text-align: center; width: 100%; max-width: 320px; }
|
|
||||||
input[type="password"] { padding: 12px; margin: 15px 0; border: 1px solid #333; background: #2b2b2b; color: #fff; border-radius: 5px; width: 100%; box-sizing: border-box;}
|
|
||||||
button { background: #e50914; color: white; border: none; padding: 12px 20px; border-radius: 5px; cursor: pointer; width: 100%; font-size: 1rem; font-weight: bold; }
|
|
||||||
button:hover { opacity: 0.9; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="login-box">
|
|
||||||
<h2 style="color: #e50914; margin-top: 0;">🎬 Tonieflix</h2>
|
|
||||||
<form method="POST">
|
|
||||||
<input type="password" name="password" placeholder="Passwort" required autofocus>
|
|
||||||
<button type="submit">Eintreten</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"""
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Backend Logik & Routen
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
|
|
||||||
@app.before_request
|
|
||||||
def require_login():
|
|
||||||
if request.method == 'OPTIONS':
|
|
||||||
return None
|
|
||||||
if request.path.startswith('/api/'):
|
|
||||||
return None
|
|
||||||
if request.args.get('pw') == PASSWORD:
|
|
||||||
return None
|
|
||||||
if request.endpoint not in ['login', 'static'] and not session.get('logged_in'):
|
|
||||||
return redirect(url_for('login', next=request.full_path if request.full_path != '/' else None))
|
|
||||||
|
|
||||||
@app.route('/login', methods=['GET', 'POST'])
|
|
||||||
def login():
|
|
||||||
next_url = request.args.get('next') or url_for('index')
|
|
||||||
if request.method == 'POST':
|
|
||||||
if request.form.get('password') == PASSWORD:
|
|
||||||
session['logged_in'] = True
|
|
||||||
return redirect(next_url)
|
|
||||||
else:
|
|
||||||
return "Falsches Passwort!", 403
|
|
||||||
return render_template_string(LOGIN_TEMPLATE)
|
|
||||||
|
|
||||||
@app.route('/logout')
|
|
||||||
def logout():
|
|
||||||
session.clear()
|
|
||||||
return redirect(url_for('login'))
|
|
||||||
|
|
||||||
@app.route('/')
|
|
||||||
def index():
|
|
||||||
return render_template_string(TEMPLATE, books=get_books(), auto_play="", single_mode=False)
|
|
||||||
|
|
||||||
@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)
|
|
||||||
|
|
||||||
@app.route('/cover/<path:book_name>')
|
|
||||||
def serve_cover(book_name):
|
|
||||||
book_path = os.path.join(BASE_DIR, book_name)
|
|
||||||
return send_from_directory(book_path, 'cover.jpg')
|
|
||||||
|
|
||||||
@app.route('/stream/<path:book_name>/<path:track_name>')
|
|
||||||
def serve_audio(book_name, track_name):
|
|
||||||
book_path = os.path.join(BASE_DIR, book_name)
|
|
||||||
return send_from_directory(book_path, track_name)
|
|
||||||
|
|
||||||
@app.route('/download/<path:book_name>')
|
|
||||||
def download_book(book_name):
|
|
||||||
book_path = os.path.join(BASE_DIR, book_name)
|
|
||||||
if not os.path.isdir(book_path):
|
|
||||||
return "Hörbuch nicht gefunden", 404
|
|
||||||
|
|
||||||
memory_file = io.BytesIO()
|
|
||||||
with zipfile.ZipFile(memory_file, 'w', zipfile.ZIP_STORED) as zf:
|
|
||||||
for root, dirs, files in os.walk(book_path):
|
|
||||||
for file in files:
|
|
||||||
file_path = os.path.join(root, file)
|
|
||||||
zf.write(file_path, arcname=file)
|
|
||||||
|
|
||||||
memory_file.seek(0)
|
|
||||||
return send_file(
|
|
||||||
memory_file,
|
|
||||||
download_name=f"{book_name}.zip",
|
|
||||||
as_attachment=True,
|
|
||||||
mimetype='application/zip'
|
|
||||||
)
|
|
||||||
|
|
||||||
@app.route('/api/books')
|
|
||||||
def api_books():
|
|
||||||
client_password = request.headers.get('X-Password')
|
|
||||||
if client_password != PASSWORD:
|
|
||||||
return jsonify({"error": "Falsches Passwort"}), 403
|
|
||||||
return jsonify(get_books())
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
app.run(host='0.0.0.0', port=5005)
|
|
||||||
-215
@@ -1,215 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
from pathlib import Path
|
|
||||||
from playwright.async_api import async_playwright
|
|
||||||
import requests
|
|
||||||
|
|
||||||
async def get_tonie_api_token(username: str, password: str) -> tuple[str | None, str | None]:
|
|
||||||
"""Loggt sich automatisiert bei Tonies ein und gibt Access- und Refresh-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})
|
|
||||||
page = await context.new_page()
|
|
||||||
|
|
||||||
login_url = (
|
|
||||||
"https://login.tonies.com/auth/realms/tonies/protocol/openid-connect/auth?"
|
|
||||||
"client_id=my-tonies&redirect_uri=https%3A%2F%2Fmy.tonies.com%2Flogin%3Fredirect%3D%252F&"
|
|
||||||
"state=24115267-a4f0-4033-a2d4-bef68c83b07d&response_mode=fragment&"
|
|
||||||
"response_type=code&scope=openid&nonce=ee64d559-2b1c-4f4d-a01f-40d84c4dced1&"
|
|
||||||
"ui_locales=de&code_challenge=_lFACYXyUjTQEnb3JiqUWQ7xu4zA50xMNyXhhp3uUe8&"
|
|
||||||
"code_challenge_method=S256"
|
|
||||||
)
|
|
||||||
|
|
||||||
await page.goto(login_url)
|
|
||||||
|
|
||||||
try:
|
|
||||||
await page.wait_for_selector("[data-testid='email-toggle']", timeout=5000)
|
|
||||||
await page.click("[data-testid='email-toggle']")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
await page.wait_for_timeout(1000)
|
|
||||||
await page.wait_for_selector("input[id='username'], input[name='username']", timeout=5000)
|
|
||||||
|
|
||||||
await page.type("input[id='username'], input[name='username']", username, delay=50)
|
|
||||||
await page.type("input[id='password'], input[name='password']", password, delay=50)
|
|
||||||
await page.click("input[type='submit'], button[type='submit']")
|
|
||||||
|
|
||||||
await page.wait_for_timeout(5000)
|
|
||||||
|
|
||||||
access_token = await page.evaluate("() => window.localStorage.getItem('authorization')")
|
|
||||||
refresh_token = await page.evaluate("() => window.localStorage.getItem('refreshToken')")
|
|
||||||
|
|
||||||
await browser.close()
|
|
||||||
return access_token, refresh_token
|
|
||||||
|
|
||||||
|
|
||||||
def get_household_id(access_token: str) -> str:
|
|
||||||
"""Ermittelt die Haushalts-ID des Accounts."""
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {access_token}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"User-Agent": "Mozilla/5.0"
|
|
||||||
}
|
|
||||||
resp = requests.get("https://api.tonie.cloud/v2/households", headers=headers)
|
|
||||||
if resp.status_code != 200:
|
|
||||||
raise Exception(f"Fehler beim Laden der Haushalte: {resp.status_code} - {resp.text}")
|
|
||||||
|
|
||||||
households = resp.json()
|
|
||||||
household_list = households if isinstance(households, list) else households.get("households", [])
|
|
||||||
if not household_list:
|
|
||||||
raise Exception("Kein Haushalt im Account gefunden.")
|
|
||||||
return household_list[0]["id"]
|
|
||||||
|
|
||||||
|
|
||||||
def get_creative_tonies(access_token: str, household_id: str) -> list[dict]:
|
|
||||||
"""Ruft alle Kreativ-Tonies ab."""
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {access_token}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"User-Agent": "Mozilla/5.0"
|
|
||||||
}
|
|
||||||
resp = requests.get(f"https://api.tonie.cloud/v2/households/{household_id}/creativetonies", headers=headers)
|
|
||||||
if resp.status_code != 200:
|
|
||||||
raise Exception(f"Fehler beim Laden der Kreativ-Tonies: {resp.status_code} - {resp.text}")
|
|
||||||
|
|
||||||
raw_tonies = resp.json()
|
|
||||||
if isinstance(raw_tonies, dict):
|
|
||||||
raw_tonies = raw_tonies.get("creativeTonies", raw_tonies.get("items", []))
|
|
||||||
|
|
||||||
return [{"id": t.get("id"), "name": t.get("name"), "imageUrl": t.get("imageUrl"), "chapters": t.get("chapters", [])} for t in raw_tonies]
|
|
||||||
|
|
||||||
def upload_audio_to_creative_tonie(access_token: str, household_id: str, tonie_id: str, file_path: str, title: str, delete_all_chapters: bool):
|
|
||||||
"""Lädt eine Audiodatei hoch und fügt sie als Kapitel an den angegebenen Kreativ-Tonie an."""
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {access_token}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"User-Agent": "Mozilla/5.0"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Step 1: Upload-Berechtigung bei Toniecloud anfordern
|
|
||||||
print("Fordere S3-Upload-Berechtigung an...")
|
|
||||||
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.status_code}): {upload_req_resp.text}")
|
|
||||||
|
|
||||||
amazon_data = upload_req_resp.json()
|
|
||||||
print("DEBUG S3 Response:", amazon_data) # Hilft beim Debuggen falls es immer noch zickt
|
|
||||||
s3_request = amazon_data.get("request", {})
|
|
||||||
fields = s3_request.get("fields", {})
|
|
||||||
file_id = amazon_data.get("fileId")
|
|
||||||
s3_url = s3_request.get("url", "https://bxn-toniecloud-prod-upload.s3.amazonaws.com/")
|
|
||||||
|
|
||||||
# Step 2: Datei an Amazon S3 hochladen (multipart/form-data)
|
|
||||||
print(f"Lade Datei '{file_path}' zu S3 hoch...")
|
|
||||||
if not Path(file_path).exists():
|
|
||||||
raise FileNotFoundError(f"Die Datei '{file_path}' wurde nicht gefunden.")
|
|
||||||
|
|
||||||
multipart_fields = {
|
|
||||||
"key": fields.get("key"),
|
|
||||||
"x-amz-algorithm": fields.get("x-amz-algorithm"),
|
|
||||||
"x-amz-credential": fields.get("x-amz-credential"),
|
|
||||||
"x-amz-date": fields.get("x-amz-date"),
|
|
||||||
"policy": fields.get("policy"),
|
|
||||||
"x-amz-signature": fields.get("x-amz-signature"),
|
|
||||||
"x-amz-security-token": fields.get("x-amz-security-token"),
|
|
||||||
}
|
|
||||||
|
|
||||||
with open(file_path, "rb") as f:
|
|
||||||
files = {"file": (fields.get("key"), f, "audio/mpeg")}
|
|
||||||
s3_resp = requests.post(s3_url, data=multipart_fields, files=files)
|
|
||||||
|
|
||||||
if s3_resp.status_code not in (200, 204):
|
|
||||||
raise Exception(f"S3-Upload fehlgeschlagen mit Status {s3_resp.status_code}: {s3_resp.text}")
|
|
||||||
|
|
||||||
print("S3-Upload erfolgreich.")
|
|
||||||
|
|
||||||
# Step 3: Kapitelkonfiguration aktualisieren / an den Tonie anfügen
|
|
||||||
print("Füge Kapitel zum Kreativ-Tonie hinzu...")
|
|
||||||
|
|
||||||
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}")
|
|
||||||
|
|
||||||
tonie_data = tonie_detail_resp.json()
|
|
||||||
|
|
||||||
cleaned_chapters = []
|
|
||||||
if not delete_all_chapters:
|
|
||||||
for ch in tonie_data.get("chapters", []):
|
|
||||||
c_id = ch.get("id")
|
|
||||||
c_file = ch.get("file")
|
|
||||||
c_title = ch.get("title", "")
|
|
||||||
|
|
||||||
# Falls file ein Dictionary ist oder leer/None, direkt verwerfen
|
|
||||||
if isinstance(c_file, dict):
|
|
||||||
c_file = c_file.get("id") or c_file.get("file")
|
|
||||||
|
|
||||||
# Streng prüfen, ob ein echter String vorhanden ist (kein None, kein leerer String, kein "none")
|
|
||||||
if c_id and c_file and str(c_file).strip() 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
|
|
||||||
new_chapter = {
|
|
||||||
"id": str(file_id),
|
|
||||||
"file": str(file_id),
|
|
||||||
"title": str(title)
|
|
||||||
}
|
|
||||||
cleaned_chapters.append(new_chapter)
|
|
||||||
|
|
||||||
# Payload für den Update-Endpunkt zusammenbauen
|
|
||||||
update_payload = {
|
|
||||||
"name": tonie_data.get("name"),
|
|
||||||
"imageUrl": tonie_data.get("imageUrl"),
|
|
||||||
"chapters": cleaned_chapters
|
|
||||||
}
|
|
||||||
|
|
||||||
update_resp = requests.put(
|
|
||||||
f"https://api.tonie.cloud/v2/households/{household_id}/creativetonies/{tonie_id}",
|
|
||||||
json=update_payload,
|
|
||||||
headers=headers
|
|
||||||
)
|
|
||||||
|
|
||||||
if update_resp.status_code not in (200, 204):
|
|
||||||
raise Exception(f"Fehler beim Speichern der Kapitel auf dem Tonie: {update_resp.status_code} - {update_resp.text}")
|
|
||||||
|
|
||||||
print("Kapitel erfolgreich zum Kreativ-Tonie hinzugefügt!")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import json
|
|
||||||
|
|
||||||
USER = "sandromormile@gmail.com"
|
|
||||||
PASS = "Sandro92@tonies"
|
|
||||||
|
|
||||||
print("Starte automatisierten Login...")
|
|
||||||
access_token, refresh_token = asyncio.run(get_tonie_api_token(USER, PASS))
|
|
||||||
|
|
||||||
if access_token:
|
|
||||||
print("Login erfolgreich.\n")
|
|
||||||
household_id = get_household_id(access_token)
|
|
||||||
|
|
||||||
# Beispiel 1: Alle Kreativ-Tonies auflisten
|
|
||||||
tonies = get_creative_tonies(access_token, household_id)
|
|
||||||
print(f"Gefundene Kreativ-Tonies: {len(tonies)}")
|
|
||||||
print(json.dumps(tonies, indent=4, ensure_ascii=False))
|
|
||||||
|
|
||||||
# Beispiel 2: Datei auf den ersten Kreativ-Tonie in der Liste hochladen (falls vorhanden)
|
|
||||||
if tonies:
|
|
||||||
target_tonie = tonies[1]
|
|
||||||
print(f"\nLade Datei auf Kreativ-Tonie '{target_tonie['name']}' (ID: {target_tonie['id']}) hoch...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
upload_audio_to_creative_tonie(
|
|
||||||
access_token=access_token,
|
|
||||||
household_id=household_id,
|
|
||||||
tonie_id=target_tonie["id"],
|
|
||||||
file_path="beispiel.mp3",
|
|
||||||
title="beispiel.mp3",
|
|
||||||
delete_all_chapters=True
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Upload fehlgeschlagen: {e}")
|
|
||||||
else:
|
|
||||||
print("Login fehlgeschlagen: Kein Token erhalten.")
|
|
||||||
Reference in New Issue
Block a user