163 lines
5.3 KiB
Python
163 lines
5.3 KiB
Python
"""Configuration management for if-curator."""
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
from pathlib import Path
|
|
from typing import ClassVar
|
|
|
|
from dotenv import load_dotenv
|
|
from rich.prompt import Prompt
|
|
|
|
load_dotenv()
|
|
|
|
CONFIG_FILE = Path(".immich_config.json")
|
|
|
|
|
|
class _Config:
|
|
"""Singleton configuration with uppercase attribute access for backward compatibility."""
|
|
|
|
_instance: ClassVar["_Config | None"] = None
|
|
|
|
# Configuration values
|
|
IMMICH_URL: str | None = None
|
|
API_KEY: str | None = None
|
|
OUTPUT_DIR: str = "./frigate_train"
|
|
YEARS_FILTER: int = 10
|
|
|
|
# Quality filtering
|
|
MIN_FACE_WIDTH: int = 50
|
|
BLUR_THRESHOLD: float = 100.0
|
|
MIN_CONFIDENCE: float = 0.7
|
|
MAX_AUTO_IMAGES: int = 80
|
|
|
|
# People filtering
|
|
MIN_FACE_COUNT: int = 0
|
|
|
|
# Output quality
|
|
FACE_MARGIN: float = 0.15
|
|
USE_FULL_RESOLUTION: bool = True
|
|
ENABLE_FACE_ALIGNMENT: bool = True
|
|
|
|
# Caching (opt-in to avoid unexpected files)
|
|
ENABLE_CACHE: bool = False
|
|
CACHE_DIR: str = ".if_cache"
|
|
|
|
def __new__(cls) -> "_Config":
|
|
if cls._instance is None:
|
|
cls._instance = super().__new__(cls)
|
|
cls._instance._load()
|
|
return cls._instance
|
|
|
|
def _load(self) -> None:
|
|
"""Load configuration from environment and config file."""
|
|
# Load from environment (highest priority)
|
|
self.IMMICH_URL = os.getenv("IMMICH_URL")
|
|
self.API_KEY = os.getenv("API_KEY")
|
|
self.OUTPUT_DIR = os.getenv("OUTPUT_DIR", "./frigate_train")
|
|
self.YEARS_FILTER = int(os.getenv("YEARS_FILTER", "10"))
|
|
self.MIN_FACE_WIDTH = int(os.getenv("MIN_FACE_WIDTH", "50"))
|
|
self.MIN_FACE_COUNT = int(os.getenv("MIN_FACE_COUNT", "0"))
|
|
self.ENABLE_CACHE = os.getenv("ENABLE_CACHE", "false").lower() in ("true", "1", "yes")
|
|
self.CACHE_DIR = os.getenv("CACHE_DIR", ".if_cache")
|
|
|
|
# Fall back to config file for missing values
|
|
if CONFIG_FILE.exists():
|
|
try:
|
|
data = json.loads(CONFIG_FILE.read_text())
|
|
self.IMMICH_URL = self.IMMICH_URL or data.get("IMMICH_URL")
|
|
self.API_KEY = self.API_KEY or data.get("API_KEY")
|
|
if not os.getenv("OUTPUT_DIR"):
|
|
self.OUTPUT_DIR = data.get("OUTPUT_DIR", self.OUTPUT_DIR)
|
|
except (json.JSONDecodeError, OSError) as e:
|
|
logging.warning(f"Failed to load config file: {e}")
|
|
|
|
@classmethod
|
|
def reset(cls) -> None:
|
|
"""Reset the singleton — mainly useful for testing or delayed env setup."""
|
|
cls._instance = None
|
|
|
|
def save(self) -> None:
|
|
"""Persist configuration to file."""
|
|
try:
|
|
CONFIG_FILE.write_text(
|
|
json.dumps(
|
|
{
|
|
"IMMICH_URL": self.IMMICH_URL,
|
|
"API_KEY": self.API_KEY,
|
|
"OUTPUT_DIR": self.OUTPUT_DIR,
|
|
},
|
|
indent=2,
|
|
)
|
|
)
|
|
logging.info(f"Configuration saved to {CONFIG_FILE}")
|
|
except OSError as e:
|
|
logging.error(f"Failed to save config: {e}")
|
|
|
|
def interactive_setup(self) -> None:
|
|
"""Prompt user for missing configuration."""
|
|
from rich.console import Console
|
|
|
|
console = Console()
|
|
|
|
if not self.IMMICH_URL:
|
|
console.print("[yellow]Immich URL not found.[/yellow]")
|
|
self.IMMICH_URL = Prompt.ask("Enter Immich URL (e.g. http://192.168.1.5:2283)")
|
|
self.save()
|
|
|
|
if not self.API_KEY:
|
|
console.print("[yellow]Immich API Key not found.[/yellow]")
|
|
self.API_KEY = Prompt.ask("Enter Immich API Key", password=True)
|
|
self.save()
|
|
|
|
def validate(self) -> None:
|
|
"""Raise ValueError if required config is missing."""
|
|
if not self.IMMICH_URL or not self.API_KEY:
|
|
raise ValueError("Missing Immich URL or API Key.")
|
|
|
|
|
|
# Singleton instance — use a lazy property pattern to avoid import-time side effects
|
|
# when env vars aren't yet set. Call Config.instance() or just access attributes on
|
|
# the module-level `Config` (which delegates to the singleton).
|
|
class _ConfigAccessor:
|
|
"""Lazy accessor that defers singleton creation until first attribute access.
|
|
|
|
This avoids reading .env and config files at import time, so environment
|
|
variables set after importing the module are properly picked up.
|
|
"""
|
|
|
|
def __getattr__(self, name: str):
|
|
return getattr(_Config(), name)
|
|
|
|
def __setattr__(self, name: str, value):
|
|
if name.startswith("_"):
|
|
super().__setattr__(name, value)
|
|
else:
|
|
setattr(_Config(), name, value)
|
|
|
|
def reset(self) -> None:
|
|
"""Reset the underlying singleton."""
|
|
_Config.reset()
|
|
|
|
def interactive_setup(self) -> None:
|
|
"""Delegate to the singleton."""
|
|
_Config().interactive_setup()
|
|
|
|
def validate(self) -> None:
|
|
"""Delegate to the singleton."""
|
|
_Config().validate()
|
|
|
|
def save(self) -> None:
|
|
"""Delegate to the singleton."""
|
|
_Config().save()
|
|
|
|
|
|
Config = _ConfigAccessor()
|
|
ConfigManager = type("ConfigManager", (), {"get": staticmethod(lambda: _Config())})
|
|
|
|
|
|
def get_headers() -> dict[str, str]:
|
|
"""Return HTTP headers for Immich API requests."""
|
|
return {"x-api-key": Config.API_KEY or "", "Accept": "application/json"}
|
|
|