feat: Implement Music Player Service with VLC integration
- Added MusicPlayerService for managing VLC music playback with playlist support. - Implemented methods for loading playlists, controlling playback (play, pause, stop, next, previous), and managing volume and play modes. - Integrated real-time synchronization with VLC state using a background thread. - Added SocketIO event emissions for player state updates. - Enhanced logging for better debugging and tracking of player state changes. fix: Improve SocketIO service logging and event handling - Added detailed logging for SocketIO events and user authentication. - Implemented a test event handler to verify SocketIO functionality. - Enhanced error handling and logging for better traceability. chore: Update dependencies and logging configuration - Added python-vlc dependency for VLC integration. - Configured logging to show INFO and DEBUG messages for better visibility during development. - Updated main application entry point to allow unsafe Werkzeug for debugging purposes.
This commit is contained in:
244
app/routes/player.py
Normal file
244
app/routes/player.py
Normal file
@@ -0,0 +1,244 @@
|
||||
"""Music player API routes."""
|
||||
|
||||
from flask import Blueprint, jsonify, request
|
||||
|
||||
from app.services.decorators import require_auth
|
||||
from app.services.error_handling_service import ErrorHandlingService
|
||||
from app.services.music_player_service import music_player_service
|
||||
|
||||
bp = Blueprint("player", __name__)
|
||||
|
||||
|
||||
@bp.route("/state", methods=["GET"])
|
||||
@require_auth
|
||||
def get_player_state():
|
||||
"""Get current player state."""
|
||||
try:
|
||||
state = music_player_service.get_player_state()
|
||||
return jsonify(state), 200
|
||||
except Exception as e:
|
||||
return ErrorHandlingService.handle_generic_error(e)
|
||||
|
||||
|
||||
@bp.route("/play", methods=["POST"])
|
||||
@require_auth
|
||||
def play():
|
||||
"""Start playback."""
|
||||
try:
|
||||
success = music_player_service.play()
|
||||
if success:
|
||||
return jsonify({"message": "Playback started"}), 200
|
||||
return jsonify({"error": "Failed to start playback"}), 400
|
||||
except Exception as e:
|
||||
return ErrorHandlingService.handle_generic_error(e)
|
||||
|
||||
|
||||
@bp.route("/pause", methods=["POST"])
|
||||
@require_auth
|
||||
def pause():
|
||||
"""Pause playback."""
|
||||
try:
|
||||
success = music_player_service.pause()
|
||||
if success:
|
||||
return jsonify({"message": "Playback paused"}), 200
|
||||
return jsonify({"error": "Failed to pause playback"}), 400
|
||||
except Exception as e:
|
||||
return ErrorHandlingService.handle_generic_error(e)
|
||||
|
||||
|
||||
@bp.route("/stop", methods=["POST"])
|
||||
@require_auth
|
||||
def stop():
|
||||
"""Stop playback."""
|
||||
try:
|
||||
success = music_player_service.stop()
|
||||
if success:
|
||||
return jsonify({"message": "Playback stopped"}), 200
|
||||
return jsonify({"error": "Failed to stop playback"}), 400
|
||||
except Exception as e:
|
||||
return ErrorHandlingService.handle_generic_error(e)
|
||||
|
||||
|
||||
@bp.route("/next", methods=["POST"])
|
||||
@require_auth
|
||||
def next_track():
|
||||
"""Skip to next track."""
|
||||
try:
|
||||
success = music_player_service.next_track()
|
||||
if success:
|
||||
return jsonify({"message": "Skipped to next track"}), 200
|
||||
return jsonify({"error": "Failed to skip to next track"}), 400
|
||||
except Exception as e:
|
||||
return ErrorHandlingService.handle_generic_error(e)
|
||||
|
||||
|
||||
@bp.route("/previous", methods=["POST"])
|
||||
@require_auth
|
||||
def previous_track():
|
||||
"""Skip to previous track."""
|
||||
try:
|
||||
success = music_player_service.previous_track()
|
||||
if success:
|
||||
return jsonify({"message": "Skipped to previous track"}), 200
|
||||
return jsonify({"error": "Failed to skip to previous track"}), 400
|
||||
except Exception as e:
|
||||
return ErrorHandlingService.handle_generic_error(e)
|
||||
|
||||
|
||||
@bp.route("/seek", methods=["POST"])
|
||||
@require_auth
|
||||
def seek():
|
||||
"""Seek to position."""
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data or "position" not in data:
|
||||
return jsonify({"error": "Position required"}), 400
|
||||
|
||||
position = float(data["position"])
|
||||
if not 0.0 <= position <= 1.0:
|
||||
return jsonify({"error": "Position must be between 0.0 and 1.0"}), 400
|
||||
|
||||
success = music_player_service.seek(position)
|
||||
if success:
|
||||
return jsonify({"message": "Seek successful"}), 200
|
||||
return jsonify({"error": "Failed to seek"}), 400
|
||||
except (ValueError, TypeError):
|
||||
return jsonify({"error": "Invalid position value"}), 400
|
||||
except Exception as e:
|
||||
return ErrorHandlingService.handle_generic_error(e)
|
||||
|
||||
|
||||
@bp.route("/volume", methods=["POST"])
|
||||
@require_auth
|
||||
def set_volume():
|
||||
"""Set volume."""
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data or "volume" not in data:
|
||||
return jsonify({"error": "Volume required"}), 400
|
||||
|
||||
volume = int(data["volume"])
|
||||
if not 0 <= volume <= 100:
|
||||
return jsonify({"error": "Volume must be between 0 and 100"}), 400
|
||||
|
||||
success = music_player_service.set_volume(volume)
|
||||
if success:
|
||||
return jsonify({"message": "Volume set successfully"}), 200
|
||||
return jsonify({"error": "Failed to set volume"}), 400
|
||||
except (ValueError, TypeError):
|
||||
return jsonify({"error": "Invalid volume value"}), 400
|
||||
except Exception as e:
|
||||
return ErrorHandlingService.handle_generic_error(e)
|
||||
|
||||
|
||||
@bp.route("/mode", methods=["POST"])
|
||||
@require_auth
|
||||
def set_play_mode():
|
||||
"""Set play mode."""
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data or "mode" not in data:
|
||||
return jsonify({"error": "Mode required"}), 400
|
||||
|
||||
mode = data["mode"]
|
||||
valid_modes = ["continuous", "loop-playlist", "loop-one", "random"]
|
||||
if mode not in valid_modes:
|
||||
return jsonify({"error": f"Mode must be one of: {', '.join(valid_modes)}"}), 400
|
||||
|
||||
success = music_player_service.set_play_mode(mode)
|
||||
if success:
|
||||
return jsonify({"message": "Play mode set successfully"}), 200
|
||||
return jsonify({"error": "Failed to set play mode"}), 400
|
||||
except Exception as e:
|
||||
return ErrorHandlingService.handle_generic_error(e)
|
||||
|
||||
|
||||
@bp.route("/playlist", methods=["POST"])
|
||||
@require_auth
|
||||
def load_playlist():
|
||||
"""Load a playlist into the player."""
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data or "playlist_id" not in data:
|
||||
return jsonify({"error": "Playlist ID required"}), 400
|
||||
|
||||
playlist_id = int(data["playlist_id"])
|
||||
success = music_player_service.load_playlist(playlist_id)
|
||||
if success:
|
||||
return jsonify({"message": "Playlist loaded successfully"}), 200
|
||||
return jsonify({"error": "Failed to load playlist"}), 400
|
||||
except (ValueError, TypeError):
|
||||
return jsonify({"error": "Invalid playlist ID"}), 400
|
||||
except Exception as e:
|
||||
return ErrorHandlingService.handle_generic_error(e)
|
||||
|
||||
|
||||
@bp.route("/play-track", methods=["POST"])
|
||||
@require_auth
|
||||
def play_track():
|
||||
"""Play track at specific index."""
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data or "index" not in data:
|
||||
return jsonify({"error": "Track index required"}), 400
|
||||
|
||||
index = int(data["index"])
|
||||
success = music_player_service.play_track_at_index(index)
|
||||
if success:
|
||||
return jsonify({"message": "Track playing"}), 200
|
||||
return jsonify({"error": "Failed to play track"}), 400
|
||||
except (ValueError, TypeError):
|
||||
return jsonify({"error": "Invalid track index"}), 400
|
||||
except Exception as e:
|
||||
return ErrorHandlingService.handle_generic_error(e)
|
||||
|
||||
|
||||
@bp.route("/start-instance", methods=["POST"])
|
||||
@require_auth
|
||||
def start_vlc_instance():
|
||||
"""Start the VLC player instance."""
|
||||
try:
|
||||
success = music_player_service.start_vlc_instance()
|
||||
if success:
|
||||
return jsonify({"message": "VLC instance started successfully"}), 200
|
||||
return jsonify({"error": "Failed to start VLC instance"}), 500
|
||||
except Exception as e:
|
||||
return ErrorHandlingService.handle_generic_error(e)
|
||||
|
||||
|
||||
@bp.route("/stop-instance", methods=["POST"])
|
||||
@require_auth
|
||||
def stop_vlc_instance():
|
||||
"""Stop the VLC player instance."""
|
||||
try:
|
||||
success = music_player_service.stop_vlc_instance()
|
||||
if success:
|
||||
return jsonify({"message": "VLC instance stopped successfully"}), 200
|
||||
return jsonify({"error": "Failed to stop VLC instance"}), 500
|
||||
except Exception as e:
|
||||
return ErrorHandlingService.handle_generic_error(e)
|
||||
|
||||
|
||||
@bp.route("/load-main-playlist", methods=["POST"])
|
||||
@require_auth
|
||||
def load_main_playlist():
|
||||
"""Load the main playlist into the player."""
|
||||
try:
|
||||
success = music_player_service.load_main_playlist()
|
||||
if success:
|
||||
return jsonify({"message": "Main playlist loaded successfully"}), 200
|
||||
return jsonify({"error": "Failed to load main playlist"}), 400
|
||||
except Exception as e:
|
||||
return ErrorHandlingService.handle_generic_error(e)
|
||||
|
||||
|
||||
@bp.route("/test-emit", methods=["POST"])
|
||||
@require_auth
|
||||
def test_emit():
|
||||
"""Test SocketIO emission manually."""
|
||||
try:
|
||||
# Force emit player state
|
||||
music_player_service._emit_player_state()
|
||||
return jsonify({"message": "Test emission sent"}), 200
|
||||
except Exception as e:
|
||||
return ErrorHandlingService.handle_generic_error(e)
|
||||
Reference in New Issue
Block a user