diff --git a/app/routes/main.py b/app/routes/main.py index 09511e1..f269652 100644 --- a/app/routes/main.py +++ b/app/routes/main.py @@ -1,6 +1,17 @@ """Main routes for the application.""" -from flask import Blueprint +from datetime import datetime, timedelta +from zoneinfo import ZoneInfo + +from flask import Blueprint, request +from sqlalchemy import desc, func + +from app.database import db +from app.models.playlist import Playlist +from app.models.sound import Sound +from app.models.sound_played import SoundPlayed +from app.models.user import User +from app.services.decorators import require_auth bp = Blueprint("main", __name__) @@ -9,3 +20,213 @@ bp = Blueprint("main", __name__) def health() -> dict[str, str]: """Health check endpoint.""" return {"status": "ok"} + + +def get_period_filter(period: str) -> datetime | None: + """Get the start date for the specified period.""" + now = datetime.now(tz=ZoneInfo("UTC")) + + if period == "today": + return now.replace(hour=0, minute=0, second=0, microsecond=0) + if period == "week": + return now - timedelta(days=7) + if period == "month": + return now - timedelta(days=30) + if period == "year": + return now - timedelta(days=365) + if period == "all": + return None + # Default to all time + return None + + +@bp.route("/dashboard/stats") +@require_auth +def dashboard_stats() -> dict: + """Get dashboard statistics.""" + # Count soundboard sounds (type = SDB) + soundboard_count = Sound.query.filter_by(type="SDB").count() + + # Count tracks (type = STR) + track_count = Sound.query.filter_by(type="STR").count() + + # Count playlists + playlist_count = Playlist.query.count() + + # Calculate total size of all sounds (original + normalized) + total_size_result = db.session.query( + func.sum(Sound.size).label("original_size"), + func.sum(Sound.normalized_size).label("normalized_size"), + ).first() + + original_size = getattr(total_size_result, "original_size", 0) or 0 + normalized_size = getattr(total_size_result, "normalized_size", 0) or 0 + total_size = original_size + normalized_size + + return { + "soundboard_sounds": soundboard_count, + "tracks": track_count, + "playlists": playlist_count, + "total_size": total_size, + "original_size": original_size, + "normalized_size": normalized_size, + } + + +@bp.route("/dashboard/top-sounds") +@require_auth +def top_sounds() -> dict: + """Get top played sounds for a specific period.""" + period = request.args.get("period", "all") + limit = int(request.args.get("limit", 5)) + + period_start = get_period_filter(period) + + # Base query for soundboard sounds with play counts + query = ( + db.session.query( + Sound.id, + Sound.name, + Sound.filename, + Sound.thumbnail, + Sound.type, + func.count(SoundPlayed.id).label("play_count"), + ) + .outerjoin(SoundPlayed, Sound.id == SoundPlayed.sound_id) + .filter(Sound.type == "SDB") # Only soundboard sounds + .group_by( + Sound.id, + Sound.name, + Sound.filename, + Sound.thumbnail, + Sound.type, + ) + ) + + # Apply period filter if specified + if period_start: + query = query.filter(SoundPlayed.played_at >= period_start) + + # Order by play count and limit results + results = query.order_by(desc("play_count")).limit(limit).all() + + # Convert to list of dictionaries + top_sounds_list = [ + { + "id": result.id, + "name": result.name, + "filename": result.filename, + "thumbnail": result.thumbnail, + "type": result.type, + "play_count": result.play_count, + } + for result in results + ] + + return { + "period": period, + "sounds": top_sounds_list, + } + + +@bp.route("/dashboard/top-tracks") +@require_auth +def top_tracks() -> dict: + """Get top played tracks for a specific period.""" + period = request.args.get("period", "all") + limit = int(request.args.get("limit", 10)) + + period_start = get_period_filter(period) + + # Base query for tracks with play counts + query = ( + db.session.query( + Sound.id, + Sound.name, + Sound.filename, + Sound.thumbnail, + Sound.type, + func.count(SoundPlayed.id).label("play_count"), + ) + .outerjoin(SoundPlayed, Sound.id == SoundPlayed.sound_id) + .filter(Sound.type == "STR") # Only tracks + .group_by( + Sound.id, + Sound.name, + Sound.filename, + Sound.thumbnail, + Sound.type, + ) + ) + + # Apply period filter if specified + if period_start: + query = query.filter(SoundPlayed.played_at >= period_start) + + # Order by play count and limit results + results = query.order_by(desc("play_count")).limit(limit).all() + + # Convert to list of dictionaries + top_tracks_list = [ + { + "id": result.id, + "name": result.name, + "filename": result.filename, + "thumbnail": result.thumbnail, + "type": result.type, + "play_count": result.play_count, + } + for result in results + ] + + return { + "period": period, + "tracks": top_tracks_list, + } + + +@bp.route("/dashboard/top-users") +@require_auth +def top_users() -> dict: + """Get top users by play count for a specific period.""" + period = request.args.get("period", "all") + limit = int(request.args.get("limit", 10)) + + period_start = get_period_filter(period) + + # Base query for users with play counts + query = ( + db.session.query( + User.id, + User.name, + User.email, + User.picture, + func.count(SoundPlayed.id).label("play_count"), + ) + .outerjoin(SoundPlayed, User.id == SoundPlayed.user_id) + .group_by(User.id, User.name, User.email, User.picture) + ) + + # Apply period filter if specified + if period_start: + query = query.filter(SoundPlayed.played_at >= period_start) + + # Order by play count and limit results + results = query.order_by(desc("play_count")).limit(limit).all() + + # Convert to list of dictionaries + top_users_list = [ + { + "id": result.id, + "name": result.name, + "email": result.email, + "picture": result.picture, + "play_count": result.play_count, + } + for result in results + ] + + return { + "period": period, + "users": top_users_list, + } diff --git a/app/routes/soundboard.py b/app/routes/soundboard.py index dd31871..faf333e 100644 --- a/app/routes/soundboard.py +++ b/app/routes/soundboard.py @@ -29,6 +29,9 @@ def get_sounds(): # Get sounds from database sounds = Sound.find_by_type(sound_type) + # Order by name + sounds = sorted(sounds, key=lambda s: s.name.lower()) + # Convert to dict format sounds_data = [sound.to_dict() for sound in sounds] @@ -65,13 +68,19 @@ def play_sound(sound_id: int): # Emit sound_changed event to all connected clients try: from app.services.socketio_service import SocketIOService - SocketIOService.emit_sound_play_count_changed(sound_id, sound.play_count) + + SocketIOService.emit_sound_play_count_changed( + sound_id, sound.play_count + ) except Exception as e: # Don't fail the request if socket emission fails import logging + logger = logging.getLogger(__name__) - logger.warning(f"Failed to emit sound_play_count_changed event: {e}") - + logger.warning( + f"Failed to emit sound_play_count_changed event: {e}" + ) + return jsonify({"message": "Sound playing", "sound_id": sound_id}) return ( jsonify({"error": "Sound not found or cannot be played"}),