"""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()