Initial commit: AudiOhm - Alternative Spotify avec streaming YouTube
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>
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user