d4d8d8a3b6
- Migrated monolithic main.py to feature-scoped routers in app/routers/ - Added GEMINI.md for project context and AI instructional guidelines - Updated README.md with a comprehensive modernization plan (SQL migration, robust scraping DSL, frontend modernization) - Improved authentication with cookie support and modular JS - Updated test suite and documentation
95 lines
2.6 KiB
Python
95 lines
2.6 KiB
Python
"""Application configuration using environment variables"""
|
|
import secrets
|
|
|
|
|
|
from pydantic_settings import BaseSettings
|
|
from pydantic import model_validator
|
|
from typing import List
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
"""Application settings loaded from environment variables"""
|
|
|
|
# Application
|
|
app_name: str = "Ohm Stream Downloader"
|
|
app_version: str = "2.2"
|
|
debug: bool = False
|
|
|
|
# Server
|
|
host: str = "0.0.0.0"
|
|
port: int = 3000
|
|
reload: bool = True
|
|
|
|
# Authentication
|
|
jwt_secret_key: str = "dev-secret-change-in-production"
|
|
jwt_algorithm: str = "HS256"
|
|
access_token_expire_minutes: int = 60 * 24 # 24 hours (short-lived for security)
|
|
refresh_token_expire_days: int = 30
|
|
|
|
@model_validator(mode="after")
|
|
def validate_jwt_secret_key(self) -> "Settings":
|
|
"""Validate JWT_SECRET_KEY is not the default or too short"""
|
|
default_secret = "dev-secret-change-in-production"
|
|
|
|
if self.jwt_secret_key == default_secret:
|
|
raise ValueError(
|
|
f"JWT_SECRET_KEY cannot be the default value '{default_secret}'. "
|
|
f"Please set a secure secret in your .env file. "
|
|
f"Use Settings.generate_secret() to generate a secure secret."
|
|
)
|
|
|
|
if len(self.jwt_secret_key) < 32:
|
|
raise ValueError(
|
|
f"JWT_SECRET_KEY must be at least 32 characters long. "
|
|
f"Current length: {len(self.jwt_secret_key)} characters. "
|
|
f"Use Settings.generate_secret() to generate a secure secret."
|
|
)
|
|
|
|
return self
|
|
|
|
@staticmethod
|
|
def generate_secret() -> str:
|
|
"""Generate a cryptographically secure JWT secret key"""
|
|
return secrets.token_urlsafe(32)
|
|
|
|
# Downloads
|
|
download_dir: str = "downloads"
|
|
max_parallel_downloads: int = 3
|
|
chunk_size: int = 1024 * 1024 # 1MB chunks
|
|
|
|
# CORS
|
|
cors_origins: List[str] = [
|
|
"http://localhost:3000",
|
|
"http://127.0.0.1:3000",
|
|
"http://192.168.1.204:3000",
|
|
"http://192.168.1.204",
|
|
]
|
|
|
|
# Storage
|
|
favorites_storage_path: str = "favorites.json"
|
|
|
|
# Sonarr
|
|
sonarr_config_path: str = "config/sonarr.json"
|
|
sonarr_mappings_path: str = "config/sonarr_mappings.json"
|
|
|
|
# API Timeouts
|
|
http_timeout: float = 10.0
|
|
download_timeout: int = 300 # 5 minutes
|
|
|
|
# Logging
|
|
log_level: str = "INFO"
|
|
|
|
class Config:
|
|
env_file = ".env"
|
|
env_file_encoding = "utf-8"
|
|
case_sensitive = False
|
|
|
|
|
|
# Global settings instance
|
|
settings = Settings()
|
|
|
|
|
|
def get_settings() -> Settings:
|
|
"""Get the global settings instance"""
|
|
return settings
|