a89c7894cf
Backend: - FastAPI avec PostgreSQL et Redis - Authentification JWT complète - API REST pour musique, playlists, recherche - Streaming audio via yt-dlp - SQLAlchemy 2.0 async Frontend: - Flutter avec thème néon cyberpunk - State management Riverpod - Layout adaptatif desktop/mobile - Lecteur audio avec mini-player Infrastructure: - Docker Compose (PostgreSQL + Redis) - Scripts d'installation automatisés - Scripts de build pour exécutables Fichiers ajoutés: - BUILD_CLIENT_*.bat/sh: Scripts de compilation - BUILD_CLIENT_README.md: Documentation compilation - CHECK_FLUTTER.sh: Vérificateur d'environnement - requirements.txt mis à jour pour Python 3.13 - Modèles SQLAlchemy corrigés (metadata -> extra_metadata) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
159 lines
4.4 KiB
Python
159 lines
4.4 KiB
Python
"""Application configuration using Pydantic Settings."""
|
|
from functools import lru_cache
|
|
from typing import Literal
|
|
|
|
from pydantic import Field, field_validator
|
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
"""Application settings loaded from environment variables."""
|
|
|
|
model_config = SettingsConfigDict(
|
|
env_file=".env",
|
|
env_file_encoding="utf-8",
|
|
case_sensitive=False,
|
|
extra="ignore",
|
|
)
|
|
|
|
# Application
|
|
APP_NAME: str = "AudiOhm"
|
|
APP_VERSION: str = "1.0.0"
|
|
DEBUG: bool = False
|
|
API_V1_PREFIX: str = "/api/v1"
|
|
|
|
# Server
|
|
HOST: str = "0.0.0.0"
|
|
PORT: int = 8000
|
|
|
|
# CORS
|
|
BACKEND_CORS_ORIGINS: list[str] = Field(
|
|
default=["http://localhost:3000", "http://localhost:8000"],
|
|
description="List of allowed CORS origins",
|
|
)
|
|
|
|
@field_validator("BACKEND_CORS_ORIGINS", mode="before")
|
|
@classmethod
|
|
def parse_cors_origins(cls, v: str | list[str]) -> list[str]:
|
|
"""Parse CORS origins from string or list."""
|
|
if isinstance(v, str):
|
|
return [origin.strip() for origin in v.split(",")]
|
|
return v
|
|
|
|
# Database
|
|
POSTGRES_HOST: str = "localhost"
|
|
POSTGRES_PORT: int = 5432
|
|
POSTGRES_USER: str = "spotify"
|
|
POSTGRES_PASSWORD: str = "spotify_password"
|
|
POSTGRES_DB: str = "spotify_le_2"
|
|
|
|
@property
|
|
def DATABASE_URL(self) -> str:
|
|
"""Build PostgreSQL async connection URL."""
|
|
return (
|
|
f"postgresql+asyncpg://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}"
|
|
f"@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}"
|
|
)
|
|
|
|
# Redis
|
|
REDIS_HOST: str = "localhost"
|
|
REDIS_PORT: int = 6379
|
|
REDIS_PASSWORD: str | None = None
|
|
REDIS_DB: int = 0
|
|
REDIS_URL: str | None = None
|
|
|
|
@property
|
|
def FULL_REDIS_URL(self) -> str:
|
|
"""Build Redis connection URL."""
|
|
if self.REDIS_URL:
|
|
return self.REDIS_URL
|
|
auth = f":{self.REDIS_PASSWORD}@" if self.REDIS_PASSWORD else ""
|
|
return f"redis://{auth}{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}"
|
|
|
|
# JWT
|
|
SECRET_KEY: str = Field(
|
|
default="change-this-secret-key-in-production",
|
|
description="Secret key for JWT token signing",
|
|
)
|
|
ALGORITHM: str = "HS256"
|
|
ACCESS_TOKEN_EXPIRE_MINUTES: int = 15
|
|
REFRESH_TOKEN_EXPIRE_DAYS: int = 7
|
|
|
|
# Password hashing
|
|
PASSWORD_HASH_ALGORITHM: Literal["bcrypt"] = "bcrypt"
|
|
PASSWORD_HASH_ROUNDS: int = 12
|
|
|
|
# Storage
|
|
STORAGE_PATH: str = "./storage"
|
|
AUDIO_CACHE_PATH: str = "./storage/audio/cache"
|
|
AUDIO_PERMANENT_PATH: str = "./storage/audio/permanent"
|
|
THUMBNAILS_PATH: str = "./storage/thumbnails"
|
|
MAX_CACHE_SIZE_GB: int = 50
|
|
|
|
# YouTube
|
|
YOUTUBE_API_KEY: str | None = None
|
|
YTDLP_PATH: str = "yt-dlp"
|
|
|
|
# Spotify (for import)
|
|
SPOTIFY_CLIENT_ID: str | None = None
|
|
SPOTIFY_CLIENT_SECRET: str | None = None
|
|
SPOTIFY_REDIRECT_URI: str = "http://localhost:8000/api/v1/import/spotify/callback"
|
|
|
|
# Last.fm (for metadata)
|
|
LASTFM_API_KEY: str | None = None
|
|
LASTFM_SECRET: str | None = None
|
|
|
|
# Rate limiting
|
|
RATE_LIMIT_PER_MINUTE: int = 100
|
|
RATE_LIMIT_BURST: int = 200
|
|
|
|
# Upload
|
|
MAX_FILE_SIZE_MB: int = 100
|
|
ALLOWED_AUDIO_TYPES: list[str] = [
|
|
"audio/mpeg",
|
|
"audio/ogg",
|
|
"audio/wav",
|
|
"audio/flac",
|
|
]
|
|
|
|
# Audio processing
|
|
FFMPEG_PATH: str = "ffmpeg"
|
|
AUDIO_QUALITY: Literal["low", "medium", "high"] = "high"
|
|
BITRATE: int = 320
|
|
|
|
# Logging
|
|
LOG_LEVEL: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO"
|
|
LOG_FILE_PATH: str = "./logs"
|
|
|
|
# Pagination
|
|
DEFAULT_PAGE_SIZE: int = 20
|
|
MAX_PAGE_SIZE: int = 100
|
|
|
|
def __init__(self, **kwargs):
|
|
"""Initialize settings and create storage directories."""
|
|
super().__init__(**kwargs)
|
|
self._ensure_storage_directories()
|
|
|
|
def _ensure_storage_directories(self) -> None:
|
|
"""Create storage directories if they don't exist."""
|
|
from pathlib import Path
|
|
|
|
for path in [
|
|
self.STORAGE_PATH,
|
|
self.AUDIO_CACHE_PATH,
|
|
self.AUDIO_PERMANENT_PATH,
|
|
self.THUMBNAILS_PATH,
|
|
self.LOG_FILE_PATH,
|
|
]:
|
|
Path(path).mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
@lru_cache()
|
|
def get_settings() -> Settings:
|
|
"""Get cached settings instance."""
|
|
return Settings()
|
|
|
|
|
|
# Global settings instance
|
|
settings = get_settings()
|