487 lines
17 KiB
Python
487 lines
17 KiB
Python
__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)
|
||
|