diff --git a/app/__init__.py b/app/__init__.py index 7611fa2..b797b93 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -101,6 +101,7 @@ def create_app(): main, player, soundboard, + sounds, stream, ) @@ -109,6 +110,7 @@ def create_app(): app.register_blueprint(admin.bp, url_prefix="/api/admin") app.register_blueprint(admin_sounds.bp, url_prefix="/api/admin/sounds") 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(player.bp, url_prefix="/api/player") diff --git a/app/routes/sounds.py b/app/routes/sounds.py new file mode 100644 index 0000000..3a2884f --- /dev/null +++ b/app/routes/sounds.py @@ -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("//thumbnails/") +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("//audio/") +@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) \ No newline at end of file diff --git a/app/services/music_player_service.py b/app/services/music_player_service.py index ed07487..333ad0a 100644 --- a/app/services/music_player_service.py +++ b/app/services/music_player_service.py @@ -6,7 +6,7 @@ import time from typing import Any, Optional import vlc -from flask import current_app +from flask import current_app, request from app.models.playlist import Playlist from app.models.sound import Sound @@ -125,6 +125,20 @@ class MusicPlayerService: logger.error(f"Error loading playlist {playlist_id}: {e}") 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: """Load playlist with database context already established.""" try: @@ -402,7 +416,7 @@ class MusicPlayerService: "artist": None, # Could be extracted from metadata "duration": sound.duration or 0, "thumbnail": ( - f"/api/sounds/{sound.type.lower()}/thumbnails/{sound.thumbnail}" + self._build_thumbnail_url(sound.type, sound.thumbnail) if sound.thumbnail else None ), @@ -437,7 +451,7 @@ class MusicPlayerService: "artist": None, "duration": sound.duration or 0, "thumbnail": ( - f"/api/sounds/{sound.type.lower()}/thumbnails/{sound.thumbnail}" + self._build_thumbnail_url(sound.type, sound.thumbnail) if sound.thumbnail else None ),