Files
quizify/app.py
2025-11-15 18:36:13 +01:00

487 lines
17 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
__version__ = "pre-dev"
__all__ = ["quizify"]
__author__ = "SimolZimol"
from flask import Flask, redirect, request, session, url_for, render_template
import os
import spotipy
from spotipy.oauth2 import SpotifyOAuth
import random
from difflib import SequenceMatcher
import re
import json
app = Flask(__name__)
app.secret_key = os.getenv("SECRET_KEY")
# Jinja2-Filter für die Bereinigung von Titeln/Künstlern
@app.template_filter('clean')
def clean_filter(text):
"""Template-Filter um Sonderzeichen aus Titeln/Künstlern zu entfernen"""
if not text:
return ""
return clean_title(str(text))
# Erweiterte Berechtigungen für Web Playback SDK
SCOPE = "user-library-read playlist-read-private streaming user-read-email user-read-private"
def get_locale():
return os.getenv("LANG", "de-DE")
def get_translations():
lang = get_locale()
path = os.path.join(os.path.dirname(__file__), "locales", f"{lang}.json")
try:
with open(path, encoding="utf-8") as f:
return json.load(f)
except Exception:
# Fallback auf Deutsch
with open(os.path.join(os.path.dirname(__file__), "locales", "de-DE.json"), encoding="utf-8") as f:
return json.load(f)
def get_spotify_client():
token_info = session.get("token_info", None)
if not token_info:
# Kein Token, redirect handled elsewhere
return None
# Prüfen, ob Token abgelaufen ist
sp_oauth = SpotifyOAuth(
client_id=os.getenv("SPOTIPY_CLIENT_ID"),
client_secret=os.getenv("SPOTIPY_CLIENT_SECRET"),
redirect_uri=os.getenv("SPOTIPY_REDIRECT_URI"),
scope=SCOPE,
cache_path=".cache"
)
if sp_oauth.is_token_expired(token_info):
token_info = sp_oauth.refresh_access_token(token_info['refresh_token'])
session["token_info"] = token_info
return spotipy.Spotify(auth=token_info['access_token'])
def similarity(a, b):
return SequenceMatcher(None, a.lower(), b.lower()).ratio()
def clean_title(title):
# Entfernt alles in () oder []
title = re.sub(r"(\s*[\(\[][^)\]]*[\)\]])", "", title)
# Entferne alle Arten von Apostrophen, Anführungszeichen und Backticks
title = title.replace("'", "").replace("'", "").replace("'", "")
title = title.replace("`", "").replace("´", "").replace("ʼ", "")
title = title.replace('"', "").replace("", "").replace(""", "").replace(""", "")
title = title.replace("'", "")
# Entferne weitere Sonderzeichen die Probleme machen könnten
title = re.sub(r"[^\w\s\-&]", "", title, flags=re.UNICODE)
return title.strip()
def get_all_playlist_tracks(sp, playlist_id):
tracks = []
offset = 0
limit = 100
while True:
response = sp.playlist_items(playlist_id, additional_types=["track"], limit=limit, offset=offset)
items = response["items"]
if not items:
break
tracks.extend([item["track"] for item in items if item.get("track")])
if len(items) < limit:
break
offset += limit
return tracks
@app.route("/")
def home():
return render_template("login.html", translations=get_translations())
@app.route("/login")
def login():
sp_oauth = SpotifyOAuth(
client_id=os.getenv("SPOTIPY_CLIENT_ID"),
client_secret=os.getenv("SPOTIPY_CLIENT_SECRET"),
redirect_uri=os.getenv("SPOTIPY_REDIRECT_URI"),
scope=SCOPE
)
auth_url = sp_oauth.get_authorize_url()
return redirect(auth_url)
@app.route("/callback")
def callback():
sp_oauth = SpotifyOAuth(
client_id=os.getenv("SPOTIPY_CLIENT_ID"),
client_secret=os.getenv("SPOTIPY_CLIENT_SECRET"),
redirect_uri=os.getenv("SPOTIPY_REDIRECT_URI"),
scope=SCOPE
)
session.clear()
code = request.args.get('code')
token_info = sp_oauth.get_access_token(code)
session["token_info"] = token_info
# Hole User-Infos und speichere sie in der Session
sp = spotipy.Spotify(auth=token_info['access_token'])
user = sp.current_user()
session["user"] = user
return redirect("/playlists")
@app.route("/playlists")
def playlists():
# Sicherstellen, dass der Benutzer eingeloggt ist und ein gültiges Token vorhanden ist
token_info = session.get('token_info', None)
if not token_info:
return redirect('/login')
sp = get_spotify_client()
if not sp:
return redirect('/login')
playlists = sp.current_user_playlists()["items"]
user = session.get('user')
access_token = token_info.get('access_token')
return render_template("playlists.html", playlists=playlists, translations=get_translations(), user=user, access_token=access_token)
@app.route("/quiz/<playlist_id>")
def quiz(playlist_id):
game_mode = request.args.get('mode', 'artist')
local_multiplayer = request.args.get('local_multiplayer', '0')
buzzer_mode = request.args.get('buzzer', '0')
sp = get_spotify_client()
tracks = get_all_playlist_tracks(sp, playlist_id)
if not tracks:
return "Keine Tracks verfügbar!"
# Verwende einen Ring-Buffer: nur die letzten 50 Songs merken (verhindert Session-Overflow)
played_tracks = session.get(f'played_tracks_{playlist_id}', [])
score = session.get(f'score_{playlist_id}', 0)
total_played = session.get(f'total_played_{playlist_id}', 0) # Zähler für alle gespielten Songs
# Wenn alle Songs gespielt wurden, played_tracks zurücksetzen, Score bleibt!
available_tracks = [t for t in tracks if t["id"] not in played_tracks]
if not available_tracks:
played_tracks = []
available_tracks = tracks
# Score NICHT zurücksetzen!
track = random.choice(available_tracks)
played_tracks.append(track["id"])
# Ring-Buffer: Halte nur die letzten 75 gespielten Track-IDs (verhindert Session-Overflow bei großen Playlists)
if len(played_tracks) > 75:
played_tracks = played_tracks[-75:]
total_played += 1
session[f'played_tracks_{playlist_id}'] = played_tracks
session[f'score_{playlist_id}'] = score
session[f'total_played_{playlist_id}'] = total_played
session.modified = True # Erzwinge Session-Speicherung
# Für die Anzeige der beantworteten Fragen (nutze total_played statt len(played_tracks))
answered = total_played - 1 if total_played > 0 else 0
token_info = session.get('token_info', None)
if not token_info:
return redirect('/login')
access_token = token_info['access_token']
all_tracks = []
for item in tracks:
track_info = {
"id": item["id"],
"name": clean_title(item["name"]), # Bereinigter Name für Frontend
"artist": clean_title(item["artists"][0]["name"]), # Bereinigter Künstler
"uri": item["uri"],
"release_date": item.get("album", {}).get("release_date", "Unbekannt")[:4]
}
all_tracks.append(track_info)
# Wähle das passende Template
if buzzer_mode == '1' and local_multiplayer == '1':
template_name = "quiz_buzzer_multiplayer.html"
# Lade Spieler-Scores
player_scores = session.get(f'player_scores_{playlist_id}', [0, 0, 0, 0])
elif buzzer_mode == '1':
template_name = "quiz_buzzer.html"
player_scores = None
elif local_multiplayer == '1':
template_name = "quiz_multiplayer.html"
player_scores = None
else:
template_name = "quiz.html"
player_scores = None
return render_template(
template_name,
track=track,
access_token=access_token,
playlist_id=playlist_id,
game_mode=game_mode,
all_tracks=all_tracks,
question_number=len(played_tracks),
total_questions=len(tracks),
score=score,
answered=answered,
translations=get_translations(),
max_points=1000,
grace_period=5,
decay_rate=50,
player_scores=player_scores
)
@app.route('/gamemodes/<playlist_id>')
def gamemodes(playlist_id):
"""Show available game modes for a playlist and forward to player selection."""
user = session.get('user')
return render_template('gamemodes.html', playlist_id=playlist_id, translations=get_translations(), user=user)
@app.route('/playerselect/<playlist_id>')
def playerselect(playlist_id):
"""Choose singleplayer or local multiplayer before starting the quiz."""
game_mode = request.args.get('mode', 'artist')
buzzer = request.args.get('buzzer', '0')
user = session.get('user')
return render_template('playerselect.html', playlist_id=playlist_id, game_mode=game_mode, buzzer=buzzer, translations=get_translations(), user=user)
@app.route('/team_setup/<playlist_id>')
def team_setup(playlist_id):
"""Setup team names and count for multiplayer buzzer mode."""
game_mode = request.args.get('mode', 'title')
user = session.get('user')
return render_template('team_setup.html',
playlist_id=playlist_id,
game_mode=game_mode,
translations=get_translations(),
user=user)
@app.route('/buzzer_settings/<playlist_id>')
def buzzer_settings(playlist_id):
"""Configure buzzer mode settings before starting."""
game_mode = request.args.get('mode', 'title')
local_multiplayer = request.args.get('local_multiplayer', '0')
user = session.get('user')
return render_template('buzzer_settings.html',
playlist_id=playlist_id,
game_mode=game_mode,
local_multiplayer=local_multiplayer == '1',
translations=get_translations(),
user=user)
@app.route("/search_track", methods=["POST"])
def search_track():
data = request.json
query = data.get('query', '').lower()
all_tracks = data.get('all_tracks', [])
if not query or not all_tracks:
return {"results": []}
# Suche nach bereinigtem Titel und Künstler
results = []
for track in all_tracks:
cleaned_name = clean_title(track["name"])
cleaned_query = clean_title(query)
cleaned_artist = clean_title(track["artist"])
name_similarity = similarity(cleaned_query, cleaned_name)
artist_similarity = similarity(cleaned_query, cleaned_artist)
# Wenn Name oder Künstler zu 80% übereinstimmt
if name_similarity >= 0.8 or artist_similarity >= 0.8:
results.append({
"id": track["id"],
"name": track["name"],
"artist": track["artist"],
"uri": track["uri"],
"similarity": max(name_similarity, artist_similarity)
})
# Nach Ähnlichkeit sortieren
results.sort(key=lambda x: x["similarity"], reverse=True)
return {"results": results}
@app.route("/check_answer", methods=["POST"])
def check_answer():
data = request.json
guess = data.get('guess', '').lower()
correct_answer = data.get('correct_answer', '').lower()
game_mode = data.get('game_mode', 'artist')
playlist_id = data.get('playlist_id')
all_tracks = data.get('all_tracks', [])
# Originalwert für Vergleich speichern
original_guess = guess
# Bei Titel und Künstler: Sonderzeichen entfernen für besseren Vergleich
if game_mode == 'title' or game_mode == 'artist':
guess = clean_title(guess)
correct_answer = clean_title(correct_answer)
if game_mode == 'year':
is_correct = guess == correct_answer
else:
is_correct = similarity(guess, correct_answer) >= 0.9
# Score erhöhen, wenn richtig
if is_correct and playlist_id:
key = f'score_{playlist_id}'
session[key] = session.get(key, 0) + 1
# Bei falscher Antwort: Finde das eingegebene Lied in all_tracks
guessed_track = None
if not is_correct and all_tracks:
for track in all_tracks:
if game_mode == 'title':
if clean_title(track['name'].lower()) == guess:
guessed_track = track
break
elif game_mode == 'artist':
if clean_title(track['artist'].lower()) == guess:
guessed_track = track
break
response = {
"correct": is_correct,
"correct_answer": correct_answer
}
if guessed_track:
response["guessed_track"] = guessed_track
return response
@app.route("/check_answer_buzzer", methods=["POST"])
def check_answer_buzzer():
"""Spezielle Route für Buzzer-Modus mit Punktesystem basierend auf Zeit"""
data = request.json
guess = data.get('guess', '').lower()
correct_answer = data.get('correct_answer', '').lower()
game_mode = data.get('game_mode', 'artist')
playlist_id = data.get('playlist_id')
earned_points = data.get('earned_points', 0)
all_tracks = data.get('all_tracks', [])
player_id = data.get('player_id') # Für Multiplayer
# Originalwert für Vergleich speichern
original_guess = guess
# Bei Titel und Künstler: Sonderzeichen entfernen für besseren Vergleich
if game_mode == 'title' or game_mode == 'artist':
guess = clean_title(guess)
correct_answer = clean_title(correct_answer)
if game_mode == 'year':
is_correct = guess == correct_answer
else:
is_correct = similarity(guess, correct_answer) >= 0.9
# Score erhöhen mit Buzzer-Punkten, wenn richtig
if is_correct and playlist_id:
if player_id:
# Multiplayer: Update Spieler-Score
key = f'player_scores_{playlist_id}'
player_scores = session.get(key, [0, 0, 0, 0])
player_scores[player_id - 1] += earned_points
session[key] = player_scores
else:
# Singleplayer: Update Gesamt-Score
key = f'score_{playlist_id}'
session[key] = session.get(key, 0) + earned_points
# Bei falscher Antwort: Finde das eingegebene Lied in all_tracks
guessed_track = None
if not is_correct and all_tracks:
for track in all_tracks:
if game_mode == 'title':
if clean_title(track['name'].lower()) == guess:
guessed_track = track
break
elif game_mode == 'artist':
if clean_title(track['artist'].lower()) == guess:
guessed_track = track
break
response = {
"correct": is_correct,
"correct_answer": correct_answer,
"earned_points": earned_points if is_correct else 0
}
if guessed_track:
response["guessed_track"] = guessed_track
return response
@app.route("/play_track", methods=["POST"])
def play_track():
device_id = request.args.get('device_id')
track_uri = request.args.get('track_uri')
position_ms = int(request.args.get('position_ms', 0))
if not device_id or not track_uri:
return {"error": "Missing device_id or track_uri"}, 400
sp = get_spotify_client()
sp.start_playback(device_id=device_id, uris=[track_uri], position_ms=position_ms)
return {"success": True}
@app.route("/toggle_playback", methods=["POST"])
def toggle_playback():
data = request.json
device_id = data.get('device_id')
if not device_id:
return {"error": "Missing device_id"}, 400
sp = get_spotify_client()
# Playback-Status für das richtige Gerät prüfen
current_playback = sp.current_playback()
is_playing = False
if current_playback and current_playback.get('device', {}).get('id') == device_id:
is_playing = current_playback.get('is_playing', False)
if is_playing:
sp.pause_playback(device_id=device_id)
else:
sp.start_playback(device_id=device_id)
return {"success": True}
@app.route('/logout')
def logout():
session.clear()
return redirect(url_for('home'))
@app.route('/')
def index():
user = session.get('user') # Benutzerinfos aus der Session holen, falls vorhanden
return render_template('index.html', user=user, translations=get_translations())
@app.route("/reset_quiz/<playlist_id>")
def reset_quiz(playlist_id):
session.pop(f'played_tracks_{playlist_id}', None)
session.pop(f'score_{playlist_id}', None)
session.pop(f'total_played_{playlist_id}', None) # Auch den Zähler zurücksetzen
session.pop(f'player_scores_{playlist_id}', None) # Multiplayer-Scores zurücksetzen
next_mode = request.args.get('next_mode')
if next_mode:
return redirect(url_for('quiz', playlist_id=playlist_id, mode=next_mode))
return redirect(url_for('playlists'))
@app.route("/reset_scores/<playlist_id>")
def reset_scores(playlist_id):
"""Reset nur die Scores, aber behalte gespielte Tracks."""
session[f'score_{playlist_id}'] = 0
session[f'player_scores_{playlist_id}'] = [0, 0, 0, 0]
# Zurück zum Quiz mit aktuellen Parametern
mode = request.args.get('mode', 'title')
buzzer = request.args.get('buzzer', '0')
local_multiplayer = request.args.get('local_multiplayer', '0')
return redirect(url_for('quiz', playlist_id=playlist_id, mode=mode, buzzer=buzzer, local_multiplayer=local_multiplayer))
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)