diff --git a/app/__init__.py b/app/__init__.py index d103bda..81d2c50 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -61,6 +61,9 @@ def create_app(): # Initialize authentication service with app auth_service.init_app(app) + # Initialize scheduler service with app + scheduler_service.app = app + # Start scheduler for background tasks scheduler_service.start() diff --git a/app/models/__init__.py b/app/models/__init__.py index 5dcd082..30a3198 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -1,7 +1,8 @@ """Database models.""" from .plan import Plan +from .sound import Sound from .user import User from .user_oauth import UserOAuth -__all__ = ["Plan", "User", "UserOAuth"] +__all__ = ["Plan", "Sound", "User", "UserOAuth"] diff --git a/app/models/sound.py b/app/models/sound.py new file mode 100644 index 0000000..9d75142 --- /dev/null +++ b/app/models/sound.py @@ -0,0 +1,225 @@ +"""Sound model for storing sound file information.""" + +from datetime import datetime +from enum import Enum +from typing import Optional + +from app.database import db +from sqlalchemy import Boolean, DateTime, Integer, String +from sqlalchemy.orm import Mapped, mapped_column + + +class SoundType(Enum): + """Sound type enumeration.""" + + SDB = "SDB" # Soundboard sound + SAY = "SAY" # Text-to-speech + STR = "STR" # Stream sound + + +class Sound(db.Model): + """Sound model for storing sound file information.""" + + __tablename__ = "sounds" + + id: Mapped[int] = mapped_column(primary_key=True) + + # Sound type (SDB, SAY, or STR) + type: Mapped[str] = mapped_column(String(3), nullable=False) + + # Basic sound information + name: Mapped[str] = mapped_column(String(255), nullable=False) + filename: Mapped[str] = mapped_column(String(500), nullable=False) + duration: Mapped[int] = mapped_column(Integer, nullable=False) + size: Mapped[int] = mapped_column(Integer, nullable=False) # Size in bytes + hash: Mapped[str] = mapped_column(String(64), nullable=False) # SHA256 hash + + # Normalized sound information + normalized_filename: Mapped[str | None] = mapped_column( + String(500), + nullable=True, + ) + normalized_duration: Mapped[int | None] = mapped_column( + Integer, + nullable=True, + ) + normalized_size: Mapped[int | None] = mapped_column( + Integer, + nullable=True, + ) + normalized_hash: Mapped[str | None] = mapped_column( + String(64), + nullable=True, + ) + + # Sound properties + is_normalized: Mapped[bool] = mapped_column( + Boolean, + nullable=False, + default=False, + ) + is_music: Mapped[bool] = mapped_column( + Boolean, + nullable=False, + default=False, + ) + is_deletable: Mapped[bool] = mapped_column( + Boolean, + nullable=False, + default=True, + ) + + # Usage tracking + play_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) + + # Timestamps + created_at: Mapped[datetime] = mapped_column( + DateTime, + default=datetime.utcnow, + nullable=False, + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime, + default=datetime.utcnow, + onupdate=datetime.utcnow, + nullable=False, + ) + + def __repr__(self) -> str: + """String representation of Sound.""" + return f"" + + def to_dict(self) -> dict: + """Convert sound to dictionary.""" + return { + "id": self.id, + "type": self.type, + "name": self.name, + "filename": self.filename, + "duration": self.duration, + "size": self.size, + "hash": self.hash, + "normalized_filename": self.normalized_filename, + "normalized_duration": self.normalized_duration, + "normalized_size": self.normalized_size, + "normalized_hash": self.normalized_hash, + "is_normalized": self.is_normalized, + "is_music": self.is_music, + "is_deletable": self.is_deletable, + "play_count": self.play_count, + "created_at": self.created_at.isoformat(), + "updated_at": self.updated_at.isoformat(), + } + + def increment_play_count(self) -> None: + """Increment the play count for this sound.""" + self.play_count += 1 + self.updated_at = datetime.utcnow() + db.session.commit() + + def set_normalized_info( + self, + normalized_filename: str, + normalized_duration: float, + normalized_size: int, + normalized_hash: str, + ) -> None: + """Set normalized sound information.""" + self.normalized_filename = normalized_filename + self.normalized_duration = normalized_duration + self.normalized_size = normalized_size + self.normalized_hash = normalized_hash + self.is_normalized = True + self.updated_at = datetime.utcnow() + + def clear_normalized_info(self) -> None: + """Clear normalized sound information.""" + self.normalized_filename = None + self.normalized_duration = None + self.normalized_hash = None + self.normalized_size = None + self.is_normalized = False + self.updated_at = datetime.utcnow() + + def update_file_info( + self, + filename: str, + duration: float, + size: int, + hash_value: str, + ) -> None: + """Update file information for existing sound.""" + self.filename = filename + self.duration = duration + self.size = size + self.hash = hash_value + self.updated_at = datetime.utcnow() + + @classmethod + def find_by_hash(cls, hash_value: str) -> Optional["Sound"]: + """Find sound by hash.""" + return cls.query.filter_by(hash=hash_value).first() + + @classmethod + def find_by_name(cls, name: str) -> Optional["Sound"]: + """Find sound by name.""" + return cls.query.filter_by(name=name).first() + + @classmethod + def find_by_filename(cls, filename: str) -> Optional["Sound"]: + """Find sound by filename.""" + return cls.query.filter_by(filename=filename).first() + + @classmethod + def find_by_type(cls, sound_type: str) -> list["Sound"]: + """Find all sounds by type.""" + return cls.query.filter_by(type=sound_type).all() + + @classmethod + def get_most_played(cls, limit: int = 10) -> list["Sound"]: + """Get the most played sounds.""" + return cls.query.order_by(cls.play_count.desc()).limit(limit).all() + + @classmethod + def get_music_sounds(cls) -> list["Sound"]: + """Get all music sounds.""" + return cls.query.filter_by(is_music=True).all() + + @classmethod + def get_deletable_sounds(cls) -> list["Sound"]: + """Get all deletable sounds.""" + return cls.query.filter_by(is_deletable=True).all() + + @classmethod + def create_sound( + cls, + sound_type: str, + name: str, + filename: str, + duration: float, + size: int, + hash_value: str, + is_music: bool = False, + is_deletable: bool = True, + commit: bool = True, + ) -> "Sound": + """Create a new sound.""" + # Validate sound type + if sound_type not in [t.value for t in SoundType]: + raise ValueError(f"Invalid sound type: {sound_type}") + + sound = cls( + type=sound_type, + name=name, + filename=filename, + duration=duration, + size=size, + hash=hash_value, + is_music=is_music, + is_deletable=is_deletable, + ) + + db.session.add(sound) + if commit: + db.session.commit() + return sound diff --git a/app/routes/main.py b/app/routes/main.py index d525053..26bf73a 100644 --- a/app/routes/main.py +++ b/app/routes/main.py @@ -1,6 +1,6 @@ """Main routes for the application.""" -from flask import Blueprint +from flask import Blueprint, request from app.services.decorators import ( get_current_user, @@ -9,6 +9,8 @@ from app.services.decorators import ( require_role, ) from app.services.scheduler_service import scheduler_service +from app.services.sound_normalizer_service import SoundNormalizerService +from app.services.sound_scanner_service import SoundScannerService bp = Blueprint("main", __name__) @@ -101,3 +103,55 @@ def scheduler_status() -> dict: def manual_credit_refill() -> dict: """Manually trigger credit refill for all users (admin only).""" return scheduler_service.trigger_credit_refill_now() + + +@bp.route("/admin/sounds/scan", methods=["POST"]) +@require_auth +@require_role("admin") +def manual_sound_scan() -> dict: + """Manually trigger sound directory scan (admin only).""" + return scheduler_service.trigger_sound_scan_now() + + +@bp.route("/admin/sounds/stats") +@require_auth +@require_role("admin") +def sound_statistics() -> dict: + """Get sound database statistics (admin only).""" + return SoundScannerService.get_scan_statistics() + + +@bp.route("/admin/sounds/normalize/", methods=["POST"]) +@require_auth +@require_role("admin") +def normalize_sound(sound_id: int) -> dict: + """Normalize a specific sound file (admin only).""" + overwrite = request.args.get("overwrite", "false").lower() == "true" + return SoundNormalizerService.normalize_sound(sound_id, overwrite) + + +@bp.route("/admin/sounds/normalize-all", methods=["POST"]) +@require_auth +@require_role("admin") +def normalize_all_sounds() -> dict: + """Normalize all soundboard files (admin only).""" + overwrite = request.args.get("overwrite", "false").lower() == "true" + limit_str = request.args.get("limit") + limit = int(limit_str) if limit_str else None + return SoundNormalizerService.normalize_all_sounds(overwrite, limit) + + +@bp.route("/admin/sounds/normalization-status") +@require_auth +@require_role("admin") +def normalization_status() -> dict: + """Get normalization status statistics (admin only).""" + return SoundNormalizerService.get_normalization_status() + + +@bp.route("/admin/sounds/ffmpeg-check") +@require_auth +@require_role("admin") +def ffmpeg_check() -> dict: + """Check ffmpeg availability and capabilities (admin only).""" + return SoundNormalizerService.check_ffmpeg_availability() diff --git a/app/services/credit_service.py b/app/services/credit_service.py index ee6bd69..5299709 100644 --- a/app/services/credit_service.py +++ b/app/services/credit_service.py @@ -14,8 +14,7 @@ class CreditService: @staticmethod def refill_all_users_credits() -> dict: - """ - Refill credits for all active users based on their plan. + """Refill credits for all active users based on their plan. This function: 1. Gets all active users @@ -25,100 +24,101 @@ class CreditService: Returns: dict: Summary of the refill operation + """ try: # Get all active users with their plans users = User.query.filter_by(is_active=True).all() - + if not users: logger.info("No active users found for credit refill") return { "success": True, "users_processed": 0, "credits_added": 0, - "message": "No active users found" + "message": "No active users found", } - + users_processed = 0 total_credits_added = 0 - + for user in users: if not user.plan: logger.warning(f"User {user.email} has no plan assigned, skipping") continue - + # Calculate new credit amount, capped at plan max current_credits = user.credits or 0 plan_daily_credits = user.plan.credits max_credits = user.plan.max_credits - + # Add daily credits but don't exceed maximum new_credits = min(current_credits + plan_daily_credits, max_credits) credits_added = new_credits - current_credits - + if credits_added > 0: user.credits = new_credits user.updated_at = datetime.utcnow() total_credits_added += credits_added - + logger.debug( f"User {user.email}: {current_credits} -> {new_credits} " - f"(+{credits_added} credits, plan: {user.plan.code})" + f"(+{credits_added} credits, plan: {user.plan.code})", ) else: logger.debug( f"User {user.email}: Already at max credits " - f"({current_credits}/{max_credits})" + f"({current_credits}/{max_credits})", ) - + users_processed += 1 - + # Commit all changes in a single transaction db.session.commit() - + logger.info( f"Daily credit refill completed: {users_processed} users processed, " - f"{total_credits_added} total credits added" + f"{total_credits_added} total credits added", ) - + return { "success": True, "users_processed": users_processed, "credits_added": total_credits_added, - "message": f"Successfully refilled credits for {users_processed} users" + "message": f"Successfully refilled credits for {users_processed} users", } - + except Exception as e: # Rollback transaction on error db.session.rollback() - logger.error(f"Error during daily credit refill: {str(e)}") - + logger.error(f"Error during daily credit refill: {e!s}") + return { "success": False, "users_processed": 0, "credits_added": 0, "error": str(e), - "message": "Credit refill failed" + "message": "Credit refill failed", } - + @staticmethod def get_user_credit_info(user_id: int) -> dict: - """ - Get detailed credit information for a specific user. + """Get detailed credit information for a specific user. Args: user_id: The user's ID Returns: dict: User's credit information + """ user = User.query.get(user_id) if not user: return {"error": "User not found"} - + if not user.plan: return {"error": "User has no plan assigned"} - + return { "user_id": user.id, "email": user.email, @@ -127,7 +127,7 @@ class CreditService: "code": user.plan.code, "name": user.plan.name, "daily_credits": user.plan.credits, - "max_credits": user.plan.max_credits + "max_credits": user.plan.max_credits, }, - "is_active": user.is_active - } \ No newline at end of file + "is_active": user.is_active, + } diff --git a/app/services/scheduler_service.py b/app/services/scheduler_service.py index 4f05e9c..e207728 100644 --- a/app/services/scheduler_service.py +++ b/app/services/scheduler_service.py @@ -1,12 +1,13 @@ """Scheduler service for managing background tasks with APScheduler.""" import logging -from typing import Optional from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.cron import CronTrigger +from flask import current_app from app.services.credit_service import CreditService +from app.services.sound_scanner_service import SoundScannerService logger = logging.getLogger(__name__) @@ -14,9 +15,10 @@ logger = logging.getLogger(__name__) class SchedulerService: """Service for managing scheduled background tasks.""" - def __init__(self) -> None: + def __init__(self, app=None) -> None: """Initialize the scheduler service.""" - self.scheduler: Optional[BackgroundScheduler] = None + self.scheduler: BackgroundScheduler | None = None + self.app = app def start(self) -> None: """Start the scheduler and add all scheduled jobs.""" @@ -25,10 +27,13 @@ class SchedulerService: return self.scheduler = BackgroundScheduler() - + # Add daily credit refill job self._add_daily_credit_refill_job() - + + # Add sound scanning job + self._add_sound_scanning_job() + # Start the scheduler self.scheduler.start() logger.info("Scheduler started successfully") @@ -47,7 +52,7 @@ class SchedulerService: # Schedule daily at 00:00 UTC trigger = CronTrigger(hour=0, minute=0) - + self.scheduler.add_job( func=self._run_daily_credit_refill, trigger=trigger, @@ -55,47 +60,99 @@ class SchedulerService: name="Daily Credit Refill", replace_existing=True, ) - + logger.info("Daily credit refill job scheduled for 00:00 UTC") + def _add_sound_scanning_job(self) -> None: + """Add the sound scanning job.""" + if self.scheduler is None: + raise RuntimeError("Scheduler not initialized") + + # Schedule every 5 minutes for sound scanning + trigger = CronTrigger(minute="*/5") + + self.scheduler.add_job( + func=self._run_sound_scan, + trigger=trigger, + id="sound_scan", + name="Sound Directory Scan", + replace_existing=True, + ) + + logger.info("Sound scanning job scheduled every 5 minutes") + def _run_daily_credit_refill(self) -> None: """Execute the daily credit refill task.""" logger.info("Starting daily credit refill task") - - try: - result = CreditService.refill_all_users_credits() - - if result["success"]: - logger.info( - f"Daily credit refill completed successfully: " - f"{result['users_processed']} users processed, " - f"{result['credits_added']} credits added" - ) - else: - logger.error(f"Daily credit refill failed: {result['message']}") - - except Exception as e: - logger.exception(f"Error during daily credit refill: {e}") + + app = self.app or current_app + with app.app_context(): + try: + result = CreditService.refill_all_users_credits() + + if result["success"]: + logger.info( + f"Daily credit refill completed successfully: " + f"{result['users_processed']} users processed, " + f"{result['credits_added']} credits added", + ) + else: + logger.error(f"Daily credit refill failed: {result['message']}") + + except Exception as e: + logger.exception(f"Error during daily credit refill: {e}") + + def _run_sound_scan(self) -> None: + """Execute the sound scanning task.""" + logger.info("Starting sound directory scan") + + app = self.app or current_app + with app.app_context(): + try: + result = SoundScannerService.scan_soundboard_directory() + + if result["success"]: + if result["files_added"] > 0: + logger.info( + f"Sound scan completed: {result['files_added']} new sounds added", + ) + else: + logger.debug("Sound scan completed: no new files found") + else: + logger.error(f"Sound scan failed: {result.get('error', 'Unknown error')}") + + except Exception as e: + logger.exception(f"Error during sound scan: {e}") def trigger_credit_refill_now(self) -> dict: """Manually trigger credit refill for testing purposes.""" logger.info("Manually triggering credit refill") - return CreditService.refill_all_users_credits() + app = self.app or current_app + with app.app_context(): + return CreditService.refill_all_users_credits() + + def trigger_sound_scan_now(self) -> dict: + """Manually trigger sound scan for testing purposes.""" + logger.info("Manually triggering sound scan") + app = self.app or current_app + with app.app_context(): + return SoundScannerService.scan_soundboard_directory() def get_scheduler_status(self) -> dict: """Get the current status of the scheduler.""" if self.scheduler is None: return {"running": False, "jobs": []} - jobs = [] - for job in self.scheduler.get_jobs(): - jobs.append({ + jobs = [ + { "id": job.id, "name": job.name, - "next_run": job.next_run_time.isoformat() + "next_run": job.next_run_time.isoformat() if job.next_run_time else None, "trigger": str(job.trigger), - }) + } + for job in self.scheduler.get_jobs() + ] return { "running": self.scheduler.running, @@ -104,4 +161,4 @@ class SchedulerService: # Global scheduler instance -scheduler_service = SchedulerService() \ No newline at end of file +scheduler_service = SchedulerService() diff --git a/app/services/sound_normalizer_service.py b/app/services/sound_normalizer_service.py new file mode 100644 index 0000000..821cdff --- /dev/null +++ b/app/services/sound_normalizer_service.py @@ -0,0 +1,491 @@ +"""Sound normalization service using ffmpeg loudnorm filter.""" + +import hashlib +import logging +from pathlib import Path + +import ffmpeg +from pydub import AudioSegment + +from app.database import db +from app.models.sound import Sound + +logger = logging.getLogger(__name__) + + +class SoundNormalizerService: + """Service for normalizing sound files using ffmpeg loudnorm.""" + + SUPPORTED_EXTENSIONS = { + ".mp3", + ".wav", + ".ogg", + ".flac", + ".m4a", + ".aac", + ".opus", + } + SOUNDS_DIR = "sounds/soundboard" + NORMALIZED_DIR = "sounds/normalized/soundboard" + + LOUDNORM_PARAMS = { + "integrated": -16, + "true_peak": -1.5, + "lra": 11.0, + "print_format": "summary", + } + + @staticmethod + def normalize_sound(sound_id: int, overwrite: bool = False) -> dict: + """Normalize a specific sound file using ffmpeg loudnorm. + + Args: + sound_id: ID of the sound to normalize + overwrite: Whether to overwrite existing normalized file + + Returns: + dict: Result of the normalization operation + + """ + try: + sound = Sound.query.get(sound_id) + if not sound: + return { + "success": False, + "error": f"Sound with ID {sound_id} not found", + } + + source_path = Path(SoundNormalizerService.SOUNDS_DIR) / sound.filename + if not source_path.exists(): + return { + "success": False, + "error": f"Source file not found: {source_path}", + } + + # Always output as WAV regardless of input format + filename_without_ext = Path(sound.filename).stem + normalized_filename = f"{filename_without_ext}.wav" + normalized_path = Path(SoundNormalizerService.NORMALIZED_DIR) / normalized_filename + + normalized_path.parent.mkdir(parents=True, exist_ok=True) + + if normalized_path.exists() and not overwrite: + return { + "success": False, + "error": f"Normalized file already exists: {normalized_path}. Use overwrite=True to replace it.", + } + + logger.info( + f"Starting normalization of {sound.name} ({sound.filename})", + ) + + result = SoundNormalizerService._normalize_with_ffmpeg( + str(source_path), str(normalized_path), + ) + + if result["success"]: + # Calculate normalized file metadata + normalized_metadata = ( + SoundNormalizerService._get_normalized_metadata( + str(normalized_path), + ) + ) + + # Update sound record with normalized information + sound.set_normalized_info( + normalized_filename=normalized_filename, + normalized_duration=normalized_metadata["duration"], + normalized_size=normalized_metadata["size"], + normalized_hash=normalized_metadata["hash"], + ) + + # Commit the database changes + db.session.commit() + + logger.info(f"Successfully normalized {sound.name}") + return { + "success": True, + "sound_id": sound_id, + "sound_name": sound.name, + "source_path": str(source_path), + "normalized_path": str(normalized_path), + "normalized_filename": normalized_filename, + "normalized_duration": normalized_metadata["duration"], + "normalized_size": normalized_metadata["size"], + "normalized_hash": normalized_metadata["hash"], + "loudnorm_stats": result.get("stats", {}), + } + return result + + except Exception as e: + logger.error(f"Error normalizing sound {sound_id}: {e}") + return {"success": False, "error": str(e)} + + @staticmethod + def normalize_all_sounds( + overwrite: bool = False, limit: int = None, + ) -> dict: + """Normalize all soundboard files. + + Args: + overwrite: Whether to overwrite existing normalized files + limit: Maximum number of files to process (None for all) + + Returns: + dict: Summary of the normalization operation + + """ + try: + query = Sound.query.filter_by(type="SDB") + if limit: + query = query.limit(limit) + + sounds = query.all() + + if not sounds: + return { + "success": True, + "message": "No soundboard files found to normalize", + "processed": 0, + "successful": 0, + "failed": 0, + "skipped": 0, + } + + logger.info(f"Starting bulk normalization of {len(sounds)} sounds") + + processed = 0 + successful = 0 + failed = 0 + skipped = 0 + errors = [] + + for sound in sounds: + result = SoundNormalizerService.normalize_sound( + sound.id, overwrite, + ) + processed += 1 + + if result["success"]: + successful += 1 + elif "already exists" in result.get("error", ""): + skipped += 1 + else: + failed += 1 + errors.append(f"{sound.name}: {result['error']}") + + logger.info( + f"Bulk normalization completed: {successful} successful, {failed} failed, {skipped} skipped", + ) + + return { + "success": True, + "message": f"Processed {processed} sounds: {successful} successful, {failed} failed, {skipped} skipped", + "processed": processed, + "successful": successful, + "failed": failed, + "skipped": skipped, + "errors": errors, + } + + except Exception as e: + logger.error(f"Error during bulk normalization: {e}") + return { + "success": False, + "error": str(e), + "processed": 0, + "successful": 0, + "failed": 0, + "skipped": 0, + } + + @staticmethod + def _normalize_with_ffmpeg(source_path: str, output_path: str) -> dict: + """Run ffmpeg loudnorm on a single file using python-ffmpeg. + + Args: + source_path: Path to source audio file + output_path: Path for normalized output file (will be WAV format) + + Returns: + dict: Result with success status and loudnorm statistics + + """ + try: + params = SoundNormalizerService.LOUDNORM_PARAMS + + logger.debug( + f"Running ffmpeg normalization: {source_path} -> {output_path}", + ) + + # Create ffmpeg input stream + input_stream = ffmpeg.input(source_path) + + # Apply loudnorm filter + loudnorm_filter = f"loudnorm=I={params['integrated']}:TP={params['true_peak']}:LRA={params['lra']}:print_format={params['print_format']}" + + # Create output stream with WAV format + output_stream = ffmpeg.output( + input_stream, + output_path, + acodec="pcm_s16le", # 16-bit PCM for WAV + ar=44100, # 44.1kHz sample rate + af=loudnorm_filter, + y=None, # Overwrite output file + ) + + # Run the ffmpeg process + out, err = ffmpeg.run( + output_stream, capture_stdout=True, capture_stderr=True, + ) + + # Parse loudnorm statistics from stderr + stats = SoundNormalizerService._parse_loudnorm_stats( + err.decode() if err else "", + ) + + if not Path(output_path).exists(): + return { + "success": False, + "error": "Output file was not created", + } + + return {"success": True, "stats": stats} + + except ffmpeg.Error as e: + error_msg = ( + f"FFmpeg error: {e.stderr.decode() if e.stderr else str(e)}" + ) + logger.error(error_msg) + return {"success": False, "error": error_msg} + except Exception as e: + logger.error(f"Error running ffmpeg: {e}") + return {"success": False, "error": str(e)} + + @staticmethod + def _parse_loudnorm_stats(stderr_output: str) -> dict: + """Parse loudnorm statistics from ffmpeg stderr output. + + Args: + stderr_output: ffmpeg stderr output containing loudnorm stats + + Returns: + dict: Parsed loudnorm statistics + + """ + stats = {} + + if not stderr_output: + return stats + + lines = stderr_output.split("\n") + + for line in lines: + line = line.strip() + if "Input Integrated:" in line: + try: + stats["input_integrated"] = float(line.split()[-2]) + except (ValueError, IndexError): + pass + elif "Input True Peak:" in line: + try: + stats["input_true_peak"] = float(line.split()[-2]) + except (ValueError, IndexError): + pass + elif "Input LRA:" in line: + try: + stats["input_lra"] = float(line.split()[-1]) + except (ValueError, IndexError): + pass + elif "Output Integrated:" in line: + try: + stats["output_integrated"] = float(line.split()[-2]) + except (ValueError, IndexError): + pass + elif "Output True Peak:" in line: + try: + stats["output_true_peak"] = float(line.split()[-2]) + except (ValueError, IndexError): + pass + elif "Output LRA:" in line: + try: + stats["output_lra"] = float(line.split()[-1]) + except (ValueError, IndexError): + pass + + return stats + + @staticmethod + def _get_normalized_metadata(file_path: str) -> dict: + """Calculate metadata for normalized file. + + Args: + file_path: Path to the normalized audio file + + Returns: + dict: Metadata including duration and hash + + """ + try: + # Get file size + file_size = Path(file_path).stat().st_size + + # Calculate file hash + file_hash = SoundNormalizerService._calculate_file_hash(file_path) + + # Get duration using pydub + audio = AudioSegment.from_wav(file_path) + duration = len(audio) # Duration in milliseconds + + return { + "duration": duration, + "size": file_size, + "hash": file_hash, + } + + except Exception as e: + logger.error(f"Error calculating metadata for {file_path}: {e}") + return { + "duration": 0, + "size": Path(file_path).stat().st_size, + "hash": "", + } + + @staticmethod + def _calculate_file_hash(file_path: str) -> str: + """Calculate SHA256 hash of file contents.""" + sha256_hash = hashlib.sha256() + + with Path(file_path).open("rb") as f: + # Read file in chunks to handle large files + for chunk in iter(lambda: f.read(4096), b""): + sha256_hash.update(chunk) + + return sha256_hash.hexdigest() + + @staticmethod + def get_normalization_status() -> dict: + """Get statistics about normalized vs original files. + + Returns: + dict: Statistics about normalization status + + """ + try: + total_sounds = Sound.query.filter_by(type="SDB").count() + + normalized_count = 0 + total_original_size = 0 + total_normalized_size = 0 + + sounds = Sound.query.filter_by(type="SDB").all() + + for sound in sounds: + original_path = Path(SoundNormalizerService.SOUNDS_DIR) / sound.filename + + if original_path.exists(): + total_original_size += original_path.stat().st_size + + # Use database field to check if normalized, not file existence + if sound.is_normalized and sound.normalized_filename: + normalized_count += 1 + normalized_path = Path(SoundNormalizerService.NORMALIZED_DIR) / sound.normalized_filename + if normalized_path.exists(): + total_normalized_size += normalized_path.stat().st_size + + return { + "total_sounds": total_sounds, + "normalized_count": normalized_count, + "normalization_percentage": ( + (normalized_count / total_sounds * 100) + if total_sounds > 0 + else 0 + ), + "total_original_size": total_original_size, + "total_normalized_size": total_normalized_size, + "size_difference": ( + total_normalized_size - total_original_size + if normalized_count > 0 + else 0 + ), + } + + except Exception as e: + logger.error(f"Error getting normalization status: {e}") + return { + "error": str(e), + "total_sounds": 0, + "normalized_count": 0, + "normalization_percentage": 0, + } + + @staticmethod + def check_ffmpeg_availability() -> dict: + """Check if ffmpeg is available and supports loudnorm filter. + + Returns: + dict: Information about ffmpeg availability and capabilities + + """ + try: + # Create a minimal test audio file to check ffmpeg + import tempfile + + with tempfile.NamedTemporaryFile( + suffix=".wav", delete=False, + ) as temp_file: + temp_path = temp_file.name + + try: + # Try a simple ffmpeg operation to check availability + test_input = ffmpeg.input( + "anullsrc=channel_layout=stereo:sample_rate=44100", + f="lavfi", + t=0.1, + ) + test_output = ffmpeg.output(test_input, temp_path) + ffmpeg.run( + test_output, + capture_stdout=True, + capture_stderr=True, + quiet=True, + ) + + # If we get here, basic ffmpeg is working + # Now test loudnorm filter + try: + norm_input = ffmpeg.input(temp_path) + norm_output = ffmpeg.output( + norm_input, + "/dev/null", + af="loudnorm=I=-16:TP=-1.5:LRA=11.0", + f="null", + ) + ffmpeg.run( + norm_output, + capture_stdout=True, + capture_stderr=True, + quiet=True, + ) + has_loudnorm = True + except ffmpeg.Error: + has_loudnorm = False + + return { + "available": True, + "version": "ffmpeg-python wrapper available", + "has_loudnorm": has_loudnorm, + "ready": has_loudnorm, + } + + finally: + # Clean up temp file + temp_file_path = Path(temp_path) + if temp_file_path.exists(): + temp_file_path.unlink() + + except Exception as e: + return { + "available": False, + "error": f"ffmpeg not available via python-ffmpeg: {e!s}", + } diff --git a/app/services/sound_scanner_service.py b/app/services/sound_scanner_service.py new file mode 100644 index 0000000..5441d34 --- /dev/null +++ b/app/services/sound_scanner_service.py @@ -0,0 +1,316 @@ +"""Sound file scanning service for discovering and importing audio files.""" + +import hashlib +import logging +from pathlib import Path + +from pydub import AudioSegment +from pydub.utils import mediainfo + +from app.database import db +from app.models.sound import Sound + +logger = logging.getLogger(__name__) + + +class SoundScannerService: + """Service for scanning and importing sound files.""" + + # Supported audio file extensions + SUPPORTED_EXTENSIONS = {".mp3", ".wav", ".ogg", ".flac", ".m4a", ".aac"} + + # Default soundboard directory + DEFAULT_SOUNDBOARD_DIR = "sounds/soundboard" + + @staticmethod + def scan_soundboard_directory( + directory: str | None = None, + ) -> dict: + """Scan the soundboard directory and add new files to the database. + + Args: + directory: Directory to scan (defaults to sounds/soundboard) + + Returns: + dict: Summary of the scan operation + + """ + scan_dir = directory or SoundScannerService.DEFAULT_SOUNDBOARD_DIR + + try: + # Ensure directory exists + scan_path = Path(scan_dir) + if not scan_path.exists(): + logger.warning( + f"Soundboard directory does not exist: {scan_dir}", + ) + return { + "success": False, + "error": f"Directory not found: {scan_dir}", + "files_found": 0, + "files_added": 0, + "files_skipped": 0, + } + + logger.info(f"Starting soundboard scan in: {scan_dir}") + + files_found = 0 + files_added = 0 + files_skipped = 0 + errors = [] + + # Walk through directory and subdirectories + for file_path in scan_path.rglob("*"): + if file_path.is_file(): + filename = file_path.name + + # Check if file has supported extension + if not SoundScannerService._is_supported_audio_file( + filename, + ): + continue + + files_found += 1 + + try: + # Process the audio file + result = SoundScannerService._process_audio_file( + str(file_path), + scan_dir, + ) + + if result["added"]: + files_added += 1 + logger.debug(f"Added sound: {filename}") + elif result.get("updated"): + files_added += 1 # Count updates as additions for reporting + logger.debug(f"Updated sound: {filename}") + else: + files_skipped += 1 + logger.debug( + f"Skipped sound: {filename} ({result['reason']})", + ) + + except Exception as e: + error_msg = f"Error processing {filename}: {e!s}" + logger.error(error_msg) + errors.append(error_msg) + files_skipped += 1 + + # Commit all changes + db.session.commit() + + logger.info( + f"Soundboard scan completed: {files_found} files found, " + f"{files_added} added, {files_skipped} skipped", + ) + + return { + "success": True, + "directory": scan_dir, + "files_found": files_found, + "files_added": files_added, + "files_skipped": files_skipped, + "errors": errors, + "message": f"Scan completed: {files_added} new sounds added", + } + + except Exception as e: + db.session.rollback() + logger.error(f"Error during soundboard scan: {e!s}") + + return { + "success": False, + "error": str(e), + "files_found": 0, + "files_added": 0, + "files_skipped": 0, + "message": "Soundboard scan failed", + } + + @staticmethod + def _is_supported_audio_file(filename: str) -> bool: + """Check if file has a supported audio extension.""" + return ( + Path(filename).suffix.lower() + in SoundScannerService.SUPPORTED_EXTENSIONS + ) + + @staticmethod + def _process_audio_file(file_path: str, base_dir: str) -> dict: + """Process a single audio file and add it to database if new. + + Args: + file_path: Full path to the audio file + base_dir: Base directory for relative path calculation + + Returns: + dict: Processing result with added flag and reason + + """ + # Calculate file hash for deduplication + file_hash = SoundScannerService._calculate_file_hash(file_path) + + # Get file metadata + metadata = SoundScannerService._extract_audio_metadata(file_path) + + # Calculate relative filename from base directory + relative_path = Path(file_path).relative_to(Path(base_dir)) + + # Check if file already exists in database by hash + existing_sound = Sound.find_by_hash(file_hash) + if existing_sound: + return { + "added": False, + "reason": f"File already exists as '{existing_sound.name}'", + } + + # Check if filename already exists in database + existing_filename_sound = Sound.find_by_filename(str(relative_path)) + if existing_filename_sound: + # Remove normalized files and clear normalized info + SoundScannerService._clear_normalized_files(existing_filename_sound) + existing_filename_sound.clear_normalized_info() + + # Update existing sound with new file information + existing_filename_sound.update_file_info( + filename=str(relative_path), + duration=metadata["duration"], + size=metadata["size"], + hash_value=file_hash, + ) + + return { + "added": False, + "updated": True, + "sound_id": existing_filename_sound.id, + "reason": f"Updated existing sound '{existing_filename_sound.name}' with new file data", + } + + # Generate sound name from filename (without extension) + sound_name = Path(file_path).stem + + # Check if name already exists and make it unique if needed + counter = 1 + original_name = sound_name + while Sound.find_by_name(sound_name): + sound_name = f"{original_name}_{counter}" + counter += 1 + + # Create new sound record + sound = Sound.create_sound( + sound_type="SDB", # Soundboard type + name=sound_name, + filename=str(relative_path), + duration=metadata["duration"], + size=metadata["size"], + hash_value=file_hash, + is_music=False, + is_deletable=False, + commit=False, # Don't commit individually, let scanner handle transaction + ) + + return { + "added": True, + "sound_id": sound.id, + "reason": "New file added successfully", + } + + @staticmethod + def _calculate_file_hash(file_path: str) -> str: + """Calculate SHA256 hash of file contents.""" + sha256_hash = hashlib.sha256() + + with Path(file_path).open("rb") as f: + # Read file in chunks to handle large files + for chunk in iter(lambda: f.read(4096), b""): + sha256_hash.update(chunk) + + return sha256_hash.hexdigest() + + @staticmethod + def _clear_normalized_files(sound: Sound) -> None: + """Remove normalized files for a sound if they exist.""" + if sound.is_normalized and sound.normalized_filename: + # Import here to avoid circular imports + from app.services.sound_normalizer_service import SoundNormalizerService + + normalized_path = Path(SoundNormalizerService.NORMALIZED_DIR) / sound.normalized_filename + if normalized_path.exists(): + try: + normalized_path.unlink() + logger.info(f"Removed normalized file: {normalized_path}") + except Exception as e: + logger.warning(f"Could not remove normalized file {normalized_path}: {e}") + + @staticmethod + def _extract_audio_metadata(file_path: str) -> dict: + """Extract metadata from audio file using pydub and mediainfo.""" + try: + # Get file size + file_size = Path(file_path).stat().st_size + + # Load audio file with pydub for basic info + audio = AudioSegment.from_file(file_path) + + # Extract basic metadata from AudioSegment + duration = len(audio) + channels = audio.channels + sample_rate = audio.frame_rate + + # Use mediainfo for more accurate bitrate information + bitrate = None + try: + info = mediainfo(file_path) + if info and "bit_rate" in info: + bitrate = int(info["bit_rate"]) + elif info and "bitrate" in info: + bitrate = int(info["bitrate"]) + except (ValueError, KeyError, TypeError): + # Fallback to calculated bitrate if mediainfo fails + if duration > 0: + file_size_bits = file_size * 8 + bitrate = int(file_size_bits / duration / 1000) + + return { + "duration": duration, + "size": file_size, + "bitrate": bitrate, + "channels": channels, + "sample_rate": sample_rate, + } + + except Exception as e: + logger.warning(f"Could not extract metadata from {file_path}: {e}") + return { + "duration": 0, + "size": Path(file_path).stat().st_size, + "bitrate": None, + "channels": None, + "sample_rate": None, + } + + @staticmethod + def get_scan_statistics() -> dict: + """Get statistics about sounds in the database.""" + total_sounds = Sound.query.count() + sdb_sounds = Sound.query.filter_by(type="SDB").count() + music_sounds = Sound.query.filter_by(is_music=True).count() + + # Calculate total size and duration + sounds = Sound.query.all() + total_size = sum(sound.size for sound in sounds) + total_duration = sum(sound.duration for sound in sounds) + total_plays = sum(sound.play_count for sound in sounds) + + return { + "total_sounds": total_sounds, + "soundboard_sounds": sdb_sounds, + "music_sounds": music_sounds, + "total_size_bytes": total_size, + "total_duration": total_duration, + "total_plays": total_plays, + "most_played": [ + sound.to_dict() for sound in Sound.get_most_played(5) + ], + } diff --git a/pyproject.toml b/pyproject.toml index ba960ec..1c41ed2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,11 +8,13 @@ requires-python = ">=3.12" dependencies = [ "apscheduler==3.11.0", "authlib==1.6.0", + "ffmpeg-python>=0.2.0", "flask==3.1.1", "flask-cors==6.0.1", "flask-jwt-extended==4.7.1", "flask-migrate==4.1.0", "flask-sqlalchemy==3.1.1", + "pydub==0.25.1", "python-dotenv==1.1.1", "requests==2.32.4", "werkzeug==3.1.3", diff --git a/reset.sh b/reset.sh index 62a133d..27c75fb 100755 --- a/reset.sh +++ b/reset.sh @@ -1,5 +1,17 @@ #!/bin/bash +shopt -s extglob + rm instance/soundboard.db -uv run migrate_db.py init-db + +rm -rf alembic/versions/!(.gitignore) +rm -rf sounds/say/!(.gitignore) +rm -rf sounds/stream/!(.gitignore|thumbnails) +rm -rf sounds/stream/thumbnails/!(.gitignore) +rm -rf sounds/temp/!(.gitignore) +rm -rf sounds/normalized/say/!(.gitignore) +rm -rf sounds/normalized/soundboard/!(.gitignore) +rm -rf sounds/normalized/stream/!(.gitignore) + +# uv run migrate_db.py init-db uv run main.py \ No newline at end of file diff --git a/sounds/normalized/.gitignore b/sounds/normalized/.gitignore new file mode 100644 index 0000000..edcfa1f --- /dev/null +++ b/sounds/normalized/.gitignore @@ -0,0 +1,5 @@ +* +!.gitignore +!say +!soundboard +!stream \ No newline at end of file diff --git a/sounds/normalized/say/.gitignore b/sounds/normalized/say/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/sounds/normalized/say/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/sounds/normalized/soundboard/.gitignore b/sounds/normalized/soundboard/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/sounds/normalized/soundboard/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/sounds/normalized/stream/.gitignore b/sounds/normalized/stream/.gitignore new file mode 100644 index 0000000..c840a7e --- /dev/null +++ b/sounds/normalized/stream/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!thumbnails \ No newline at end of file diff --git a/sounds/say/.gitignore b/sounds/say/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/sounds/say/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/sounds/soundboard/20th_century_fox.mp3 b/sounds/soundboard/20th_century_fox.mp3 new file mode 100644 index 0000000..7a43cf7 Binary files /dev/null and b/sounds/soundboard/20th_century_fox.mp3 differ diff --git a/sounds/soundboard/3corde.wav b/sounds/soundboard/3corde.wav new file mode 100644 index 0000000..4c0ae83 Binary files /dev/null and b/sounds/soundboard/3corde.wav differ diff --git a/sounds/soundboard/a_few_moments_later.mp3 b/sounds/soundboard/a_few_moments_later.mp3 new file mode 100644 index 0000000..e7e52b6 Binary files /dev/null and b/sounds/soundboard/a_few_moments_later.mp3 differ diff --git a/sounds/soundboard/aallez.wav b/sounds/soundboard/aallez.wav new file mode 100644 index 0000000..8f2e46b Binary files /dev/null and b/sounds/soundboard/aallez.wav differ diff --git a/sounds/soundboard/ah_denis_brogniart.mp3 b/sounds/soundboard/ah_denis_brogniart.mp3 new file mode 100644 index 0000000..0267f95 Binary files /dev/null and b/sounds/soundboard/ah_denis_brogniart.mp3 differ diff --git a/sounds/soundboard/alerte_gogole.mp3 b/sounds/soundboard/alerte_gogole.mp3 new file mode 100644 index 0000000..4798f86 Binary files /dev/null and b/sounds/soundboard/alerte_gogole.mp3 differ diff --git a/sounds/soundboard/allez.wav b/sounds/soundboard/allez.wav new file mode 100644 index 0000000..29bf9ae Binary files /dev/null and b/sounds/soundboard/allez.wav differ diff --git a/sounds/soundboard/among_us.mp3 b/sounds/soundboard/among_us.mp3 new file mode 100644 index 0000000..7ee512d Binary files /dev/null and b/sounds/soundboard/among_us.mp3 differ diff --git a/sounds/soundboard/and_his_name_is_john_cena.mp3 b/sounds/soundboard/and_his_name_is_john_cena.mp3 new file mode 100644 index 0000000..3a0550d Binary files /dev/null and b/sounds/soundboard/and_his_name_is_john_cena.mp3 differ diff --git a/sounds/soundboard/animal_crossing_bla.mp3 b/sounds/soundboard/animal_crossing_bla.mp3 new file mode 100644 index 0000000..cb32536 Binary files /dev/null and b/sounds/soundboard/animal_crossing_bla.mp3 differ diff --git a/sounds/soundboard/another_one.mp3 b/sounds/soundboard/another_one.mp3 new file mode 100644 index 0000000..f4fa64b Binary files /dev/null and b/sounds/soundboard/another_one.mp3 differ diff --git a/sounds/soundboard/as_tu_vu_les_quenouilles.mp3 b/sounds/soundboard/as_tu_vu_les_quenouilles.mp3 new file mode 100644 index 0000000..f090e0a Binary files /dev/null and b/sounds/soundboard/as_tu_vu_les_quenouilles.mp3 differ diff --git a/sounds/soundboard/as_tu_vu_les_quenouilles_long.mp3 b/sounds/soundboard/as_tu_vu_les_quenouilles_long.mp3 new file mode 100644 index 0000000..5f8f99b Binary files /dev/null and b/sounds/soundboard/as_tu_vu_les_quenouilles_long.mp3 differ diff --git a/sounds/soundboard/aughhhhh_aughhhhh.mp3 b/sounds/soundboard/aughhhhh_aughhhhh.mp3 new file mode 100644 index 0000000..dcae401 Binary files /dev/null and b/sounds/soundboard/aughhhhh_aughhhhh.mp3 differ diff --git a/sounds/soundboard/awwww.mp3 b/sounds/soundboard/awwww.mp3 new file mode 100644 index 0000000..a689041 Binary files /dev/null and b/sounds/soundboard/awwww.mp3 differ diff --git a/sounds/soundboard/bebou.mp3 b/sounds/soundboard/bebou.mp3 new file mode 100644 index 0000000..d830e38 Binary files /dev/null and b/sounds/soundboard/bebou.mp3 differ diff --git a/sounds/soundboard/bebou_long.mp3 b/sounds/soundboard/bebou_long.mp3 new file mode 100644 index 0000000..697e158 Binary files /dev/null and b/sounds/soundboard/bebou_long.mp3 differ diff --git a/sounds/soundboard/bizarre.opus b/sounds/soundboard/bizarre.opus new file mode 100644 index 0000000..055e8a9 Binary files /dev/null and b/sounds/soundboard/bizarre.opus differ diff --git a/sounds/soundboard/bonk.mp3 b/sounds/soundboard/bonk.mp3 new file mode 100644 index 0000000..7b89b86 Binary files /dev/null and b/sounds/soundboard/bonk.mp3 differ diff --git a/sounds/soundboard/brother_ewwwwwww.mp3 b/sounds/soundboard/brother_ewwwwwww.mp3 new file mode 100644 index 0000000..934e0b3 Binary files /dev/null and b/sounds/soundboard/brother_ewwwwwww.mp3 differ diff --git a/sounds/soundboard/c_est_honteux.mp3 b/sounds/soundboard/c_est_honteux.mp3 new file mode 100644 index 0000000..6a048b0 Binary files /dev/null and b/sounds/soundboard/c_est_honteux.mp3 differ diff --git a/sounds/soundboard/c_est_l_heure_de_manger.mp3 b/sounds/soundboard/c_est_l_heure_de_manger.mp3 new file mode 100644 index 0000000..5481926 Binary files /dev/null and b/sounds/soundboard/c_est_l_heure_de_manger.mp3 differ diff --git a/sounds/soundboard/c_est_la_mer_noir.mp3 b/sounds/soundboard/c_est_la_mer_noir.mp3 new file mode 100644 index 0000000..203d153 Binary files /dev/null and b/sounds/soundboard/c_est_la_mer_noir.mp3 differ diff --git a/sounds/soundboard/c_t_sur_sard.mp3 b/sounds/soundboard/c_t_sur_sard.mp3 new file mode 100644 index 0000000..afbd9e3 Binary files /dev/null and b/sounds/soundboard/c_t_sur_sard.mp3 differ diff --git a/sounds/soundboard/ca_va_peter.mp3 b/sounds/soundboard/ca_va_peter.mp3 new file mode 100644 index 0000000..9b71bff Binary files /dev/null and b/sounds/soundboard/ca_va_peter.mp3 differ diff --git a/sounds/soundboard/careless_whisper_short.mp3 b/sounds/soundboard/careless_whisper_short.mp3 new file mode 100644 index 0000000..b2e2b50 Binary files /dev/null and b/sounds/soundboard/careless_whisper_short.mp3 differ diff --git a/sounds/soundboard/carrefour.mp3 b/sounds/soundboard/carrefour.mp3 new file mode 100644 index 0000000..a1113d7 Binary files /dev/null and b/sounds/soundboard/carrefour.mp3 differ diff --git a/sounds/soundboard/cest_moi.wav b/sounds/soundboard/cest_moi.wav new file mode 100644 index 0000000..0202ddc Binary files /dev/null and b/sounds/soundboard/cest_moi.wav differ diff --git a/sounds/soundboard/cloche_de_boxe.mp3 b/sounds/soundboard/cloche_de_boxe.mp3 new file mode 100644 index 0000000..08b2aff Binary files /dev/null and b/sounds/soundboard/cloche_de_boxe.mp3 differ diff --git a/sounds/soundboard/combien.mp3 b/sounds/soundboard/combien.mp3 new file mode 100644 index 0000000..a56fd6f Binary files /dev/null and b/sounds/soundboard/combien.mp3 differ diff --git a/sounds/soundboard/comment_ca_mon_reuf_sans_le_quoi.mp3 b/sounds/soundboard/comment_ca_mon_reuf_sans_le_quoi.mp3 new file mode 100644 index 0000000..7c1b962 Binary files /dev/null and b/sounds/soundboard/comment_ca_mon_reuf_sans_le_quoi.mp3 differ diff --git a/sounds/soundboard/community_chang_gay.mp3 b/sounds/soundboard/community_chang_gay.mp3 new file mode 100644 index 0000000..248a391 Binary files /dev/null and b/sounds/soundboard/community_chang_gay.mp3 differ diff --git a/sounds/soundboard/cou.wav b/sounds/soundboard/cou.wav new file mode 100644 index 0000000..6e42872 Binary files /dev/null and b/sounds/soundboard/cou.wav differ diff --git a/sounds/soundboard/coucou.mp3 b/sounds/soundboard/coucou.mp3 new file mode 100644 index 0000000..1da4cdd Binary files /dev/null and b/sounds/soundboard/coucou.mp3 differ diff --git a/sounds/soundboard/dancehall_horn.mp3 b/sounds/soundboard/dancehall_horn.mp3 new file mode 100644 index 0000000..46fc645 Binary files /dev/null and b/sounds/soundboard/dancehall_horn.mp3 differ diff --git a/sounds/soundboard/decathlon.mp3 b/sounds/soundboard/decathlon.mp3 new file mode 100644 index 0000000..30853af Binary files /dev/null and b/sounds/soundboard/decathlon.mp3 differ diff --git a/sounds/soundboard/dikkenek_ou_tu_sors_ou_j_te_sors.mp3 b/sounds/soundboard/dikkenek_ou_tu_sors_ou_j_te_sors.mp3 new file mode 100644 index 0000000..6efdbdb Binary files /dev/null and b/sounds/soundboard/dikkenek_ou_tu_sors_ou_j_te_sors.mp3 differ diff --git a/sounds/soundboard/directed_by_robert_b_weide.mp3 b/sounds/soundboard/directed_by_robert_b_weide.mp3 new file mode 100644 index 0000000..e0f3786 Binary files /dev/null and b/sounds/soundboard/directed_by_robert_b_weide.mp3 differ diff --git a/sounds/soundboard/downer_noise.mp3 b/sounds/soundboard/downer_noise.mp3 new file mode 100644 index 0000000..f9c3c1f Binary files /dev/null and b/sounds/soundboard/downer_noise.mp3 differ diff --git a/sounds/soundboard/dry_fart.mp3 b/sounds/soundboard/dry_fart.mp3 new file mode 100644 index 0000000..af36de8 Binary files /dev/null and b/sounds/soundboard/dry_fart.mp3 differ diff --git a/sounds/soundboard/emotional_damage.mp3 b/sounds/soundboard/emotional_damage.mp3 new file mode 100644 index 0000000..9da899e Binary files /dev/null and b/sounds/soundboard/emotional_damage.mp3 differ diff --git a/sounds/soundboard/epic_sax_guy.mp3 b/sounds/soundboard/epic_sax_guy.mp3 new file mode 100644 index 0000000..629bf8c Binary files /dev/null and b/sounds/soundboard/epic_sax_guy.mp3 differ diff --git a/sounds/soundboard/etchebest_c_est_con_ça.mp3 b/sounds/soundboard/etchebest_c_est_con_ça.mp3 new file mode 100644 index 0000000..dd8eb35 Binary files /dev/null and b/sounds/soundboard/etchebest_c_est_con_ça.mp3 differ diff --git a/sounds/soundboard/excuse_moiiii.mp3 b/sounds/soundboard/excuse_moiiii.mp3 new file mode 100644 index 0000000..7fd8105 Binary files /dev/null and b/sounds/soundboard/excuse_moiiii.mp3 differ diff --git a/sounds/soundboard/expecto_patronum.mp3 b/sounds/soundboard/expecto_patronum.mp3 new file mode 100644 index 0000000..b98e855 Binary files /dev/null and b/sounds/soundboard/expecto_patronum.mp3 differ diff --git a/sounds/soundboard/fart_with_extra_reverb.mp3 b/sounds/soundboard/fart_with_extra_reverb.mp3 new file mode 100644 index 0000000..c0fa177 Binary files /dev/null and b/sounds/soundboard/fart_with_extra_reverb.mp3 differ diff --git a/sounds/soundboard/fbi_open_up.mp3 b/sounds/soundboard/fbi_open_up.mp3 new file mode 100644 index 0000000..2030068 Binary files /dev/null and b/sounds/soundboard/fbi_open_up.mp3 differ diff --git a/sounds/soundboard/fdp.mp3 b/sounds/soundboard/fdp.mp3 new file mode 100644 index 0000000..386336b Binary files /dev/null and b/sounds/soundboard/fdp.mp3 differ diff --git a/sounds/soundboard/flute.wav b/sounds/soundboard/flute.wav new file mode 100644 index 0000000..a16d049 Binary files /dev/null and b/sounds/soundboard/flute.wav differ diff --git a/sounds/soundboard/flute_anniv_LIP.mp3 b/sounds/soundboard/flute_anniv_LIP.mp3 new file mode 100644 index 0000000..1aca6b9 Binary files /dev/null and b/sounds/soundboard/flute_anniv_LIP.mp3 differ diff --git a/sounds/soundboard/fonctionnaire.mp3 b/sounds/soundboard/fonctionnaire.mp3 new file mode 100644 index 0000000..97dd57c Binary files /dev/null and b/sounds/soundboard/fonctionnaire.mp3 differ diff --git a/sounds/soundboard/gay_echo.mp3 b/sounds/soundboard/gay_echo.mp3 new file mode 100644 index 0000000..aea3587 Binary files /dev/null and b/sounds/soundboard/gay_echo.mp3 differ diff --git a/sounds/soundboard/goku_drip.mp3 b/sounds/soundboard/goku_drip.mp3 new file mode 100644 index 0000000..ad4ca24 Binary files /dev/null and b/sounds/soundboard/goku_drip.mp3 differ diff --git a/sounds/soundboard/gta_mission_complete.mp3 b/sounds/soundboard/gta_mission_complete.mp3 new file mode 100644 index 0000000..f823c9f Binary files /dev/null and b/sounds/soundboard/gta_mission_complete.mp3 differ diff --git a/sounds/soundboard/gtav_wasted.mp3 b/sounds/soundboard/gtav_wasted.mp3 new file mode 100644 index 0000000..c39c29e Binary files /dev/null and b/sounds/soundboard/gtav_wasted.mp3 differ diff --git a/sounds/soundboard/happy_happy_happy.mp3 b/sounds/soundboard/happy_happy_happy.mp3 new file mode 100644 index 0000000..849b392 Binary files /dev/null and b/sounds/soundboard/happy_happy_happy.mp3 differ diff --git a/sounds/soundboard/hugooo.mp3 b/sounds/soundboard/hugooo.mp3 new file mode 100644 index 0000000..12a5439 Binary files /dev/null and b/sounds/soundboard/hugooo.mp3 differ diff --git a/sounds/soundboard/i_will_be_back.mp3 b/sounds/soundboard/i_will_be_back.mp3 new file mode 100644 index 0000000..c6f105a Binary files /dev/null and b/sounds/soundboard/i_will_be_back.mp3 differ diff --git a/sounds/soundboard/initial_d_deja_vu.mp3 b/sounds/soundboard/initial_d_deja_vu.mp3 new file mode 100644 index 0000000..ba80b78 Binary files /dev/null and b/sounds/soundboard/initial_d_deja_vu.mp3 differ diff --git a/sounds/soundboard/initial_d_gas_gas_gas.mp3 b/sounds/soundboard/initial_d_gas_gas_gas.mp3 new file mode 100644 index 0000000..b43f422 Binary files /dev/null and b/sounds/soundboard/initial_d_gas_gas_gas.mp3 differ diff --git a/sounds/soundboard/insult.wav b/sounds/soundboard/insult.wav new file mode 100644 index 0000000..9d4f74a Binary files /dev/null and b/sounds/soundboard/insult.wav differ diff --git a/sounds/soundboard/je_suis_pas_venue_ici_pour_souffrir_ok.mp3 b/sounds/soundboard/je_suis_pas_venue_ici_pour_souffrir_ok.mp3 new file mode 100644 index 0000000..0833b28 Binary files /dev/null and b/sounds/soundboard/je_suis_pas_venue_ici_pour_souffrir_ok.mp3 differ diff --git a/sounds/soundboard/je_te_demande_pardon.mp3 b/sounds/soundboard/je_te_demande_pardon.mp3 new file mode 100644 index 0000000..fde5d5a Binary files /dev/null and b/sounds/soundboard/je_te_demande_pardon.mp3 differ diff --git a/sounds/soundboard/je_vous_demande_de_vous_arreter.mp3 b/sounds/soundboard/je_vous_demande_de_vous_arreter.mp3 new file mode 100644 index 0000000..807544f Binary files /dev/null and b/sounds/soundboard/je_vous_demande_de_vous_arreter.mp3 differ diff --git a/sounds/soundboard/julien_lepers_Ah_ouai_ouai_ouai_question.mp3 b/sounds/soundboard/julien_lepers_Ah_ouai_ouai_ouai_question.mp3 new file mode 100644 index 0000000..66744dc Binary files /dev/null and b/sounds/soundboard/julien_lepers_Ah_ouai_ouai_ouai_question.mp3 differ diff --git a/sounds/soundboard/julien_lepers_ah_oui_oui_oui_oui_oui_oui_oui.mp3 b/sounds/soundboard/julien_lepers_ah_oui_oui_oui_oui_oui_oui_oui.mp3 new file mode 100644 index 0000000..8844243 Binary files /dev/null and b/sounds/soundboard/julien_lepers_ah_oui_oui_oui_oui_oui_oui_oui.mp3 differ diff --git a/sounds/soundboard/kabuki.mp3 b/sounds/soundboard/kabuki.mp3 new file mode 100644 index 0000000..b34a1e9 Binary files /dev/null and b/sounds/soundboard/kabuki.mp3 differ diff --git a/sounds/soundboard/karime_cuisiniere.mp3 b/sounds/soundboard/karime_cuisiniere.mp3 new file mode 100644 index 0000000..190e783 Binary files /dev/null and b/sounds/soundboard/karime_cuisiniere.mp3 differ diff --git a/sounds/soundboard/karime_enfant_gache_court.mp3 b/sounds/soundboard/karime_enfant_gache_court.mp3 new file mode 100644 index 0000000..4fa7ff2 Binary files /dev/null and b/sounds/soundboard/karime_enfant_gache_court.mp3 differ diff --git a/sounds/soundboard/karime_enfant_gache_long.mp3 b/sounds/soundboard/karime_enfant_gache_long.mp3 new file mode 100644 index 0000000..ec7b465 Binary files /dev/null and b/sounds/soundboard/karime_enfant_gache_long.mp3 differ diff --git a/sounds/soundboard/karime_enfant_gache_medium.mp3 b/sounds/soundboard/karime_enfant_gache_medium.mp3 new file mode 100644 index 0000000..09f8cf7 Binary files /dev/null and b/sounds/soundboard/karime_enfant_gache_medium.mp3 differ diff --git a/sounds/soundboard/kendrick_mustard.mp3 b/sounds/soundboard/kendrick_mustard.mp3 new file mode 100644 index 0000000..9b8b8d1 Binary files /dev/null and b/sounds/soundboard/kendrick_mustard.mp3 differ diff --git a/sounds/soundboard/la_place_de_la_femme.mp3 b/sounds/soundboard/la_place_de_la_femme.mp3 new file mode 100644 index 0000000..eb0e1b6 Binary files /dev/null and b/sounds/soundboard/la_place_de_la_femme.mp3 differ diff --git a/sounds/soundboard/leroy_merlin.mp3 b/sounds/soundboard/leroy_merlin.mp3 new file mode 100644 index 0000000..894a414 Binary files /dev/null and b/sounds/soundboard/leroy_merlin.mp3 differ diff --git a/sounds/soundboard/les_cons_sur_orbite.mp3 b/sounds/soundboard/les_cons_sur_orbite.mp3 new file mode 100644 index 0000000..c1daf9f Binary files /dev/null and b/sounds/soundboard/les_cons_sur_orbite.mp3 differ diff --git a/sounds/soundboard/les_trois_freres_c_est_la_ca_ta_strophe.mp3 b/sounds/soundboard/les_trois_freres_c_est_la_ca_ta_strophe.mp3 new file mode 100644 index 0000000..dd61dc0 Binary files /dev/null and b/sounds/soundboard/les_trois_freres_c_est_la_ca_ta_strophe.mp3 differ diff --git a/sounds/soundboard/lets_go.wav b/sounds/soundboard/lets_go.wav new file mode 100644 index 0000000..687200c Binary files /dev/null and b/sounds/soundboard/lets_go.wav differ diff --git a/sounds/soundboard/loading.mp3 b/sounds/soundboard/loading.mp3 new file mode 100644 index 0000000..9b65481 Binary files /dev/null and b/sounds/soundboard/loading.mp3 differ diff --git a/sounds/soundboard/mac_quack.mp3 b/sounds/soundboard/mac_quack.mp3 new file mode 100644 index 0000000..dfe1ff5 Binary files /dev/null and b/sounds/soundboard/mac_quack.mp3 differ diff --git a/sounds/soundboard/magneto_serge.mp3 b/sounds/soundboard/magneto_serge.mp3 new file mode 100644 index 0000000..dac9dfa Binary files /dev/null and b/sounds/soundboard/magneto_serge.mp3 differ diff --git a/sounds/soundboard/maman_va_me_niquer_long.mp3 b/sounds/soundboard/maman_va_me_niquer_long.mp3 new file mode 100644 index 0000000..60889d7 Binary files /dev/null and b/sounds/soundboard/maman_va_me_niquer_long.mp3 differ diff --git a/sounds/soundboard/maman_va_me_niquer_medium.mp3 b/sounds/soundboard/maman_va_me_niquer_medium.mp3 new file mode 100644 index 0000000..4532b8a Binary files /dev/null and b/sounds/soundboard/maman_va_me_niquer_medium.mp3 differ diff --git a/sounds/soundboard/mans_not_hot_1.mp3 b/sounds/soundboard/mans_not_hot_1.mp3 new file mode 100644 index 0000000..c8e2474 Binary files /dev/null and b/sounds/soundboard/mans_not_hot_1.mp3 differ diff --git a/sounds/soundboard/mans_not_hot_2.mp3 b/sounds/soundboard/mans_not_hot_2.mp3 new file mode 100644 index 0000000..a7e5e8c Binary files /dev/null and b/sounds/soundboard/mans_not_hot_2.mp3 differ diff --git a/sounds/soundboard/mans_not_hot_3.mp3 b/sounds/soundboard/mans_not_hot_3.mp3 new file mode 100644 index 0000000..eaa358c Binary files /dev/null and b/sounds/soundboard/mans_not_hot_3.mp3 differ diff --git a/sounds/soundboard/maro_jump.mp3 b/sounds/soundboard/maro_jump.mp3 new file mode 100644 index 0000000..51120ea Binary files /dev/null and b/sounds/soundboard/maro_jump.mp3 differ diff --git a/sounds/soundboard/meaw.mp3 b/sounds/soundboard/meaw.mp3 new file mode 100644 index 0000000..5c0ea0a Binary files /dev/null and b/sounds/soundboard/meaw.mp3 differ diff --git a/sounds/soundboard/mercurochrome.mp3 b/sounds/soundboard/mercurochrome.mp3 new file mode 100644 index 0000000..cecc503 Binary files /dev/null and b/sounds/soundboard/mercurochrome.mp3 differ diff --git a/sounds/soundboard/metal_gear_solid.mp3 b/sounds/soundboard/metal_gear_solid.mp3 new file mode 100644 index 0000000..576427b Binary files /dev/null and b/sounds/soundboard/metal_gear_solid.mp3 differ diff --git a/sounds/soundboard/michael_scott_noooo_long.mp3 b/sounds/soundboard/michael_scott_noooo_long.mp3 new file mode 100644 index 0000000..22f63c6 Binary files /dev/null and b/sounds/soundboard/michael_scott_noooo_long.mp3 differ diff --git a/sounds/soundboard/michael_scott_noooo_medium.mp3 b/sounds/soundboard/michael_scott_noooo_medium.mp3 new file mode 100644 index 0000000..92d184d Binary files /dev/null and b/sounds/soundboard/michael_scott_noooo_medium.mp3 differ diff --git a/sounds/soundboard/michael_scott_noooo_short.mp3 b/sounds/soundboard/michael_scott_noooo_short.mp3 new file mode 100644 index 0000000..cbf5717 Binary files /dev/null and b/sounds/soundboard/michael_scott_noooo_short.mp3 differ diff --git a/sounds/soundboard/minecraft_villager_huh.mp3 b/sounds/soundboard/minecraft_villager_huh.mp3 new file mode 100644 index 0000000..8050e41 Binary files /dev/null and b/sounds/soundboard/minecraft_villager_huh.mp3 differ diff --git a/sounds/soundboard/nani.mp3 b/sounds/soundboard/nani.mp3 new file mode 100644 index 0000000..0bf989d Binary files /dev/null and b/sounds/soundboard/nani.mp3 differ diff --git a/sounds/soundboard/nein_nein_nein.mp3 b/sounds/soundboard/nein_nein_nein.mp3 new file mode 100644 index 0000000..6f60209 Binary files /dev/null and b/sounds/soundboard/nein_nein_nein.mp3 differ diff --git a/sounds/soundboard/netflix.mp3 b/sounds/soundboard/netflix.mp3 new file mode 100644 index 0000000..319cf2b Binary files /dev/null and b/sounds/soundboard/netflix.mp3 differ diff --git a/sounds/soundboard/nokia.mp3 b/sounds/soundboard/nokia.mp3 new file mode 100644 index 0000000..a37adee Binary files /dev/null and b/sounds/soundboard/nokia.mp3 differ diff --git a/sounds/soundboard/nul_homer.mp3 b/sounds/soundboard/nul_homer.mp3 new file mode 100644 index 0000000..df1a910 Binary files /dev/null and b/sounds/soundboard/nul_homer.mp3 differ diff --git a/sounds/soundboard/obi_wan_hello_there.mp3 b/sounds/soundboard/obi_wan_hello_there.mp3 new file mode 100644 index 0000000..660d01f Binary files /dev/null and b/sounds/soundboard/obi_wan_hello_there.mp3 differ diff --git a/sounds/soundboard/oh_my_god.wav b/sounds/soundboard/oh_my_god.wav new file mode 100644 index 0000000..619caaf Binary files /dev/null and b/sounds/soundboard/oh_my_god.wav differ diff --git a/sounds/soundboard/ok.wav b/sounds/soundboard/ok.wav new file mode 100644 index 0000000..544c7ee Binary files /dev/null and b/sounds/soundboard/ok.wav differ diff --git a/sounds/soundboard/olala.wav b/sounds/soundboard/olala.wav new file mode 100644 index 0000000..a9801eb Binary files /dev/null and b/sounds/soundboard/olala.wav differ diff --git a/sounds/soundboard/olivier_long.mp3 b/sounds/soundboard/olivier_long.mp3 new file mode 100644 index 0000000..5c06207 Binary files /dev/null and b/sounds/soundboard/olivier_long.mp3 differ diff --git a/sounds/soundboard/olivier_medium.mp3 b/sounds/soundboard/olivier_medium.mp3 new file mode 100644 index 0000000..4b1cb55 Binary files /dev/null and b/sounds/soundboard/olivier_medium.mp3 differ diff --git a/sounds/soundboard/olivier_short.mp3 b/sounds/soundboard/olivier_short.mp3 new file mode 100644 index 0000000..a31f178 Binary files /dev/null and b/sounds/soundboard/olivier_short.mp3 differ diff --git a/sounds/soundboard/olivier_very_short.mp3 b/sounds/soundboard/olivier_very_short.mp3 new file mode 100644 index 0000000..905f710 Binary files /dev/null and b/sounds/soundboard/olivier_very_short.mp3 differ diff --git a/sounds/soundboard/omar.mp3 b/sounds/soundboard/omar.mp3 new file mode 100644 index 0000000..a1915ac Binary files /dev/null and b/sounds/soundboard/omar.mp3 differ diff --git a/sounds/soundboard/omar_2.mp3 b/sounds/soundboard/omar_2.mp3 new file mode 100644 index 0000000..a5e65e4 Binary files /dev/null and b/sounds/soundboard/omar_2.mp3 differ diff --git a/sounds/soundboard/ooh_wah_ah_ah_ah.mp3 b/sounds/soundboard/ooh_wah_ah_ah_ah.mp3 new file mode 100644 index 0000000..1a20d71 Binary files /dev/null and b/sounds/soundboard/ooh_wah_ah_ah_ah.mp3 differ diff --git a/sounds/soundboard/ouais_cest_greg.mp3 b/sounds/soundboard/ouais_cest_greg.mp3 new file mode 100644 index 0000000..7aba64c Binary files /dev/null and b/sounds/soundboard/ouais_cest_greg.mp3 differ diff --git a/sounds/soundboard/ouais_ouais_ouais_1.mp3 b/sounds/soundboard/ouais_ouais_ouais_1.mp3 new file mode 100644 index 0000000..f7d0524 Binary files /dev/null and b/sounds/soundboard/ouais_ouais_ouais_1.mp3 differ diff --git a/sounds/soundboard/ouais_ouais_ouais_2.mp3 b/sounds/soundboard/ouais_ouais_ouais_2.mp3 new file mode 100644 index 0000000..109bafc Binary files /dev/null and b/sounds/soundboard/ouais_ouais_ouais_2.mp3 differ diff --git a/sounds/soundboard/outch.mp3 b/sounds/soundboard/outch.mp3 new file mode 100644 index 0000000..1516905 Binary files /dev/null and b/sounds/soundboard/outch.mp3 differ diff --git a/sounds/soundboard/outro_song.mp3 b/sounds/soundboard/outro_song.mp3 new file mode 100644 index 0000000..75452bf Binary files /dev/null and b/sounds/soundboard/outro_song.mp3 differ diff --git a/sounds/soundboard/pain.mp3 b/sounds/soundboard/pain.mp3 new file mode 100644 index 0000000..d5476a8 Binary files /dev/null and b/sounds/soundboard/pain.mp3 differ diff --git a/sounds/soundboard/pas_maintenant.mp3 b/sounds/soundboard/pas_maintenant.mp3 new file mode 100644 index 0000000..8a14a07 Binary files /dev/null and b/sounds/soundboard/pas_maintenant.mp3 differ diff --git a/sounds/soundboard/pedro.opus b/sounds/soundboard/pedro.opus new file mode 100644 index 0000000..206100d Binary files /dev/null and b/sounds/soundboard/pedro.opus differ diff --git a/sounds/soundboard/pew_pew.mp3 b/sounds/soundboard/pew_pew.mp3 new file mode 100644 index 0000000..de22eba Binary files /dev/null and b/sounds/soundboard/pew_pew.mp3 differ diff --git a/sounds/soundboard/pikachouchous.mp3 b/sounds/soundboard/pikachouchous.mp3 new file mode 100644 index 0000000..ffb108d Binary files /dev/null and b/sounds/soundboard/pikachouchous.mp3 differ diff --git a/sounds/soundboard/plankton_augh.mp3 b/sounds/soundboard/plankton_augh.mp3 new file mode 100644 index 0000000..0c50702 Binary files /dev/null and b/sounds/soundboard/plankton_augh.mp3 differ diff --git a/sounds/soundboard/pornhub.mp3 b/sounds/soundboard/pornhub.mp3 new file mode 100644 index 0000000..a3b25d8 Binary files /dev/null and b/sounds/soundboard/pornhub.mp3 differ diff --git a/sounds/soundboard/pull.mp3 b/sounds/soundboard/pull.mp3 new file mode 100644 index 0000000..81f7abc Binary files /dev/null and b/sounds/soundboard/pull.mp3 differ diff --git a/sounds/soundboard/quest_ce_quon_dit_au_chauffeur.mp3 b/sounds/soundboard/quest_ce_quon_dit_au_chauffeur.mp3 new file mode 100644 index 0000000..097a32f Binary files /dev/null and b/sounds/soundboard/quest_ce_quon_dit_au_chauffeur.mp3 differ diff --git a/sounds/soundboard/ronaldo.mp3 b/sounds/soundboard/ronaldo.mp3 new file mode 100644 index 0000000..7314194 Binary files /dev/null and b/sounds/soundboard/ronaldo.mp3 differ diff --git a/sounds/soundboard/seinfeld.mp3 b/sounds/soundboard/seinfeld.mp3 new file mode 100644 index 0000000..2fe0a38 Binary files /dev/null and b/sounds/soundboard/seinfeld.mp3 differ diff --git a/sounds/soundboard/sheh.mp3 b/sounds/soundboard/sheh.mp3 new file mode 100644 index 0000000..51c81f2 Binary files /dev/null and b/sounds/soundboard/sheh.mp3 differ diff --git a/sounds/soundboard/shocking.mp3 b/sounds/soundboard/shocking.mp3 new file mode 100644 index 0000000..eeef21c Binary files /dev/null and b/sounds/soundboard/shocking.mp3 differ diff --git a/sounds/soundboard/sigma.mp3 b/sounds/soundboard/sigma.mp3 new file mode 100644 index 0000000..28edd23 Binary files /dev/null and b/sounds/soundboard/sigma.mp3 differ diff --git a/sounds/soundboard/sirene_nucleaire_short.mp3 b/sounds/soundboard/sirene_nucleaire_short.mp3 new file mode 100644 index 0000000..2e07071 Binary files /dev/null and b/sounds/soundboard/sirene_nucleaire_short.mp3 differ diff --git a/sounds/soundboard/skull_trumpet.mp3 b/sounds/soundboard/skull_trumpet.mp3 new file mode 100644 index 0000000..8a8a036 Binary files /dev/null and b/sounds/soundboard/skull_trumpet.mp3 differ diff --git a/sounds/soundboard/slack.mp3 b/sounds/soundboard/slack.mp3 new file mode 100644 index 0000000..30ab1a6 Binary files /dev/null and b/sounds/soundboard/slack.mp3 differ diff --git a/sounds/soundboard/sncf_france_jingle.mp3 b/sounds/soundboard/sncf_france_jingle.mp3 new file mode 100644 index 0000000..ed7bcba Binary files /dev/null and b/sounds/soundboard/sncf_france_jingle.mp3 differ diff --git a/sounds/soundboard/spectaculaire_renversant.mp3 b/sounds/soundboard/spectaculaire_renversant.mp3 new file mode 100644 index 0000000..3f7de5f Binary files /dev/null and b/sounds/soundboard/spectaculaire_renversant.mp3 differ diff --git a/sounds/soundboard/spiderman.mp3 b/sounds/soundboard/spiderman.mp3 new file mode 100644 index 0000000..607a6ed Binary files /dev/null and b/sounds/soundboard/spiderman.mp3 differ diff --git a/sounds/soundboard/splendide_the_mask.mp3 b/sounds/soundboard/splendide_the_mask.mp3 new file mode 100644 index 0000000..ed0f3c1 Binary files /dev/null and b/sounds/soundboard/splendide_the_mask.mp3 differ diff --git a/sounds/soundboard/surprise_motherfucker.mp3 b/sounds/soundboard/surprise_motherfucker.mp3 new file mode 100644 index 0000000..537148b Binary files /dev/null and b/sounds/soundboard/surprise_motherfucker.mp3 differ diff --git a/sounds/soundboard/ta_da.mp3 b/sounds/soundboard/ta_da.mp3 new file mode 100644 index 0000000..f4dd030 Binary files /dev/null and b/sounds/soundboard/ta_da.mp3 differ diff --git a/sounds/soundboard/taco_bell_bong.mp3 b/sounds/soundboard/taco_bell_bong.mp3 new file mode 100644 index 0000000..83e2faf Binary files /dev/null and b/sounds/soundboard/taco_bell_bong.mp3 differ diff --git a/sounds/soundboard/tequila_heineken.mp3 b/sounds/soundboard/tequila_heineken.mp3 new file mode 100644 index 0000000..2154aa7 Binary files /dev/null and b/sounds/soundboard/tequila_heineken.mp3 differ diff --git a/sounds/soundboard/the_rock.mp3 b/sounds/soundboard/the_rock.mp3 new file mode 100644 index 0000000..65a3a91 Binary files /dev/null and b/sounds/soundboard/the_rock.mp3 differ diff --git a/sounds/soundboard/timmy_trumpet_short.mp3 b/sounds/soundboard/timmy_trumpet_short.mp3 new file mode 100644 index 0000000..7282f4f Binary files /dev/null and b/sounds/soundboard/timmy_trumpet_short.mp3 differ diff --git a/sounds/soundboard/timmy_trumpet_very_short.mp3 b/sounds/soundboard/timmy_trumpet_very_short.mp3 new file mode 100644 index 0000000..5278ff7 Binary files /dev/null and b/sounds/soundboard/timmy_trumpet_very_short.mp3 differ diff --git a/sounds/soundboard/to_be_continued.mp3 b/sounds/soundboard/to_be_continued.mp3 new file mode 100644 index 0000000..da6c90c Binary files /dev/null and b/sounds/soundboard/to_be_continued.mp3 differ diff --git a/sounds/soundboard/travail_terminer.opus b/sounds/soundboard/travail_terminer.opus new file mode 100644 index 0000000..5fec61e Binary files /dev/null and b/sounds/soundboard/travail_terminer.opus differ diff --git a/sounds/soundboard/trumpets_mexican.mp3 b/sounds/soundboard/trumpets_mexican.mp3 new file mode 100644 index 0000000..5200a81 Binary files /dev/null and b/sounds/soundboard/trumpets_mexican.mp3 differ diff --git a/sounds/soundboard/tu_es_le_maillon_faible_au_revoir.mp3 b/sounds/soundboard/tu_es_le_maillon_faible_au_revoir.mp3 new file mode 100644 index 0000000..60f9a25 Binary files /dev/null and b/sounds/soundboard/tu_es_le_maillon_faible_au_revoir.mp3 differ diff --git a/sounds/soundboard/tu_pousses_le_bouchon_un_peu_trop_loin_maurice.mp3 b/sounds/soundboard/tu_pousses_le_bouchon_un_peu_trop_loin_maurice.mp3 new file mode 100644 index 0000000..a3fbf62 Binary files /dev/null and b/sounds/soundboard/tu_pousses_le_bouchon_un_peu_trop_loin_maurice.mp3 differ diff --git a/sounds/soundboard/tuturu.mp3 b/sounds/soundboard/tuturu.mp3 new file mode 100644 index 0000000..fd0fac5 Binary files /dev/null and b/sounds/soundboard/tuturu.mp3 differ diff --git a/sounds/soundboard/une_tuileeeeeeee.mp3 b/sounds/soundboard/une_tuileeeeeeee.mp3 new file mode 100644 index 0000000..ca6ffef Binary files /dev/null and b/sounds/soundboard/une_tuileeeeeeee.mp3 differ diff --git a/sounds/soundboard/uwu.mp3 b/sounds/soundboard/uwu.mp3 new file mode 100644 index 0000000..9e9852f Binary files /dev/null and b/sounds/soundboard/uwu.mp3 differ diff --git a/sounds/soundboard/vega_missile_satellise.mp3 b/sounds/soundboard/vega_missile_satellise.mp3 new file mode 100644 index 0000000..d1a7fc6 Binary files /dev/null and b/sounds/soundboard/vega_missile_satellise.mp3 differ diff --git a/sounds/soundboard/vous_etes_vraiement_degueulasse_paul.mp3 b/sounds/soundboard/vous_etes_vraiement_degueulasse_paul.mp3 new file mode 100644 index 0000000..d2decd9 Binary files /dev/null and b/sounds/soundboard/vous_etes_vraiement_degueulasse_paul.mp3 differ diff --git a/sounds/soundboard/weee_hahaha_oh.mp3 b/sounds/soundboard/weee_hahaha_oh.mp3 new file mode 100644 index 0000000..1a0b4f4 Binary files /dev/null and b/sounds/soundboard/weee_hahaha_oh.mp3 differ diff --git a/sounds/soundboard/windows_xp_erreur.mp3 b/sounds/soundboard/windows_xp_erreur.mp3 new file mode 100644 index 0000000..f476db3 Binary files /dev/null and b/sounds/soundboard/windows_xp_erreur.mp3 differ diff --git a/sounds/soundboard/windows_xp_shutdown.mp3 b/sounds/soundboard/windows_xp_shutdown.mp3 new file mode 100644 index 0000000..b31a99f Binary files /dev/null and b/sounds/soundboard/windows_xp_shutdown.mp3 differ diff --git a/sounds/soundboard/windows_xp_startup.mp3 b/sounds/soundboard/windows_xp_startup.mp3 new file mode 100644 index 0000000..7307c0a Binary files /dev/null and b/sounds/soundboard/windows_xp_startup.mp3 differ diff --git a/sounds/soundboard/woooow.wav b/sounds/soundboard/woooow.wav new file mode 100644 index 0000000..6a589d8 Binary files /dev/null and b/sounds/soundboard/woooow.wav differ diff --git a/sounds/soundboard/y2mate.mp3 b/sounds/soundboard/y2mate.mp3 new file mode 100644 index 0000000..875b4c7 Binary files /dev/null and b/sounds/soundboard/y2mate.mp3 differ diff --git a/sounds/soundboard/yaaa.mp3 b/sounds/soundboard/yaaa.mp3 new file mode 100644 index 0000000..3e34b3a Binary files /dev/null and b/sounds/soundboard/yaaa.mp3 differ diff --git a/sounds/soundboard/yeah_boiii.mp3 b/sounds/soundboard/yeah_boiii.mp3 new file mode 100644 index 0000000..5801e7b Binary files /dev/null and b/sounds/soundboard/yeah_boiii.mp3 differ diff --git a/sounds/soundboard/yeepee.mp3 b/sounds/soundboard/yeepee.mp3 new file mode 100644 index 0000000..0be2e43 Binary files /dev/null and b/sounds/soundboard/yeepee.mp3 differ diff --git a/sounds/soundboard/zemmour_tousse.mp3 b/sounds/soundboard/zemmour_tousse.mp3 new file mode 100644 index 0000000..7d09dca Binary files /dev/null and b/sounds/soundboard/zemmour_tousse.mp3 differ diff --git a/sounds/stream/.gitignore b/sounds/stream/.gitignore new file mode 100644 index 0000000..3473033 --- /dev/null +++ b/sounds/stream/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!thumbnails/ \ No newline at end of file diff --git a/sounds/stream/thumbnails/.gitignore b/sounds/stream/thumbnails/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/sounds/stream/thumbnails/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/sounds/temp/.gitignore b/sounds/temp/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/sounds/temp/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/uv.lock b/uv.lock index 33b65f2..9a1860e 100644 --- a/uv.lock +++ b/uv.lock @@ -206,6 +206,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/4b/3256759723b7e66380397d958ca07c59cfc3fb5c794fb5516758afd05d41/cryptography-45.0.4-cp37-abi3-win_amd64.whl", hash = "sha256:627ba1bc94f6adf0b0a2e35d87020285ead22d9f648c7e75bb64f367375f3b22", size = 3395508 }, ] +[[package]] +name = "ffmpeg-python" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "future" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/5e/d5f9105d59c1325759d838af4e973695081fbbc97182baf73afc78dec266/ffmpeg-python-0.2.0.tar.gz", hash = "sha256:65225db34627c578ef0e11c8b1eb528bb35e024752f6f10b78c011f6f64c4127", size = 21543 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/0c/56be52741f75bad4dc6555991fabd2e07b432d333da82c11ad701123888a/ffmpeg_python-0.2.0-py3-none-any.whl", hash = "sha256:ac441a0404e053f8b6a1113a77c0f452f1cfc62f6344a769475ffdc0f56c23c5", size = 25024 }, +] + [[package]] name = "flask" version = "3.1.1" @@ -277,6 +289,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1d/6a/89963a5c6ecf166e8be29e0d1bf6806051ee8fe6c82e232842e3aeac9204/flask_sqlalchemy-3.1.1-py3-none-any.whl", hash = "sha256:4ba4be7f419dc72f4efd8802d69974803c37259dd42f3913b0dcf75c9447e0a0", size = 25125 }, ] +[[package]] +name = "future" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/b2/4140c69c6a66432916b26158687e821ba631a4c9273c474343badf84d3ba/future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05", size = 1228490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/71/ae30dadffc90b9006d77af76b393cb9dfbfc9629f339fc1574a1c52e6806/future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216", size = 491326 }, +] + [[package]] name = "greenlet" version = "3.2.3" @@ -453,6 +474,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, ] +[[package]] +name = "pydub" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/9a/e6bca0eed82db26562c73b5076539a4a08d3cffd19c3cc5913a3e61145fd/pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f", size = 38326 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/53/d78dc063216e62fc55f6b2eebb447f6a4b0a59f55c8406376f76bf959b08/pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6", size = 32327 }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -543,11 +573,13 @@ source = { virtual = "." } dependencies = [ { name = "apscheduler" }, { name = "authlib" }, + { name = "ffmpeg-python" }, { name = "flask" }, { name = "flask-cors" }, { name = "flask-jwt-extended" }, { name = "flask-migrate" }, { name = "flask-sqlalchemy" }, + { name = "pydub" }, { name = "python-dotenv" }, { name = "requests" }, { name = "werkzeug" }, @@ -564,11 +596,13 @@ dev = [ requires-dist = [ { name = "apscheduler", specifier = "==3.11.0" }, { name = "authlib", specifier = "==1.6.0" }, + { name = "ffmpeg-python", specifier = ">=0.2.0" }, { name = "flask", specifier = "==3.1.1" }, { name = "flask-cors", specifier = "==6.0.1" }, { name = "flask-jwt-extended", specifier = "==4.7.1" }, { name = "flask-migrate", specifier = "==4.1.0" }, { name = "flask-sqlalchemy", specifier = "==3.1.1" }, + { name = "pydub", specifier = "==0.25.1" }, { name = "python-dotenv", specifier = "==1.1.1" }, { name = "requests", specifier = "==2.32.4" }, { name = "werkzeug", specifier = "==3.1.3" },