Compare commits
2 Commits
bcd6ca8104
...
193bd5ebf4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
193bd5ebf4 | ||
|
|
96ab2bdf77 |
@@ -101,6 +101,7 @@ def create_app():
|
|||||||
main,
|
main,
|
||||||
player,
|
player,
|
||||||
soundboard,
|
soundboard,
|
||||||
|
sounds,
|
||||||
stream,
|
stream,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -109,6 +110,7 @@ def create_app():
|
|||||||
app.register_blueprint(admin.bp, url_prefix="/api/admin")
|
app.register_blueprint(admin.bp, url_prefix="/api/admin")
|
||||||
app.register_blueprint(admin_sounds.bp, url_prefix="/api/admin/sounds")
|
app.register_blueprint(admin_sounds.bp, url_prefix="/api/admin/sounds")
|
||||||
app.register_blueprint(soundboard.bp, url_prefix="/api/soundboard")
|
app.register_blueprint(soundboard.bp, url_prefix="/api/soundboard")
|
||||||
|
app.register_blueprint(sounds.bp, url_prefix="/api/sounds")
|
||||||
app.register_blueprint(stream.bp, url_prefix="/api/stream")
|
app.register_blueprint(stream.bp, url_prefix="/api/stream")
|
||||||
app.register_blueprint(player.bp, url_prefix="/api/player")
|
app.register_blueprint(player.bp, url_prefix="/api/player")
|
||||||
|
|
||||||
|
|||||||
@@ -219,18 +219,6 @@ def stop_vlc_instance():
|
|||||||
return ErrorHandlingService.handle_generic_error(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"])
|
@bp.route("/test-emit", methods=["POST"])
|
||||||
@require_auth
|
@require_auth
|
||||||
|
|||||||
99
app/routes/sounds.py
Normal file
99
app/routes/sounds.py
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
"""Routes for serving sound files and thumbnails."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from flask import Blueprint, send_from_directory, abort
|
||||||
|
|
||||||
|
from app.services.decorators import require_auth
|
||||||
|
|
||||||
|
bp = Blueprint("sounds", __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/<sound_type>/thumbnails/<path:filename>")
|
||||||
|
def serve_thumbnail(sound_type, filename):
|
||||||
|
"""Serve thumbnail files for sounds."""
|
||||||
|
try:
|
||||||
|
# Map sound type codes to directory names
|
||||||
|
type_mapping = {
|
||||||
|
"str": "stream",
|
||||||
|
"sdb": "soundboard",
|
||||||
|
"say": "say"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Security: validate sound type
|
||||||
|
if sound_type not in type_mapping:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
# Basic filename validation (no path traversal)
|
||||||
|
if ".." in filename or "/" in filename or "\\" in filename:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
if not filename or not filename.strip():
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
# Get the actual directory name
|
||||||
|
directory_name = type_mapping[sound_type]
|
||||||
|
|
||||||
|
# Construct the thumbnail directory path
|
||||||
|
sounds_dir = os.path.join(os.getcwd(), "sounds")
|
||||||
|
thumbnail_dir = os.path.join(sounds_dir, directory_name, "thumbnails")
|
||||||
|
|
||||||
|
# Check if thumbnail directory exists
|
||||||
|
if not os.path.exists(thumbnail_dir):
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
# Check if file exists
|
||||||
|
file_path = os.path.join(thumbnail_dir, filename)
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
# Serve the file
|
||||||
|
return send_from_directory(thumbnail_dir, filename)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/<sound_type>/audio/<path:filename>")
|
||||||
|
@require_auth
|
||||||
|
def serve_audio(sound_type, filename):
|
||||||
|
"""Serve audio files for sounds."""
|
||||||
|
try:
|
||||||
|
# Map sound type codes to directory names
|
||||||
|
type_mapping = {
|
||||||
|
"str": "stream",
|
||||||
|
"sdb": "soundboard",
|
||||||
|
"say": "say"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Security: validate sound type
|
||||||
|
if sound_type not in type_mapping:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
# Basic filename validation (no path traversal)
|
||||||
|
if ".." in filename or "/" in filename or "\\" in filename:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
if not filename or not filename.strip():
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
# Get the actual directory name
|
||||||
|
directory_name = type_mapping[sound_type]
|
||||||
|
|
||||||
|
# Construct the audio directory path
|
||||||
|
sounds_dir = os.path.join(os.getcwd(), "sounds")
|
||||||
|
audio_dir = os.path.join(sounds_dir, directory_name)
|
||||||
|
|
||||||
|
# Check if audio directory exists
|
||||||
|
if not os.path.exists(audio_dir):
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
# Check if file exists
|
||||||
|
file_path = os.path.join(audio_dir, filename)
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
# Serve the file
|
||||||
|
return send_from_directory(audio_dir, filename)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
abort(404)
|
||||||
@@ -6,7 +6,7 @@ import time
|
|||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
import vlc
|
import vlc
|
||||||
from flask import current_app
|
from flask import current_app, request
|
||||||
|
|
||||||
from app.models.playlist import Playlist
|
from app.models.playlist import Playlist
|
||||||
from app.models.sound import Sound
|
from app.models.sound import Sound
|
||||||
@@ -23,7 +23,7 @@ class MusicPlayerService:
|
|||||||
"""Initialize the music player service."""
|
"""Initialize the music player service."""
|
||||||
self.instance: Optional[vlc.Instance] = None
|
self.instance: Optional[vlc.Instance] = None
|
||||||
self.player: Optional[vlc.MediaPlayer] = None
|
self.player: Optional[vlc.MediaPlayer] = None
|
||||||
self.app = None # Store Flask app instance for context
|
self.app: Optional[Any] = None # Store Flask app instance for context
|
||||||
|
|
||||||
self.current_playlist_id: Optional[int] = None
|
self.current_playlist_id: Optional[int] = None
|
||||||
self.current_track_index = 0
|
self.current_track_index = 0
|
||||||
@@ -67,6 +67,10 @@ class MusicPlayerService:
|
|||||||
self.player.audio_set_volume(self.volume)
|
self.player.audio_set_volume(self.volume)
|
||||||
|
|
||||||
logger.info("VLC music player started successfully")
|
logger.info("VLC music player started successfully")
|
||||||
|
|
||||||
|
# Automatically load the main playlist
|
||||||
|
self._load_main_playlist_on_startup()
|
||||||
|
|
||||||
self._start_sync_thread()
|
self._start_sync_thread()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -121,6 +125,20 @@ class MusicPlayerService:
|
|||||||
logger.error(f"Error loading playlist {playlist_id}: {e}")
|
logger.error(f"Error loading playlist {playlist_id}: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _build_thumbnail_url(self, sound_type: str, thumbnail_filename: str) -> str:
|
||||||
|
"""Build absolute thumbnail URL."""
|
||||||
|
try:
|
||||||
|
# Try to get base URL from current request context
|
||||||
|
if request:
|
||||||
|
base_url = request.url_root.rstrip('/')
|
||||||
|
else:
|
||||||
|
# Fallback to localhost if no request context
|
||||||
|
base_url = "http://localhost:5000"
|
||||||
|
return f"{base_url}/api/sounds/{sound_type.lower()}/thumbnails/{thumbnail_filename}"
|
||||||
|
except Exception:
|
||||||
|
# Fallback if request context is not available
|
||||||
|
return f"http://localhost:5000/api/sounds/{sound_type.lower()}/thumbnails/{thumbnail_filename}"
|
||||||
|
|
||||||
def _load_playlist_with_context(self, playlist) -> bool:
|
def _load_playlist_with_context(self, playlist) -> bool:
|
||||||
"""Load playlist with database context already established."""
|
"""Load playlist with database context already established."""
|
||||||
try:
|
try:
|
||||||
@@ -398,7 +416,7 @@ class MusicPlayerService:
|
|||||||
"artist": None, # Could be extracted from metadata
|
"artist": None, # Could be extracted from metadata
|
||||||
"duration": sound.duration or 0,
|
"duration": sound.duration or 0,
|
||||||
"thumbnail": (
|
"thumbnail": (
|
||||||
f"/api/sounds/{sound.type.lower()}/thumbnails/{sound.thumbnail}"
|
self._build_thumbnail_url(sound.type, sound.thumbnail)
|
||||||
if sound.thumbnail
|
if sound.thumbnail
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
@@ -433,7 +451,7 @@ class MusicPlayerService:
|
|||||||
"artist": None,
|
"artist": None,
|
||||||
"duration": sound.duration or 0,
|
"duration": sound.duration or 0,
|
||||||
"thumbnail": (
|
"thumbnail": (
|
||||||
f"/api/sounds/{sound.type.lower()}/thumbnails/{sound.thumbnail}"
|
self._build_thumbnail_url(sound.type, sound.thumbnail)
|
||||||
if sound.thumbnail
|
if sound.thumbnail
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
@@ -581,33 +599,29 @@ class MusicPlayerService:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"Error syncing VLC state: {e}")
|
logger.debug(f"Error syncing VLC state: {e}")
|
||||||
|
|
||||||
def load_main_playlist(self) -> bool:
|
|
||||||
"""Load main playlist if available (to be called from within Flask context)."""
|
def _load_main_playlist_on_startup(self):
|
||||||
|
"""Load the main playlist automatically on startup."""
|
||||||
try:
|
try:
|
||||||
logger.info("Attempting to load main playlist...")
|
if not self.app:
|
||||||
main_playlist = Playlist.query.filter_by(name="Main").first()
|
logger.warning("No Flask app context available, skipping main playlist load")
|
||||||
|
return
|
||||||
|
|
||||||
|
with self.app.app_context():
|
||||||
|
# Find the main playlist
|
||||||
|
main_playlist = Playlist.find_main_playlist()
|
||||||
|
|
||||||
if main_playlist:
|
if main_playlist:
|
||||||
logger.info(
|
success = self.load_playlist(main_playlist.id)
|
||||||
f"Found main playlist with {len(main_playlist.playlist_sounds)} tracks"
|
if success:
|
||||||
)
|
logger.info(f"Automatically loaded main playlist '{main_playlist.name}' with {len(self.playlist_files)} tracks")
|
||||||
result = self.load_playlist(main_playlist.id)
|
|
||||||
logger.info(f"Load playlist result: {result}")
|
|
||||||
if result:
|
|
||||||
logger.info(
|
|
||||||
f"Successfully loaded main playlist with {len(main_playlist.playlist_sounds)} tracks"
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
else:
|
else:
|
||||||
logger.warning("Failed to load main playlist")
|
logger.warning("Failed to load main playlist on startup")
|
||||||
return False
|
|
||||||
else:
|
else:
|
||||||
logger.warning(
|
logger.info("No main playlist found to load on startup")
|
||||||
"Main playlist not found, player ready without playlist"
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error loading main playlist: {e}")
|
logger.error(f"Error loading main playlist on startup: {e}")
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
# Global music player service instance
|
# Global music player service instance
|
||||||
|
|||||||
Reference in New Issue
Block a user