Files
Aniworld/src/config/settings.py
Lukas 4895e487c0 feat: Add NFO metadata infrastructure (Task 3 - partial)
- Created TMDB API client with async requests, caching, and retry logic
- Implemented NFO XML generator for Kodi/XBMC format
- Created image downloader for poster/logo/fanart with validation
- Added NFO service to orchestrate metadata creation
- Added NFO-related configuration settings
- Updated requirements.txt with aiohttp, lxml, pillow
- Created unit tests (need refinement due to implementation mismatch)

Components created:
- src/core/services/tmdb_client.py (270 lines)
- src/core/services/nfo_service.py (390 lines)
- src/core/utils/nfo_generator.py (180 lines)
- src/core/utils/image_downloader.py (296 lines)
- tests/unit/test_tmdb_client.py
- tests/unit/test_nfo_generator.py
- tests/unit/test_image_downloader.py

Note: Tests need to be updated to match actual implementation APIs.
Dependencies installed: aiohttp, lxml, pillow
2026-01-11 20:33:33 +01:00

134 lines
4.1 KiB
Python

import secrets
from typing import Optional
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
"""Application settings from environment variables."""
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
jwt_secret_key: str = Field(
default_factory=lambda: secrets.token_urlsafe(32),
validation_alias="JWT_SECRET_KEY",
)
password_salt: str = Field(
default="default-salt",
validation_alias="PASSWORD_SALT"
)
master_password_hash: Optional[str] = Field(
default=None,
validation_alias="MASTER_PASSWORD_HASH"
)
# ⚠️ WARNING: DEVELOPMENT ONLY - NEVER USE IN PRODUCTION ⚠️
# This field allows setting a plaintext master password via environment
# variable for development/testing purposes only. In production
# deployments, use MASTER_PASSWORD_HASH instead and NEVER set this field.
master_password: Optional[str] = Field(
default=None,
validation_alias="MASTER_PASSWORD",
description=(
"**DEVELOPMENT ONLY** - Plaintext master password. "
"NEVER enable in production. Use MASTER_PASSWORD_HASH instead."
),
)
token_expiry_hours: int = Field(
default=24,
validation_alias="SESSION_TIMEOUT_HOURS"
)
anime_directory: str = Field(
default="",
validation_alias="ANIME_DIRECTORY"
)
log_level: str = Field(
default="INFO",
validation_alias="LOG_LEVEL"
)
# Additional settings from .env
database_url: str = Field(
default="sqlite:///./data/aniworld.db",
validation_alias="DATABASE_URL"
)
cors_origins: str = Field(
default="http://localhost:3000",
validation_alias="CORS_ORIGINS",
)
api_rate_limit: int = Field(
default=100,
validation_alias="API_RATE_LIMIT"
)
default_provider: str = Field(
default="aniworld.to",
validation_alias="DEFAULT_PROVIDER"
)
provider_timeout: int = Field(
default=30,
validation_alias="PROVIDER_TIMEOUT"
)
retry_attempts: int = Field(
default=3,
validation_alias="RETRY_ATTEMPTS"
)
# NFO / TMDB Settings
tmdb_api_key: Optional[str] = Field(
default=None,
validation_alias="TMDB_API_KEY",
description="TMDB API key for scraping TV show metadata"
)
nfo_auto_create: bool = Field(
default=False,
validation_alias="NFO_AUTO_CREATE",
description="Automatically create NFO files when scanning series"
)
nfo_update_on_scan: bool = Field(
default=False,
validation_alias="NFO_UPDATE_ON_SCAN",
description="Update existing NFO files when scanning series"
)
nfo_download_poster: bool = Field(
default=True,
validation_alias="NFO_DOWNLOAD_POSTER",
description="Download poster.jpg when creating NFO"
)
nfo_download_logo: bool = Field(
default=True,
validation_alias="NFO_DOWNLOAD_LOGO",
description="Download logo.png when creating NFO"
)
nfo_download_fanart: bool = Field(
default=True,
validation_alias="NFO_DOWNLOAD_FANART",
description="Download fanart.jpg when creating NFO"
)
nfo_image_size: str = Field(
default="original",
validation_alias="NFO_IMAGE_SIZE",
description="Image size to download (original, w500, etc.)"
)
@property
def allowed_origins(self) -> list[str]:
"""Return the list of allowed CORS origins.
The environment variable should contain a comma-separated list.
When ``*`` is provided we fall back to a safe local development
default instead of allowing every origin in production.
"""
raw = (self.cors_origins or "").strip()
if not raw:
return []
if raw == "*":
return [
"http://localhost:3000",
"http://localhost:8000",
]
return [origin.strip() for origin in raw.split(",") if origin.strip()]
settings = Settings()