__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/") 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 # 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/') 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/') 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') # Wenn Buzzer-Modus, redirect zu Einstellungen if buzzer == '1': return redirect(url_for('buzzer_settings', playlist_id=playlist_id, mode=game_mode)) return render_template('playerselect.html', playlist_id=playlist_id, game_mode=game_mode, translations=get_translations(), user=user) @app.route('/buzzer_settings/') 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/") 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 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')) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=True)